diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc329279f..eb331135b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -158,7 +158,7 @@ jobs: - uses: actions/checkout@v6 - uses: taiki-e/install-action@just - uses: dtolnay/rust-toolchain@stable - if: contains(matrix.generator, 'rust-') || matrix.generator == 'typescript-fetch-vp' + if: contains(matrix.generator, 'rust-') || contains(matrix.generator, 'python-') || matrix.generator == 'typescript-fetch-vp' - uses: Swatinem/rust-cache@v2 if: contains(matrix.generator, 'rust-') with: @@ -168,8 +168,12 @@ jobs: if: matrix.generator == 'typescript-fetch-vp' with: key: typescript-client-tests + - uses: Swatinem/rust-cache@v2 + if: contains(matrix.generator, 'python-') + with: + key: golden-${{ matrix.generator }} - uses: actions/setup-node@v6 - if: contains(matrix.generator, 'typescript-fetch') + if: contains(matrix.generator, 'typescript-fetch') || contains(matrix.generator, 'python-') with: node-version: 'lts/*' - if: contains(matrix.generator, 'typescript-fetch') @@ -183,11 +187,7 @@ jobs: - uses: actions/setup-python@v6 if: contains(matrix.generator, 'python-') with: - python-version: '3.12' - - uses: actions/setup-node@v6 - if: contains(matrix.generator, 'python-') - with: - node-version: '20' + python-version: '3.x' - uses: actions/setup-java@v5 if: contains(matrix.generator, 'java-') || contains(matrix.generator, 'kotlin-') with: diff --git a/.gitignore b/.gitignore index da5737e99..86b11597c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ examples/*/generated/ examples/*/target/ tests/generated-clients/rust/*/generated/ tests/generated-clients/typescript/generated/ +tests/generated-clients/python/generated/ tests/generated-clients/jvm/*/generated/ tests/generated-clients/jvm/**/.gradle/ tests/generated-clients/jvm/**/build/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 213072ac6..a0b35b51b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Convert Python httpx/requests untagged-union members in models, nested containers, + aliases, JSON/multipart requests, responses, and lazy error bodies. Share enum- + aware selection across both transports with `firstMatch`, `strict`, and dynamic + `raw` policies, preserving nested strict errors and productive recursion. Keep + shared helpers import-safe and restore tagged wire wrappers from typed payload + members. Encode required nullable union request bodies as JSON `null` instead + of omitting them. - Convert camelCase TypeScript untagged-union members in both JSON directions, including mixed client/search tools and containing requests. Add configurable `firstMatch`, `strict`, and wire-preserving `raw` selection policies instead diff --git a/Justfile b/Justfile index 561b8beb2..f9f82e3d2 100644 --- a/Justfile +++ b/Justfile @@ -7,6 +7,7 @@ mod golden-typescript-clients 'just/golden-typescript-clients.just' mod golden-go 'just/golden-go.just' mod golden-rust 'just/golden-rust.just' mod golden-python 'just/golden-python.just' +mod golden-python-clients 'just/golden-python-clients.just' mod golden-java 'just/golden-java.just' mod golden-kotlin 'just/golden-kotlin.just' mod golden-jvm 'just/golden-jvm.just' @@ -154,7 +155,7 @@ golden-check: # Update .golden files for all generators golden-update: golden-typescript::update golden-go::update golden-rust::update golden-python::update golden-java::update golden-kotlin::update -# Compile-check all goldens +# Validate all goldens and their integrated consumer contracts golden-build-all: golden-typescript::build golden-go::build golden-rust::build golden-python::build golden-java::build golden-kotlin::build # ---------- Publish ---------- diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index f5c3ad8d0..11d3c9243 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -8,6 +8,7 @@ - [Response Headers](response_headers.md) - [Rust Generator Configuration](rust_config.md) - [TypeScript Generator Configuration](ts_config.md) +- [Python Generator Configuration](python_config.md) - [Architecture](architecture.md) - [Adding a Generator](adding_a_generator.md) - [IR Reference](ir_reference.md) diff --git a/docs/src/golden_testing.md b/docs/src/golden_testing.md index e39647afe..96563e042 100644 --- a/docs/src/golden_testing.md +++ b/docs/src/golden_testing.md @@ -178,6 +178,27 @@ round-trips, not only wrapper identity or discriminator substrings. There are no JVM build subprocesses or global directory locks in the Rust test suite. The independent JVM Client Tests CI matrix and `just ci` run these checks. +## Native Python Union Consumers + +`just golden-python-clients::test httpx` and +`just golden-python-clients::test requests` generate fresh clients for +`firstMatch`, `strict`, and `raw`, run strict pyright on the unchanged files, then +run ordinary `unittest` consumers. The same contracts cover dataclasses, nested +containers, aliases, finite values, recursion, JSON/multipart transport encoding, +tagged payloads, nulls, and lazy error decoding. In-process HTTP transports avoid +network IO without bypassing each library's request encoder. Native tests do not +launch generation, type checkers, or other runners. + +The same `just golden-python::build-httpx` / `build-requests` entrypoints run all +golden checks, fresh public-declaration checks and native consumers, both locally +and in CI. `golden-build-all` and `just ci` inherit this complete validation; CI +does not append a Python-specific test step. Dependencies are installed once in +a temporary environment created with the active Python (locally 3.15.0, CI `3.x`), +and the same checker and interpreter are used throughout the backend's checks. +The generated SDK's minimum and explicit pyright target remain 3.12, a static contract, +not a claim that the native tests execute on the minimum runtime. See the +[Python configuration](python_config.md) for selection-policy semantics. + ## Validation Contracts and Toolchains - Missing fixture files fail their parser tests. Nested fixture paths are relative @@ -190,7 +211,8 @@ suite. The independent JVM Client Tests CI matrix and `just ci` run these checks - Rust golden builds retain their cache under `target/golden-build/rust-` or the explicitly configured `CARGO_TARGET_DIR`. CI caches the same directory. - Python prepares dependencies and resolves pyright once per backend, sharing - that environment across its fixtures. `PYTHON` selects the install interpreter; + that environment across goldens, declaration checks and runtime consumers. + `PYTHON` selects the interpreter used to create the temporary environment; the active `pyright` is used when available, otherwise npm resolves it once. - Development and ordinary CI validation select current Rust `stable` (locally validated with Rust 1.99.0). MSRV checks explicitly select Rust 1.90.0; they do @@ -211,7 +233,7 @@ normally; no test harness launches a compiler, build tool, or validation script. to just/CI, which directly run the golden builds and ordinary consumer projects. `just ci` includes generator tests and coverage, all nine generators' native -golden builds, and TypeScript/Rust/JVM consumer tests. Golden comparison is already included +golden builds, and TypeScript/Python/Rust/JVM consumer tests. Golden comparison is already included in `test-all`, so the full recipe does not repeat it as another standalone step. ## Extra File Detection diff --git a/docs/src/python_config.md b/docs/src/python_config.md new file mode 100644 index 000000000..20b68aeac --- /dev/null +++ b/docs/src/python_config.md @@ -0,0 +1,80 @@ +# Python Generator Configuration + +Both `python-httpx` and `python-requests` accept the same options. Generated +clients require Python 3.12 or newer. + +```toml +[generators.python-httpx] +package_name = "my_client" +untagged_union_strategy = "firstMatch" + +[generators.python-requests] +package_name = "my_client" +untagged_union_strategy = "strict" +``` + +The equivalent CLI override is: + +```sh +openapi-nexus generate --input openapi.yaml --output client \ + --generators python-httpx \ + --generator-config python-httpx.untagged_union_strategy=strict +``` + +## `package_name` + +Overrides the generated import package and project name. The default is derived +from the API title. + +## `untagged_union_strategy` + +Controls conversion of untagged `oneOf` and `anyOf` values, not general schema +validation. Both keywords use this policy; `strict` does not impose `oneOf` +cardinality on every `anyOf` elsewhere in a specification. + +| Value | Member selection | No match | Public representation | +| ------------ | --------------------------------------------- | --------------------------------- | --------------------------------- | +| `firstMatch` | First concrete match, in schema order | Preserve the original value | Existing dataclass/primitive union | +| `strict` | Reject multiple concrete matches | Raise `ValueError` | Existing dataclass/primitive union | +| `raw` | Do not select or convert the union subtree | Preserve the original value | Dynamic `object` | + +The default is `firstMatch`. Concrete alternatives take precedence over opaque +`Any`/empty-object fallbacks, even when the fallback appears first. Strict counts +original concrete members before calling any converter; an opaque fallback does +not create false ambiguity. Selected nested unions retain their own policy and +errors rather than silently trying another member. Nullable wrappers permit +null without inventing an extra authored member. + +Matching uses required fields, present optional fields, JSON kinds, and retained +finite enum/literal constraints. Booleans are not numbers despite Python's +`True == 1`. Arrays, dictionary values, aliases, and productive recursion retain +the same conversion boundaries. Formats and numeric/string bounds do not serve +as member-selection validators. + +For tagged members inside an untagged union, outbound matching follows the +existing public payload representation. The selected tagged converter restores +its wire wrapper only after selection; strict ambiguity counting never calls a +converter to probe whether a member matches. + +## Conversion entrypoints + +Existing dataclass constructors and `to_dict` / `from_dict` methods remain public. +For a named untagged union `Choice`, its module also provides +`choice_from_dict(value)` and `choice_to_dict(value)`. These helpers accept or +return arbitrary JSON values, not only dictionaries; their names align with the +existing Python tagged-union convention. They are not added to package barrels. + +Model fields, JSON HTTP payloads, success responses, and lazy error bodies use +the same conversion plan. Direct typed members can therefore be used in union +fields without manual conversion before `json.dumps`. Required union request +bodies containing `None` are encoded as JSON `null`, not an absent HTTP body. +Optional request bodies retain the existing convention that `None` means omit. + +In `raw` mode, supply JSON-compatible values yourself; generated dataclasses +inside a raw subtree are deliberately not auto-serialized. Ordinary containing +objects still use their dataclass conversion. Default unmatched values are kept +as an escape hatch, so annotations describe the normal matching path rather than +proving validation. Choose `strict` to reject unmatched inputs. + +Dictionary/additional-property model preservation and multi-member `allOf` +composition are separate concerns and are not redesigned by this option. diff --git a/just/golden-python-clients.just b/just/golden-python-clients.just new file mode 100644 index 000000000..5ea76845f --- /dev/null +++ b/just/golden-python-clients.just @@ -0,0 +1,19 @@ +# Native Python consumers of freshly generated, unmodified output. + +set working-directory := '..' + +@prepare backend: + #!/usr/bin/env bash + set -euo pipefail + case '{{backend}}' in httpx|requests) ;; *) echo 'Expected httpx or requests' >&2; exit 2 ;; esac + for policy in default strict raw; do + strategy="$policy" + [ "$policy" != default ] || strategy=firstMatch + cargo run --quiet --bin openapi-nexus -- generate --config tests/generated-clients/config.toml --input tests/fixtures/valid/python-untagged-union.yaml --output "tests/generated-clients/python/generated/{{backend}}/$policy" --generators 'python-{{backend}}' --generator-config "python-{{backend}}.package_name=union_$policy" --generator-config "python-{{backend}}.untagged_union_strategy=$strategy" + done + +@build backend: (prepare backend) + "${PYRIGHT:-pyright}" --pythonpath "$(command -v "${PYTHON:-python3}")" --project tests/generated-clients/python/pyrightconfig.{{backend}}.json tests/generated-clients/python/generated/{{backend}}/default/union_default tests/generated-clients/python/generated/{{backend}}/strict/union_strict tests/generated-clients/python/generated/{{backend}}/raw/union_raw tests/generated-clients/python/typing_contracts.py + +@test backend: (build backend) + NEXUS_PYTHON_BACKEND={{backend}} PYTHONPATH="tests/generated-clients/python/generated/{{backend}}/default:tests/generated-clients/python/generated/{{backend}}/strict:tests/generated-clients/python/generated/{{backend}}/raw" "${PYTHON:-python3}" -m unittest discover -v -s tests/generated-clients/python/tests diff --git a/just/golden-python.just b/just/golden-python.just index 86ce72b41..768976fb5 100644 --- a/just/golden-python.just +++ b/just/golden-python.just @@ -13,13 +13,13 @@ update: update-httpx update-requests @update-requests: UPDATE_GOLDEN=1 cargo test --test golden_tests_python_requests -- --nocapture -# Type-check all Python goldens +# Validate all Python goldens, public declarations and native consumers build: build-httpx build-requests -# Type-check Python httpx goldens with pyright +# Validate Python httpx goldens and fresh native consumers @build-httpx: ./scripts/golden-build-python.sh httpx -# Type-check Python requests goldens with pyright +# Validate Python requests goldens and fresh native consumers @build-requests: ./scripts/golden-build-python.sh requests diff --git a/scripts/golden-build-python.sh b/scripts/golden-build-python.sh index 7a135b465..7016ade5d 100755 --- a/scripts/golden-build-python.sh +++ b/scripts/golden-build-python.sh @@ -10,7 +10,7 @@ golden="$root/tests/golden/python/python-$backend" source "$root/scripts/golden-build-common.sh" golden_require_projects "$golden" pyproject.toml.golden -# One dependency installation and checker resolution per backend, not per fixture. +# One active-interpreter environment for goldens and fresh native consumers. deps=$(mktemp -d) tmp=""; log="" cleanup() { @@ -19,7 +19,9 @@ cleanup() { [ -z "$log" ] || rm -f "$log" } trap cleanup EXIT -"${PYTHON:-python3}" -m pip install "$backend" --quiet --target "$deps" +"${PYTHON:-python3}" -m venv "$deps" +python="$deps/bin/python" +"$python" -m pip install "$backend" --quiet if command -v pyright >/dev/null 2>&1; then checker=$(command -v pyright) else @@ -47,13 +49,12 @@ for test_dir in "$golden"/*/; do { "pythonVersion": "3.12", "typeCheckingMode": "strict", - "extraPaths": ["$deps"], "reportUnnecessaryIsInstance": "none" } PYRIGHT log=$(mktemp) - if (cd "$tmp" && "$checker") >"$log" 2>&1; then + if (cd "$tmp" && "$checker" --pythonpath "$python") >"$log" 2>&1; then echo "ok" passed=$((passed + 1)) else @@ -75,3 +76,8 @@ if [ "$failed" -gt 0 ]; then for n in "${fails[@]}"; do echo " - $n"; done exit 1 fi + +# The public golden-build entrypoint owns the complete backend contract. CI and +# `just ci` must not append a second, Python-specific validation path. +export PYTHON="$python" PYRIGHT="$checker" +(cd "$root" && just golden-python-clients::test "$backend") diff --git a/src/generators/python/emit_api_values.rs b/src/generators/python/emit_api_values.rs new file mode 100644 index 000000000..0a8ae1ef2 --- /dev/null +++ b/src/generators/python/emit_api_values.rs @@ -0,0 +1,170 @@ +//! Transport-independent Python multipart and lazy union-error conversion. + +use sigil_stitch::lang::python::Python; +use sigil_stitch::prelude::*; + +use super::emit_models::is_object_schema; +use super::value::{Direction, ValuePlan}; +use crate::generators::multipart::MultipartValueEncoding; +use crate::ir::types::{IrSpec, IrTypeExpr}; + +pub(super) struct MultipartPart { + pub wire_name: String, + pub field_name: String, + pub type_expr: IrTypeExpr, + pub is_binary: bool, + pub required: bool, + pub content_type: String, + pub value_encoding: MultipartValueEncoding, +} + +pub(super) fn multipart_data( + body_name: &str, + required: bool, + parts: &[MultipartPart], + ir: &IrSpec, + values: &ValuePlan<'_>, +) -> CodeBlock { + if parts.iter().any(|part| values.has_union(&part.type_expr)) { + let assignments = parts.iter().map(|part| { + let source = sigil_quote!(Python { $N(body_name).$N(part.field_name.as_str()) }).expect("Python multipart input access builds"); + let assignment = if part.value_encoding == MultipartValueEncoding::Json && values.has_union(&part.type_expr) { + let converted = values.call(&part.type_expr, source.clone(), Direction::To); + sigil_quote!(Python { + files[$S(part.wire_name.as_str())] = (None, json.dumps($C(converted)), $S(part.content_type.as_str())) + }).expect("Python union multipart assignment builds") + } else { + multipart_part_assignment(part, &format!("{body_name}.{}", part.field_name), ir) + }; + sigil_quote!(Python { + $if(part.required) { $C(assignment) } $else { + if $C(source) is not None { $C(assignment) } + } + }).expect("Python optional multipart field builds") + }).collect::>(); + return sigil_quote!(Python { + files: dict[str, object] = {} + $if(required) { $C_each(assignments) } $else { + if $N(body_name) is not None { $C_each(assignments) } + } + }) + .expect("Python multipart body with union parts builds"); + } + + // Keep the existing non-union rendering byte-identical while both transports + // share it; union-aware source above uses structured fragments throughout. + let mut cb = CodeBlock::builder(); + cb.add_statement("files: dict[str, object] = {}", ()); + if !required { + cb.add_statement(&format!("if {body_name} is not None:%>"), ()); + } + for part in parts { + let access = format!("{body_name}.{}", part.field_name); + if part.required { + cb.add_code(multipart_part_assignment(part, &access, ir)); + } else { + cb.add_statement(&format!("if {access} is not None:%>"), ()); + cb.add_code(multipart_part_assignment(part, &access, ir)); + cb.add_statement("%<", ()); + } + } + if !required { + cb.add_statement("%<", ()); + } + cb.build().expect("Python multipart body builds") +} + +fn multipart_part_assignment(part: &MultipartPart, access: &str, ir: &IrSpec) -> CodeBlock { + let binary_stmt = format!( + "files[\"{}\"] = ({}.filename_or_default(\"{}\"), {}.data, \"{}\")", + part.wire_name, access, part.wire_name, access, part.content_type + ); + let json_value = render_multipart_json_value(access, &part.type_expr, ir); + let json_stmt = format!( + "files[\"{}\"] = (None, json.dumps({json_value}), \"{}\")", + part.wire_name, part.content_type + ); + let unsupported_stmt = "raise ValueError(\"unsupported multipart part content type\")"; + let scalar_stmt = format!( + "files[\"{}\"] = (None, str({access}), \"{}\")", + part.wire_name, part.content_type + ); + + sigil_quote!(Python { + $if(part.is_binary) { + $L(binary_stmt.as_str()) + } $else_if(part.value_encoding == MultipartValueEncoding::Json) { + $L(json_stmt.as_str()) + } $else_if(part.value_encoding == MultipartValueEncoding::Unsupported) { + $L(unsupported_stmt) + } $else { + $L(scalar_stmt.as_str()) + } + }) + .expect("multipart part assignment builds") +} + +fn render_multipart_json_value(access: &str, expr: &IrTypeExpr, ir: &IrSpec) -> String { + match expr { + IrTypeExpr::Named(name) if is_object_schema(name, ir) => format!("{access}.to_dict()"), + IrTypeExpr::Nullable(inner) => render_multipart_json_value(access, inner, ir), + IrTypeExpr::Array(inner) => { + if let IrTypeExpr::Named(name) = inner.as_ref() + && is_object_schema(name, ir) + { + format!("[item.to_dict() for item in {access}]") + } else { + access.to_string() + } + } + _ => access.to_string(), + } +} + +pub(super) fn union_error_body_property( + type_expr: &IrTypeExpr, + ir: &IrSpec, + values: &ValuePlan<'_>, +) -> CodeBlock { + let converted = values.call( + type_expr, + sigil_quote!(Python { json.loads(self.raw_body.decode("utf-8")) }) + .expect("Python union error JSON input builds"), + Direction::From, + ); + let ty = values.api_type(type_expr); + let converted = values.coerce(type_expr, ty.clone(), converted); + let cached = + sigil_quote!(Python { self._body_value }).expect("Python cached union error value builds"); + let cached = if crate::generators::union_match::accepts_null(type_expr, ir) + || values.is_dynamic(type_expr) + { + cached + } else { + values.coerce(type_expr, ty.clone(), cached) + }; + let body = sigil_quote!(Python { + if not self._body_loaded { + try { + self._body_value = $C(converted) + } except Exception as exc { + self._body_error = exc + } + self._body_loaded = True + } + if self._body_error is not None { + raise self._body_error + } + return $C(cached) + }) + .expect("Python union error body builds"); + FunSpec::builder("body") + .annotate(AnnotationSpec::new("property")) + .add_param(ParameterSpec::of("self", TypeName::primitive(""))) + .returns(vec![ty]) + .body(body) + .build() + .expect("Python union error body property builds") + .emit(&Python::new(), DeclarationContext::Member) + .expect("Python union error body property emits") +} diff --git a/src/generators/python/emit_models.rs b/src/generators/python/emit_models.rs new file mode 100644 index 000000000..2b33fa0bf --- /dev/null +++ b/src/generators/python/emit_models.rs @@ -0,0 +1,1426 @@ +//! Shared model emission for both Python transports (dataclasses/enums/aliases). +//! +//! Uses sigil-stitch high-level APIs (TypeSpec, FieldSpec, FunSpec, TypeName, +//! sigil_quote!) for structured code generation with automatic import tracking. +//! Each schema produces one `.py` file via `FileSpec`. + +use super::value::{Direction, UntaggedUnionStrategy, ValuePlan, optional_type}; +use crate::codegen::traits::file_writer::FileInfo; +use crate::generators::GeneratorError; +pub use crate::generators::python::type_names::python_type_name; +use crate::generators::request_inputs::{ + RequestInputField, RequestInputFieldKind, RequestInputModel, RequestInputPlan, +}; +use crate::generators::type_application; +use crate::ir::types::{ + IrEnum, IrEnumValueType, IrIntersection, IrObject, IrPrimitive, IrProperty, IrSchema, + IrSchemaKind, IrSpec, IrTaggedUnion, IrTaggedVariant, IrTypeExpr, IrUnion, TaggingStyle, +}; +use heck::{ToPascalCase, ToSnakeCase}; +use sigil_stitch::code_block::CodeBlock; +use sigil_stitch::lang::python::Python; +use sigil_stitch::prelude::*; + +/// Generate every model file from the IR. +pub fn generate_model_files( + ir: &IrSpec, + header: &str, + request_inputs: &RequestInputPlan, +) -> Result, GeneratorError> { + generate_model_files_with_strategy( + ir, + header, + request_inputs, + UntaggedUnionStrategy::FirstMatch, + ) +} + +/// Generate models using an explicit untagged-union conversion policy. +pub fn generate_model_files_with_strategy( + ir: &IrSpec, + header: &str, + request_inputs: &RequestInputPlan, + strategy: UntaggedUnionStrategy, +) -> Result, GeneratorError> { + let values = ValuePlan::new(ir, strategy); + let mut files = Vec::new(); + for (_name, schema) in &ir.schemas { + let body = emit_model_body(schema, ir, &values).ok_or_else(|| { + GeneratorError::UnsupportedSchema { + schema: schema.name.clone(), + kind: format!("{:?}", schema.kind), + } + })?; + let stem = schema.name.to_snake_case(); + let filename = format!("{stem}.py"); + let mut content = String::with_capacity(header.len() + body.len()); + content.push_str(header); + content.push_str(&body); + files.push(FileInfo::model(filename, content)); + } + for model in request_inputs.models() { + files.push(request_input_model_file(model, header, &values)); + } + files.extend(values.files(header)?); + Ok(files) +} + +fn request_input_model_file( + model: &RequestInputModel, + header: &str, + values: &ValuePlan<'_>, +) -> FileInfo { + if model + .fields + .iter() + .any(|field| values.has_union(&field.type_expr)) + { + let mut declaration = + TypeSpec::builder(&model.name.to_pascal_case(), TypeKind::Class).annotate( + AnnotationSpec::importable(TypeName::importable("dataclasses", "dataclass")), + ); + for field in model + .fields + .iter() + .filter(|field| field.required) + .chain(model.fields.iter().filter(|field| !field.required)) + { + let ty = if field.is_upload() { + TypeName::importable("..runtime", "UploadFile") + } else { + values.model_type(&field.type_expr, &model.name) + }; + let mut spec = FieldSpec::builder( + &python_field_name(&field.wire_name), + if field.required { + ty + } else { + optional_type(ty) + }, + ); + if !field.required { + spec = spec.initializer( + sigil_quote!(Python { None }) + .expect("Python optional union multipart field default builds"), + ); + } + declaration = + declaration.add_field(spec.build().expect("Python union multipart field builds")); + } + let filename = format!("{}.py", model.name.to_snake_case()); + let source = FileSpec::builder_with(&filename, Python::new()) + .header(future_annotations_header()) + .add_type( + declaration + .build() + .expect("Python union multipart input declaration builds"), + ) + .build() + .expect("Python union multipart input file builds") + .render(100) + .expect("Python union multipart input renders"); + // Keep the final file boundary at exactly one terminating newline. + return FileInfo::model( + filename, + format!("{header}{}\n", source.trim_end_matches('\n')), + ); + } + let class_name = model.name.to_pascal_case(); + let filename = format!("{}.py", model.name.to_snake_case()); + let mut imports = std::collections::BTreeSet::new(); + let mut needs_upload = false; + for field in &model.fields { + if field.is_upload() { + needs_upload = true; + } else { + collect_request_input_imports(&field.type_expr, &mut imports); + } + } + + let mut content = String::new(); + content.push_str(header); + content.push_str("from __future__ import annotations\n\n"); + content.push_str("from dataclasses import dataclass\n"); + if needs_upload { + content.push_str("from ..runtime import UploadFile\n"); + } + for import in &imports { + content.push_str(import); + content.push('\n'); + } + content.push('\n'); + content.push_str("@dataclass\n"); + content.push_str(&format!("class {class_name}:\n")); + if model.fields.is_empty() { + content.push_str(" pass\n"); + } else { + let required = model.fields.iter().filter(|field| field.required); + let optional = model.fields.iter().filter(|field| !field.required); + for field in required.chain(optional) { + let field_name = python_field_name(&field.wire_name); + let ty = request_input_python_type(field); + if field.required { + content.push_str(&format!(" {field_name}: {ty}\n")); + } else { + content.push_str(&format!(" {field_name}: {ty} | None = None\n")); + } + } + } + + FileInfo::model(filename, content) +} + +fn request_input_python_type(field: &RequestInputField) -> String { + match field.kind { + RequestInputFieldKind::UploadFile { .. } => "UploadFile".to_string(), + RequestInputFieldKind::SchemaValue => python_type_str(&field.type_expr), + } +} + +fn emit_model_body(schema: &IrSchema, ir: &IrSpec, values: &ValuePlan<'_>) -> Option { + let file_spec = match &schema.kind { + IrSchemaKind::Object(obj) => emit_object(schema, obj, ir, values), + IrSchemaKind::Enum(en) => emit_enum(schema, en), + IrSchemaKind::Alias(expr) => emit_alias(schema, expr, values), + IrSchemaKind::Union(u) => emit_union(schema, u, values), + IrSchemaKind::Intersection(i) => emit_intersection(schema, i, ir, values), + IrSchemaKind::TaggedUnion(tu) => emit_tagged_union(schema, tu, ir, values), + }?; + file_spec.render(100).ok() +} + +pub fn future_annotations_header() -> CodeBlock { + CodeBlock::of("from __future__ import annotations", ()).expect("static header") +} + +// --------------------------------------------------------------------------- +// Object -> @dataclass +// --------------------------------------------------------------------------- + +fn emit_object( + schema: &IrSchema, + obj: &IrObject, + ir: &IrSpec, + values: &ValuePlan<'_>, +) -> Option { + let name = schema.name.to_pascal_case(); + + let mut file = + FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); + + if needs_typing_literal_in_props(&obj.properties) { + file = file.add_import(ImportSpec::named("typing", "Literal")); + } + if obj + .properties + .values() + .any(|property| values.has_union(&property.type_expr)) + { + file = file.add_import(ImportSpec::named(".", "_union")); + } + + // Import tagged-union helper functions for fields referencing TaggedUnion schemas + for (_, prop) in &obj.properties { + if values.has_union(&prop.type_expr) { + continue; + } + for named_ref in collect_tagged_union_refs(&prop.type_expr, ir) { + let snake = named_ref.to_snake_case(); + let module = format!(".{snake}"); + file = file.add_import(ImportSpec::named(&module, &format!("{snake}_from_dict"))); + file = file.add_import(ImportSpec::named(&module, &format!("{snake}_to_dict"))); + } + } + + let dataclass_tn = TypeName::importable("dataclasses", "dataclass"); + let mut cls = TypeSpec::builder(&name, TypeKind::Class) + .annotate(AnnotationSpec::importable(dataclass_tn)); + + if let Some(doc) = &schema.description { + cls = cls.doc(&format!("{}.", escape_docstring(doc))); + } + + let mut required: Vec<(&String, &IrProperty)> = Vec::new(); + let mut optional: Vec<(&String, &IrProperty)> = Vec::new(); + for (json_name, prop) in &obj.properties { + if prop.required && !prop.nullable { + required.push((json_name, prop)); + } else { + optional.push((json_name, prop)); + } + } + + let all_fields: Vec<(&String, &IrProperty)> = + required.iter().chain(optional.iter()).copied().collect(); + + if all_fields.is_empty() { + cls = cls.extra_member(CodeBlock::of("pass", ()).expect("pass")); + } else { + for (_json_name, prop) in &required { + let field_name = python_field_name(&prop.name); + let type_name = values.model_type(&prop.type_expr, &schema.name); + cls = cls.add_field( + FieldSpec::builder(&field_name, type_name) + .build() + .expect("required field"), + ); + } + for (_json_name, prop) in &optional { + let field_name = python_field_name(&prop.name); + let type_name = values.model_type(&prop.type_expr, &schema.name); + cls = cls.add_field( + FieldSpec::builder(&field_name, optional_type(type_name)) + .initializer(CodeBlock::of("None", ()).expect("None init")) + .build() + .expect("optional field"), + ); + } + + cls = cls.add_method(build_to_dict_method( + &all_fields, + ir, + &obj.properties, + values, + )); + cls = cls.add_method(build_from_dict_method( + &name, + &all_fields, + ir, + &obj.properties, + values, + &schema.name, + )); + } + + file = file.add_type(cls.build().ok()?); + file.build().ok() +} + +fn build_to_dict_method( + all_fields: &[(&String, &IrProperty)], + ir: &IrSpec, + properties: &indexmap::IndexMap, + values: &ValuePlan<'_>, +) -> FunSpec { + let self_param = ParameterSpec::of("self", TypeName::primitive("")); + let return_type = type_application( + TypeName::primitive("dict"), + vec![TypeName::primitive("str"), TypeName::primitive("object")], + ); + + let mut body = CodeBlock::builder(); + body.add_statement("result: dict[str, object] = {}", ()); + for (json_name, prop) in all_fields { + let field_name = python_field_name(&prop.name); + if values.has_union(&prop.type_expr) { + let access = sigil_quote!(Python { self.$N(field_name.as_str()) }) + .expect("Python union field access builds"); + let converted = values.call(&values.property_expr(prop), access, Direction::To); + body.add_code( + sigil_quote!(Python { + $if(prop.required) { + result[$S(json_name.as_str())] = $C(converted) + } $else { + if self.$N(field_name) is not None { + result[$S(json_name.as_str())] = $C(converted) + } + } + }) + .expect("Python union field serialization builds"), + ); + continue; + } + let to_expr = render_to_dict_expr(&format!("self.{field_name}"), json_name, ir, properties); + if prop.required && !prop.nullable { + body.add_statement(&format!("result[\"{json_name}\"] = {to_expr}"), ()); + } else { + body.add_statement(&format!("if self.{field_name} is not None:%>"), ()); + body.add_statement(&format!("result[\"{json_name}\"] = {to_expr}%<"), ()); + } + } + body.add_statement("return result", ()); + + FunSpec::builder("to_dict") + .add_param(self_param) + .returns(vec![return_type]) + .body(body.build().expect("to_dict body")) + .build() + .expect("to_dict method") +} + +fn build_from_dict_method( + class_name: &str, + all_fields: &[(&String, &IrProperty)], + ir: &IrSpec, + properties: &indexmap::IndexMap, + values: &ValuePlan<'_>, + schema_name: &str, +) -> FunSpec { + let cls_param = ParameterSpec::of("cls", TypeName::primitive("")); + let data_param = ParameterSpec::of( + "data", + type_application( + TypeName::primitive("dict"), + vec![TypeName::primitive("str"), TypeName::primitive("object")], + ), + ); + + let mut body = CodeBlock::builder(); + body.add_statement("return cls(%>", ()); + for (json_name, prop) in all_fields { + let field_name = python_field_name(&prop.name); + let is_required = prop.required && !prop.nullable; + if values.has_union(&prop.type_expr) { + let access = if prop.required { + sigil_quote!(Python { data[$S(json_name.as_str())] }) + } else { + sigil_quote!(Python { data.get($S(json_name.as_str())) }) + } + .expect("Python union field wire access builds"); + let expression = values.property_expr(prop); + let converted = values.call(&expression, access.clone(), Direction::From); + let ty = values.model_type(&prop.type_expr, schema_name); + let ty = if is_required { ty } else { optional_type(ty) }; + let converted = sigil_quote!(Python { $if(prop.required) { $C(converted) } $else { None if $S(json_name.as_str()) not in data else $C(converted) } }).expect("Python optional union field projection builds"); + let converted = values.coerce(&expression, ty, converted); + body.add_code( + sigil_quote!(Python { $N(field_name) = $C(converted), }) + .expect("Python union field deserialization builds"), + ); + continue; + } + let expr = if is_required { + render_from_dict_expr(json_name, ir, properties) + } else { + render_from_dict_optional_expr(json_name, ir, properties) + }; + if let Some(comment_start) = expr.find(" #") { + let (value_part, comment_part) = expr.split_at(comment_start); + body.add_statement(&format!("{field_name}={value_part},{comment_part}"), ()); + } else { + body.add_statement(&format!("{field_name}={expr},"), ()); + } + } + body.add("%<", ()); + body.add_statement(")", ()); + + FunSpec::builder("from_dict") + .annotation(CodeBlock::of("@classmethod", ()).expect("classmethod")) + .add_param(cls_param) + .add_param(data_param) + .returns(vec![TypeName::primitive(class_name)]) + .body(body.build().expect("from_dict body")) + .build() + .expect("from_dict method") +} + +// --------------------------------------------------------------------------- +// Enum -> class(str, Enum) or class(int, Enum) +// --------------------------------------------------------------------------- + +fn emit_enum(schema: &IrSchema, en: &IrEnum) -> Option { + if en.value_type == IrEnumValueType::Mixed { + return emit_type_alias_raw(schema, "object"); + } + + let name = schema.name.to_pascal_case(); + let base = match en.value_type { + IrEnumValueType::String => TypeName::primitive("str"), + IrEnumValueType::Integer | IrEnumValueType::Number => TypeName::primitive("int"), + IrEnumValueType::Mixed => unreachable!(), + }; + + let mut ts = TypeSpec::builder(&name, TypeKind::Enum) + .extends(base) + .extends(TypeName::importable("enum", "Enum")); + + if let Some(doc) = &schema.description { + ts = ts.doc(&format!("{}.", escape_docstring(doc))); + } + + for v in &en.values { + let (member_name, value_code) = match en.value_type { + IrEnumValueType::String => { + let s = v.value.as_str()?; + ( + python_enum_member_name(s), + format!("\"{}\"", escape_python_string(s)), + ) + } + IrEnumValueType::Integer | IrEnumValueType::Number => { + let n = v + .value + .as_i64() + .or_else(|| v.value.as_f64().map(|f| f as i64))?; + (format!("N{n}").replace('-', "NEG"), format!("{n}")) + } + IrEnumValueType::Mixed => unreachable!(), + }; + ts = ts.add_variant( + EnumVariantSpec::builder(&member_name) + .discriminant(CodeBlock::of(&value_code, ()).expect("enum value")) + .build() + .expect("enum variant"), + ); + } + + let file = FileSpec::builder_with("model.py", Python::new()) + .header(future_annotations_header()) + .add_type(ts.build().ok()?); + file.build().ok() +} + +// --------------------------------------------------------------------------- +// Alias -> type X = Y (PEP 695) +// --------------------------------------------------------------------------- + +fn emit_alias(schema: &IrSchema, expr: &IrTypeExpr, values: &ValuePlan<'_>) -> Option { + let name = schema.name.to_pascal_case(); + let rhs_type = values.model_type(expr, &schema.name); + + let type_alias = sigil_quote!(Python { + type $N(name.as_str()) = ($T(rhs_type)); + }) + .ok()?; + + let mut file = + FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); + if needs_typing_literal(expr) { + file = file.add_import(ImportSpec::named("typing", "Literal")); + } + if let Some(doc) = &schema.description { + file = file.add_raw(&format!("# {}\n", escape_docstring(doc))); + } + file = file.add_code(type_alias); + file.build().ok() +} + +fn emit_type_alias_raw(schema: &IrSchema, rhs: &str) -> Option { + let name = schema.name.to_pascal_case(); + + let type_alias = sigil_quote!(Python { + type $N(name.as_str()) = $L(rhs); + }) + .ok()?; + + let mut file = + FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); + if let Some(doc) = &schema.description { + file = file.add_raw(&format!("# {}\n", escape_docstring(doc))); + } + file = file.add_code(type_alias); + file.build().ok() +} + +// --------------------------------------------------------------------------- +// Type alias builder with proper multi-line formatting +// --------------------------------------------------------------------------- + +fn format_type_alias(name: &str, members: &[TypeName]) -> CodeBlock { + if members.is_empty() { + return sigil_quote!(Python { + type $N(name) = ($T(TypeName::importable("typing", "Any"))); + }) + .unwrap(); + } + if members.len() == 1 { + return sigil_quote!(Python { + type $N(name) = ($T(members[0].clone())); + }) + .unwrap(); + } + sigil_quote!(Python { + type $N(name) = ( + $L(" ")$for(member in members; separator = "\n | ") { $T((*member).clone()) } + ) + }) + .unwrap() +} + +// --------------------------------------------------------------------------- +// Union -> type X = A | B | C +// --------------------------------------------------------------------------- + +fn emit_union(schema: &IrSchema, u: &IrUnion, values: &ValuePlan<'_>) -> Option { + let name = schema.name.to_pascal_case(); + + let mut members: Vec = if values.strategy == UntaggedUnionStrategy::Raw { + vec![TypeName::primitive("object")] + } else { + u.members + .iter() + .map(|member| values.model_type(member, &schema.name)) + .collect() + }; + if u.nullable { + members.push(TypeName::primitive("None")); + } + + let type_alias = format_type_alias(&name, &members); + + let mut file = + FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); + if needs_typing_literal_in_exprs(&u.members) { + file = file.add_import(ImportSpec::named("typing", "Literal")); + } + if let Some(doc) = &schema.description { + file = file.add_raw(&format!("# {}\n", escape_docstring(doc))); + } + file = file + .add_code(type_alias) + .add_import(ImportSpec::named(".", "_union")); + let expr = IrTypeExpr::Named(schema.name.clone()); + for direction in [Direction::From, Direction::To] { + let helper = format!( + "{}_{}", + schema.name.to_snake_case(), + match direction { + Direction::From => "from_dict", + Direction::To => "to_dict", + } + ); + let converted = values.call( + &expr, + sigil_quote!(Python { value }).expect("union helper input builds"), + direction, + ); + let output = match direction { + Direction::From => TypeName::primitive(&name), + Direction::To => TypeName::primitive("object"), + }; + let body = match direction { + Direction::From => { + let converted = values.coerce(&expr, output.clone(), converted); + sigil_quote!(Python { return $C(converted) }) + } + Direction::To => sigil_quote!(Python { return $C(converted) }), + } + .expect("Python union helper body builds"); + file = file.add_function( + FunSpec::builder(&helper) + .add_param(ParameterSpec::of( + "value", + match direction { + Direction::From => TypeName::primitive("object"), + Direction::To => TypeName::primitive(&name), + }, + )) + .returns(vec![output]) + .body(body) + .build() + .expect("Python union helper builds"), + ); + } + file.build().ok() +} + +// --------------------------------------------------------------------------- +// Intersection -> merged @dataclass +// --------------------------------------------------------------------------- + +fn emit_intersection( + schema: &IrSchema, + inter: &IrIntersection, + ir: &IrSpec, + values: &ValuePlan<'_>, +) -> Option { + let mut all_props: indexmap::IndexMap = indexmap::IndexMap::new(); + for member in &inter.members { + if let IrTypeExpr::Named(ref_name) = member + && let Some(s) = ir.schemas.get(ref_name.as_str()) + && let IrSchemaKind::Object(obj) = &s.kind + { + for (k, v) in &obj.properties { + all_props.entry(k.clone()).or_insert_with(|| v.clone()); + } + } + } + + if all_props.is_empty() { + return emit_intersection_as_alias(schema, inter); + } + + emit_intersection_as_dataclass(schema, &all_props, ir, values) +} + +fn emit_intersection_as_alias(schema: &IrSchema, inter: &IrIntersection) -> Option { + let name = schema.name.to_pascal_case(); + let members: Vec = inter.members.iter().map(python_type_name).collect(); + + let type_alias = format_type_alias(&name, &members); + + let mut file = + FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); + if needs_typing_literal_in_exprs(&inter.members) { + file = file.add_import(ImportSpec::named("typing", "Literal")); + } + file = file.add_code(type_alias); + file.build().ok() +} + +fn emit_intersection_as_dataclass( + schema: &IrSchema, + all_props: &indexmap::IndexMap, + ir: &IrSpec, + values: &ValuePlan<'_>, +) -> Option { + let name = schema.name.to_pascal_case(); + + let mut file = + FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); + + if needs_typing_literal_in_props(all_props) { + file = file.add_import(ImportSpec::named("typing", "Literal")); + } + if all_props + .values() + .any(|property| values.has_union(&property.type_expr)) + { + file = file.add_import(ImportSpec::named(".", "_union")); + } + + let dataclass_tn = TypeName::importable("dataclasses", "dataclass"); + let mut cls = TypeSpec::builder(&name, TypeKind::Class) + .annotate(AnnotationSpec::importable(dataclass_tn)); + + if let Some(doc) = &schema.description { + cls = cls.doc(&format!("{}.", escape_docstring(doc))); + } + + let mut required: Vec<(&String, &IrProperty)> = Vec::new(); + let mut optional: Vec<(&String, &IrProperty)> = Vec::new(); + for (json_name, prop) in all_props { + if prop.required && !prop.nullable { + required.push((json_name, prop)); + } else { + optional.push((json_name, prop)); + } + } + + if required.is_empty() && optional.is_empty() { + cls = cls.extra_member(CodeBlock::of("pass", ()).expect("pass")); + } else { + for (_json_name, prop) in &required { + let field_name = python_field_name(&prop.name); + let type_name = values.model_type(&prop.type_expr, &schema.name); + cls = cls.add_field( + FieldSpec::builder(&field_name, type_name) + .build() + .expect("required field"), + ); + } + for (_json_name, prop) in &optional { + let field_name = python_field_name(&prop.name); + let type_name = values.model_type(&prop.type_expr, &schema.name); + cls = cls.add_field( + FieldSpec::builder(&field_name, optional_type(type_name)) + .initializer(CodeBlock::of("None", ()).expect("None init")) + .build() + .expect("optional field"), + ); + } + + let all_fields: Vec<(&String, &IrProperty)> = + required.iter().chain(optional.iter()).copied().collect(); + cls = cls.add_method(build_to_dict_method(&all_fields, ir, all_props, values)); + cls = cls.add_method(build_from_dict_method( + &name, + &all_fields, + ir, + all_props, + values, + &schema.name, + )); + } + + file = file.add_type(cls.build().ok()?); + file.build().ok() +} + +// --------------------------------------------------------------------------- +// TaggedUnion -> type X = A | B | C +// --------------------------------------------------------------------------- + +fn emit_tagged_union( + schema: &IrSchema, + tu: &IrTaggedUnion, + ir: &IrSpec, + values: &ValuePlan<'_>, +) -> Option { + let name = schema.name.to_pascal_case(); + let snake_name = schema.name.to_snake_case(); + + let members: Vec = tu + .variants + .iter() + .map(|v| values.model_type(&v.content_type, &schema.name)) + .collect(); + + let type_alias = format_type_alias(&name, &members); + + let hint = match &tu.tagging { + TaggingStyle::Internal => { + format!("Discriminator: {} (internal).", tu.discriminator_field) + } + TaggingStyle::Adjacent { content_field } => format!( + "Discriminator: {} / content: {} (adjacent).", + tu.discriminator_field, content_field + ), + TaggingStyle::External => "Discriminator: variant key (external).".to_string(), + }; + + let doc = match &schema.description { + Some(desc) => format!("{desc}\n\n{hint}"), + None => hint, + }; + + let mut file = + FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); + let exprs: Vec<&IrTypeExpr> = tu.variants.iter().map(|v| &v.content_type).collect(); + if exprs.iter().any(|e| needs_typing_literal(e)) { + file = file.add_import(ImportSpec::named("typing", "Literal")); + } + let mut doc_block = String::new(); + for line in doc.lines() { + doc_block.push_str(&format!("# {line}\n")); + } + file = file.add_raw(&doc_block); + file = file.add_code(type_alias); + + let expr = IrTypeExpr::Named(schema.name.clone()); + if values.has_union(&expr) { + file = file.add_import(ImportSpec::named(".", "_union")); + for direction in [Direction::From, Direction::To] { + let helper = format!( + "{snake_name}_{}", + match direction { + Direction::From => "from_dict", + Direction::To => "to_dict", + } + ); + let converted = values.call( + &expr, + sigil_quote!(Python { value }).expect("Python tagged payload input builds"), + direction, + ); + let output = match direction { + Direction::From => TypeName::primitive(&name), + Direction::To => type_application( + TypeName::primitive("dict"), + vec![TypeName::primitive("str"), TypeName::primitive("object")], + ), + }; + let converted = match direction { + Direction::From => values.coerce(&expr, output.clone(), converted), + Direction::To => sigil_quote!(Python { $T(TypeName::importable("typing", "cast"))($T(output.clone()), $C(converted)) }).expect("Python tagged output projection builds"), + }; + file = file.add_function( + FunSpec::builder(&helper) + .add_param(ParameterSpec::of("value", TypeName::primitive("object"))) + .returns(vec![output]) + .body( + sigil_quote!(Python { return $C(converted) }) + .expect("Python tagged union payload helper body builds"), + ) + .build() + .expect("Python tagged union payload helper builds"), + ); + } + } else if !tu.variants.is_empty() { + let helpers = build_tagged_union_helpers(&name, &snake_name, tu, ir); + file = file.add_code(helpers); + } + + file.build().ok() +} + +fn build_tagged_union_helpers( + pascal_name: &str, + snake_name: &str, + tu: &IrTaggedUnion, + ir: &IrSpec, +) -> CodeBlock { + let tag_field = &tu.discriminator_field; + + // Only generate helpers for variants that resolve to Object schemas + let resolved_variants: Vec<(&IrTaggedVariant, String)> = tu + .variants + .iter() + .filter_map(|v| { + if let IrTypeExpr::Named(ref_name) = &v.content_type + && is_object_schema(ref_name, ir) + { + return Some((v, ref_name.to_pascal_case())); + } + None + }) + .collect(); + + let mut cb = CodeBlock::builder(); + + if resolved_variants.is_empty() { + return cb.build_unwrap(); + } + + // from_dict + cb.add_line(); + cb.begin_control_flow( + &format!("def {snake_name}_from_dict(data: dict[str, object]) -> {pascal_name}"), + (), + ); + match &tu.tagging { + TaggingStyle::Internal => { + cb.add_statement(&format!("_tag = data[\"{tag_field}\"]"), ()); + for (i, (variant, py_class)) in resolved_variants.iter().enumerate() { + let cond = format!("_tag == \"{}\"", variant.discriminator_value); + emit_elif(&mut cb, i == 0, false, &cond); + cb.add_statement(&format!("return {py_class}.from_dict(data)"), ()); + } + cb.end_control_flow_no_newline(); + } + TaggingStyle::Adjacent { content_field } => { + cb.add_statement(&format!("_tag = data[\"{tag_field}\"]"), ()); + cb.add_statement( + &format!("_content = data[\"{content_field}\"] # type: ignore[assignment]"), + (), + ); + for (i, (variant, py_class)) in resolved_variants.iter().enumerate() { + let cond = format!("_tag == \"{}\"", variant.discriminator_value); + emit_elif(&mut cb, i == 0, false, &cond); + cb.add_statement( + &format!("return {py_class}.from_dict(_content) # type: ignore[arg-type]"), + (), + ); + } + cb.end_control_flow_no_newline(); + } + TaggingStyle::External => { + for (i, (variant, py_class)) in resolved_variants.iter().enumerate() { + let cond = format!("\"{}\" in data", variant.discriminator_value); + emit_elif(&mut cb, i == 0, false, &cond); + cb.add_statement( + &format!( + "return {py_class}.from_dict(data[\"{}\"]) # type: ignore[arg-type]", + variant.discriminator_value + ), + (), + ); + } + cb.end_control_flow_no_newline(); + } + } + cb.add_statement( + "raise ValueError(%V)", + VerbatimStrArg(format!( + "Unknown discriminator value for {pascal_name}: {{data}}" + )), + ); + cb.end_control_flow(); + + // to_dict + cb.add_line(); + cb.begin_control_flow( + &format!("def {snake_name}_to_dict(obj: {pascal_name}) -> dict[str, object]"), + (), + ); + match &tu.tagging { + TaggingStyle::Internal => { + for (i, (variant, py_class)) in resolved_variants.iter().enumerate() { + let cond = format!("isinstance(obj, {py_class})"); + emit_elif(&mut cb, i == 0, false, &cond); + cb.add_statement("result = obj.to_dict()", ()); + cb.add_statement( + &format!( + "result[\"{tag_field}\"] = \"{}\"", + variant.discriminator_value + ), + (), + ); + cb.add_statement("return result", ()); + } + cb.end_control_flow_no_newline(); + } + TaggingStyle::Adjacent { content_field } => { + for (i, (variant, py_class)) in resolved_variants.iter().enumerate() { + let cond = format!("isinstance(obj, {py_class})"); + emit_elif(&mut cb, i == 0, false, &cond); + cb.add_statement( + &format!( + "return {{\"{tag_field}\": \"{}\", \"{content_field}\": obj.to_dict()}}", + variant.discriminator_value + ), + (), + ); + } + cb.end_control_flow_no_newline(); + } + TaggingStyle::External => { + for (i, (variant, py_class)) in resolved_variants.iter().enumerate() { + let cond = format!("isinstance(obj, {py_class})"); + emit_elif(&mut cb, i == 0, false, &cond); + cb.add_statement( + &format!( + "return {{\"{}\": obj.to_dict()}}", + variant.discriminator_value + ), + (), + ); + } + cb.end_control_flow_no_newline(); + } + } + cb.add_statement( + "raise ValueError(%V)", + VerbatimStrArg(format!("Unknown variant for {pascal_name}: {{type(obj)}}")), + ); + cb.end_control_flow_no_newline(); + + cb.build_unwrap() +} + +fn emit_elif( + cb: &mut sigil_stitch::code_block::CodeBlockBuilder, + is_first: bool, + is_last: bool, + cond: &str, +) { + if !is_first { + cb.end_control_flow_no_newline(); + } + if is_last && !is_first { + cb.begin_control_flow("else", ()); + } else { + let kw = if is_first { "if" } else { "elif" }; + cb.begin_control_flow(&format!("{kw} {cond}"), ()); + } +} + +// --------------------------------------------------------------------------- +// Type mapping +// --------------------------------------------------------------------------- + +/// Like `python_type_name` but Named types import from `..models.{snake}` (for API files). +pub fn api_type_name(expr: &IrTypeExpr) -> TypeName { + match expr.representation() { + IrTypeExpr::Named(name) => { + let py_name = name.to_pascal_case(); + let module = format!("..models.{}", name.to_snake_case()); + TypeName::importable(&module, &py_name) + } + IrTypeExpr::Array(inner) => { + type_application(TypeName::primitive("list"), vec![api_type_name(inner)]) + } + IrTypeExpr::Map(inner) => type_application( + TypeName::primitive("dict"), + vec![TypeName::primitive("str"), api_type_name(inner)], + ), + IrTypeExpr::Union(members) => { + if members.is_empty() { + TypeName::importable("typing", "Any") + } else { + TypeName::union(members.iter().map(api_type_name).collect()) + } + } + IrTypeExpr::Nullable(inner) => TypeName::optional(api_type_name(inner)), + _ => python_type_name(expr), + } +} + +/// Map an IR type expression to a Python type string (for serialization helpers). +pub fn python_type_str(expr: &IrTypeExpr) -> String { + match expr { + IrTypeExpr::ScalarEnum { representation, .. } => python_type_str(representation), + IrTypeExpr::Named(name) => name.to_pascal_case(), + IrTypeExpr::Primitive(p) => python_primitive(p).to_string(), + IrTypeExpr::StringLiteral(s) => { + format!("Literal[\"{}\"]", escape_python_string(s)) + } + IrTypeExpr::StringEnum(values) => { + let members: Vec = values + .iter() + .map(|v| format!("\"{}\"", escape_python_string(v))) + .collect(); + format!("Literal[{}]", members.join(", ")) + } + IrTypeExpr::Array(inner) => { + let inner_ty = python_type_str(inner); + format!("list[{inner_ty}]") + } + IrTypeExpr::Map(inner) => { + let inner_ty = python_type_str(inner); + format!("dict[str, {inner_ty}]") + } + IrTypeExpr::Union(members) => { + let parts: Vec = members.iter().map(python_type_str).collect(); + if parts.is_empty() { + "Any".to_string() + } else { + parts.join(" | ") + } + } + IrTypeExpr::Nullable(inner) => { + let inner_ty = python_type_str(inner); + format!("{inner_ty} | None") + } + IrTypeExpr::Any => "Any".to_string(), + } +} + +fn python_primitive(p: &IrPrimitive) -> &'static str { + match p { + IrPrimitive::String | IrPrimitive::StringWithFormat(_) => "str", + IrPrimitive::Integer | IrPrimitive::IntegerWithFormat(_) => "int", + IrPrimitive::Number | IrPrimitive::NumberWithFormat(_) => "float", + IrPrimitive::Boolean => "bool", + IrPrimitive::Binary => "bytes", + IrPrimitive::Date => "datetime.date", + IrPrimitive::DateTime => "datetime.datetime", + IrPrimitive::Uuid => "uuid.UUID", + } +} + +fn collect_request_input_imports( + expr: &IrTypeExpr, + imports: &mut std::collections::BTreeSet, +) { + match expr.representation() { + IrTypeExpr::Named(name) => { + let py_name = name.to_pascal_case(); + let module = name.to_snake_case(); + imports.insert(format!("from .{module} import {py_name}")); + } + IrTypeExpr::Primitive(IrPrimitive::Date | IrPrimitive::DateTime) => { + imports.insert("import datetime".to_string()); + } + IrTypeExpr::Primitive(IrPrimitive::Uuid) => { + imports.insert("import uuid".to_string()); + } + IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => { + imports.insert("from typing import Literal".to_string()); + } + IrTypeExpr::Union(members) => { + if members.is_empty() { + imports.insert("from typing import Any".to_string()); + } + for member in members { + collect_request_input_imports(member, imports); + } + } + IrTypeExpr::Any => { + imports.insert("from typing import Any".to_string()); + } + IrTypeExpr::Array(inner) | IrTypeExpr::Map(inner) | IrTypeExpr::Nullable(inner) => { + collect_request_input_imports(inner, imports); + } + _ => {} + } +} + +fn needs_typing_literal(expr: &IrTypeExpr) -> bool { + match expr.representation() { + IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => true, + IrTypeExpr::Array(inner) | IrTypeExpr::Map(inner) | IrTypeExpr::Nullable(inner) => { + needs_typing_literal(inner) + } + IrTypeExpr::Union(members) => members.iter().any(needs_typing_literal), + _ => false, + } +} + +fn needs_typing_literal_in_props(props: &indexmap::IndexMap) -> bool { + props.values().any(|p| needs_typing_literal(&p.type_expr)) +} + +fn needs_typing_literal_in_exprs(exprs: &[IrTypeExpr]) -> bool { + exprs.iter().any(needs_typing_literal) +} + +// --------------------------------------------------------------------------- +// Serialization helpers +// --------------------------------------------------------------------------- + +fn render_to_dict_expr( + value_expr: &str, + json_name: &str, + ir: &IrSpec, + properties: &indexmap::IndexMap, +) -> String { + let prop = properties.get(json_name); + let type_expr = prop.map(|p| &p.type_expr); + match type_expr { + Some(IrTypeExpr::Named(ref_name)) => { + if is_object_schema(ref_name, ir) { + format!("{value_expr}.to_dict()") + } else if is_tagged_union_schema(ref_name, ir) { + let snake = ref_name.to_snake_case(); + format!("{snake}_to_dict({value_expr})") + } else { + value_expr.to_string() + } + } + Some(IrTypeExpr::Array(inner)) => { + if let IrTypeExpr::Named(ref_name) = inner.as_ref() + && is_object_schema(ref_name, ir) + { + return format!("[item.to_dict() for item in {value_expr}]"); + } + if let IrTypeExpr::Named(ref_name) = inner.as_ref() + && is_tagged_union_schema(ref_name, ir) + { + let snake = ref_name.to_snake_case(); + return format!("[{snake}_to_dict(item) for item in {value_expr}]"); + } + value_expr.to_string() + } + Some(IrTypeExpr::Nullable(inner)) => { + if let IrTypeExpr::Named(ref_name) = inner.as_ref() + && is_object_schema(ref_name, ir) + { + return format!("{value_expr}.to_dict() if {value_expr} is not None else None"); + } + if let IrTypeExpr::Named(ref_name) = inner.as_ref() + && is_tagged_union_schema(ref_name, ir) + { + let snake = ref_name.to_snake_case(); + return format!( + "{snake}_to_dict({value_expr}) if {value_expr} is not None else None" + ); + } + value_expr.to_string() + } + Some(IrTypeExpr::Map(inner)) => { + if let IrTypeExpr::Named(ref_name) = inner.as_ref() + && is_object_schema(ref_name, ir) + { + return format!("{{k: v.to_dict() for k, v in {value_expr}.items()}}"); + } + if let IrTypeExpr::Named(ref_name) = inner.as_ref() + && is_tagged_union_schema(ref_name, ir) + { + let snake = ref_name.to_snake_case(); + return format!("{{k: {snake}_to_dict(v) for k, v in {value_expr}.items()}}"); + } + value_expr.to_string() + } + _ => value_expr.to_string(), + } +} + +fn render_from_dict_expr( + json_name: &str, + ir: &IrSpec, + properties: &indexmap::IndexMap, +) -> String { + let prop = properties.get(json_name); + let type_expr = prop.map(|p| &p.type_expr); + let accessor = format!("data[\"{json_name}\"]"); + match type_expr { + Some(IrTypeExpr::Named(ref_name)) => { + if is_object_schema(ref_name, ir) { + let py_name = ref_name.to_pascal_case(); + format!("{py_name}.from_dict({accessor}) # type: ignore[arg-type]") + } else if is_tagged_union_schema(ref_name, ir) { + let snake = ref_name.to_snake_case(); + format!("{snake}_from_dict({accessor}) # type: ignore[arg-type]") + } else { + format!("{accessor} # type: ignore[assignment]") + } + } + Some(IrTypeExpr::Array(inner)) => { + if let IrTypeExpr::Named(ref_name) = inner.as_ref() + && is_object_schema(ref_name, ir) + { + let py_name = ref_name.to_pascal_case(); + return format!( + "[{py_name}.from_dict(item) for item in {accessor}] # type: ignore[union-attr]" + ); + } + if let IrTypeExpr::Named(ref_name) = inner.as_ref() + && is_tagged_union_schema(ref_name, ir) + { + let snake = ref_name.to_snake_case(); + return format!( + "[{snake}_from_dict(item) for item in {accessor}] # type: ignore[union-attr]" + ); + } + format!("{accessor} # type: ignore[assignment]") + } + Some(IrTypeExpr::Map(inner)) => { + if let IrTypeExpr::Named(ref_name) = inner.as_ref() + && is_object_schema(ref_name, ir) + { + let py_name = ref_name.to_pascal_case(); + return format!( + "{{k: {py_name}.from_dict(v) for k, v in {accessor}.items()}} # type: ignore[union-attr]" + ); + } + if let IrTypeExpr::Named(ref_name) = inner.as_ref() + && is_tagged_union_schema(ref_name, ir) + { + let snake = ref_name.to_snake_case(); + return format!( + "{{k: {snake}_from_dict(v) for k, v in {accessor}.items()}} # type: ignore[union-attr]" + ); + } + format!("{accessor} # type: ignore[assignment]") + } + _ => format!("{accessor} # type: ignore[assignment]"), + } +} + +fn render_from_dict_optional_expr( + json_name: &str, + ir: &IrSpec, + properties: &indexmap::IndexMap, +) -> String { + let prop = properties.get(json_name); + let type_expr = prop.map(|p| &p.type_expr); + let raw_type = type_expr.map(|t| match t { + IrTypeExpr::Nullable(inner) => inner.as_ref(), + _ => t, + }); + let accessor = format!("data.get(\"{json_name}\")"); + match raw_type { + Some(IrTypeExpr::Named(ref_name)) => { + if is_object_schema(ref_name, ir) { + let py_name = ref_name.to_pascal_case(); + format!( + "{py_name}.from_dict({accessor}) if {accessor} is not None else None # type: ignore[arg-type]" + ) + } else if is_tagged_union_schema(ref_name, ir) { + let snake = ref_name.to_snake_case(); + format!( + "{snake}_from_dict({accessor}) if {accessor} is not None else None # type: ignore[arg-type]" + ) + } else { + format!("{accessor} # type: ignore[assignment]") + } + } + Some(IrTypeExpr::Array(inner)) => { + if let IrTypeExpr::Named(ref_name) = inner.as_ref() + && is_object_schema(ref_name, ir) + { + let py_name = ref_name.to_pascal_case(); + return format!( + "[{py_name}.from_dict(item) for item in {accessor}] if {accessor} is not None else None # type: ignore[union-attr]" + ); + } + if let IrTypeExpr::Named(ref_name) = inner.as_ref() + && is_tagged_union_schema(ref_name, ir) + { + let snake = ref_name.to_snake_case(); + return format!( + "[{snake}_from_dict(item) for item in {accessor}] if {accessor} is not None else None # type: ignore[union-attr]" + ); + } + format!("{accessor} # type: ignore[assignment]") + } + Some(IrTypeExpr::Map(inner)) => { + if let IrTypeExpr::Named(ref_name) = inner.as_ref() + && is_object_schema(ref_name, ir) + { + let py_name = ref_name.to_pascal_case(); + return format!( + "{{k: {py_name}.from_dict(v) for k, v in {accessor}.items()}} if {accessor} is not None else None # type: ignore[union-attr]" + ); + } + if let IrTypeExpr::Named(ref_name) = inner.as_ref() + && is_tagged_union_schema(ref_name, ir) + { + let snake = ref_name.to_snake_case(); + return format!( + "{{k: {snake}_from_dict(v) for k, v in {accessor}.items()}} if {accessor} is not None else None # type: ignore[union-attr]" + ); + } + format!("{accessor} # type: ignore[assignment]") + } + _ => format!("{accessor} # type: ignore[assignment]"), + } +} + +pub fn is_object_schema(name: &str, ir: &IrSpec) -> bool { + ir.schemas.get(name).is_some_and(|s| match &s.kind { + IrSchemaKind::Object(_) => true, + IrSchemaKind::Intersection(inter) => inter.members.iter().any(|m| { + if let IrTypeExpr::Named(ref_name) = m { + ir.schemas + .get(ref_name.as_str()) + .is_some_and(|ms| matches!(ms.kind, IrSchemaKind::Object(_))) + } else { + false + } + }), + _ => false, + }) +} + +pub fn is_tagged_union_schema(name: &str, ir: &IrSpec) -> bool { + ir.schemas + .get(name) + .is_some_and(|s| matches!(s.kind, IrSchemaKind::TaggedUnion(_))) +} + +/// Collect all tagged-union schema names referenced (directly or nested) in a type expression. +fn collect_tagged_union_refs(expr: &IrTypeExpr, ir: &IrSpec) -> Vec { + let mut refs = Vec::new(); + match expr { + IrTypeExpr::Named(name) => { + if is_tagged_union_schema(name, ir) { + refs.push(name.clone()); + } + } + IrTypeExpr::Array(inner) | IrTypeExpr::Nullable(inner) => { + refs.extend(collect_tagged_union_refs(inner, ir)); + } + IrTypeExpr::Map(inner) => { + refs.extend(collect_tagged_union_refs(inner, ir)); + } + _ => {} + } + refs +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +pub fn python_field_name(name: &str) -> String { + let snake = name.to_snake_case(); + if snake.is_empty() { + return "field_".to_string(); + } + match snake.as_str() { + "and" | "as" | "assert" | "async" | "await" | "break" | "class" | "continue" | "def" + | "del" | "elif" | "else" | "except" | "finally" | "for" | "from" | "global" | "if" + | "import" | "in" | "is" | "lambda" | "nonlocal" | "not" | "or" | "pass" | "raise" + | "return" | "try" | "while" | "with" | "yield" | "type" => { + format!("{snake}_") + } + _ => snake, + } +} + +fn python_enum_member_name(value: &str) -> String { + let upper = value + .to_uppercase() + .replace(|c: char| !c.is_alphanumeric(), "_"); + if upper.is_empty() { + return "EMPTY".to_string(); + } + if upper.starts_with(|c: char| c.is_ascii_digit()) { + return format!("N{upper}"); + } + upper +} + +fn escape_python_string(s: &str) -> String { + s.replace('\\', "\\\\").replace('"', "\\\"") +} + +fn escape_docstring(s: &str) -> String { + s.replace("\"\"\"", "\\\"\\\"\\\"") + .lines() + .next() + .unwrap_or("") + .to_string() +} diff --git a/src/generators/python/httpx/codegen.rs b/src/generators/python/httpx/codegen.rs index f024fd8ab..f471cd37c 100644 --- a/src/generators/python/httpx/codegen.rs +++ b/src/generators/python/httpx/codegen.rs @@ -53,13 +53,19 @@ impl PythonHttpxCodeGenerator { let mut files = Vec::new(); - files.extend(emit_models::generate_model_files( + files.extend(emit_models::generate_model_files_with_strategy( ir, &header, &request_inputs, + self.config.untagged_union_strategy, )?); - files.extend(emit_api::generate_api_files(ir, &header, &request_inputs)); + files.extend(emit_api::generate_api_files_with_strategy( + ir, + &header, + &request_inputs, + self.config.untagged_union_strategy, + )); files.extend(runtime_files(&header, request_inputs.has_uploads())); diff --git a/src/generators/python/httpx/config.rs b/src/generators/python/httpx/config.rs index d0b5e9ef7..a97f7e30e 100644 --- a/src/generators/python/httpx/config.rs +++ b/src/generators/python/httpx/config.rs @@ -1,5 +1,6 @@ //! Python httpx generator-specific configuration. +pub use crate::generators::python::value::UntaggedUnionStrategy; use serde::{Deserialize, Serialize}; use tracing::error; @@ -7,6 +8,9 @@ use tracing::error; pub struct PythonHttpxConfig { #[serde(default)] pub package_name: Option, + /// Select and convert structural members, reject ambiguity, or keep raw values. + #[serde(default)] + pub untagged_union_strategy: UntaggedUnionStrategy, } impl From for PythonHttpxConfig { diff --git a/src/generators/python/httpx/emit_api.rs b/src/generators/python/httpx/emit_api.rs index 2f0b40054..5be7a0067 100644 --- a/src/generators/python/httpx/emit_api.rs +++ b/src/generators/python/httpx/emit_api.rs @@ -26,8 +26,12 @@ use sigil_stitch::spec::fun_spec::FunSpecBuilder; use super::emit_models::{ api_type_name, future_annotations_header, is_object_schema, python_field_name, }; +use crate::generators::python::emit_api_values::{ + MultipartPart, multipart_data, union_error_body_property, +}; use crate::generators::python::operation_names::plan_python_operation_names; use crate::generators::python::response_headers::build_header_accessors; +use crate::generators::python::value::{Direction, UntaggedUnionStrategy, ValuePlan}; use crate::generators::type_application; /// Generate every API file from the IR. @@ -36,12 +40,28 @@ pub fn generate_api_files( header: &str, request_inputs: &RequestInputPlan, ) -> Vec { + generate_api_files_with_strategy( + ir, + header, + request_inputs, + UntaggedUnionStrategy::FirstMatch, + ) +} + +/// Generate API files with the same union policy used by the model emitter. +pub fn generate_api_files_with_strategy( + ir: &IrSpec, + header: &str, + request_inputs: &RequestInputPlan, + strategy: UntaggedUnionStrategy, +) -> Vec { + let values = ValuePlan::new(ir, strategy); let by_tag = group_by_tag(&ir.operations); let mut files = Vec::with_capacity(by_tag.len()); for (tag, ops) in &by_tag { let stem = tag.to_snake_case(); let filename = format!("{stem}_api.py"); - let body = emit_api_file(tag, ops, ir, header, request_inputs); + let body = emit_api_file(tag, ops, ir, header, request_inputs, &values); files.push(FileInfo::api(filename, body)); } files @@ -68,6 +88,7 @@ fn emit_api_file( ir: &IrSpec, header: &str, request_inputs: &RequestInputPlan, + values: &ValuePlan<'_>, ) -> String { let class_name = format!("{}Api", tag.to_pascal_case()); let mut plans: Vec = ops @@ -97,19 +118,24 @@ fn emit_api_file( let mut cls = TypeSpec::builder(&class_name, TypeKind::Class).add_method(init); for plan in &plans { - cls = cls.add_method(build_api_method_with_http_info(plan, ir, &error_type)); - cls = cls.add_method(build_api_method(plan, ir, &error_type)); + cls = cls.add_method(build_api_method_with_http_info( + plan, + ir, + &error_type, + values, + )); + cls = cls.add_method(build_api_method(plan, ir, &error_type, values)); } let mut fb = FileSpec::builder_with(&format!("{}_api.py", tag.to_snake_case()), Python::new()) .header(future_annotations_header()); for plan in &plans { if !plan.success_headers.is_empty() { - fb = fb.add_type(build_response_class(plan)); + fb = fb.add_type(build_response_class(plan, values)); } } fb = fb - .add_code(build_error_classes_block(&plans, ir)) + .add_code(build_error_classes_block(&plans, ir, values)) .add_type(cls.build().expect("API TypeSpec builds")); if plans .iter() @@ -118,6 +144,26 @@ fn emit_api_file( { fb = fb.add_import(ImportSpec::side_effect("json")); } + if plans.iter().any(|plan| { + plan.body.as_ref().is_some_and(|body| { + body.multipart_parts + .as_ref() + .is_some_and(|parts| parts.iter().any(|part| values.has_union(&part.type_expr))) + }) + }) || ops.iter().any(|op| { + op.request_body + .iter() + .flat_map(|body| body.content.values()) + .chain( + op.responses + .iter() + .flat_map(|response| response.content.values()), + ) + .any(|expr| values.has_union(expr)) + }) { + fb = fb.add_import(ImportSpec::named("..models", "_union")); + fb = fb.add_import(ImportSpec::side_effect("json")); + } if plans.iter().any(|plan| { plan.body.as_ref().is_some_and(|body| { body.multipart_parts.as_ref().is_some_and(|parts| { @@ -159,10 +205,15 @@ fn emit_api_file( content } -fn build_api_method(plan: &OpPlan<'_>, ir: &IrSpec, error_type: &TypeName) -> FunSpec { +fn build_api_method( + plan: &OpPlan<'_>, + ir: &IrSpec, + error_type: &TypeName, + values: &ValuePlan<'_>, +) -> FunSpec { let mut fun = FunSpec::builder(&plan.method_name); - fun = add_api_method_params(fun, plan); - fun = fun.returns(vec![response_payload_type(plan)]); + fun = add_api_method_params(fun, plan, values); + fun = fun.returns(vec![response_payload_type(plan, values)]); if let Some(summary) = &plan.op.summary { fun = fun.doc(&format!("{summary}.")); @@ -201,27 +252,32 @@ fn build_api_method_with_http_info( plan: &OpPlan<'_>, ir: &IrSpec, error_type: &TypeName, + values: &ValuePlan<'_>, ) -> FunSpec { let mut fun = FunSpec::builder(&plan.with_http_info_method_name); - fun = add_api_method_params(fun, plan); - fun = fun.returns(vec![response_metadata_type(plan)]); + fun = add_api_method_params(fun, plan, values); + fun = fun.returns(vec![response_metadata_type(plan, values)]); if let Some(summary) = &plan.op.summary { fun = fun.doc(&format!("{summary}, including HTTP response metadata.")); } - fun = fun.body(build_method_body(plan, ir, error_type)); + fun = fun.body(build_method_body(plan, ir, error_type, values)); fun.build() .expect("API with-http-info method FunSpec builds") } -fn add_api_method_params(mut fun: FunSpecBuilder, plan: &OpPlan<'_>) -> FunSpecBuilder { +fn add_api_method_params( + mut fun: FunSpecBuilder, + plan: &OpPlan<'_>, + values: &ValuePlan<'_>, +) -> FunSpecBuilder { fun = fun.add_param(ParameterSpec::of("self", TypeName::primitive(""))); for p in &plan.path_params { fun = fun.add_param(ParameterSpec::of( &p.var_name, - api_type_name(&p.param.type_expr), + values.api_type(&p.param.type_expr), )); } @@ -237,14 +293,14 @@ fn add_api_method_params(mut fun: FunSpecBuilder, plan: &OpPlan<'_>) -> FunSpecB if p.param.required { fun = fun.add_param(ParameterSpec::of( &p.var_name, - api_type_name(&p.param.type_expr), + values.api_type(&p.param.type_expr), )); } } // Body param if let Some(b) = &plan.body { - let ty = api_type_name(&b.type_expr); + let ty = values.api_type(&b.type_expr); if b.required { fun = fun.add_param(ParameterSpec::of(&b.var_name, ty)); } else { @@ -260,7 +316,7 @@ fn add_api_method_params(mut fun: FunSpecBuilder, plan: &OpPlan<'_>) -> FunSpecB // Optional query/header params last for p in plan.query_params.iter().chain(&plan.header_params) { if !p.param.required { - let param_ty = api_type_name(&p.param.type_expr); + let param_ty = values.api_type(&p.param.type_expr); let param_ty = if is_already_optional(&p.param.type_expr) { param_ty } else { @@ -278,26 +334,31 @@ fn add_api_method_params(mut fun: FunSpecBuilder, plan: &OpPlan<'_>) -> FunSpecB fun } -fn response_payload_type(plan: &OpPlan<'_>) -> TypeName { +fn response_payload_type(plan: &OpPlan<'_>, values: &ValuePlan<'_>) -> TypeName { if plan.typed_responses.is_empty() { TypeName::primitive("None") } else { - response_type_name(&plan.typed_responses[0]) + response_type_name(&plan.typed_responses[0], values) } } -fn response_metadata_type(plan: &OpPlan<'_>) -> TypeName { +fn response_metadata_type(plan: &OpPlan<'_>, values: &ValuePlan<'_>) -> TypeName { if plan.success_headers.is_empty() { type_application( TypeName::importable("..runtime.client", "ApiResponse"), - vec![response_payload_type(plan)], + vec![response_payload_type(plan, values)], ) } else { TypeName::primitive(&plan.response_type) } } -fn build_method_body(plan: &OpPlan<'_>, ir: &IrSpec, _error_type: &TypeName) -> CodeBlock { +fn build_method_body( + plan: &OpPlan<'_>, + ir: &IrSpec, + _error_type: &TypeName, + values: &ValuePlan<'_>, +) -> CodeBlock { let mut cb = CodeBlock::builder(); // Path interpolation @@ -359,7 +420,15 @@ fn build_method_body(plan: &OpPlan<'_>, ir: &IrSpec, _error_type: &TypeName) -> // Body serialization let body_expr = if let Some(b) = &plan.body { - if is_object_type(&b.type_expr, ir) { + if values.has_union(&b.type_expr) { + let access = sigil_quote!(Python { $N(b.var_name.as_str()) }) + .expect("Python HTTP union input builds"); + let converted = values.call(&b.type_expr, access, Direction::To); + cb.add_code(sigil_quote!(Python { + _union_body = $if(b.required) { $C(converted) } $else { None if $N(b.var_name.as_str()) is None else $C(converted) } + }).expect("Python HTTP union request conversion builds")); + "_union_body".to_string() + } else if is_object_type(&b.type_expr, ir) { if b.required { format!("{}.to_dict()", b.var_name) } else { @@ -389,13 +458,20 @@ fn build_method_body(plan: &OpPlan<'_>, ir: &IrSpec, _error_type: &TypeName) -> format!("\"{}\"", plan.op.method.to_uppercase()), "path".to_string(), ]; + let mut union_json_argument = None; if has_query { request_args.push("params=params".to_string()); } if let Some(body) = &plan.body { if media_type_base(&body.media_type) == "multipart/form-data" { if let Some(parts) = &body.multipart_parts { - emit_multipart_data(&mut cb, body, parts, ir); + cb.add_code(multipart_data( + &body.var_name, + body.required, + parts, + ir, + values, + )); request_args.push("files=files if files else None".to_string()); } else { cb.add_statement( @@ -405,6 +481,11 @@ fn build_method_body(plan: &OpPlan<'_>, ir: &IrSpec, _error_type: &TypeName) -> } } else { match body.encoding { + BodyEncoding::Json if values.has_union(&body.type_expr) => { + union_json_argument = Some(sigil_quote!(Python { + content = $if(body.required) { json.dumps(_union_body, allow_nan = False) } $else { None if $N(body.var_name.as_str()) is None else json.dumps(_union_body, allow_nan = False) } + }).expect("Python httpx union JSON argument builds")); + } BodyEncoding::Json => request_args.push(format!("json={body_expr}")), BodyEncoding::FormUrlEncoded => request_args.push(format!("data={body_expr}")), BodyEncoding::TextPlain | BodyEncoding::OctetStream => { @@ -438,17 +519,39 @@ fn build_method_body(plan: &OpPlan<'_>, ir: &IrSpec, _error_type: &TypeName) -> request_args.push("headers=headers".to_string()); } - cb.add_code( + let request_call = if let Some(argument) = union_json_argument { + sigil_quote!(Python { + response = self._client.request($for(arg in &request_args; separator = ", ") { $L(arg.as_str()) }, $C(argument)) + }) + } else { sigil_quote!(Python { response = self._client.request($for(arg in &request_args; separator = ", ") { $L(arg.as_str()) }) }) - .expect("request call"), - ); + }.expect("Python HTTP request call builds"); + cb.add_code(request_call); // Error handling cb.add_code(emit_error_raise(plan, "response.reason_phrase")); - let data_expr = if !plan.typed_responses.is_empty() { + let union_response = plan.typed_responses.first().filter(|response| { + response.decoding == ResponseDecoding::Json && values.has_union(&response.type_expr) + }); + if let Some(response) = union_response { + let converted = values.call( + &response.type_expr, + sigil_quote!(Python { response.json() }).expect("Python HTTP union JSON input builds"), + Direction::From, + ); + let ty = values.api_type(&response.type_expr); + let converted = values.coerce(&response.type_expr, ty, converted); + cb.add_code( + sigil_quote!(Python { _union_data = $C(converted) }) + .expect("Python HTTP union response conversion builds"), + ); + } + let data_expr = if union_response.is_some() { + "_union_data".to_string() + } else if !plan.typed_responses.is_empty() { let tr = &plan.typed_responses[0]; render_response_parse(tr, ir) } else { @@ -470,87 +573,6 @@ fn build_method_body(plan: &OpPlan<'_>, ir: &IrSpec, _error_type: &TypeName) -> cb.build().expect("API method body builds") } -fn emit_multipart_data( - cb: &mut sigil_stitch::code_block::CodeBlockBuilder, - body: &BodyBinding, - parts: &[MultipartPart], - ir: &IrSpec, -) { - cb.add_statement("files: dict[str, object] = {}", ()); - if !body.required { - cb.add_statement(&format!("if {} is not None:%>", body.var_name), ()); - } - for part in parts { - let access = format!("{}.{}", body.var_name, part.field_name); - if part.required { - emit_required_multipart_part(cb, part, &access, ir); - } else { - cb.add_statement(&format!("if {access} is not None:%>"), ()); - emit_required_multipart_part(cb, part, &access, ir); - cb.add_statement("%<", ()); - } - } - if !body.required { - cb.add_statement("%<", ()); - } -} - -fn emit_required_multipart_part( - cb: &mut sigil_stitch::code_block::CodeBlockBuilder, - part: &MultipartPart, - access: &str, - ir: &IrSpec, -) { - cb.add_code(multipart_part_assignment(part, access, ir)); -} - -fn multipart_part_assignment(part: &MultipartPart, access: &str, ir: &IrSpec) -> CodeBlock { - let binary_stmt = format!( - "files[\"{}\"] = ({}.filename_or_default(\"{}\"), {}.data, \"{}\")", - part.wire_name, access, part.wire_name, access, part.content_type - ); - let json_value = render_multipart_json_value(access, &part.type_expr, ir); - let json_stmt = format!( - "files[\"{}\"] = (None, json.dumps({json_value}), \"{}\")", - part.wire_name, part.content_type - ); - let unsupported_stmt = "raise ValueError(\"unsupported multipart part content type\")"; - let scalar_stmt = format!( - "files[\"{}\"] = (None, str({access}), \"{}\")", - part.wire_name, part.content_type - ); - - sigil_quote!(Python { - $if(part.is_binary) { - $L(binary_stmt.as_str()) - } $else_if(part.value_encoding == MultipartValueEncoding::Json) { - $L(json_stmt.as_str()) - } $else_if(part.value_encoding == MultipartValueEncoding::Unsupported) { - $L(unsupported_stmt) - } $else { - $L(scalar_stmt.as_str()) - } - }) - .expect("multipart part assignment builds") -} - -fn render_multipart_json_value(access: &str, expr: &IrTypeExpr, ir: &IrSpec) -> String { - match expr { - IrTypeExpr::Named(name) if is_object_schema(name, ir) => format!("{access}.to_dict()"), - IrTypeExpr::Nullable(inner) => render_multipart_json_value(access, inner, ir), - IrTypeExpr::Array(inner) => { - if let IrTypeExpr::Named(name) = inner.as_ref() - && is_object_schema(name, ir) - { - format!("[item.to_dict() for item in {access}]") - } else { - access.to_string() - } - } - _ => access.to_string(), - } -} - fn render_stringify(var: &str, type_expr: &IrTypeExpr) -> String { match type_expr.representation() { IrTypeExpr::Primitive( @@ -576,9 +598,9 @@ fn render_stringify(var: &str, type_expr: &IrTypeExpr) -> String { } } -fn response_type_name(response: &TypedResponse) -> TypeName { +fn response_type_name(response: &TypedResponse, values: &ValuePlan<'_>) -> TypeName { match response.decoding { - ResponseDecoding::Json => api_type_name(&response.type_expr), + ResponseDecoding::Json => values.api_type(&response.type_expr), ResponseDecoding::Text => TypeName::primitive("str"), ResponseDecoding::Bytes => TypeName::primitive("bytes"), } @@ -618,7 +640,11 @@ fn render_json_response_parse(type_expr: &IrTypeExpr, ir: &IrSpec) -> String { } } -fn build_error_classes_block(plans: &[OpPlan<'_>], ir: &IrSpec) -> CodeBlock { +fn build_error_classes_block( + plans: &[OpPlan<'_>], + ir: &IrSpec, + values: &ValuePlan<'_>, +) -> CodeBlock { let mut cb = CodeBlock::builder(); for plan in plans { let mut seen = HashSet::new(); @@ -639,12 +665,12 @@ fn build_error_classes_block(plans: &[OpPlan<'_>], ir: &IrSpec) -> CodeBlock { (TypeName::importable("collections.abc", "Mapping"),), ); if let (Some(type_expr), Some(decoding)) = (&response.type_expr, response.decoding) { - let return_ty = error_body_return_type(type_expr, decoding); + let return_ty = error_body_return_type(type_expr, decoding, values); cb.add_statement("self.raw_body: bytes = raw_body", ()); cb.add_statement("self._body_loaded: bool = False", ()); cb.add_statement("self._body_value: %T | None = None", (return_ty,)); cb.add_statement("self._body_error: Exception | None = None%<", ()); - cb.add_code(error_body_property(type_expr, decoding, ir)); + cb.add_code(error_body_property(type_expr, decoding, ir, values)); } else { cb.add_statement("self.raw_body: bytes = raw_body%<", ()); } @@ -711,11 +737,11 @@ fn build_error_classes_block(plans: &[OpPlan<'_>], ir: &IrSpec) -> CodeBlock { cb.build().expect("Python error classes block builds") } -fn build_response_class(plan: &OpPlan<'_>) -> TypeSpec { +fn build_response_class(plan: &OpPlan<'_>, values: &ValuePlan<'_>) -> TypeSpec { let mut response = TypeSpec::builder(&plan.response_type, TypeKind::Class).extends(type_application( TypeName::importable("..runtime.client", "ApiResponse"), - vec![response_payload_type(plan)], + vec![response_payload_type(plan, values)], )); for accessor in build_header_accessors(&plan.success_headers, false) { response = response.add_method(accessor); @@ -727,7 +753,11 @@ fn error_body_property( type_expr: &IrTypeExpr, decoding: ResponseDecoding, ir: &IrSpec, + values: &ValuePlan<'_>, ) -> CodeBlock { + if decoding == ResponseDecoding::Json && values.has_union(type_expr) { + return union_error_body_property(type_expr, ir, values); + } let mut cb = CodeBlock::builder(); match decoding { ResponseDecoding::Json => { @@ -776,9 +806,13 @@ fn error_body_property( cb.build().expect("Python error body property builds") } -fn error_body_return_type(type_expr: &IrTypeExpr, decoding: ResponseDecoding) -> TypeName { +fn error_body_return_type( + type_expr: &IrTypeExpr, + decoding: ResponseDecoding, + values: &ValuePlan<'_>, +) -> TypeName { match decoding { - ResponseDecoding::Json => api_type_name(type_expr), + ResponseDecoding::Json => values.api_type(type_expr), ResponseDecoding::Text => TypeName::primitive("str"), ResponseDecoding::Bytes => TypeName::primitive("bytes"), } @@ -959,16 +993,6 @@ struct BodyBinding { multipart_parts: Option>, } -struct MultipartPart { - wire_name: String, - field_name: String, - type_expr: IrTypeExpr, - is_binary: bool, - required: bool, - content_type: String, - value_encoding: MultipartValueEncoding, -} - #[derive(Clone, Copy, PartialEq, Eq)] enum BodyEncoding { Json, diff --git a/src/generators/python/httpx/emit_models.rs b/src/generators/python/httpx/emit_models.rs index 9e1a70c05..3360e84a5 100644 --- a/src/generators/python/httpx/emit_models.rs +++ b/src/generators/python/httpx/emit_models.rs @@ -1,1173 +1 @@ -//! Model emission for IR schemas (Python dataclasses/enums/aliases). -//! -//! Uses sigil-stitch high-level APIs (TypeSpec, FieldSpec, FunSpec, TypeName, -//! sigil_quote!) for structured code generation with automatic import tracking. -//! Each schema produces one `.py` file via `FileSpec`. - -use crate::codegen::traits::file_writer::FileInfo; -use crate::generators::GeneratorError; -pub use crate::generators::python::type_names::python_type_name; -use crate::generators::python::type_names::python_type_name_for_schema; -use crate::generators::request_inputs::{ - RequestInputField, RequestInputFieldKind, RequestInputModel, RequestInputPlan, -}; -use crate::generators::type_application; -use crate::ir::types::{ - IrEnum, IrEnumValueType, IrIntersection, IrObject, IrPrimitive, IrProperty, IrSchema, - IrSchemaKind, IrSpec, IrTaggedUnion, IrTaggedVariant, IrTypeExpr, IrUnion, TaggingStyle, -}; -use heck::{ToPascalCase, ToSnakeCase}; -use sigil_stitch::code_block::CodeBlock; -use sigil_stitch::lang::python::Python; -use sigil_stitch::prelude::*; - -/// Generate every model file from the IR. -pub fn generate_model_files( - ir: &IrSpec, - header: &str, - request_inputs: &RequestInputPlan, -) -> Result, GeneratorError> { - let mut files = Vec::new(); - for (_name, schema) in &ir.schemas { - let body = - emit_model_body(schema, ir).ok_or_else(|| GeneratorError::UnsupportedSchema { - schema: schema.name.clone(), - kind: format!("{:?}", schema.kind), - })?; - let stem = schema.name.to_snake_case(); - let filename = format!("{stem}.py"); - let mut content = String::with_capacity(header.len() + body.len()); - content.push_str(header); - content.push_str(&body); - files.push(FileInfo::model(filename, content)); - } - for model in request_inputs.models() { - files.push(request_input_model_file(model, header)); - } - Ok(files) -} - -fn request_input_model_file(model: &RequestInputModel, header: &str) -> FileInfo { - let class_name = model.name.to_pascal_case(); - let filename = format!("{}.py", model.name.to_snake_case()); - let mut imports = std::collections::BTreeSet::new(); - let mut needs_upload = false; - for field in &model.fields { - if field.is_upload() { - needs_upload = true; - } else { - collect_request_input_imports(&field.type_expr, &mut imports); - } - } - - let mut content = String::new(); - content.push_str(header); - content.push_str("from __future__ import annotations\n\n"); - content.push_str("from dataclasses import dataclass\n"); - if needs_upload { - content.push_str("from ..runtime import UploadFile\n"); - } - for import in &imports { - content.push_str(import); - content.push('\n'); - } - content.push('\n'); - content.push_str("@dataclass\n"); - content.push_str(&format!("class {class_name}:\n")); - if model.fields.is_empty() { - content.push_str(" pass\n"); - } else { - let required = model.fields.iter().filter(|field| field.required); - let optional = model.fields.iter().filter(|field| !field.required); - for field in required.chain(optional) { - let field_name = python_field_name(&field.wire_name); - let ty = request_input_python_type(field); - if field.required { - content.push_str(&format!(" {field_name}: {ty}\n")); - } else { - content.push_str(&format!(" {field_name}: {ty} | None = None\n")); - } - } - } - - FileInfo::model(filename, content) -} - -fn request_input_python_type(field: &RequestInputField) -> String { - match field.kind { - RequestInputFieldKind::UploadFile { .. } => "UploadFile".to_string(), - RequestInputFieldKind::SchemaValue => python_type_str(&field.type_expr), - } -} - -fn emit_model_body(schema: &IrSchema, ir: &IrSpec) -> Option { - let file_spec = match &schema.kind { - IrSchemaKind::Object(obj) => emit_object(schema, obj, ir), - IrSchemaKind::Enum(en) => emit_enum(schema, en), - IrSchemaKind::Alias(expr) => emit_alias(schema, expr), - IrSchemaKind::Union(u) => emit_union(schema, u), - IrSchemaKind::Intersection(i) => emit_intersection(schema, i, ir), - IrSchemaKind::TaggedUnion(tu) => emit_tagged_union(schema, tu, ir), - }?; - file_spec.render(100).ok() -} - -pub fn future_annotations_header() -> CodeBlock { - CodeBlock::of("from __future__ import annotations", ()).expect("static header") -} - -// --------------------------------------------------------------------------- -// Object -> @dataclass -// --------------------------------------------------------------------------- - -fn emit_object(schema: &IrSchema, obj: &IrObject, ir: &IrSpec) -> Option { - let name = schema.name.to_pascal_case(); - - let mut file = - FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); - - if needs_typing_literal_in_props(&obj.properties) { - file = file.add_import(ImportSpec::named("typing", "Literal")); - } - - // Import tagged-union helper functions for fields referencing TaggedUnion schemas - for (_, prop) in &obj.properties { - for named_ref in collect_tagged_union_refs(&prop.type_expr, ir) { - let snake = named_ref.to_snake_case(); - let module = format!(".{snake}"); - file = file.add_import(ImportSpec::named(&module, &format!("{snake}_from_dict"))); - file = file.add_import(ImportSpec::named(&module, &format!("{snake}_to_dict"))); - } - } - - let dataclass_tn = TypeName::importable("dataclasses", "dataclass"); - let mut cls = TypeSpec::builder(&name, TypeKind::Class) - .annotate(AnnotationSpec::importable(dataclass_tn)); - - if let Some(doc) = &schema.description { - cls = cls.doc(&format!("{}.", escape_docstring(doc))); - } - - let mut required: Vec<(&String, &IrProperty)> = Vec::new(); - let mut optional: Vec<(&String, &IrProperty)> = Vec::new(); - for (json_name, prop) in &obj.properties { - if prop.required && !prop.nullable { - required.push((json_name, prop)); - } else { - optional.push((json_name, prop)); - } - } - - let all_fields: Vec<(&String, &IrProperty)> = - required.iter().chain(optional.iter()).copied().collect(); - - if all_fields.is_empty() { - cls = cls.extra_member(CodeBlock::of("pass", ()).expect("pass")); - } else { - for (_json_name, prop) in &required { - let field_name = python_field_name(&prop.name); - let type_name = python_type_name_for_schema(&prop.type_expr, &schema.name); - cls = cls.add_field( - FieldSpec::builder(&field_name, type_name) - .build() - .expect("required field"), - ); - } - for (_json_name, prop) in &optional { - let field_name = python_field_name(&prop.name); - let type_name = python_type_name_for_schema(&prop.type_expr, &schema.name); - cls = cls.add_field( - FieldSpec::builder(&field_name, TypeName::optional(type_name)) - .initializer(CodeBlock::of("None", ()).expect("None init")) - .build() - .expect("optional field"), - ); - } - - cls = cls.add_method(build_to_dict_method(&all_fields, ir, &obj.properties)); - cls = cls.add_method(build_from_dict_method( - &name, - &all_fields, - ir, - &obj.properties, - )); - } - - file = file.add_type(cls.build().ok()?); - file.build().ok() -} - -fn build_to_dict_method( - all_fields: &[(&String, &IrProperty)], - ir: &IrSpec, - properties: &indexmap::IndexMap, -) -> FunSpec { - let self_param = ParameterSpec::of("self", TypeName::primitive("")); - let return_type = type_application( - TypeName::primitive("dict"), - vec![TypeName::primitive("str"), TypeName::primitive("object")], - ); - - let mut body = CodeBlock::builder(); - body.add_statement("result: dict[str, object] = {}", ()); - for (json_name, prop) in all_fields { - let field_name = python_field_name(&prop.name); - let to_expr = render_to_dict_expr(&format!("self.{field_name}"), json_name, ir, properties); - if prop.required && !prop.nullable { - body.add_statement(&format!("result[\"{json_name}\"] = {to_expr}"), ()); - } else { - body.add_statement(&format!("if self.{field_name} is not None:%>"), ()); - body.add_statement(&format!("result[\"{json_name}\"] = {to_expr}%<"), ()); - } - } - body.add_statement("return result", ()); - - FunSpec::builder("to_dict") - .add_param(self_param) - .returns(vec![return_type]) - .body(body.build().expect("to_dict body")) - .build() - .expect("to_dict method") -} - -fn build_from_dict_method( - class_name: &str, - all_fields: &[(&String, &IrProperty)], - ir: &IrSpec, - properties: &indexmap::IndexMap, -) -> FunSpec { - let cls_param = ParameterSpec::of("cls", TypeName::primitive("")); - let data_param = ParameterSpec::of( - "data", - type_application( - TypeName::primitive("dict"), - vec![TypeName::primitive("str"), TypeName::primitive("object")], - ), - ); - - let mut body = CodeBlock::builder(); - body.add_statement("return cls(%>", ()); - for (json_name, prop) in all_fields { - let field_name = python_field_name(&prop.name); - let is_required = prop.required && !prop.nullable; - let expr = if is_required { - render_from_dict_expr(json_name, ir, properties) - } else { - render_from_dict_optional_expr(json_name, ir, properties) - }; - if let Some(comment_start) = expr.find(" #") { - let (value_part, comment_part) = expr.split_at(comment_start); - body.add_statement(&format!("{field_name}={value_part},{comment_part}"), ()); - } else { - body.add_statement(&format!("{field_name}={expr},"), ()); - } - } - body.add("%<", ()); - body.add_statement(")", ()); - - FunSpec::builder("from_dict") - .annotation(CodeBlock::of("@classmethod", ()).expect("classmethod")) - .add_param(cls_param) - .add_param(data_param) - .returns(vec![TypeName::primitive(class_name)]) - .body(body.build().expect("from_dict body")) - .build() - .expect("from_dict method") -} - -// --------------------------------------------------------------------------- -// Enum -> class(str, Enum) or class(int, Enum) -// --------------------------------------------------------------------------- - -fn emit_enum(schema: &IrSchema, en: &IrEnum) -> Option { - if en.value_type == IrEnumValueType::Mixed { - return emit_type_alias_raw(schema, "object"); - } - - let name = schema.name.to_pascal_case(); - let base = match en.value_type { - IrEnumValueType::String => TypeName::primitive("str"), - IrEnumValueType::Integer | IrEnumValueType::Number => TypeName::primitive("int"), - IrEnumValueType::Mixed => unreachable!(), - }; - - let mut ts = TypeSpec::builder(&name, TypeKind::Enum) - .extends(base) - .extends(TypeName::importable("enum", "Enum")); - - if let Some(doc) = &schema.description { - ts = ts.doc(&format!("{}.", escape_docstring(doc))); - } - - for v in &en.values { - let (member_name, value_code) = match en.value_type { - IrEnumValueType::String => { - let s = v.value.as_str()?; - ( - python_enum_member_name(s), - format!("\"{}\"", escape_python_string(s)), - ) - } - IrEnumValueType::Integer | IrEnumValueType::Number => { - let n = v - .value - .as_i64() - .or_else(|| v.value.as_f64().map(|f| f as i64))?; - (format!("N{n}").replace('-', "NEG"), format!("{n}")) - } - IrEnumValueType::Mixed => unreachable!(), - }; - ts = ts.add_variant( - EnumVariantSpec::builder(&member_name) - .discriminant(CodeBlock::of(&value_code, ()).expect("enum value")) - .build() - .expect("enum variant"), - ); - } - - let file = FileSpec::builder_with("model.py", Python::new()) - .header(future_annotations_header()) - .add_type(ts.build().ok()?); - file.build().ok() -} - -// --------------------------------------------------------------------------- -// Alias -> type X = Y (PEP 695) -// --------------------------------------------------------------------------- - -fn emit_alias(schema: &IrSchema, expr: &IrTypeExpr) -> Option { - let name = schema.name.to_pascal_case(); - let rhs_type = python_type_name(expr); - - let type_alias = sigil_quote!(Python { - type $N(name.as_str()) = ($T(rhs_type)); - }) - .ok()?; - - let mut file = - FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); - if needs_typing_literal(expr) { - file = file.add_import(ImportSpec::named("typing", "Literal")); - } - if let Some(doc) = &schema.description { - file = file.add_raw(&format!("# {}\n", escape_docstring(doc))); - } - file = file.add_code(type_alias); - file.build().ok() -} - -fn emit_type_alias_raw(schema: &IrSchema, rhs: &str) -> Option { - let name = schema.name.to_pascal_case(); - - let type_alias = sigil_quote!(Python { - type $N(name.as_str()) = $L(rhs); - }) - .ok()?; - - let mut file = - FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); - if let Some(doc) = &schema.description { - file = file.add_raw(&format!("# {}\n", escape_docstring(doc))); - } - file = file.add_code(type_alias); - file.build().ok() -} - -// --------------------------------------------------------------------------- -// Type alias builder with proper multi-line formatting -// --------------------------------------------------------------------------- - -fn format_type_alias(name: &str, members: &[TypeName]) -> CodeBlock { - if members.is_empty() { - return sigil_quote!(Python { - type $N(name) = ($T(TypeName::importable("typing", "Any"))); - }) - .unwrap(); - } - if members.len() == 1 { - return sigil_quote!(Python { - type $N(name) = ($T(members[0].clone())); - }) - .unwrap(); - } - sigil_quote!(Python { - type $N(name) = ( - $L(" ")$for(member in members; separator = "\n | ") { $T((*member).clone()) } - ) - }) - .unwrap() -} - -// --------------------------------------------------------------------------- -// Union -> type X = A | B | C -// --------------------------------------------------------------------------- - -fn emit_union(schema: &IrSchema, u: &IrUnion) -> Option { - let name = schema.name.to_pascal_case(); - - let mut members: Vec = u.members.iter().map(python_type_name).collect(); - if u.nullable { - members.push(TypeName::primitive("None")); - } - - let type_alias = format_type_alias(&name, &members); - - let mut file = - FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); - if needs_typing_literal_in_exprs(&u.members) { - file = file.add_import(ImportSpec::named("typing", "Literal")); - } - if let Some(doc) = &schema.description { - file = file.add_raw(&format!("# {}\n", escape_docstring(doc))); - } - file = file.add_code(type_alias); - file.build().ok() -} - -// --------------------------------------------------------------------------- -// Intersection -> merged @dataclass -// --------------------------------------------------------------------------- - -fn emit_intersection(schema: &IrSchema, inter: &IrIntersection, ir: &IrSpec) -> Option { - let mut all_props: indexmap::IndexMap = indexmap::IndexMap::new(); - for member in &inter.members { - if let IrTypeExpr::Named(ref_name) = member - && let Some(s) = ir.schemas.get(ref_name.as_str()) - && let IrSchemaKind::Object(obj) = &s.kind - { - for (k, v) in &obj.properties { - all_props.entry(k.clone()).or_insert_with(|| v.clone()); - } - } - } - - if all_props.is_empty() { - return emit_intersection_as_alias(schema, inter); - } - - emit_intersection_as_dataclass(schema, &all_props, ir) -} - -fn emit_intersection_as_alias(schema: &IrSchema, inter: &IrIntersection) -> Option { - let name = schema.name.to_pascal_case(); - let members: Vec = inter.members.iter().map(python_type_name).collect(); - - let type_alias = format_type_alias(&name, &members); - - let mut file = - FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); - if needs_typing_literal_in_exprs(&inter.members) { - file = file.add_import(ImportSpec::named("typing", "Literal")); - } - file = file.add_code(type_alias); - file.build().ok() -} - -fn emit_intersection_as_dataclass( - schema: &IrSchema, - all_props: &indexmap::IndexMap, - ir: &IrSpec, -) -> Option { - let name = schema.name.to_pascal_case(); - - let mut file = - FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); - - if needs_typing_literal_in_props(all_props) { - file = file.add_import(ImportSpec::named("typing", "Literal")); - } - - let dataclass_tn = TypeName::importable("dataclasses", "dataclass"); - let mut cls = TypeSpec::builder(&name, TypeKind::Class) - .annotate(AnnotationSpec::importable(dataclass_tn)); - - if let Some(doc) = &schema.description { - cls = cls.doc(&format!("{}.", escape_docstring(doc))); - } - - let mut required: Vec<(&String, &IrProperty)> = Vec::new(); - let mut optional: Vec<(&String, &IrProperty)> = Vec::new(); - for (json_name, prop) in all_props { - if prop.required && !prop.nullable { - required.push((json_name, prop)); - } else { - optional.push((json_name, prop)); - } - } - - if required.is_empty() && optional.is_empty() { - cls = cls.extra_member(CodeBlock::of("pass", ()).expect("pass")); - } else { - for (_json_name, prop) in &required { - let field_name = python_field_name(&prop.name); - let type_name = python_type_name_for_schema(&prop.type_expr, &schema.name); - cls = cls.add_field( - FieldSpec::builder(&field_name, type_name) - .build() - .expect("required field"), - ); - } - for (_json_name, prop) in &optional { - let field_name = python_field_name(&prop.name); - let type_name = python_type_name_for_schema(&prop.type_expr, &schema.name); - cls = cls.add_field( - FieldSpec::builder(&field_name, TypeName::optional(type_name)) - .initializer(CodeBlock::of("None", ()).expect("None init")) - .build() - .expect("optional field"), - ); - } - - let all_fields: Vec<(&String, &IrProperty)> = - required.iter().chain(optional.iter()).copied().collect(); - cls = cls.add_method(build_to_dict_method(&all_fields, ir, all_props)); - cls = cls.add_method(build_from_dict_method(&name, &all_fields, ir, all_props)); - } - - file = file.add_type(cls.build().ok()?); - file.build().ok() -} - -// --------------------------------------------------------------------------- -// TaggedUnion -> type X = A | B | C -// --------------------------------------------------------------------------- - -fn emit_tagged_union(schema: &IrSchema, tu: &IrTaggedUnion, ir: &IrSpec) -> Option { - let name = schema.name.to_pascal_case(); - let snake_name = schema.name.to_snake_case(); - - let members: Vec = tu - .variants - .iter() - .map(|v| python_type_name(&v.content_type)) - .collect(); - - let type_alias = format_type_alias(&name, &members); - - let hint = match &tu.tagging { - TaggingStyle::Internal => { - format!("Discriminator: {} (internal).", tu.discriminator_field) - } - TaggingStyle::Adjacent { content_field } => format!( - "Discriminator: {} / content: {} (adjacent).", - tu.discriminator_field, content_field - ), - TaggingStyle::External => "Discriminator: variant key (external).".to_string(), - }; - - let doc = match &schema.description { - Some(desc) => format!("{desc}\n\n{hint}"), - None => hint, - }; - - let mut file = - FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); - let exprs: Vec<&IrTypeExpr> = tu.variants.iter().map(|v| &v.content_type).collect(); - if exprs.iter().any(|e| needs_typing_literal(e)) { - file = file.add_import(ImportSpec::named("typing", "Literal")); - } - let mut doc_block = String::new(); - for line in doc.lines() { - doc_block.push_str(&format!("# {line}\n")); - } - file = file.add_raw(&doc_block); - file = file.add_code(type_alias); - - if !tu.variants.is_empty() { - let helpers = build_tagged_union_helpers(&name, &snake_name, tu, ir); - file = file.add_code(helpers); - } - - file.build().ok() -} - -fn build_tagged_union_helpers( - pascal_name: &str, - snake_name: &str, - tu: &IrTaggedUnion, - ir: &IrSpec, -) -> CodeBlock { - let tag_field = &tu.discriminator_field; - - // Only generate helpers for variants that resolve to Object schemas - let resolved_variants: Vec<(&IrTaggedVariant, String)> = tu - .variants - .iter() - .filter_map(|v| { - if let IrTypeExpr::Named(ref_name) = &v.content_type - && is_object_schema(ref_name, ir) - { - return Some((v, ref_name.to_pascal_case())); - } - None - }) - .collect(); - - let mut cb = CodeBlock::builder(); - - if resolved_variants.is_empty() { - return cb.build_unwrap(); - } - - // from_dict - cb.add_line(); - cb.begin_control_flow( - &format!("def {snake_name}_from_dict(data: dict[str, object]) -> {pascal_name}"), - (), - ); - match &tu.tagging { - TaggingStyle::Internal => { - cb.add_statement(&format!("_tag = data[\"{tag_field}\"]"), ()); - for (i, (variant, py_class)) in resolved_variants.iter().enumerate() { - let cond = format!("_tag == \"{}\"", variant.discriminator_value); - emit_elif(&mut cb, i == 0, false, &cond); - cb.add_statement(&format!("return {py_class}.from_dict(data)"), ()); - } - cb.end_control_flow_no_newline(); - } - TaggingStyle::Adjacent { content_field } => { - cb.add_statement(&format!("_tag = data[\"{tag_field}\"]"), ()); - cb.add_statement( - &format!("_content = data[\"{content_field}\"] # type: ignore[assignment]"), - (), - ); - for (i, (variant, py_class)) in resolved_variants.iter().enumerate() { - let cond = format!("_tag == \"{}\"", variant.discriminator_value); - emit_elif(&mut cb, i == 0, false, &cond); - cb.add_statement( - &format!("return {py_class}.from_dict(_content) # type: ignore[arg-type]"), - (), - ); - } - cb.end_control_flow_no_newline(); - } - TaggingStyle::External => { - for (i, (variant, py_class)) in resolved_variants.iter().enumerate() { - let cond = format!("\"{}\" in data", variant.discriminator_value); - emit_elif(&mut cb, i == 0, false, &cond); - cb.add_statement( - &format!( - "return {py_class}.from_dict(data[\"{}\"]) # type: ignore[arg-type]", - variant.discriminator_value - ), - (), - ); - } - cb.end_control_flow_no_newline(); - } - } - cb.add_statement( - "raise ValueError(%V)", - VerbatimStrArg(format!( - "Unknown discriminator value for {pascal_name}: {{data}}" - )), - ); - cb.end_control_flow(); - - // to_dict - cb.add_line(); - cb.begin_control_flow( - &format!("def {snake_name}_to_dict(obj: {pascal_name}) -> dict[str, object]"), - (), - ); - match &tu.tagging { - TaggingStyle::Internal => { - for (i, (variant, py_class)) in resolved_variants.iter().enumerate() { - let cond = format!("isinstance(obj, {py_class})"); - emit_elif(&mut cb, i == 0, false, &cond); - cb.add_statement("result = obj.to_dict()", ()); - cb.add_statement( - &format!( - "result[\"{tag_field}\"] = \"{}\"", - variant.discriminator_value - ), - (), - ); - cb.add_statement("return result", ()); - } - cb.end_control_flow_no_newline(); - } - TaggingStyle::Adjacent { content_field } => { - for (i, (variant, py_class)) in resolved_variants.iter().enumerate() { - let cond = format!("isinstance(obj, {py_class})"); - emit_elif(&mut cb, i == 0, false, &cond); - cb.add_statement( - &format!( - "return {{\"{tag_field}\": \"{}\", \"{content_field}\": obj.to_dict()}}", - variant.discriminator_value - ), - (), - ); - } - cb.end_control_flow_no_newline(); - } - TaggingStyle::External => { - for (i, (variant, py_class)) in resolved_variants.iter().enumerate() { - let cond = format!("isinstance(obj, {py_class})"); - emit_elif(&mut cb, i == 0, false, &cond); - cb.add_statement( - &format!( - "return {{\"{}\": obj.to_dict()}}", - variant.discriminator_value - ), - (), - ); - } - cb.end_control_flow_no_newline(); - } - } - cb.add_statement( - "raise ValueError(%V)", - VerbatimStrArg(format!("Unknown variant for {pascal_name}: {{type(obj)}}")), - ); - cb.end_control_flow(); - - cb.build_unwrap() -} - -fn emit_elif( - cb: &mut sigil_stitch::code_block::CodeBlockBuilder, - is_first: bool, - is_last: bool, - cond: &str, -) { - if !is_first { - cb.end_control_flow_no_newline(); - } - if is_last && !is_first { - cb.begin_control_flow("else", ()); - } else { - let kw = if is_first { "if" } else { "elif" }; - cb.begin_control_flow(&format!("{kw} {cond}"), ()); - } -} - -// --------------------------------------------------------------------------- -// Type mapping -// --------------------------------------------------------------------------- - -/// Like `python_type_name` but Named types import from `..models.{snake}` (for API files). -pub fn api_type_name(expr: &IrTypeExpr) -> TypeName { - match expr.representation() { - IrTypeExpr::Named(name) => { - let py_name = name.to_pascal_case(); - let module = format!("..models.{}", name.to_snake_case()); - TypeName::importable(&module, &py_name) - } - IrTypeExpr::Array(inner) => { - type_application(TypeName::primitive("list"), vec![api_type_name(inner)]) - } - IrTypeExpr::Map(inner) => type_application( - TypeName::primitive("dict"), - vec![TypeName::primitive("str"), api_type_name(inner)], - ), - IrTypeExpr::Union(members) => { - if members.is_empty() { - TypeName::importable("typing", "Any") - } else { - TypeName::union(members.iter().map(api_type_name).collect()) - } - } - IrTypeExpr::Nullable(inner) => TypeName::optional(api_type_name(inner)), - _ => python_type_name(expr), - } -} - -/// Map an IR type expression to a Python type string (for serialization helpers). -pub fn python_type_str(expr: &IrTypeExpr) -> String { - match expr { - IrTypeExpr::ScalarEnum { representation, .. } => python_type_str(representation), - IrTypeExpr::Named(name) => name.to_pascal_case(), - IrTypeExpr::Primitive(p) => python_primitive(p).to_string(), - IrTypeExpr::StringLiteral(s) => { - format!("Literal[\"{}\"]", escape_python_string(s)) - } - IrTypeExpr::StringEnum(values) => { - let members: Vec = values - .iter() - .map(|v| format!("\"{}\"", escape_python_string(v))) - .collect(); - format!("Literal[{}]", members.join(", ")) - } - IrTypeExpr::Array(inner) => { - let inner_ty = python_type_str(inner); - format!("list[{inner_ty}]") - } - IrTypeExpr::Map(inner) => { - let inner_ty = python_type_str(inner); - format!("dict[str, {inner_ty}]") - } - IrTypeExpr::Union(members) => { - let parts: Vec = members.iter().map(python_type_str).collect(); - if parts.is_empty() { - "Any".to_string() - } else { - parts.join(" | ") - } - } - IrTypeExpr::Nullable(inner) => { - let inner_ty = python_type_str(inner); - format!("{inner_ty} | None") - } - IrTypeExpr::Any => "Any".to_string(), - } -} - -fn python_primitive(p: &IrPrimitive) -> &'static str { - match p { - IrPrimitive::String | IrPrimitive::StringWithFormat(_) => "str", - IrPrimitive::Integer | IrPrimitive::IntegerWithFormat(_) => "int", - IrPrimitive::Number | IrPrimitive::NumberWithFormat(_) => "float", - IrPrimitive::Boolean => "bool", - IrPrimitive::Binary => "bytes", - IrPrimitive::Date => "datetime.date", - IrPrimitive::DateTime => "datetime.datetime", - IrPrimitive::Uuid => "uuid.UUID", - } -} - -fn collect_request_input_imports( - expr: &IrTypeExpr, - imports: &mut std::collections::BTreeSet, -) { - match expr.representation() { - IrTypeExpr::Named(name) => { - let py_name = name.to_pascal_case(); - let module = name.to_snake_case(); - imports.insert(format!("from .{module} import {py_name}")); - } - IrTypeExpr::Primitive(IrPrimitive::Date | IrPrimitive::DateTime) => { - imports.insert("import datetime".to_string()); - } - IrTypeExpr::Primitive(IrPrimitive::Uuid) => { - imports.insert("import uuid".to_string()); - } - IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => { - imports.insert("from typing import Literal".to_string()); - } - IrTypeExpr::Union(members) => { - if members.is_empty() { - imports.insert("from typing import Any".to_string()); - } - for member in members { - collect_request_input_imports(member, imports); - } - } - IrTypeExpr::Any => { - imports.insert("from typing import Any".to_string()); - } - IrTypeExpr::Array(inner) | IrTypeExpr::Map(inner) | IrTypeExpr::Nullable(inner) => { - collect_request_input_imports(inner, imports); - } - _ => {} - } -} - -fn needs_typing_literal(expr: &IrTypeExpr) -> bool { - match expr.representation() { - IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => true, - IrTypeExpr::Array(inner) | IrTypeExpr::Map(inner) | IrTypeExpr::Nullable(inner) => { - needs_typing_literal(inner) - } - IrTypeExpr::Union(members) => members.iter().any(needs_typing_literal), - _ => false, - } -} - -fn needs_typing_literal_in_props(props: &indexmap::IndexMap) -> bool { - props.values().any(|p| needs_typing_literal(&p.type_expr)) -} - -fn needs_typing_literal_in_exprs(exprs: &[IrTypeExpr]) -> bool { - exprs.iter().any(needs_typing_literal) -} - -// --------------------------------------------------------------------------- -// Serialization helpers -// --------------------------------------------------------------------------- - -fn render_to_dict_expr( - value_expr: &str, - json_name: &str, - ir: &IrSpec, - properties: &indexmap::IndexMap, -) -> String { - let prop = properties.get(json_name); - let type_expr = prop.map(|p| &p.type_expr); - match type_expr { - Some(IrTypeExpr::Named(ref_name)) => { - if is_object_schema(ref_name, ir) { - format!("{value_expr}.to_dict()") - } else if is_tagged_union_schema(ref_name, ir) { - let snake = ref_name.to_snake_case(); - format!("{snake}_to_dict({value_expr})") - } else { - value_expr.to_string() - } - } - Some(IrTypeExpr::Array(inner)) => { - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_object_schema(ref_name, ir) - { - return format!("[item.to_dict() for item in {value_expr}]"); - } - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_tagged_union_schema(ref_name, ir) - { - let snake = ref_name.to_snake_case(); - return format!("[{snake}_to_dict(item) for item in {value_expr}]"); - } - value_expr.to_string() - } - Some(IrTypeExpr::Nullable(inner)) => { - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_object_schema(ref_name, ir) - { - return format!("{value_expr}.to_dict() if {value_expr} is not None else None"); - } - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_tagged_union_schema(ref_name, ir) - { - let snake = ref_name.to_snake_case(); - return format!( - "{snake}_to_dict({value_expr}) if {value_expr} is not None else None" - ); - } - value_expr.to_string() - } - Some(IrTypeExpr::Map(inner)) => { - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_object_schema(ref_name, ir) - { - return format!("{{k: v.to_dict() for k, v in {value_expr}.items()}}"); - } - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_tagged_union_schema(ref_name, ir) - { - let snake = ref_name.to_snake_case(); - return format!("{{k: {snake}_to_dict(v) for k, v in {value_expr}.items()}}"); - } - value_expr.to_string() - } - _ => value_expr.to_string(), - } -} - -fn render_from_dict_expr( - json_name: &str, - ir: &IrSpec, - properties: &indexmap::IndexMap, -) -> String { - let prop = properties.get(json_name); - let type_expr = prop.map(|p| &p.type_expr); - let accessor = format!("data[\"{json_name}\"]"); - match type_expr { - Some(IrTypeExpr::Named(ref_name)) => { - if is_object_schema(ref_name, ir) { - let py_name = ref_name.to_pascal_case(); - format!("{py_name}.from_dict({accessor}) # type: ignore[arg-type]") - } else if is_tagged_union_schema(ref_name, ir) { - let snake = ref_name.to_snake_case(); - format!("{snake}_from_dict({accessor}) # type: ignore[arg-type]") - } else { - format!("{accessor} # type: ignore[assignment]") - } - } - Some(IrTypeExpr::Array(inner)) => { - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_object_schema(ref_name, ir) - { - let py_name = ref_name.to_pascal_case(); - return format!( - "[{py_name}.from_dict(item) for item in {accessor}] # type: ignore[union-attr]" - ); - } - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_tagged_union_schema(ref_name, ir) - { - let snake = ref_name.to_snake_case(); - return format!( - "[{snake}_from_dict(item) for item in {accessor}] # type: ignore[union-attr]" - ); - } - format!("{accessor} # type: ignore[assignment]") - } - Some(IrTypeExpr::Map(inner)) => { - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_object_schema(ref_name, ir) - { - let py_name = ref_name.to_pascal_case(); - return format!( - "{{k: {py_name}.from_dict(v) for k, v in {accessor}.items()}} # type: ignore[union-attr]" - ); - } - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_tagged_union_schema(ref_name, ir) - { - let snake = ref_name.to_snake_case(); - return format!( - "{{k: {snake}_from_dict(v) for k, v in {accessor}.items()}} # type: ignore[union-attr]" - ); - } - format!("{accessor} # type: ignore[assignment]") - } - _ => format!("{accessor} # type: ignore[assignment]"), - } -} - -fn render_from_dict_optional_expr( - json_name: &str, - ir: &IrSpec, - properties: &indexmap::IndexMap, -) -> String { - let prop = properties.get(json_name); - let type_expr = prop.map(|p| &p.type_expr); - let raw_type = type_expr.map(|t| match t { - IrTypeExpr::Nullable(inner) => inner.as_ref(), - _ => t, - }); - let accessor = format!("data.get(\"{json_name}\")"); - match raw_type { - Some(IrTypeExpr::Named(ref_name)) => { - if is_object_schema(ref_name, ir) { - let py_name = ref_name.to_pascal_case(); - format!( - "{py_name}.from_dict({accessor}) if {accessor} is not None else None # type: ignore[arg-type]" - ) - } else if is_tagged_union_schema(ref_name, ir) { - let snake = ref_name.to_snake_case(); - format!( - "{snake}_from_dict({accessor}) if {accessor} is not None else None # type: ignore[arg-type]" - ) - } else { - format!("{accessor} # type: ignore[assignment]") - } - } - Some(IrTypeExpr::Array(inner)) => { - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_object_schema(ref_name, ir) - { - let py_name = ref_name.to_pascal_case(); - return format!( - "[{py_name}.from_dict(item) for item in {accessor}] if {accessor} is not None else None # type: ignore[union-attr]" - ); - } - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_tagged_union_schema(ref_name, ir) - { - let snake = ref_name.to_snake_case(); - return format!( - "[{snake}_from_dict(item) for item in {accessor}] if {accessor} is not None else None # type: ignore[union-attr]" - ); - } - format!("{accessor} # type: ignore[assignment]") - } - Some(IrTypeExpr::Map(inner)) => { - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_object_schema(ref_name, ir) - { - let py_name = ref_name.to_pascal_case(); - return format!( - "{{k: {py_name}.from_dict(v) for k, v in {accessor}.items()}} if {accessor} is not None else None # type: ignore[union-attr]" - ); - } - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_tagged_union_schema(ref_name, ir) - { - let snake = ref_name.to_snake_case(); - return format!( - "{{k: {snake}_from_dict(v) for k, v in {accessor}.items()}} if {accessor} is not None else None # type: ignore[union-attr]" - ); - } - format!("{accessor} # type: ignore[assignment]") - } - _ => format!("{accessor} # type: ignore[assignment]"), - } -} - -pub fn is_object_schema(name: &str, ir: &IrSpec) -> bool { - ir.schemas.get(name).is_some_and(|s| match &s.kind { - IrSchemaKind::Object(_) => true, - IrSchemaKind::Intersection(inter) => inter.members.iter().any(|m| { - if let IrTypeExpr::Named(ref_name) = m { - ir.schemas - .get(ref_name.as_str()) - .is_some_and(|ms| matches!(ms.kind, IrSchemaKind::Object(_))) - } else { - false - } - }), - _ => false, - }) -} - -pub fn is_tagged_union_schema(name: &str, ir: &IrSpec) -> bool { - ir.schemas - .get(name) - .is_some_and(|s| matches!(s.kind, IrSchemaKind::TaggedUnion(_))) -} - -/// Collect all tagged-union schema names referenced (directly or nested) in a type expression. -fn collect_tagged_union_refs(expr: &IrTypeExpr, ir: &IrSpec) -> Vec { - let mut refs = Vec::new(); - match expr { - IrTypeExpr::Named(name) => { - if is_tagged_union_schema(name, ir) { - refs.push(name.clone()); - } - } - IrTypeExpr::Array(inner) | IrTypeExpr::Nullable(inner) => { - refs.extend(collect_tagged_union_refs(inner, ir)); - } - IrTypeExpr::Map(inner) => { - refs.extend(collect_tagged_union_refs(inner, ir)); - } - _ => {} - } - refs -} - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -pub fn python_field_name(name: &str) -> String { - let snake = name.to_snake_case(); - if snake.is_empty() { - return "field_".to_string(); - } - match snake.as_str() { - "and" | "as" | "assert" | "async" | "await" | "break" | "class" | "continue" | "def" - | "del" | "elif" | "else" | "except" | "finally" | "for" | "from" | "global" | "if" - | "import" | "in" | "is" | "lambda" | "nonlocal" | "not" | "or" | "pass" | "raise" - | "return" | "try" | "while" | "with" | "yield" | "type" => { - format!("{snake}_") - } - _ => snake, - } -} - -fn python_enum_member_name(value: &str) -> String { - let upper = value - .to_uppercase() - .replace(|c: char| !c.is_alphanumeric(), "_"); - if upper.is_empty() { - return "EMPTY".to_string(); - } - if upper.starts_with(|c: char| c.is_ascii_digit()) { - return format!("N{upper}"); - } - upper -} - -fn escape_python_string(s: &str) -> String { - s.replace('\\', "\\\\").replace('"', "\\\"") -} - -fn escape_docstring(s: &str) -> String { - s.replace("\"\"\"", "\\\"\\\"\\\"") - .lines() - .next() - .unwrap_or("") - .to_string() -} +pub use crate::generators::python::emit_models::*; diff --git a/src/generators/python/mod.rs b/src/generators/python/mod.rs index 74f544b4e..42925c0b5 100644 --- a/src/generators/python/mod.rs +++ b/src/generators/python/mod.rs @@ -1,5 +1,8 @@ +mod emit_api_values; +mod emit_models; pub mod httpx; mod operation_names; pub mod requests; mod response_headers; mod type_names; +mod value; diff --git a/src/generators/python/requests/codegen.rs b/src/generators/python/requests/codegen.rs index 8b7493b17..c0221f67b 100644 --- a/src/generators/python/requests/codegen.rs +++ b/src/generators/python/requests/codegen.rs @@ -53,13 +53,19 @@ impl PythonRequestsCodeGenerator { let mut files = Vec::new(); - files.extend(emit_models::generate_model_files( + files.extend(emit_models::generate_model_files_with_strategy( ir, &header, &request_inputs, + self.config.untagged_union_strategy, )?); - files.extend(emit_api::generate_api_files(ir, &header, &request_inputs)); + files.extend(emit_api::generate_api_files_with_strategy( + ir, + &header, + &request_inputs, + self.config.untagged_union_strategy, + )); files.extend(runtime_files(&header, request_inputs.has_uploads())); diff --git a/src/generators/python/requests/config.rs b/src/generators/python/requests/config.rs index 0d4762c33..65c1fc316 100644 --- a/src/generators/python/requests/config.rs +++ b/src/generators/python/requests/config.rs @@ -1,5 +1,6 @@ //! Python requests generator-specific configuration. +pub use crate::generators::python::value::UntaggedUnionStrategy; use serde::{Deserialize, Serialize}; use tracing::error; @@ -7,6 +8,9 @@ use tracing::error; pub struct PythonRequestsConfig { #[serde(default)] pub package_name: Option, + /// Select and convert structural members, reject ambiguity, or keep raw values. + #[serde(default)] + pub untagged_union_strategy: UntaggedUnionStrategy, } impl From for PythonRequestsConfig { diff --git a/src/generators/python/requests/emit_api.rs b/src/generators/python/requests/emit_api.rs index 48d1b38ca..9a1336318 100644 --- a/src/generators/python/requests/emit_api.rs +++ b/src/generators/python/requests/emit_api.rs @@ -26,8 +26,12 @@ use sigil_stitch::spec::fun_spec::FunSpecBuilder; use super::emit_models::{ api_type_name, future_annotations_header, is_object_schema, python_field_name, }; +use crate::generators::python::emit_api_values::{ + MultipartPart, multipart_data, union_error_body_property, +}; use crate::generators::python::operation_names::plan_python_operation_names; use crate::generators::python::response_headers::build_header_accessors; +use crate::generators::python::value::{Direction, UntaggedUnionStrategy, ValuePlan}; use crate::generators::type_application; /// Generate every API file from the IR. @@ -36,12 +40,28 @@ pub fn generate_api_files( header: &str, request_inputs: &RequestInputPlan, ) -> Vec { + generate_api_files_with_strategy( + ir, + header, + request_inputs, + UntaggedUnionStrategy::FirstMatch, + ) +} + +/// Generate API files with the same union policy used by the model emitter. +pub fn generate_api_files_with_strategy( + ir: &IrSpec, + header: &str, + request_inputs: &RequestInputPlan, + strategy: UntaggedUnionStrategy, +) -> Vec { + let values = ValuePlan::new(ir, strategy); let by_tag = group_by_tag(&ir.operations); let mut files = Vec::with_capacity(by_tag.len()); for (tag, ops) in &by_tag { let stem = tag.to_snake_case(); let filename = format!("{stem}_api.py"); - let body = emit_api_file(tag, ops, ir, header, request_inputs); + let body = emit_api_file(tag, ops, ir, header, request_inputs, &values); files.push(FileInfo::api(filename, body)); } files @@ -68,6 +88,7 @@ fn emit_api_file( ir: &IrSpec, header: &str, request_inputs: &RequestInputPlan, + values: &ValuePlan<'_>, ) -> String { let class_name = format!("{}Api", tag.to_pascal_case()); let mut plans: Vec = ops @@ -97,19 +118,24 @@ fn emit_api_file( let mut cls = TypeSpec::builder(&class_name, TypeKind::Class).add_method(init); for plan in &plans { - cls = cls.add_method(build_api_method_with_http_info(plan, ir, &error_type)); - cls = cls.add_method(build_api_method(plan, ir, &error_type)); + cls = cls.add_method(build_api_method_with_http_info( + plan, + ir, + &error_type, + values, + )); + cls = cls.add_method(build_api_method(plan, ir, &error_type, values)); } let mut fb = FileSpec::builder_with(&format!("{}_api.py", tag.to_snake_case()), Python::new()) .header(future_annotations_header()); for plan in &plans { if !plan.success_headers.is_empty() { - fb = fb.add_type(build_response_class(plan)); + fb = fb.add_type(build_response_class(plan, values)); } } fb = fb - .add_code(build_error_classes_block(&plans, ir)) + .add_code(build_error_classes_block(&plans, ir, values)) .add_type(cls.build().expect("API TypeSpec builds")); if plans .iter() @@ -118,6 +144,26 @@ fn emit_api_file( { fb = fb.add_import(ImportSpec::side_effect("json")); } + if plans.iter().any(|plan| { + plan.body.as_ref().is_some_and(|body| { + body.multipart_parts + .as_ref() + .is_some_and(|parts| parts.iter().any(|part| values.has_union(&part.type_expr))) + }) + }) || ops.iter().any(|op| { + op.request_body + .iter() + .flat_map(|body| body.content.values()) + .chain( + op.responses + .iter() + .flat_map(|response| response.content.values()), + ) + .any(|expr| values.has_union(expr)) + }) { + fb = fb.add_import(ImportSpec::named("..models", "_union")); + fb = fb.add_import(ImportSpec::side_effect("json")); + } if plans.iter().any(|plan| { plan.body.as_ref().is_some_and(|body| { body.multipart_parts.as_ref().is_some_and(|parts| { @@ -159,10 +205,15 @@ fn emit_api_file( content } -fn build_api_method(plan: &OpPlan<'_>, ir: &IrSpec, error_type: &TypeName) -> FunSpec { +fn build_api_method( + plan: &OpPlan<'_>, + ir: &IrSpec, + error_type: &TypeName, + values: &ValuePlan<'_>, +) -> FunSpec { let mut fun = FunSpec::builder(&plan.method_name); - fun = add_api_method_params(fun, plan); - fun = fun.returns(vec![response_payload_type(plan)]); + fun = add_api_method_params(fun, plan, values); + fun = fun.returns(vec![response_payload_type(plan, values)]); if let Some(summary) = &plan.op.summary { fun = fun.doc(&format!("{summary}.")); @@ -201,27 +252,32 @@ fn build_api_method_with_http_info( plan: &OpPlan<'_>, ir: &IrSpec, error_type: &TypeName, + values: &ValuePlan<'_>, ) -> FunSpec { let mut fun = FunSpec::builder(&plan.with_http_info_method_name); - fun = add_api_method_params(fun, plan); - fun = fun.returns(vec![response_metadata_type(plan)]); + fun = add_api_method_params(fun, plan, values); + fun = fun.returns(vec![response_metadata_type(plan, values)]); if let Some(summary) = &plan.op.summary { fun = fun.doc(&format!("{summary}, including HTTP response metadata.")); } - fun = fun.body(build_method_body(plan, ir, error_type)); + fun = fun.body(build_method_body(plan, ir, error_type, values)); fun.build() .expect("API with-http-info method FunSpec builds") } -fn add_api_method_params(mut fun: FunSpecBuilder, plan: &OpPlan<'_>) -> FunSpecBuilder { +fn add_api_method_params( + mut fun: FunSpecBuilder, + plan: &OpPlan<'_>, + values: &ValuePlan<'_>, +) -> FunSpecBuilder { fun = fun.add_param(ParameterSpec::of("self", TypeName::primitive(""))); for p in &plan.path_params { fun = fun.add_param(ParameterSpec::of( &p.var_name, - api_type_name(&p.param.type_expr), + values.api_type(&p.param.type_expr), )); } @@ -237,14 +293,14 @@ fn add_api_method_params(mut fun: FunSpecBuilder, plan: &OpPlan<'_>) -> FunSpecB if p.param.required { fun = fun.add_param(ParameterSpec::of( &p.var_name, - api_type_name(&p.param.type_expr), + values.api_type(&p.param.type_expr), )); } } // Body param if let Some(b) = &plan.body { - let ty = api_type_name(&b.type_expr); + let ty = values.api_type(&b.type_expr); if b.required { fun = fun.add_param(ParameterSpec::of(&b.var_name, ty)); } else { @@ -260,7 +316,7 @@ fn add_api_method_params(mut fun: FunSpecBuilder, plan: &OpPlan<'_>) -> FunSpecB // Optional query/header params last for p in plan.query_params.iter().chain(&plan.header_params) { if !p.param.required { - let param_ty = api_type_name(&p.param.type_expr); + let param_ty = values.api_type(&p.param.type_expr); let param_ty = if is_already_optional(&p.param.type_expr) { param_ty } else { @@ -278,26 +334,31 @@ fn add_api_method_params(mut fun: FunSpecBuilder, plan: &OpPlan<'_>) -> FunSpecB fun } -fn response_payload_type(plan: &OpPlan<'_>) -> TypeName { +fn response_payload_type(plan: &OpPlan<'_>, values: &ValuePlan<'_>) -> TypeName { if plan.typed_responses.is_empty() { TypeName::primitive("None") } else { - response_type_name(&plan.typed_responses[0]) + response_type_name(&plan.typed_responses[0], values) } } -fn response_metadata_type(plan: &OpPlan<'_>) -> TypeName { +fn response_metadata_type(plan: &OpPlan<'_>, values: &ValuePlan<'_>) -> TypeName { if plan.success_headers.is_empty() { type_application( TypeName::importable("..runtime.client", "ApiResponse"), - vec![response_payload_type(plan)], + vec![response_payload_type(plan, values)], ) } else { TypeName::primitive(&plan.response_type) } } -fn build_method_body(plan: &OpPlan<'_>, ir: &IrSpec, _error_type: &TypeName) -> CodeBlock { +fn build_method_body( + plan: &OpPlan<'_>, + ir: &IrSpec, + _error_type: &TypeName, + values: &ValuePlan<'_>, +) -> CodeBlock { let mut cb = CodeBlock::builder(); // Path interpolation @@ -359,7 +420,15 @@ fn build_method_body(plan: &OpPlan<'_>, ir: &IrSpec, _error_type: &TypeName) -> // Body serialization let body_expr = if let Some(b) = &plan.body { - if is_object_type(&b.type_expr, ir) { + if values.has_union(&b.type_expr) { + let access = sigil_quote!(Python { $N(b.var_name.as_str()) }) + .expect("Python HTTP union input builds"); + let converted = values.call(&b.type_expr, access, Direction::To); + cb.add_code(sigil_quote!(Python { + _union_body = $if(b.required) { $C(converted) } $else { None if $N(b.var_name.as_str()) is None else $C(converted) } + }).expect("Python HTTP union request conversion builds")); + "_union_body".to_string() + } else if is_object_type(&b.type_expr, ir) { if b.required { format!("{}.to_dict()", b.var_name) } else { @@ -389,13 +458,20 @@ fn build_method_body(plan: &OpPlan<'_>, ir: &IrSpec, _error_type: &TypeName) -> format!("\"{}\"", plan.op.method.to_uppercase()), "path".to_string(), ]; + let mut union_json_argument = None; if has_query { request_args.push("params=params".to_string()); } if let Some(body) = &plan.body { if media_type_base(&body.media_type) == "multipart/form-data" { if let Some(parts) = &body.multipart_parts { - emit_multipart_data(&mut cb, body, parts, ir); + cb.add_code(multipart_data( + &body.var_name, + body.required, + parts, + ir, + values, + )); request_args.push("files=files if files else None".to_string()); } else { cb.add_statement( @@ -405,6 +481,11 @@ fn build_method_body(plan: &OpPlan<'_>, ir: &IrSpec, _error_type: &TypeName) -> } } else { match body.encoding { + BodyEncoding::Json if values.has_union(&body.type_expr) => { + union_json_argument = Some(sigil_quote!(Python { + data = $if(body.required) { json.dumps(_union_body, allow_nan = False) } $else { None if $N(body.var_name.as_str()) is None else json.dumps(_union_body, allow_nan = False) } + }).expect("Python requests union JSON argument builds")); + } BodyEncoding::Json => request_args.push(format!("json={body_expr}")), BodyEncoding::FormUrlEncoded | BodyEncoding::TextPlain @@ -437,17 +518,39 @@ fn build_method_body(plan: &OpPlan<'_>, ir: &IrSpec, _error_type: &TypeName) -> request_args.push("headers=headers".to_string()); } - cb.add_code( + let request_call = if let Some(argument) = union_json_argument { + sigil_quote!(Python { + response = self._client.request($for(arg in &request_args; separator = ", ") { $L(arg.as_str()) }, $C(argument)) + }) + } else { sigil_quote!(Python { response = self._client.request($for(arg in &request_args; separator = ", ") { $L(arg.as_str()) }) }) - .expect("request call"), - ); + }.expect("Python HTTP request call builds"); + cb.add_code(request_call); // Error handling cb.add_code(emit_error_raise(plan, "response.reason")); - let data_expr = if !plan.typed_responses.is_empty() { + let union_response = plan.typed_responses.first().filter(|response| { + response.decoding == ResponseDecoding::Json && values.has_union(&response.type_expr) + }); + if let Some(response) = union_response { + let converted = values.call( + &response.type_expr, + sigil_quote!(Python { response.json() }).expect("Python HTTP union JSON input builds"), + Direction::From, + ); + let ty = values.api_type(&response.type_expr); + let converted = values.coerce(&response.type_expr, ty, converted); + cb.add_code( + sigil_quote!(Python { _union_data = $C(converted) }) + .expect("Python HTTP union response conversion builds"), + ); + } + let data_expr = if union_response.is_some() { + "_union_data".to_string() + } else if !plan.typed_responses.is_empty() { let tr = &plan.typed_responses[0]; render_response_parse(tr, ir) } else { @@ -469,87 +572,6 @@ fn build_method_body(plan: &OpPlan<'_>, ir: &IrSpec, _error_type: &TypeName) -> cb.build().expect("API method body builds") } -fn emit_multipart_data( - cb: &mut sigil_stitch::code_block::CodeBlockBuilder, - body: &BodyBinding, - parts: &[MultipartPart], - ir: &IrSpec, -) { - cb.add_statement("files: dict[str, object] = {}", ()); - if !body.required { - cb.add_statement(&format!("if {} is not None:%>", body.var_name), ()); - } - for part in parts { - let access = format!("{}.{}", body.var_name, part.field_name); - if part.required { - emit_required_multipart_part(cb, part, &access, ir); - } else { - cb.add_statement(&format!("if {access} is not None:%>"), ()); - emit_required_multipart_part(cb, part, &access, ir); - cb.add_statement("%<", ()); - } - } - if !body.required { - cb.add_statement("%<", ()); - } -} - -fn emit_required_multipart_part( - cb: &mut sigil_stitch::code_block::CodeBlockBuilder, - part: &MultipartPart, - access: &str, - ir: &IrSpec, -) { - cb.add_code(multipart_part_assignment(part, access, ir)); -} - -fn multipart_part_assignment(part: &MultipartPart, access: &str, ir: &IrSpec) -> CodeBlock { - let binary_stmt = format!( - "files[\"{}\"] = ({}.filename_or_default(\"{}\"), {}.data, \"{}\")", - part.wire_name, access, part.wire_name, access, part.content_type - ); - let json_value = render_multipart_json_value(access, &part.type_expr, ir); - let json_stmt = format!( - "files[\"{}\"] = (None, json.dumps({json_value}), \"{}\")", - part.wire_name, part.content_type - ); - let unsupported_stmt = "raise ValueError(\"unsupported multipart part content type\")"; - let scalar_stmt = format!( - "files[\"{}\"] = (None, str({access}), \"{}\")", - part.wire_name, part.content_type - ); - - sigil_quote!(Python { - $if(part.is_binary) { - $L(binary_stmt.as_str()) - } $else_if(part.value_encoding == MultipartValueEncoding::Json) { - $L(json_stmt.as_str()) - } $else_if(part.value_encoding == MultipartValueEncoding::Unsupported) { - $L(unsupported_stmt) - } $else { - $L(scalar_stmt.as_str()) - } - }) - .expect("multipart part assignment builds") -} - -fn render_multipart_json_value(access: &str, expr: &IrTypeExpr, ir: &IrSpec) -> String { - match expr { - IrTypeExpr::Named(name) if is_object_schema(name, ir) => format!("{access}.to_dict()"), - IrTypeExpr::Nullable(inner) => render_multipart_json_value(access, inner, ir), - IrTypeExpr::Array(inner) => { - if let IrTypeExpr::Named(name) = inner.as_ref() - && is_object_schema(name, ir) - { - format!("[item.to_dict() for item in {access}]") - } else { - access.to_string() - } - } - _ => access.to_string(), - } -} - fn render_stringify(var: &str, type_expr: &IrTypeExpr) -> String { match type_expr.representation() { IrTypeExpr::Primitive( @@ -575,9 +597,9 @@ fn render_stringify(var: &str, type_expr: &IrTypeExpr) -> String { } } -fn response_type_name(response: &TypedResponse) -> TypeName { +fn response_type_name(response: &TypedResponse, values: &ValuePlan<'_>) -> TypeName { match response.decoding { - ResponseDecoding::Json => api_type_name(&response.type_expr), + ResponseDecoding::Json => values.api_type(&response.type_expr), ResponseDecoding::Text => TypeName::primitive("str"), ResponseDecoding::Bytes => TypeName::primitive("bytes"), } @@ -617,7 +639,11 @@ fn render_json_response_parse(type_expr: &IrTypeExpr, ir: &IrSpec) -> String { } } -fn build_error_classes_block(plans: &[OpPlan<'_>], ir: &IrSpec) -> CodeBlock { +fn build_error_classes_block( + plans: &[OpPlan<'_>], + ir: &IrSpec, + values: &ValuePlan<'_>, +) -> CodeBlock { let mut cb = CodeBlock::builder(); for plan in plans { let mut seen = HashSet::new(); @@ -638,12 +664,12 @@ fn build_error_classes_block(plans: &[OpPlan<'_>], ir: &IrSpec) -> CodeBlock { (TypeName::importable("collections.abc", "Mapping"),), ); if let (Some(type_expr), Some(decoding)) = (&response.type_expr, response.decoding) { - let return_ty = error_body_return_type(type_expr, decoding); + let return_ty = error_body_return_type(type_expr, decoding, values); cb.add_statement("self.raw_body: bytes = raw_body", ()); cb.add_statement("self._body_loaded: bool = False", ()); cb.add_statement("self._body_value: %T | None = None", (return_ty,)); cb.add_statement("self._body_error: Exception | None = None%<", ()); - cb.add_code(error_body_property(type_expr, decoding, ir)); + cb.add_code(error_body_property(type_expr, decoding, ir, values)); } else { cb.add_statement("self.raw_body: bytes = raw_body%<", ()); } @@ -710,11 +736,11 @@ fn build_error_classes_block(plans: &[OpPlan<'_>], ir: &IrSpec) -> CodeBlock { cb.build().expect("Python error classes block builds") } -fn build_response_class(plan: &OpPlan<'_>) -> TypeSpec { +fn build_response_class(plan: &OpPlan<'_>, values: &ValuePlan<'_>) -> TypeSpec { let mut response = TypeSpec::builder(&plan.response_type, TypeKind::Class).extends(type_application( TypeName::importable("..runtime.client", "ApiResponse"), - vec![response_payload_type(plan)], + vec![response_payload_type(plan, values)], )); for accessor in build_header_accessors(&plan.success_headers, false) { response = response.add_method(accessor); @@ -726,7 +752,11 @@ fn error_body_property( type_expr: &IrTypeExpr, decoding: ResponseDecoding, ir: &IrSpec, + values: &ValuePlan<'_>, ) -> CodeBlock { + if decoding == ResponseDecoding::Json && values.has_union(type_expr) { + return union_error_body_property(type_expr, ir, values); + } let mut cb = CodeBlock::builder(); match decoding { ResponseDecoding::Json => { @@ -775,9 +805,13 @@ fn error_body_property( cb.build().expect("Python error body property builds") } -fn error_body_return_type(type_expr: &IrTypeExpr, decoding: ResponseDecoding) -> TypeName { +fn error_body_return_type( + type_expr: &IrTypeExpr, + decoding: ResponseDecoding, + values: &ValuePlan<'_>, +) -> TypeName { match decoding { - ResponseDecoding::Json => api_type_name(type_expr), + ResponseDecoding::Json => values.api_type(type_expr), ResponseDecoding::Text => TypeName::primitive("str"), ResponseDecoding::Bytes => TypeName::primitive("bytes"), } @@ -958,16 +992,6 @@ struct BodyBinding { multipart_parts: Option>, } -struct MultipartPart { - wire_name: String, - field_name: String, - type_expr: IrTypeExpr, - is_binary: bool, - required: bool, - content_type: String, - value_encoding: MultipartValueEncoding, -} - #[derive(Clone, Copy, PartialEq, Eq)] enum BodyEncoding { Json, diff --git a/src/generators/python/requests/emit_models.rs b/src/generators/python/requests/emit_models.rs index 9e1a70c05..3360e84a5 100644 --- a/src/generators/python/requests/emit_models.rs +++ b/src/generators/python/requests/emit_models.rs @@ -1,1173 +1 @@ -//! Model emission for IR schemas (Python dataclasses/enums/aliases). -//! -//! Uses sigil-stitch high-level APIs (TypeSpec, FieldSpec, FunSpec, TypeName, -//! sigil_quote!) for structured code generation with automatic import tracking. -//! Each schema produces one `.py` file via `FileSpec`. - -use crate::codegen::traits::file_writer::FileInfo; -use crate::generators::GeneratorError; -pub use crate::generators::python::type_names::python_type_name; -use crate::generators::python::type_names::python_type_name_for_schema; -use crate::generators::request_inputs::{ - RequestInputField, RequestInputFieldKind, RequestInputModel, RequestInputPlan, -}; -use crate::generators::type_application; -use crate::ir::types::{ - IrEnum, IrEnumValueType, IrIntersection, IrObject, IrPrimitive, IrProperty, IrSchema, - IrSchemaKind, IrSpec, IrTaggedUnion, IrTaggedVariant, IrTypeExpr, IrUnion, TaggingStyle, -}; -use heck::{ToPascalCase, ToSnakeCase}; -use sigil_stitch::code_block::CodeBlock; -use sigil_stitch::lang::python::Python; -use sigil_stitch::prelude::*; - -/// Generate every model file from the IR. -pub fn generate_model_files( - ir: &IrSpec, - header: &str, - request_inputs: &RequestInputPlan, -) -> Result, GeneratorError> { - let mut files = Vec::new(); - for (_name, schema) in &ir.schemas { - let body = - emit_model_body(schema, ir).ok_or_else(|| GeneratorError::UnsupportedSchema { - schema: schema.name.clone(), - kind: format!("{:?}", schema.kind), - })?; - let stem = schema.name.to_snake_case(); - let filename = format!("{stem}.py"); - let mut content = String::with_capacity(header.len() + body.len()); - content.push_str(header); - content.push_str(&body); - files.push(FileInfo::model(filename, content)); - } - for model in request_inputs.models() { - files.push(request_input_model_file(model, header)); - } - Ok(files) -} - -fn request_input_model_file(model: &RequestInputModel, header: &str) -> FileInfo { - let class_name = model.name.to_pascal_case(); - let filename = format!("{}.py", model.name.to_snake_case()); - let mut imports = std::collections::BTreeSet::new(); - let mut needs_upload = false; - for field in &model.fields { - if field.is_upload() { - needs_upload = true; - } else { - collect_request_input_imports(&field.type_expr, &mut imports); - } - } - - let mut content = String::new(); - content.push_str(header); - content.push_str("from __future__ import annotations\n\n"); - content.push_str("from dataclasses import dataclass\n"); - if needs_upload { - content.push_str("from ..runtime import UploadFile\n"); - } - for import in &imports { - content.push_str(import); - content.push('\n'); - } - content.push('\n'); - content.push_str("@dataclass\n"); - content.push_str(&format!("class {class_name}:\n")); - if model.fields.is_empty() { - content.push_str(" pass\n"); - } else { - let required = model.fields.iter().filter(|field| field.required); - let optional = model.fields.iter().filter(|field| !field.required); - for field in required.chain(optional) { - let field_name = python_field_name(&field.wire_name); - let ty = request_input_python_type(field); - if field.required { - content.push_str(&format!(" {field_name}: {ty}\n")); - } else { - content.push_str(&format!(" {field_name}: {ty} | None = None\n")); - } - } - } - - FileInfo::model(filename, content) -} - -fn request_input_python_type(field: &RequestInputField) -> String { - match field.kind { - RequestInputFieldKind::UploadFile { .. } => "UploadFile".to_string(), - RequestInputFieldKind::SchemaValue => python_type_str(&field.type_expr), - } -} - -fn emit_model_body(schema: &IrSchema, ir: &IrSpec) -> Option { - let file_spec = match &schema.kind { - IrSchemaKind::Object(obj) => emit_object(schema, obj, ir), - IrSchemaKind::Enum(en) => emit_enum(schema, en), - IrSchemaKind::Alias(expr) => emit_alias(schema, expr), - IrSchemaKind::Union(u) => emit_union(schema, u), - IrSchemaKind::Intersection(i) => emit_intersection(schema, i, ir), - IrSchemaKind::TaggedUnion(tu) => emit_tagged_union(schema, tu, ir), - }?; - file_spec.render(100).ok() -} - -pub fn future_annotations_header() -> CodeBlock { - CodeBlock::of("from __future__ import annotations", ()).expect("static header") -} - -// --------------------------------------------------------------------------- -// Object -> @dataclass -// --------------------------------------------------------------------------- - -fn emit_object(schema: &IrSchema, obj: &IrObject, ir: &IrSpec) -> Option { - let name = schema.name.to_pascal_case(); - - let mut file = - FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); - - if needs_typing_literal_in_props(&obj.properties) { - file = file.add_import(ImportSpec::named("typing", "Literal")); - } - - // Import tagged-union helper functions for fields referencing TaggedUnion schemas - for (_, prop) in &obj.properties { - for named_ref in collect_tagged_union_refs(&prop.type_expr, ir) { - let snake = named_ref.to_snake_case(); - let module = format!(".{snake}"); - file = file.add_import(ImportSpec::named(&module, &format!("{snake}_from_dict"))); - file = file.add_import(ImportSpec::named(&module, &format!("{snake}_to_dict"))); - } - } - - let dataclass_tn = TypeName::importable("dataclasses", "dataclass"); - let mut cls = TypeSpec::builder(&name, TypeKind::Class) - .annotate(AnnotationSpec::importable(dataclass_tn)); - - if let Some(doc) = &schema.description { - cls = cls.doc(&format!("{}.", escape_docstring(doc))); - } - - let mut required: Vec<(&String, &IrProperty)> = Vec::new(); - let mut optional: Vec<(&String, &IrProperty)> = Vec::new(); - for (json_name, prop) in &obj.properties { - if prop.required && !prop.nullable { - required.push((json_name, prop)); - } else { - optional.push((json_name, prop)); - } - } - - let all_fields: Vec<(&String, &IrProperty)> = - required.iter().chain(optional.iter()).copied().collect(); - - if all_fields.is_empty() { - cls = cls.extra_member(CodeBlock::of("pass", ()).expect("pass")); - } else { - for (_json_name, prop) in &required { - let field_name = python_field_name(&prop.name); - let type_name = python_type_name_for_schema(&prop.type_expr, &schema.name); - cls = cls.add_field( - FieldSpec::builder(&field_name, type_name) - .build() - .expect("required field"), - ); - } - for (_json_name, prop) in &optional { - let field_name = python_field_name(&prop.name); - let type_name = python_type_name_for_schema(&prop.type_expr, &schema.name); - cls = cls.add_field( - FieldSpec::builder(&field_name, TypeName::optional(type_name)) - .initializer(CodeBlock::of("None", ()).expect("None init")) - .build() - .expect("optional field"), - ); - } - - cls = cls.add_method(build_to_dict_method(&all_fields, ir, &obj.properties)); - cls = cls.add_method(build_from_dict_method( - &name, - &all_fields, - ir, - &obj.properties, - )); - } - - file = file.add_type(cls.build().ok()?); - file.build().ok() -} - -fn build_to_dict_method( - all_fields: &[(&String, &IrProperty)], - ir: &IrSpec, - properties: &indexmap::IndexMap, -) -> FunSpec { - let self_param = ParameterSpec::of("self", TypeName::primitive("")); - let return_type = type_application( - TypeName::primitive("dict"), - vec![TypeName::primitive("str"), TypeName::primitive("object")], - ); - - let mut body = CodeBlock::builder(); - body.add_statement("result: dict[str, object] = {}", ()); - for (json_name, prop) in all_fields { - let field_name = python_field_name(&prop.name); - let to_expr = render_to_dict_expr(&format!("self.{field_name}"), json_name, ir, properties); - if prop.required && !prop.nullable { - body.add_statement(&format!("result[\"{json_name}\"] = {to_expr}"), ()); - } else { - body.add_statement(&format!("if self.{field_name} is not None:%>"), ()); - body.add_statement(&format!("result[\"{json_name}\"] = {to_expr}%<"), ()); - } - } - body.add_statement("return result", ()); - - FunSpec::builder("to_dict") - .add_param(self_param) - .returns(vec![return_type]) - .body(body.build().expect("to_dict body")) - .build() - .expect("to_dict method") -} - -fn build_from_dict_method( - class_name: &str, - all_fields: &[(&String, &IrProperty)], - ir: &IrSpec, - properties: &indexmap::IndexMap, -) -> FunSpec { - let cls_param = ParameterSpec::of("cls", TypeName::primitive("")); - let data_param = ParameterSpec::of( - "data", - type_application( - TypeName::primitive("dict"), - vec![TypeName::primitive("str"), TypeName::primitive("object")], - ), - ); - - let mut body = CodeBlock::builder(); - body.add_statement("return cls(%>", ()); - for (json_name, prop) in all_fields { - let field_name = python_field_name(&prop.name); - let is_required = prop.required && !prop.nullable; - let expr = if is_required { - render_from_dict_expr(json_name, ir, properties) - } else { - render_from_dict_optional_expr(json_name, ir, properties) - }; - if let Some(comment_start) = expr.find(" #") { - let (value_part, comment_part) = expr.split_at(comment_start); - body.add_statement(&format!("{field_name}={value_part},{comment_part}"), ()); - } else { - body.add_statement(&format!("{field_name}={expr},"), ()); - } - } - body.add("%<", ()); - body.add_statement(")", ()); - - FunSpec::builder("from_dict") - .annotation(CodeBlock::of("@classmethod", ()).expect("classmethod")) - .add_param(cls_param) - .add_param(data_param) - .returns(vec![TypeName::primitive(class_name)]) - .body(body.build().expect("from_dict body")) - .build() - .expect("from_dict method") -} - -// --------------------------------------------------------------------------- -// Enum -> class(str, Enum) or class(int, Enum) -// --------------------------------------------------------------------------- - -fn emit_enum(schema: &IrSchema, en: &IrEnum) -> Option { - if en.value_type == IrEnumValueType::Mixed { - return emit_type_alias_raw(schema, "object"); - } - - let name = schema.name.to_pascal_case(); - let base = match en.value_type { - IrEnumValueType::String => TypeName::primitive("str"), - IrEnumValueType::Integer | IrEnumValueType::Number => TypeName::primitive("int"), - IrEnumValueType::Mixed => unreachable!(), - }; - - let mut ts = TypeSpec::builder(&name, TypeKind::Enum) - .extends(base) - .extends(TypeName::importable("enum", "Enum")); - - if let Some(doc) = &schema.description { - ts = ts.doc(&format!("{}.", escape_docstring(doc))); - } - - for v in &en.values { - let (member_name, value_code) = match en.value_type { - IrEnumValueType::String => { - let s = v.value.as_str()?; - ( - python_enum_member_name(s), - format!("\"{}\"", escape_python_string(s)), - ) - } - IrEnumValueType::Integer | IrEnumValueType::Number => { - let n = v - .value - .as_i64() - .or_else(|| v.value.as_f64().map(|f| f as i64))?; - (format!("N{n}").replace('-', "NEG"), format!("{n}")) - } - IrEnumValueType::Mixed => unreachable!(), - }; - ts = ts.add_variant( - EnumVariantSpec::builder(&member_name) - .discriminant(CodeBlock::of(&value_code, ()).expect("enum value")) - .build() - .expect("enum variant"), - ); - } - - let file = FileSpec::builder_with("model.py", Python::new()) - .header(future_annotations_header()) - .add_type(ts.build().ok()?); - file.build().ok() -} - -// --------------------------------------------------------------------------- -// Alias -> type X = Y (PEP 695) -// --------------------------------------------------------------------------- - -fn emit_alias(schema: &IrSchema, expr: &IrTypeExpr) -> Option { - let name = schema.name.to_pascal_case(); - let rhs_type = python_type_name(expr); - - let type_alias = sigil_quote!(Python { - type $N(name.as_str()) = ($T(rhs_type)); - }) - .ok()?; - - let mut file = - FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); - if needs_typing_literal(expr) { - file = file.add_import(ImportSpec::named("typing", "Literal")); - } - if let Some(doc) = &schema.description { - file = file.add_raw(&format!("# {}\n", escape_docstring(doc))); - } - file = file.add_code(type_alias); - file.build().ok() -} - -fn emit_type_alias_raw(schema: &IrSchema, rhs: &str) -> Option { - let name = schema.name.to_pascal_case(); - - let type_alias = sigil_quote!(Python { - type $N(name.as_str()) = $L(rhs); - }) - .ok()?; - - let mut file = - FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); - if let Some(doc) = &schema.description { - file = file.add_raw(&format!("# {}\n", escape_docstring(doc))); - } - file = file.add_code(type_alias); - file.build().ok() -} - -// --------------------------------------------------------------------------- -// Type alias builder with proper multi-line formatting -// --------------------------------------------------------------------------- - -fn format_type_alias(name: &str, members: &[TypeName]) -> CodeBlock { - if members.is_empty() { - return sigil_quote!(Python { - type $N(name) = ($T(TypeName::importable("typing", "Any"))); - }) - .unwrap(); - } - if members.len() == 1 { - return sigil_quote!(Python { - type $N(name) = ($T(members[0].clone())); - }) - .unwrap(); - } - sigil_quote!(Python { - type $N(name) = ( - $L(" ")$for(member in members; separator = "\n | ") { $T((*member).clone()) } - ) - }) - .unwrap() -} - -// --------------------------------------------------------------------------- -// Union -> type X = A | B | C -// --------------------------------------------------------------------------- - -fn emit_union(schema: &IrSchema, u: &IrUnion) -> Option { - let name = schema.name.to_pascal_case(); - - let mut members: Vec = u.members.iter().map(python_type_name).collect(); - if u.nullable { - members.push(TypeName::primitive("None")); - } - - let type_alias = format_type_alias(&name, &members); - - let mut file = - FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); - if needs_typing_literal_in_exprs(&u.members) { - file = file.add_import(ImportSpec::named("typing", "Literal")); - } - if let Some(doc) = &schema.description { - file = file.add_raw(&format!("# {}\n", escape_docstring(doc))); - } - file = file.add_code(type_alias); - file.build().ok() -} - -// --------------------------------------------------------------------------- -// Intersection -> merged @dataclass -// --------------------------------------------------------------------------- - -fn emit_intersection(schema: &IrSchema, inter: &IrIntersection, ir: &IrSpec) -> Option { - let mut all_props: indexmap::IndexMap = indexmap::IndexMap::new(); - for member in &inter.members { - if let IrTypeExpr::Named(ref_name) = member - && let Some(s) = ir.schemas.get(ref_name.as_str()) - && let IrSchemaKind::Object(obj) = &s.kind - { - for (k, v) in &obj.properties { - all_props.entry(k.clone()).or_insert_with(|| v.clone()); - } - } - } - - if all_props.is_empty() { - return emit_intersection_as_alias(schema, inter); - } - - emit_intersection_as_dataclass(schema, &all_props, ir) -} - -fn emit_intersection_as_alias(schema: &IrSchema, inter: &IrIntersection) -> Option { - let name = schema.name.to_pascal_case(); - let members: Vec = inter.members.iter().map(python_type_name).collect(); - - let type_alias = format_type_alias(&name, &members); - - let mut file = - FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); - if needs_typing_literal_in_exprs(&inter.members) { - file = file.add_import(ImportSpec::named("typing", "Literal")); - } - file = file.add_code(type_alias); - file.build().ok() -} - -fn emit_intersection_as_dataclass( - schema: &IrSchema, - all_props: &indexmap::IndexMap, - ir: &IrSpec, -) -> Option { - let name = schema.name.to_pascal_case(); - - let mut file = - FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); - - if needs_typing_literal_in_props(all_props) { - file = file.add_import(ImportSpec::named("typing", "Literal")); - } - - let dataclass_tn = TypeName::importable("dataclasses", "dataclass"); - let mut cls = TypeSpec::builder(&name, TypeKind::Class) - .annotate(AnnotationSpec::importable(dataclass_tn)); - - if let Some(doc) = &schema.description { - cls = cls.doc(&format!("{}.", escape_docstring(doc))); - } - - let mut required: Vec<(&String, &IrProperty)> = Vec::new(); - let mut optional: Vec<(&String, &IrProperty)> = Vec::new(); - for (json_name, prop) in all_props { - if prop.required && !prop.nullable { - required.push((json_name, prop)); - } else { - optional.push((json_name, prop)); - } - } - - if required.is_empty() && optional.is_empty() { - cls = cls.extra_member(CodeBlock::of("pass", ()).expect("pass")); - } else { - for (_json_name, prop) in &required { - let field_name = python_field_name(&prop.name); - let type_name = python_type_name_for_schema(&prop.type_expr, &schema.name); - cls = cls.add_field( - FieldSpec::builder(&field_name, type_name) - .build() - .expect("required field"), - ); - } - for (_json_name, prop) in &optional { - let field_name = python_field_name(&prop.name); - let type_name = python_type_name_for_schema(&prop.type_expr, &schema.name); - cls = cls.add_field( - FieldSpec::builder(&field_name, TypeName::optional(type_name)) - .initializer(CodeBlock::of("None", ()).expect("None init")) - .build() - .expect("optional field"), - ); - } - - let all_fields: Vec<(&String, &IrProperty)> = - required.iter().chain(optional.iter()).copied().collect(); - cls = cls.add_method(build_to_dict_method(&all_fields, ir, all_props)); - cls = cls.add_method(build_from_dict_method(&name, &all_fields, ir, all_props)); - } - - file = file.add_type(cls.build().ok()?); - file.build().ok() -} - -// --------------------------------------------------------------------------- -// TaggedUnion -> type X = A | B | C -// --------------------------------------------------------------------------- - -fn emit_tagged_union(schema: &IrSchema, tu: &IrTaggedUnion, ir: &IrSpec) -> Option { - let name = schema.name.to_pascal_case(); - let snake_name = schema.name.to_snake_case(); - - let members: Vec = tu - .variants - .iter() - .map(|v| python_type_name(&v.content_type)) - .collect(); - - let type_alias = format_type_alias(&name, &members); - - let hint = match &tu.tagging { - TaggingStyle::Internal => { - format!("Discriminator: {} (internal).", tu.discriminator_field) - } - TaggingStyle::Adjacent { content_field } => format!( - "Discriminator: {} / content: {} (adjacent).", - tu.discriminator_field, content_field - ), - TaggingStyle::External => "Discriminator: variant key (external).".to_string(), - }; - - let doc = match &schema.description { - Some(desc) => format!("{desc}\n\n{hint}"), - None => hint, - }; - - let mut file = - FileSpec::builder_with("model.py", Python::new()).header(future_annotations_header()); - let exprs: Vec<&IrTypeExpr> = tu.variants.iter().map(|v| &v.content_type).collect(); - if exprs.iter().any(|e| needs_typing_literal(e)) { - file = file.add_import(ImportSpec::named("typing", "Literal")); - } - let mut doc_block = String::new(); - for line in doc.lines() { - doc_block.push_str(&format!("# {line}\n")); - } - file = file.add_raw(&doc_block); - file = file.add_code(type_alias); - - if !tu.variants.is_empty() { - let helpers = build_tagged_union_helpers(&name, &snake_name, tu, ir); - file = file.add_code(helpers); - } - - file.build().ok() -} - -fn build_tagged_union_helpers( - pascal_name: &str, - snake_name: &str, - tu: &IrTaggedUnion, - ir: &IrSpec, -) -> CodeBlock { - let tag_field = &tu.discriminator_field; - - // Only generate helpers for variants that resolve to Object schemas - let resolved_variants: Vec<(&IrTaggedVariant, String)> = tu - .variants - .iter() - .filter_map(|v| { - if let IrTypeExpr::Named(ref_name) = &v.content_type - && is_object_schema(ref_name, ir) - { - return Some((v, ref_name.to_pascal_case())); - } - None - }) - .collect(); - - let mut cb = CodeBlock::builder(); - - if resolved_variants.is_empty() { - return cb.build_unwrap(); - } - - // from_dict - cb.add_line(); - cb.begin_control_flow( - &format!("def {snake_name}_from_dict(data: dict[str, object]) -> {pascal_name}"), - (), - ); - match &tu.tagging { - TaggingStyle::Internal => { - cb.add_statement(&format!("_tag = data[\"{tag_field}\"]"), ()); - for (i, (variant, py_class)) in resolved_variants.iter().enumerate() { - let cond = format!("_tag == \"{}\"", variant.discriminator_value); - emit_elif(&mut cb, i == 0, false, &cond); - cb.add_statement(&format!("return {py_class}.from_dict(data)"), ()); - } - cb.end_control_flow_no_newline(); - } - TaggingStyle::Adjacent { content_field } => { - cb.add_statement(&format!("_tag = data[\"{tag_field}\"]"), ()); - cb.add_statement( - &format!("_content = data[\"{content_field}\"] # type: ignore[assignment]"), - (), - ); - for (i, (variant, py_class)) in resolved_variants.iter().enumerate() { - let cond = format!("_tag == \"{}\"", variant.discriminator_value); - emit_elif(&mut cb, i == 0, false, &cond); - cb.add_statement( - &format!("return {py_class}.from_dict(_content) # type: ignore[arg-type]"), - (), - ); - } - cb.end_control_flow_no_newline(); - } - TaggingStyle::External => { - for (i, (variant, py_class)) in resolved_variants.iter().enumerate() { - let cond = format!("\"{}\" in data", variant.discriminator_value); - emit_elif(&mut cb, i == 0, false, &cond); - cb.add_statement( - &format!( - "return {py_class}.from_dict(data[\"{}\"]) # type: ignore[arg-type]", - variant.discriminator_value - ), - (), - ); - } - cb.end_control_flow_no_newline(); - } - } - cb.add_statement( - "raise ValueError(%V)", - VerbatimStrArg(format!( - "Unknown discriminator value for {pascal_name}: {{data}}" - )), - ); - cb.end_control_flow(); - - // to_dict - cb.add_line(); - cb.begin_control_flow( - &format!("def {snake_name}_to_dict(obj: {pascal_name}) -> dict[str, object]"), - (), - ); - match &tu.tagging { - TaggingStyle::Internal => { - for (i, (variant, py_class)) in resolved_variants.iter().enumerate() { - let cond = format!("isinstance(obj, {py_class})"); - emit_elif(&mut cb, i == 0, false, &cond); - cb.add_statement("result = obj.to_dict()", ()); - cb.add_statement( - &format!( - "result[\"{tag_field}\"] = \"{}\"", - variant.discriminator_value - ), - (), - ); - cb.add_statement("return result", ()); - } - cb.end_control_flow_no_newline(); - } - TaggingStyle::Adjacent { content_field } => { - for (i, (variant, py_class)) in resolved_variants.iter().enumerate() { - let cond = format!("isinstance(obj, {py_class})"); - emit_elif(&mut cb, i == 0, false, &cond); - cb.add_statement( - &format!( - "return {{\"{tag_field}\": \"{}\", \"{content_field}\": obj.to_dict()}}", - variant.discriminator_value - ), - (), - ); - } - cb.end_control_flow_no_newline(); - } - TaggingStyle::External => { - for (i, (variant, py_class)) in resolved_variants.iter().enumerate() { - let cond = format!("isinstance(obj, {py_class})"); - emit_elif(&mut cb, i == 0, false, &cond); - cb.add_statement( - &format!( - "return {{\"{}\": obj.to_dict()}}", - variant.discriminator_value - ), - (), - ); - } - cb.end_control_flow_no_newline(); - } - } - cb.add_statement( - "raise ValueError(%V)", - VerbatimStrArg(format!("Unknown variant for {pascal_name}: {{type(obj)}}")), - ); - cb.end_control_flow(); - - cb.build_unwrap() -} - -fn emit_elif( - cb: &mut sigil_stitch::code_block::CodeBlockBuilder, - is_first: bool, - is_last: bool, - cond: &str, -) { - if !is_first { - cb.end_control_flow_no_newline(); - } - if is_last && !is_first { - cb.begin_control_flow("else", ()); - } else { - let kw = if is_first { "if" } else { "elif" }; - cb.begin_control_flow(&format!("{kw} {cond}"), ()); - } -} - -// --------------------------------------------------------------------------- -// Type mapping -// --------------------------------------------------------------------------- - -/// Like `python_type_name` but Named types import from `..models.{snake}` (for API files). -pub fn api_type_name(expr: &IrTypeExpr) -> TypeName { - match expr.representation() { - IrTypeExpr::Named(name) => { - let py_name = name.to_pascal_case(); - let module = format!("..models.{}", name.to_snake_case()); - TypeName::importable(&module, &py_name) - } - IrTypeExpr::Array(inner) => { - type_application(TypeName::primitive("list"), vec![api_type_name(inner)]) - } - IrTypeExpr::Map(inner) => type_application( - TypeName::primitive("dict"), - vec![TypeName::primitive("str"), api_type_name(inner)], - ), - IrTypeExpr::Union(members) => { - if members.is_empty() { - TypeName::importable("typing", "Any") - } else { - TypeName::union(members.iter().map(api_type_name).collect()) - } - } - IrTypeExpr::Nullable(inner) => TypeName::optional(api_type_name(inner)), - _ => python_type_name(expr), - } -} - -/// Map an IR type expression to a Python type string (for serialization helpers). -pub fn python_type_str(expr: &IrTypeExpr) -> String { - match expr { - IrTypeExpr::ScalarEnum { representation, .. } => python_type_str(representation), - IrTypeExpr::Named(name) => name.to_pascal_case(), - IrTypeExpr::Primitive(p) => python_primitive(p).to_string(), - IrTypeExpr::StringLiteral(s) => { - format!("Literal[\"{}\"]", escape_python_string(s)) - } - IrTypeExpr::StringEnum(values) => { - let members: Vec = values - .iter() - .map(|v| format!("\"{}\"", escape_python_string(v))) - .collect(); - format!("Literal[{}]", members.join(", ")) - } - IrTypeExpr::Array(inner) => { - let inner_ty = python_type_str(inner); - format!("list[{inner_ty}]") - } - IrTypeExpr::Map(inner) => { - let inner_ty = python_type_str(inner); - format!("dict[str, {inner_ty}]") - } - IrTypeExpr::Union(members) => { - let parts: Vec = members.iter().map(python_type_str).collect(); - if parts.is_empty() { - "Any".to_string() - } else { - parts.join(" | ") - } - } - IrTypeExpr::Nullable(inner) => { - let inner_ty = python_type_str(inner); - format!("{inner_ty} | None") - } - IrTypeExpr::Any => "Any".to_string(), - } -} - -fn python_primitive(p: &IrPrimitive) -> &'static str { - match p { - IrPrimitive::String | IrPrimitive::StringWithFormat(_) => "str", - IrPrimitive::Integer | IrPrimitive::IntegerWithFormat(_) => "int", - IrPrimitive::Number | IrPrimitive::NumberWithFormat(_) => "float", - IrPrimitive::Boolean => "bool", - IrPrimitive::Binary => "bytes", - IrPrimitive::Date => "datetime.date", - IrPrimitive::DateTime => "datetime.datetime", - IrPrimitive::Uuid => "uuid.UUID", - } -} - -fn collect_request_input_imports( - expr: &IrTypeExpr, - imports: &mut std::collections::BTreeSet, -) { - match expr.representation() { - IrTypeExpr::Named(name) => { - let py_name = name.to_pascal_case(); - let module = name.to_snake_case(); - imports.insert(format!("from .{module} import {py_name}")); - } - IrTypeExpr::Primitive(IrPrimitive::Date | IrPrimitive::DateTime) => { - imports.insert("import datetime".to_string()); - } - IrTypeExpr::Primitive(IrPrimitive::Uuid) => { - imports.insert("import uuid".to_string()); - } - IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => { - imports.insert("from typing import Literal".to_string()); - } - IrTypeExpr::Union(members) => { - if members.is_empty() { - imports.insert("from typing import Any".to_string()); - } - for member in members { - collect_request_input_imports(member, imports); - } - } - IrTypeExpr::Any => { - imports.insert("from typing import Any".to_string()); - } - IrTypeExpr::Array(inner) | IrTypeExpr::Map(inner) | IrTypeExpr::Nullable(inner) => { - collect_request_input_imports(inner, imports); - } - _ => {} - } -} - -fn needs_typing_literal(expr: &IrTypeExpr) -> bool { - match expr.representation() { - IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => true, - IrTypeExpr::Array(inner) | IrTypeExpr::Map(inner) | IrTypeExpr::Nullable(inner) => { - needs_typing_literal(inner) - } - IrTypeExpr::Union(members) => members.iter().any(needs_typing_literal), - _ => false, - } -} - -fn needs_typing_literal_in_props(props: &indexmap::IndexMap) -> bool { - props.values().any(|p| needs_typing_literal(&p.type_expr)) -} - -fn needs_typing_literal_in_exprs(exprs: &[IrTypeExpr]) -> bool { - exprs.iter().any(needs_typing_literal) -} - -// --------------------------------------------------------------------------- -// Serialization helpers -// --------------------------------------------------------------------------- - -fn render_to_dict_expr( - value_expr: &str, - json_name: &str, - ir: &IrSpec, - properties: &indexmap::IndexMap, -) -> String { - let prop = properties.get(json_name); - let type_expr = prop.map(|p| &p.type_expr); - match type_expr { - Some(IrTypeExpr::Named(ref_name)) => { - if is_object_schema(ref_name, ir) { - format!("{value_expr}.to_dict()") - } else if is_tagged_union_schema(ref_name, ir) { - let snake = ref_name.to_snake_case(); - format!("{snake}_to_dict({value_expr})") - } else { - value_expr.to_string() - } - } - Some(IrTypeExpr::Array(inner)) => { - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_object_schema(ref_name, ir) - { - return format!("[item.to_dict() for item in {value_expr}]"); - } - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_tagged_union_schema(ref_name, ir) - { - let snake = ref_name.to_snake_case(); - return format!("[{snake}_to_dict(item) for item in {value_expr}]"); - } - value_expr.to_string() - } - Some(IrTypeExpr::Nullable(inner)) => { - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_object_schema(ref_name, ir) - { - return format!("{value_expr}.to_dict() if {value_expr} is not None else None"); - } - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_tagged_union_schema(ref_name, ir) - { - let snake = ref_name.to_snake_case(); - return format!( - "{snake}_to_dict({value_expr}) if {value_expr} is not None else None" - ); - } - value_expr.to_string() - } - Some(IrTypeExpr::Map(inner)) => { - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_object_schema(ref_name, ir) - { - return format!("{{k: v.to_dict() for k, v in {value_expr}.items()}}"); - } - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_tagged_union_schema(ref_name, ir) - { - let snake = ref_name.to_snake_case(); - return format!("{{k: {snake}_to_dict(v) for k, v in {value_expr}.items()}}"); - } - value_expr.to_string() - } - _ => value_expr.to_string(), - } -} - -fn render_from_dict_expr( - json_name: &str, - ir: &IrSpec, - properties: &indexmap::IndexMap, -) -> String { - let prop = properties.get(json_name); - let type_expr = prop.map(|p| &p.type_expr); - let accessor = format!("data[\"{json_name}\"]"); - match type_expr { - Some(IrTypeExpr::Named(ref_name)) => { - if is_object_schema(ref_name, ir) { - let py_name = ref_name.to_pascal_case(); - format!("{py_name}.from_dict({accessor}) # type: ignore[arg-type]") - } else if is_tagged_union_schema(ref_name, ir) { - let snake = ref_name.to_snake_case(); - format!("{snake}_from_dict({accessor}) # type: ignore[arg-type]") - } else { - format!("{accessor} # type: ignore[assignment]") - } - } - Some(IrTypeExpr::Array(inner)) => { - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_object_schema(ref_name, ir) - { - let py_name = ref_name.to_pascal_case(); - return format!( - "[{py_name}.from_dict(item) for item in {accessor}] # type: ignore[union-attr]" - ); - } - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_tagged_union_schema(ref_name, ir) - { - let snake = ref_name.to_snake_case(); - return format!( - "[{snake}_from_dict(item) for item in {accessor}] # type: ignore[union-attr]" - ); - } - format!("{accessor} # type: ignore[assignment]") - } - Some(IrTypeExpr::Map(inner)) => { - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_object_schema(ref_name, ir) - { - let py_name = ref_name.to_pascal_case(); - return format!( - "{{k: {py_name}.from_dict(v) for k, v in {accessor}.items()}} # type: ignore[union-attr]" - ); - } - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_tagged_union_schema(ref_name, ir) - { - let snake = ref_name.to_snake_case(); - return format!( - "{{k: {snake}_from_dict(v) for k, v in {accessor}.items()}} # type: ignore[union-attr]" - ); - } - format!("{accessor} # type: ignore[assignment]") - } - _ => format!("{accessor} # type: ignore[assignment]"), - } -} - -fn render_from_dict_optional_expr( - json_name: &str, - ir: &IrSpec, - properties: &indexmap::IndexMap, -) -> String { - let prop = properties.get(json_name); - let type_expr = prop.map(|p| &p.type_expr); - let raw_type = type_expr.map(|t| match t { - IrTypeExpr::Nullable(inner) => inner.as_ref(), - _ => t, - }); - let accessor = format!("data.get(\"{json_name}\")"); - match raw_type { - Some(IrTypeExpr::Named(ref_name)) => { - if is_object_schema(ref_name, ir) { - let py_name = ref_name.to_pascal_case(); - format!( - "{py_name}.from_dict({accessor}) if {accessor} is not None else None # type: ignore[arg-type]" - ) - } else if is_tagged_union_schema(ref_name, ir) { - let snake = ref_name.to_snake_case(); - format!( - "{snake}_from_dict({accessor}) if {accessor} is not None else None # type: ignore[arg-type]" - ) - } else { - format!("{accessor} # type: ignore[assignment]") - } - } - Some(IrTypeExpr::Array(inner)) => { - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_object_schema(ref_name, ir) - { - let py_name = ref_name.to_pascal_case(); - return format!( - "[{py_name}.from_dict(item) for item in {accessor}] if {accessor} is not None else None # type: ignore[union-attr]" - ); - } - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_tagged_union_schema(ref_name, ir) - { - let snake = ref_name.to_snake_case(); - return format!( - "[{snake}_from_dict(item) for item in {accessor}] if {accessor} is not None else None # type: ignore[union-attr]" - ); - } - format!("{accessor} # type: ignore[assignment]") - } - Some(IrTypeExpr::Map(inner)) => { - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_object_schema(ref_name, ir) - { - let py_name = ref_name.to_pascal_case(); - return format!( - "{{k: {py_name}.from_dict(v) for k, v in {accessor}.items()}} if {accessor} is not None else None # type: ignore[union-attr]" - ); - } - if let IrTypeExpr::Named(ref_name) = inner.as_ref() - && is_tagged_union_schema(ref_name, ir) - { - let snake = ref_name.to_snake_case(); - return format!( - "{{k: {snake}_from_dict(v) for k, v in {accessor}.items()}} if {accessor} is not None else None # type: ignore[union-attr]" - ); - } - format!("{accessor} # type: ignore[assignment]") - } - _ => format!("{accessor} # type: ignore[assignment]"), - } -} - -pub fn is_object_schema(name: &str, ir: &IrSpec) -> bool { - ir.schemas.get(name).is_some_and(|s| match &s.kind { - IrSchemaKind::Object(_) => true, - IrSchemaKind::Intersection(inter) => inter.members.iter().any(|m| { - if let IrTypeExpr::Named(ref_name) = m { - ir.schemas - .get(ref_name.as_str()) - .is_some_and(|ms| matches!(ms.kind, IrSchemaKind::Object(_))) - } else { - false - } - }), - _ => false, - }) -} - -pub fn is_tagged_union_schema(name: &str, ir: &IrSpec) -> bool { - ir.schemas - .get(name) - .is_some_and(|s| matches!(s.kind, IrSchemaKind::TaggedUnion(_))) -} - -/// Collect all tagged-union schema names referenced (directly or nested) in a type expression. -fn collect_tagged_union_refs(expr: &IrTypeExpr, ir: &IrSpec) -> Vec { - let mut refs = Vec::new(); - match expr { - IrTypeExpr::Named(name) => { - if is_tagged_union_schema(name, ir) { - refs.push(name.clone()); - } - } - IrTypeExpr::Array(inner) | IrTypeExpr::Nullable(inner) => { - refs.extend(collect_tagged_union_refs(inner, ir)); - } - IrTypeExpr::Map(inner) => { - refs.extend(collect_tagged_union_refs(inner, ir)); - } - _ => {} - } - refs -} - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -pub fn python_field_name(name: &str) -> String { - let snake = name.to_snake_case(); - if snake.is_empty() { - return "field_".to_string(); - } - match snake.as_str() { - "and" | "as" | "assert" | "async" | "await" | "break" | "class" | "continue" | "def" - | "del" | "elif" | "else" | "except" | "finally" | "for" | "from" | "global" | "if" - | "import" | "in" | "is" | "lambda" | "nonlocal" | "not" | "or" | "pass" | "raise" - | "return" | "try" | "while" | "with" | "yield" | "type" => { - format!("{snake}_") - } - _ => snake, - } -} - -fn python_enum_member_name(value: &str) -> String { - let upper = value - .to_uppercase() - .replace(|c: char| !c.is_alphanumeric(), "_"); - if upper.is_empty() { - return "EMPTY".to_string(); - } - if upper.starts_with(|c: char| c.is_ascii_digit()) { - return format!("N{upper}"); - } - upper -} - -fn escape_python_string(s: &str) -> String { - s.replace('\\', "\\\\").replace('"', "\\\"") -} - -fn escape_docstring(s: &str) -> String { - s.replace("\"\"\"", "\\\"\\\"\\\"") - .lines() - .next() - .unwrap_or("") - .to_string() -} +pub use crate::generators::python::emit_models::*; diff --git a/src/generators/python/runtime/union_support.py.txt b/src/generators/python/runtime/union_support.py.txt new file mode 100644 index 000000000..bf1d91ea3 --- /dev/null +++ b/src/generators/python/runtime/union_support.py.txt @@ -0,0 +1,86 @@ +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/src/generators/python/value.rs b/src/generators/python/value.rs new file mode 100644 index 000000000..c48d38c16 --- /dev/null +++ b/src/generators/python/value.rs @@ -0,0 +1,1017 @@ +//! Python value conversion at untagged-union boundaries, shared by both transports. +//! +//! Matching consumes the target-independent shape plan. Codecs keep original +//! member boundaries: count candidates first, then invoke exactly one delegate. + +use std::borrow::Cow; +use std::collections::BTreeSet; + +use heck::{ToPascalCase, ToSnakeCase}; +use serde::{Deserialize, Serialize}; +use sigil_stitch::error::SigilStitchError; +use sigil_stitch::lang::python::Python; +use sigil_stitch::prelude::*; + +use super::emit_models::{is_object_schema, python_field_name}; +use super::type_names::python_type_name_for_schema; +use crate::codegen::traits::file_writer::FileInfo; +use crate::generators::union_match::{self, JsonShape, MatchScope, UnionMemberMatch}; +use crate::generators::{ArtifactKind, GeneratorError, type_application}; +use crate::ir::types::{IrSchemaKind, IrSpec, IrTypeExpr}; + +pub(super) fn optional_type(ty: TypeName) -> TypeName { + if matches!(ty, TypeName::Optional(_)) { + ty + } else { + TypeName::optional(ty) + } +} + +/// Policy for untagged `oneOf` and `anyOf` member conversion. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum UntaggedUnionStrategy { + /// Prefer concrete members in schema order; preserve unmatched wire values. + #[default] + FirstMatch, + /// Reject multiple concrete matches and values with no matching member. + Strict, + /// Leave union subtrees dynamically typed and unconverted. + Raw, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub(super) enum Direction { + From, + To, +} + +type ShapeReferences = BTreeSet<(String, Direction)>; + +impl Direction { + fn suffix(self) -> &'static str { + match self { + Self::From => "from_dict", + Self::To => "to_dict", + } + } +} + +/// A closed, deterministic graph of conversion entrypoints. Internal functions +/// use `object`; public call sites retain their semantic model/alias types. +pub(super) struct ValuePlan<'a> { + ir: &'a IrSpec, + public_shapes: Cow<'a, IrSpec>, + pub strategy: UntaggedUnionStrategy, + expressions: Vec, +} + +impl<'a> ValuePlan<'a> { + pub fn new(ir: &'a IrSpec, strategy: UntaggedUnionStrategy) -> Self { + // Python exposes tagged payloads directly, not their wire wrappers. + // Reuse the shared shape planner against this selection-only view so + // aliases, nested properties, containers and productive references all + // match that public representation. The source IR and codecs stay intact. + let mut public_shapes = Cow::Borrowed(ir); + if ir + .schemas + .values() + .any(|schema| matches!(schema.kind, IrSchemaKind::TaggedUnion(_))) + { + for schema in public_shapes.to_mut().schemas.values_mut() { + if let IrSchemaKind::TaggedUnion(union) = &schema.kind { + schema.kind = IrSchemaKind::Alias(IrTypeExpr::Union( + union + .variants + .iter() + .map(|variant| variant.content_type.clone()) + .collect(), + )); + } + } + } + let mut plan = Self { + ir, + public_shapes, + strategy, + expressions: Vec::new(), + }; + for schema in ir.schemas.values() { + if let IrSchemaKind::Object(object) = &schema.kind { + for property in object.properties.values() { + if plan.has_union(&property.type_expr) { + plan.register(&plan.property_expr(property)); + } + } + } + if plan.has_union(&IrTypeExpr::Named(schema.name.clone())) { + plan.register(&IrTypeExpr::Named(schema.name.clone())); + } + for expr in schema_expressions(&schema.kind) { + if plan.has_union(expr) { + plan.register(expr); + } + } + } + for operation in &ir.operations { + let request = operation + .request_body + .iter() + .flat_map(|body| body.content.values()); + let responses = operation + .responses + .iter() + .flat_map(|response| response.content.values()); + for expr in request.chain(responses) { + if plan.has_union(expr) { + plan.register(expr); + } + } + } + plan + } + + pub fn has_union(&self, expr: &IrTypeExpr) -> bool { + fn visit(expr: &IrTypeExpr, ir: &IrSpec, seen: &mut BTreeSet) -> bool { + match expr { + IrTypeExpr::Union(_) => true, + IrTypeExpr::ScalarEnum { representation, .. } => visit(representation, ir, seen), + IrTypeExpr::Array(inner) | IrTypeExpr::Map(inner) | IrTypeExpr::Nullable(inner) => { + visit(inner, ir, seen) + } + IrTypeExpr::Named(name) if seen.insert(name.clone()) => { + match ir.schemas.get(name).map(|schema| &schema.kind) { + Some(IrSchemaKind::Union(_)) => true, + Some(IrSchemaKind::Alias(inner)) => visit(inner, ir, seen), + Some(IrSchemaKind::TaggedUnion(union)) => union + .variants + .iter() + .any(|variant| visit(&variant.content_type, ir, seen)), + _ => false, + } + } + _ => false, + } + } + visit(expr, self.ir, &mut BTreeSet::new()) + } + + pub fn property_expr(&self, property: &crate::ir::types::IrProperty) -> IrTypeExpr { + if property.nullable + && !union_match::accepts_null(&property.type_expr, self.ir) + && !matches!( + property.type_expr, + IrTypeExpr::ScalarEnum { .. } + | IrTypeExpr::StringLiteral(_) + | IrTypeExpr::StringEnum(_) + ) + { + IrTypeExpr::Nullable(Box::new(property.type_expr.clone())) + } else { + property.type_expr.clone() + } + } + + fn register(&mut self, expr: &IrTypeExpr) { + if self.expressions.contains(expr) { + return; + } + self.expressions.push(expr.clone()); + match expr { + IrTypeExpr::ScalarEnum { .. } => self.register(&conversion_representation(expr)), + IrTypeExpr::Array(inner) | IrTypeExpr::Map(inner) | IrTypeExpr::Nullable(inner) => { + self.register(inner) + } + IrTypeExpr::Union(members) => { + for member in members { + self.register(member); + } + } + IrTypeExpr::Named(name) => match self.ir.schemas.get(name).map(|schema| &schema.kind) { + Some(IrSchemaKind::Union(union)) => { + for member in &union.members { + self.register(member); + } + } + Some(IrSchemaKind::Alias(inner)) => self.register(inner), + Some(IrSchemaKind::TaggedUnion(union)) => { + for variant in &union.variants { + self.register(&variant.content_type); + } + } + _ => {} + }, + _ => {} + } + } + + fn function(&self, expr: &IrTypeExpr, direction: Direction) -> String { + let index = self + .expressions + .iter() + .position(|candidate| candidate == expr) + .expect("union conversion expression belongs to the value plan"); + format!("value_{index}_{}", direction.suffix()) + } + + pub fn call(&self, expr: &IrTypeExpr, source: CodeBlock, direction: Direction) -> CodeBlock { + let function = self.function(expr, direction); + sigil_quote!(Python { _union.$N(function)($C(source)) }) + .expect("Python union conversion call builds") + } + + pub fn is_dynamic(&self, expr: &IrTypeExpr) -> bool { + fn visit(plan: &ValuePlan<'_>, expr: &IrTypeExpr, seen: &mut BTreeSet) -> bool { + match expr { + IrTypeExpr::Any => true, + IrTypeExpr::Union(members) => { + plan.strategy == UntaggedUnionStrategy::Raw + || members.is_empty() + || members.iter().any(|member| visit(plan, member, seen)) + } + IrTypeExpr::Nullable(inner) => visit(plan, inner, seen), + IrTypeExpr::ScalarEnum { + representation, + representation_is_constraint, + .. + } => !representation_is_constraint || visit(plan, representation, seen), + IrTypeExpr::Named(name) if seen.insert(name.clone()) => { + match plan.ir.schemas.get(name).map(|schema| &schema.kind) { + Some(IrSchemaKind::Union(union)) => { + plan.strategy == UntaggedUnionStrategy::Raw + || union.members.is_empty() + || union.members.iter().any(|member| visit(plan, member, seen)) + } + Some(IrSchemaKind::Alias(inner)) => visit(plan, inner, seen), + Some(IrSchemaKind::Enum(enumeration)) => { + enumeration.value_type == crate::ir::types::IrEnumValueType::Mixed + } + Some(IrSchemaKind::TaggedUnion(union)) => union + .variants + .iter() + .any(|variant| visit(plan, &variant.content_type, seen)), + _ => false, + } + } + _ => false, + } + } + visit(self, expr, &mut BTreeSet::new()) + } + + pub fn coerce(&self, expr: &IrTypeExpr, target: TypeName, source: CodeBlock) -> CodeBlock { + if self.is_dynamic(expr) { + return source; + } + sigil_quote!(Python { $T(TypeName::importable("typing", "cast"))($T(target), $C(source)) }) + .expect("Python typed union projection builds") + } + + /// Raw inline unions are explicitly dynamic. Productive cycles reference + /// modules instead of partially initialized classes; PEP 695 and postponed + /// annotations defer the member lookup until every model is defined. + pub fn model_type(&self, expr: &IrTypeExpr, current: &str) -> TypeName { + self.type_name(expr, Some(current)) + } + + pub fn api_type(&self, expr: &IrTypeExpr) -> TypeName { + self.type_name(expr, None) + } + + fn type_name(&self, expr: &IrTypeExpr, current: Option<&str>) -> TypeName { + match expr { + IrTypeExpr::Union(_) if self.strategy == UntaggedUnionStrategy::Raw => { + TypeName::primitive("object") + } + IrTypeExpr::Array(inner) => type_application( + TypeName::primitive("list"), + vec![self.type_name(inner, current)], + ), + IrTypeExpr::Map(inner) => type_application( + TypeName::primitive("dict"), + vec![TypeName::primitive("str"), self.type_name(inner, current)], + ), + IrTypeExpr::Nullable(inner) => optional_type(self.type_name(inner, current)), + IrTypeExpr::ScalarEnum { + representation, + representation_is_constraint, + representation_nullable, + .. + } => { + let ty = if *representation_is_constraint { + self.type_name(representation, current) + } else { + TypeName::primitive("object") + }; + if *representation_nullable { + optional_type(ty) + } else { + ty + } + } + IrTypeExpr::Union(members) if !members.is_empty() => TypeName::union( + members + .iter() + .map(|member| self.type_name(member, current)) + .collect(), + ), + IrTypeExpr::Named(name) + if !self.expressions.is_empty() + && current.is_some_and(|current| { + name != current && self.reaches(name, current, &mut BTreeSet::new()) + }) => + { + TypeName::associated_type( + TypeName::importable(".", &name.to_snake_case()), + None, + &name.to_pascal_case(), + ) + } + _ => match current { + Some(current) => python_type_name_for_schema(expr, current), + None => super::emit_models::api_type_name(expr), + }, + } + } + + fn matching_ir(&self, direction: Direction) -> &IrSpec { + match direction { + Direction::From => self.ir, + Direction::To => &self.public_shapes, + } + } + + fn reaches(&self, from: &str, target: &str, seen: &mut BTreeSet) -> bool { + if from == target { + return true; + } + if !seen.insert(from.to_string()) { + return false; + } + let Some(schema) = self.ir.schemas.get(from) else { + return false; + }; + fn names(expr: &IrTypeExpr, out: &mut Vec) { + match expr { + IrTypeExpr::Named(name) => out.push(name.clone()), + IrTypeExpr::ScalarEnum { representation, .. } => names(representation, out), + IrTypeExpr::Array(inner) | IrTypeExpr::Map(inner) | IrTypeExpr::Nullable(inner) => { + names(inner, out) + } + IrTypeExpr::Union(members) => { + for member in members { + names(member, out); + } + } + _ => {} + } + } + let mut refs = Vec::new(); + for expr in schema_expressions(&schema.kind) { + names(expr, &mut refs); + } + refs.iter().any(|name| self.reaches(name, target, seen)) + } + + pub fn files(&self, header: &str) -> Result, GeneratorError> { + if self.expressions.is_empty() { + return Ok(Vec::new()); + } + let render = || -> Result { + let mut file = FileSpec::builder_with("_union.py", Python::new()) + .header(super::emit_models::future_annotations_header()) + .add_import(ImportSpec::named("..runtime", "_union_support")); + let mut references = BTreeSet::new(); + for expr in &self.expressions { + for direction in [Direction::From, Direction::To] { + file = file.add_function( + FunSpec::builder(&self.function(expr, direction)) + .add_param(ParameterSpec::of("value", TypeName::primitive("object"))) + .returns(vec![TypeName::primitive("object")]) + .body(self.body(expr, direction, &mut references)?) + .build()?, + ); + } + } + let mut done = BTreeSet::new(); + while let Some((name, direction)) = references.pop_first() { + if !done.insert((name.clone(), direction)) { + continue; + } + let shape = union_match::plan_named_shape(&name, self.matching_ir(direction)); + let (concrete, fallback) = union_match::selection_shapes(shape.clone()); + for (scope, shape) in [ + (MatchScope::Any, shape), + (MatchScope::Concrete, concrete), + (MatchScope::Fallback, fallback), + ] { + let condition = condition( + &shape, + sigil_quote!(Python { value })?, + direction, + &mut references, + )?; + file = file.add_function( + FunSpec::builder(&shape_function(&name, scope, direction)) + .add_param(ParameterSpec::of("value", TypeName::primitive("object"))) + .returns(vec![TypeName::primitive("bool")]) + .body(sigil_quote!(Python { return $C(condition) })?) + .build()?, + ); + } + } + file.build()?.render(100) + }; + let body = render().map_err(|error| { + GeneratorError::emission(ArtifactKind::Model, "Python union conversion", error) + })?; + Ok(vec![ + FileInfo::model("_union.py".to_string(), format!("{header}{body}")), + FileInfo::runtime( + "_union_support.py".to_string(), + format!("{header}{}", include_str!("runtime/union_support.py.txt")), + ), + ]) + } + + fn invoke( + &self, + expr: &IrTypeExpr, + value: CodeBlock, + direction: Direction, + ) -> Result { + let function = self.function(expr, direction); + sigil_quote!(Python { $N(function)($C(value)) }) + } + + fn body( + &self, + expr: &IrTypeExpr, + direction: Direction, + references: &mut ShapeReferences, + ) -> Result { + let value = sigil_quote!(Python { value })?; + match expr { + IrTypeExpr::Union(members) => { + self.union_body(members, false, None, direction, references) + } + IrTypeExpr::Named(name) => match self.ir.schemas.get(name).map(|schema| &schema.kind) { + Some(IrSchemaKind::Union(union)) => self.union_body( + &union.members, + union.nullable, + Some(name), + direction, + references, + ), + Some(IrSchemaKind::Alias(inner)) => { + let conversion = self.invoke(inner, value, direction)?; + sigil_quote!(Python { return $C(conversion) }) + } + Some(IrSchemaKind::TaggedUnion(union)) if self.has_union(expr) => { + self.tagged_body(union, direction, references) + } + Some(IrSchemaKind::TaggedUnion(_)) => { + let module = name.to_snake_case(); + let helper = format!("{module}_{}", direction.suffix()); + let value = match direction { + Direction::From => sigil_quote!(Python { _union_support.as_dict(value) })?, + Direction::To => self.coerce( + expr, + // This module is imported locally below, not at file + // scope: keep its type reference lexical as well. + TypeName::associated_type( + TypeName::primitive(&module), + None, + &name.to_pascal_case(), + ), + value, + ), + }; + sigil_quote!(Python { + from . import $N(module.as_str()) + return $N(module).$N(helper)($C(value)) + }) + } + Some(_) if is_object_schema(name, self.ir) => match direction { + Direction::From => { + let module = name.to_snake_case(); + let class = name.to_pascal_case(); + // The shared helper adds dependency edges even for an + // acyclic schema. Load a model only after selection, when + // its containing module has finished initialization. + sigil_quote!(Python { + from . import $N(module.as_str()) + return $N(module).$N(class).from_dict(_union_support.as_dict(value)) + }) + } + Direction::To => { + sigil_quote!(Python { return _union_support.model_to_dict(value) }) + } + }, + Some(IrSchemaKind::Enum(enumeration)) + if !matches!( + enumeration.value_type, + crate::ir::types::IrEnumValueType::Mixed + ) => + { + match direction { + Direction::From => { + let module = name.to_snake_case(); + let class = name.to_pascal_case(); + sigil_quote!(Python { + from . import $N(module.as_str()) + return $N(module).$N(class)(value) + }) + } + Direction::To => { + sigil_quote!(Python { return _union_support.scalar(value) }) + } + } + } + _ => sigil_quote!(Python { return value }), + }, + IrTypeExpr::Array(inner) => { + let item = self.invoke(inner, sigil_quote!(Python { item })?, direction)?; + sigil_quote!(Python { return [$C(item) for item in _union_support.as_list(value)] }) + } + IrTypeExpr::Map(inner) => { + let item = self.invoke(inner, sigil_quote!(Python { item })?, direction)?; + sigil_quote!(Python { return {key: $C(item) for key, item in _union_support.as_dict(value).items()} }) + } + IrTypeExpr::Nullable(inner) => { + let conversion = self.invoke(inner, value, direction)?; + if union_match::accepts_null(inner, self.ir) { + sigil_quote!(Python { return $C(conversion) }) + } else { + sigil_quote!(Python { return None if value is None else $C(conversion) }) + } + } + IrTypeExpr::ScalarEnum { .. } => { + let conversion = self.invoke(&conversion_representation(expr), value, direction)?; + sigil_quote!(Python { return $C(conversion) }) + } + _ => sigil_quote!(Python { return _union_support.scalar(value) }), + } + } + + fn union_body( + &self, + members: &[IrTypeExpr], + nullable: bool, + schema: Option<&str>, + direction: Direction, + references: &mut ShapeReferences, + ) -> Result { + if self.strategy == UntaggedUnionStrategy::Raw { + return sigil_quote!(Python { return value }); + } + let label = schema.unwrap_or("inline"); + let candidates = if let Some(name) = schema { + let IrSchemaKind::Union(union) = &self.ir.schemas[name].kind else { + unreachable!("named union helper owns a union schema") + }; + union_match::plan_union_members(name, union, self.matching_ir(direction)) + } else { + union_match::plan_inline_union_members(members, self.matching_ir(direction)) + }; + let strict = self.strategy == UntaggedUnionStrategy::Strict; + let mut checks = Vec::new(); + let mut concrete = Vec::new(); + let mut fallbacks = Vec::new(); + for UnionMemberMatch { + type_expr, + shape, + fallback, + } in candidates + { + let check = condition( + &shape, + sigil_quote!(Python { value })?, + direction, + references, + )?; + let check = if strict && !fallback { + let index = checks.len().to_string(); + checks.push(check); + sigil_quote!(Python { matches[$L(index)] })? + } else { + check + }; + let convert = self.invoke(&type_expr, sigil_quote!(Python { value })?, direction)?; + let branch = sigil_quote!(Python { + if $C(check) { + return $C(convert) + } + })?; + if fallback { + fallbacks.push(branch); + } else { + concrete.push(branch); + } + } + let ambiguous = format!("Ambiguous {label} untagged union value"); + let unmatched = format!("No matching {label} untagged union member"); + sigil_quote!(Python { + $if(strict && !checks.is_empty()) { + matches = [$for(check in &checks; separator = ", ") { $C(check.clone()) }] + if sum(matches) > 1 { + raise ValueError($S(ambiguous)) + } + } + $C_each(concrete) + $if(nullable) { + if value is None { + return None + } + } + $C_each(fallbacks) + $if(strict) { raise ValueError($S(unmatched)) } $else { return value } + }) + } + + fn tagged_body( + &self, + union: &crate::ir::types::IrTaggedUnion, + direction: Direction, + references: &mut ShapeReferences, + ) -> Result { + use crate::ir::types::TaggingStyle; + let mut branches = Vec::new(); + for variant in &union.variants { + let tag = &union.discriminator_field; + let label = &variant.discriminator_value; + let source = match (&union.tagging, direction) { + (TaggingStyle::Adjacent { content_field }, Direction::From) => { + sigil_quote!(Python { data[$S(content_field.as_str())] })? + } + (TaggingStyle::External, Direction::From) => { + sigil_quote!(Python { data[$S(label.as_str())] })? + } + _ => sigil_quote!(Python { value })?, + }; + let converted = self.invoke(&variant.content_type, source, direction)?; + let guard = match direction { + Direction::From => match &union.tagging { + TaggingStyle::External => sigil_quote!(Python { $S(label.as_str()) in data })?, + _ => sigil_quote!(Python { data.get($S(tag.as_str())) == $S(label.as_str()) })?, + }, + Direction::To => { + let shapes = union_match::plan_inline_union_members( + std::slice::from_ref(&variant.content_type), + self.matching_ir(direction), + ); + let checks = shapes + .iter() + .map(|member| { + condition( + &member.shape, + sigil_quote!(Python { value })?, + direction, + references, + ) + }) + .collect::, _>>()?; + join_conditions(&checks, true)? + } + }; + let result = match (&union.tagging, direction) { + (_, Direction::From) => sigil_quote!(Python { return $C(converted) })?, + (TaggingStyle::Internal, Direction::To) => sigil_quote!(Python { + result = dict(_union_support.as_dict($C(converted))) + result[$S(tag.as_str())] = $S(label.as_str()) + return result + })?, + (TaggingStyle::Adjacent { content_field }, Direction::To) => sigil_quote!(Python { + return {$S(tag.as_str()): $S(label.as_str()), $S(content_field.as_str()): $C(converted)} + })?, + (TaggingStyle::External, Direction::To) => { + sigil_quote!(Python { return {$S(label.as_str()): $C(converted)} })? + } + }; + branches.push(sigil_quote!(Python { + if $C(guard) { $C(result) } + })?); + } + sigil_quote!(Python { + $if(matches!(direction, Direction::From)) { data = _union_support.as_dict(value) } + $C_each(branches) + raise ValueError("No matching tagged union variant") + }) + } +} + +fn schema_expressions(kind: &IrSchemaKind) -> Vec<&IrTypeExpr> { + match kind { + IrSchemaKind::Object(object) => object + .properties + .values() + .map(|property| &property.type_expr) + .chain(object.additional_properties.iter()) + .collect(), + IrSchemaKind::Alias(inner) => vec![inner], + IrSchemaKind::Union(union) => union.members.iter().collect(), + IrSchemaKind::Intersection(intersection) => intersection.members.iter().collect(), + IrSchemaKind::TaggedUnion(union) => union + .variants + .iter() + .map(|variant| &variant.content_type) + .collect(), + IrSchemaKind::Enum(_) => Vec::new(), + } +} + +fn conversion_representation(expr: &IrTypeExpr) -> IrTypeExpr { + let IrTypeExpr::ScalarEnum { + representation, + representation_nullable, + .. + } = expr + else { + return expr.clone(); + }; + // Restore only this lowering boundary. Inner finite facts still determine + // authored null matches and nested strict errors after delegation. + if *representation_nullable { + IrTypeExpr::Nullable(representation.clone()) + } else { + representation.as_ref().clone() + } +} + +fn shape_function(name: &str, scope: MatchScope, direction: Direction) -> String { + let suffix = match scope { + MatchScope::Any => "any", + MatchScope::Concrete => "concrete", + MatchScope::Fallback => "fallback", + }; + format!( + "match_{}_{suffix}_{}", + name.to_snake_case(), + direction.suffix() + ) +} + +fn condition( + shape: &JsonShape, + value: CodeBlock, + direction: Direction, + references: &mut ShapeReferences, +) -> Result { + match shape { + JsonShape::Any => sigil_quote!(Python { True }), + JsonShape::Never => sigil_quote!(Python { False }), + JsonShape::Null => sigil_quote!(Python { $C(value) is None }), + JsonShape::Boolean => sigil_quote!(Python { isinstance($C(value), bool) }), + JsonShape::String => sigil_quote!(Python { isinstance($C(value), str) }), + JsonShape::Binary => sigil_quote!(Python { isinstance($C(value), bytes) }), + JsonShape::Number { integer } => { + sigil_quote!(Python { _union_support.number($C(value), $if(*integer) { True } $else { False }) }) + } + JsonShape::Literal(literal) => { + let literal = python_literal(literal)?; + sigil_quote!(Python { _union_support.equal($C(value), $C(literal)) }) + } + JsonShape::Nullable(inner) => { + let inner = condition(inner, value.clone(), direction, references)?; + sigil_quote!(Python { ($C(value) is None or $C(inner)) }) + } + JsonShape::Array(inner) => { + let inner = condition( + inner, + sigil_quote!(Python { _item })?, + direction, + references, + )?; + sigil_quote!(Python { (_union_support.is_list($C(value.clone())) and all($C(inner) for _item in _union_support.as_list($C(value)))) }) + } + JsonShape::Map(inner) => { + let inner = condition( + inner, + sigil_quote!(Python { _item })?, + direction, + references, + )?; + sigil_quote!(Python { (_union_support.is_dict($C(value.clone())) and all($C(inner) for _item in _union_support.as_dict($C(value)).values())) }) + } + JsonShape::MapWithConcreteValue { values, concrete } => { + let all = condition( + &JsonShape::Map(values.clone()), + value.clone(), + direction, + references, + )?; + let one = condition( + concrete, + sigil_quote!(Python { _item })?, + direction, + references, + )?; + sigil_quote!(Python { ($C(all) and any($C(one) for _item in _union_support.as_dict($C(value)).values())) }) + } + JsonShape::Object(properties) | JsonShape::ObjectWithAdditional { properties, .. } => { + let mut checks = + vec![sigil_quote!(Python { _union_support.is_object($C(value.clone())) })?]; + for property in properties { + let field = python_field_name(&property.wire_name); + let wire = &property.wire_name; + let present = sigil_quote!(Python { _union_support.has($C(value.clone()), $S(wire.as_str()), $S(field.as_str()), $if(property.required) { True } $else { False }) })?; + let access = sigil_quote!(Python { _union_support.get($C(value.clone()), $S(wire.as_str()), $S(field.as_str())) })?; + let inner = condition(&property.shape, access, direction, references)?; + checks.push(if property.required { + sigil_quote!(Python { ($C(present) and $C(inner)) })? + } else { + sigil_quote!(Python { (not $C(present) or $C(inner)) })? + }); + } + if let JsonShape::ObjectWithAdditional { values, .. } = shape { + let inner = condition( + values, + sigil_quote!(Python { _item })?, + direction, + references, + )?; + let keys = properties + .iter() + .map(|property| &property.wire_name) + .collect::>(); + checks.push(sigil_quote!(Python { all($C(inner) for _item in _union_support.extras($C(value), [$for(key in &keys; separator = ", ") { $S(key.as_str()) }])) })?); + } + join_conditions(&checks, false) + } + JsonShape::Reference { schema, scope, .. } => { + references.insert((schema.clone(), direction)); + let function = shape_function(schema, *scope, direction); + sigil_quote!(Python { $N(function)($C(value)) }) + } + JsonShape::AnyOf(shapes) | JsonShape::AllOf(shapes) => { + let checks = shapes + .iter() + .map(|shape| condition(shape, value.clone(), direction, references)) + .collect::, _>>()?; + join_conditions(&checks, matches!(shape, JsonShape::AnyOf(_))) + } + } +} + +fn join_conditions(checks: &[CodeBlock], any: bool) -> Result { + if checks.is_empty() { + return sigil_quote!(Python { $if(any) { False } $else { True } }); + } + sigil_quote!(Python { ($if(any) { $for(check in checks; separator = " or ") { $C(check.clone()) } } $else { $for(check in checks; separator = " and ") { $C(check.clone()) } }) }) +} + +fn python_literal(value: &serde_json::Value) -> Result { + match value { + serde_json::Value::Null => sigil_quote!(Python { None }), + serde_json::Value::Bool(value) => { + sigil_quote!(Python { $if(*value) { True } $else { False } }) + } + serde_json::Value::String(value) => sigil_quote!(Python { $S(value.as_str()) }), + serde_json::Value::Number(value) => sigil_quote!(Python { $L(value.to_string()) }), + serde_json::Value::Array(values) => { + let values = values + .iter() + .map(python_literal) + .collect::, _>>()?; + sigil_quote!(Python { [$for(value in &values; separator = ", ") { $C(value.clone()) }] }) + } + serde_json::Value::Object(values) => { + let values = values + .iter() + .map(|(key, value)| Ok((key, python_literal(value)?))) + .collect::, SigilStitchError>>()?; + sigil_quote!(Python { {$for((key, value) in &values; separator = ", ") { $S(key.as_str()): $C(value.clone()) }} }) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use sigil_stitch::assert_rendered; + + #[test] + fn nullable_conversion_restoration_retains_inner_finite_facts() { + let inner = IrTypeExpr::ScalarEnum { + representation: Box::new(IrTypeExpr::Primitive( + crate::ir::types::IrPrimitive::Integer, + )), + representation_is_constraint: true, + representation_nullable: true, + values: vec![1.into(), serde_json::Value::Null], + }; + let outer = IrTypeExpr::ScalarEnum { + representation: Box::new(inner.clone()), + representation_is_constraint: true, + representation_nullable: true, + values: vec![1.into(), serde_json::Value::Null], + }; + assert_eq!( + conversion_representation(&outer), + IrTypeExpr::Nullable(Box::new(inner)) + ); + } + + #[test] + fn finite_json_literals_keep_python_punctuation_and_boolean_kinds() { + let literal = + python_literal(&serde_json::json!({"quoted'key": [true, null, 2, "line\nvalue"]})) + .unwrap(); + assert_rendered!( + Python::new(), + literal, + "{'quoted\\'key': [True, None, 2, 'line\\nvalue']}\n" + ); + } + + #[test] + fn union_selection_counts_before_delegating() { + let parsed = crate::parser::parse_content_yaml("openapi: 3.1.0\ninfo: {title: test, version: '1'}\npaths: {}\ncomponents:\n schemas:\n Choice:\n anyOf:\n - {type: boolean}\n - {type: integer}\n").unwrap(); + let ir = crate::ir::lower::lower(parsed).unwrap(); + let plan = ValuePlan::new(&ir, UntaggedUnionStrategy::Strict); + let body = plan + .body( + &IrTypeExpr::Named("Choice".into()), + Direction::From, + &mut BTreeSet::new(), + ) + .unwrap(); + assert_rendered!( + Python::new(), + width = 120, + body, + "matches = [isinstance(value, bool), _union_support.number(value, True)]\nif sum(matches) > 1:\n raise ValueError('Ambiguous Choice untagged union value')\n\nif matches[0]:\n return value_1_from_dict(value)\n\nif matches[1]:\n return value_2_from_dict(value)\n\nraise ValueError('No matching Choice untagged union member')\n" + ); + } + + #[test] + fn recursive_model_types_keep_module_imports_semantic() { + let parsed = crate::parser::parse_content_yaml(include_str!( + "../../../tests/fixtures/valid/python-untagged-union.yaml" + )) + .unwrap(); + let ir = crate::ir::lower::lower(parsed).unwrap(); + let plan = ValuePlan::new(&ir, UntaggedUnionStrategy::FirstMatch); + let ty = plan.model_type(&IrTypeExpr::Named("Recursive".into()), "Branch"); + let code = sigil_quote!(Python { children: list[$T(ty)] }).unwrap(); + assert_rendered!( + Python::new(), + code, + "from . import recursive\n\nchildren: list[recursive.Recursive]\n" + ); + } + + #[test] + fn selected_model_imports_stay_inside_the_converter() { + let parsed = crate::parser::parse_content_yaml(include_str!( + "../../../tests/fixtures/valid/python-untagged-union.yaml" + )) + .unwrap(); + let ir = crate::ir::lower::lower(parsed).unwrap(); + let plan = ValuePlan::new(&ir, UntaggedUnionStrategy::FirstMatch); + let body = plan + .body( + &IrTypeExpr::Named("DependentEnvelope".into()), + Direction::From, + &mut BTreeSet::new(), + ) + .unwrap(); + assert_rendered!( + Python::new(), + body, + "from.import dependent_envelope\nreturn dependent_envelope.DependentEnvelope.from_dict(_union_support.as_dict(value))\n" + ); + } + + #[test] + fn tagged_selection_projects_public_payloads_without_mutating_wire_ir() { + let parsed = crate::parser::parse_content_yaml(include_str!( + "../../../tests/fixtures/valid/python-untagged-union.yaml" + )) + .unwrap(); + let ir = crate::ir::lower::lower(parsed).unwrap(); + let plan = ValuePlan::new(&ir, UntaggedUnionStrategy::Strict); + let JsonShape::AnyOf(wire) = + union_match::plan_named_shape("AdjacentMember", plan.matching_ir(Direction::From)) + else { + panic!("adjacent wire variants retain their wrappers"); + }; + assert!( + wire.iter() + .all(|variant| matches!(variant, JsonShape::Object(fields) + if fields.iter().any(|field| field.wire_name == "kind") + && fields.iter().any(|field| field.wire_name == "payload"))) + ); + assert_eq!( + union_match::plan_named_shape("AdjacentMember", plan.matching_ir(Direction::To)), + JsonShape::AnyOf(vec![ + union_match::plan_named_shape("TaggedRecord", &ir), + union_match::plan_named_shape("NumericTwo", &ir), + ]), + ); + assert!(matches!( + ir.schemas["AdjacentMember"].kind, + IrSchemaKind::TaggedUnion(_) + )); + } +} diff --git a/tests/fixtures/valid/python-untagged-union.yaml b/tests/fixtures/valid/python-untagged-union.yaml new file mode 100644 index 000000000..4d9c59df7 --- /dev/null +++ b/tests/fixtures/valid/python-untagged-union.yaml @@ -0,0 +1,284 @@ +openapi: 3.1.0 +info: {title: Python union conversion, version: 1.0.0} +paths: + /tagged-member: + post: + operationId: sendTaggedMember + requestBody: + required: true + content: + application/json: {schema: {$ref: '#/components/schemas/TaggedMemberChoice'}} + responses: + '200': + description: Tagged member inside an untagged union + content: + application/json: {schema: {$ref: '#/components/schemas/TaggedMemberChoice'}} + /tagged: + post: + operationId: sendTagged + requestBody: + required: true + content: + application/json: {schema: {$ref: '#/components/schemas/TaggedChoice'}} + responses: + '200': + description: Tagged union payload + content: + application/json: {schema: {$ref: '#/components/schemas/TaggedChoice'}} + /multipart: + post: + operationId: sendMultipart + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [choice] + properties: + choice: {$ref: '#/components/schemas/Choice'} + attachment: {type: string, format: binary} + encoding: + choice: {contentType: application/json} + responses: + '204': {description: Uploaded} + /nullable: + post: + operationId: sendNullable + requestBody: + required: true + content: + application/json: {schema: {$ref: '#/components/schemas/NullableChoice'}} + responses: + '200': + description: Nullable member + content: + application/json: {schema: {$ref: '#/components/schemas/NullableChoice'}} + /choice: + post: + operationId: sendChoice + requestBody: + required: true + content: + application/json: {schema: {$ref: '#/components/schemas/Choice'}} + responses: + '200': + description: Selected member + content: + application/json: {schema: {$ref: '#/components/schemas/Choice'}} + '400': + description: Selected error member + content: + application/json: {schema: {$ref: '#/components/schemas/Choice'}} + /envelope: + post: + operationId: sendEnvelope + requestBody: + required: true + content: + application/json: {schema: {$ref: '#/components/schemas/Envelope'}} + responses: + '200': + description: Nested members + content: + application/json: {schema: {$ref: '#/components/schemas/Envelope'}} + /choices: + post: + operationId: sendChoices + requestBody: + required: true + content: + application/json: + schema: {type: array, items: {$ref: '#/components/schemas/Choice'}} + responses: + '200': + description: Members in a map + content: + application/json: + schema: {type: object, additionalProperties: {$ref: '#/components/schemas/Choice'}} +components: + schemas: + AcyclicEnvelope: + type: object + required: [value] + properties: + value: {$ref: '#/components/schemas/AcyclicScalar'} + AcyclicScalar: + anyOf: + - {type: integer} + - {type: string} + DependentEnvelope: + type: object + required: [envelope] + properties: + envelope: {$ref: '#/components/schemas/AcyclicEnvelope'} + DependentChoice: + anyOf: + - {$ref: '#/components/schemas/DependentEnvelope'} + - {type: boolean} + ClientTool: + type: object + required: [name, input_schema] + properties: + name: {type: string} + input_schema: {type: object, additionalProperties: true} + SearchTool: + type: object + required: [type, name] + properties: + type: {type: string, enum: [web_search_20250305]} + name: {type: string, enum: [web_search]} + max_uses: {type: integer} + Tool: + oneOf: + - {$ref: '#/components/schemas/ClientTool'} + - {$ref: '#/components/schemas/SearchTool'} + Request: + type: object + required: [tools] + properties: + tools: {type: array, items: {$ref: '#/components/schemas/Tool'}} + NumericOne: + type: object + required: [revision] + properties: + revision: {type: integer, enum: [1]} + first_value: {type: string} + NumericTwo: + type: object + required: [revision] + properties: + revision: {type: integer, enum: [2]} + second_value: {type: string} + Choice: + oneOf: + - {$ref: '#/components/schemas/NumericOne'} + - {$ref: '#/components/schemas/NumericTwo'} + ChoiceAlias: {$ref: '#/components/schemas/Choice'} + Envelope: + type: object + required: [choice, choice_list, choice_map, deep] + properties: + choice: {$ref: '#/components/schemas/ChoiceAlias'} + choice_list: {type: array, items: {$ref: '#/components/schemas/Choice'}} + choice_map: {type: object, additionalProperties: {$ref: '#/components/schemas/Choice'}} + deep: + type: array + items: {type: object, additionalProperties: {type: array, items: {$ref: '#/components/schemas/Choice'}}} + optional_choice: {$ref: '#/components/schemas/Choice'} + Ambiguous: + oneOf: + - {$ref: '#/components/schemas/NumericTwo'} + - {$ref: '#/components/schemas/NumericTwo'} + OpenChoice: + anyOf: + - {} + - {$ref: '#/components/schemas/NumericTwo'} + NestedAmbiguous: + anyOf: + - {$ref: '#/components/schemas/Ambiguous'} + - {type: string} + NullableChoice: + anyOf: + - {$ref: '#/components/schemas/Choice'} + - {type: 'null'} + ScalarChoice: + anyOf: + - {type: integer, enum: [1, 2]} + - {type: boolean, enum: [true]} + InnerNullChoice: + anyOf: + - {enum: [null]} + - {type: [string, 'null'], enum: [x, null]} + WrappedNullChoice: + type: [object, 'null'] + anyOf: + - {$ref: '#/components/schemas/InnerNullChoice'} + - {type: integer} + NullableEnvelope: + type: object + required: [choice] + properties: + choice: {$ref: '#/components/schemas/NullableChoice'} + ambiguous: {$ref: '#/components/schemas/WrappedNullChoice'} + FiniteEnvelope: + type: object + required: [value] + properties: + value: + type: [integer, boolean, 'null'] + enum: [1, true, null] + Recursive: + anyOf: + - {$ref: '#/components/schemas/Branch'} + - {type: string} + Branch: + type: object + required: [children] + properties: + children: {type: array, items: {$ref: '#/components/schemas/Recursive'}} + TaggedChoice: + oneOf: + - type: object + required: [kind, payload] + properties: + kind: {type: string, enum: [choice]} + payload: {$ref: '#/components/schemas/Choice'} + - type: object + required: [kind, payload] + properties: + kind: {type: string, enum: [text]} + payload: {type: string} + TaggedAmbiguous: + oneOf: + - type: object + required: [kind, payload] + properties: + kind: {type: string, enum: [choice]} + payload: {$ref: '#/components/schemas/Ambiguous'} + - type: object + required: [kind, payload] + properties: + kind: {type: string, enum: [text]} + payload: {type: string} + TaggedRecord: + type: object + required: [name] + properties: + name: {type: string} + AdjacentMember: + oneOf: + - type: object + required: [kind, payload] + properties: + kind: {type: string, enum: [record]} + payload: {$ref: '#/components/schemas/TaggedRecord'} + - type: object + required: [kind, payload] + properties: + kind: {type: string, enum: [numeric]} + payload: {$ref: '#/components/schemas/NumericTwo'} + TaggedMemberChoice: + anyOf: + - {$ref: '#/components/schemas/AdjacentMember'} + - {type: integer} + TaggedMemberAlias: {$ref: '#/components/schemas/TaggedMemberChoice'} + AmbiguousTaggedMember: + anyOf: + - {$ref: '#/components/schemas/AdjacentMember'} + - {$ref: '#/components/schemas/AdjacentMember'} + TaggedMemberEnvelope: + type: object + required: [value, values, mapping] + properties: + value: {$ref: '#/components/schemas/TaggedMemberChoice'} + values: {type: array, items: {$ref: '#/components/schemas/TaggedMemberAlias'}} + mapping: {type: object, additionalProperties: {$ref: '#/components/schemas/TaggedMemberChoice'}} + TaggedContainerChoice: + anyOf: + - {type: array, items: {$ref: '#/components/schemas/AdjacentMember'}} + - {type: string} + TaggedObjectChoice: + anyOf: + - {$ref: '#/components/schemas/TaggedMemberEnvelope'} + - {type: integer} diff --git a/tests/generated-clients/python/README.md b/tests/generated-clients/python/README.md new file mode 100644 index 000000000..ff6b508b1 --- /dev/null +++ b/tests/generated-clients/python/README.md @@ -0,0 +1,27 @@ +# Native Python union consumers + +Generation and type checking are separate from the standard-library `unittest` +runner. Tests exercise generated dataclasses, aliases, policy behavior, and each +transport's actual JSON request encoder with in-process transports. No test +launches a generator, compiler, shell, or another runner. + +Install `httpx`, `requests`, and `pyright` in the active Python environment, then: + +```sh +just golden-python-clients::test httpx +just golden-python-clients::test requests +``` + +`prepare` generates all three policies; `build` runs strict pyright against the +unchanged output; `test` uses `${PYTHON:-python3}` (locally Python 3.15.0). +The declared SDK minimum remains Python 3.12, which is also the type-check target. +Both backends run the same contracts. Tests never patch generated source. + +For complete validation, run `just golden-python::build-httpx` or +`just golden-python::build-requests`. Each checks every golden and then runs these +fresh declaration and native contracts, sharing one temporary dependency +environment and one checker. These same entrypoints are used by `golden-build-all`, +`just ci`, and the CI matrix; no extra Python test step is appended in CI. +The temporary environment uses the active interpreter (CI `3.x`); pyright retains +its explicit 3.12 static target. Standalone consumer recipes also accept `PYRIGHT` +to select the checker and require their dependencies in the selected environment. diff --git a/tests/generated-clients/python/pyrightconfig.httpx.json b/tests/generated-clients/python/pyrightconfig.httpx.json new file mode 100644 index 000000000..f2930d072 --- /dev/null +++ b/tests/generated-clients/python/pyrightconfig.httpx.json @@ -0,0 +1,4 @@ +{ + "extends": "./pyrightconfig.json", + "extraPaths": ["generated/httpx/default", "generated/httpx/strict", "generated/httpx/raw"] +} diff --git a/tests/generated-clients/python/pyrightconfig.json b/tests/generated-clients/python/pyrightconfig.json new file mode 100644 index 000000000..098733b82 --- /dev/null +++ b/tests/generated-clients/python/pyrightconfig.json @@ -0,0 +1,5 @@ +{ + "pythonVersion": "3.12", + "typeCheckingMode": "strict", + "reportUnnecessaryIsInstance": "none" +} diff --git a/tests/generated-clients/python/pyrightconfig.requests.json b/tests/generated-clients/python/pyrightconfig.requests.json new file mode 100644 index 000000000..e1e314ec8 --- /dev/null +++ b/tests/generated-clients/python/pyrightconfig.requests.json @@ -0,0 +1,4 @@ +{ + "extends": "./pyrightconfig.json", + "extraPaths": ["generated/requests/default", "generated/requests/strict", "generated/requests/raw"] +} diff --git a/tests/generated-clients/python/tests/test_union.py b/tests/generated-clients/python/tests/test_union.py new file mode 100644 index 000000000..a40741563 --- /dev/null +++ b/tests/generated-clients/python/tests/test_union.py @@ -0,0 +1,378 @@ +"""Native consumers; generation and environment setup belong to just/CI.""" + +import importlib +import json +import os +from email import policy as email_policy +from email.parser import BytesParser +import unittest +from unittest.mock import patch + + +CLIENT = {"name": "weather", "input_schema": {"type": "object"}} +SEARCH = {"type": "web_search_20250305", "name": "web_search", "max_uses": 1} +NUMERIC = {"revision": 2, "second_value": "two"} + + +def transport(reply, status=200): + """Exercise the generated transport and JSON encoder without network IO.""" + sent = [] + if os.environ["NEXUS_PYTHON_BACKEND"] == "httpx": + import httpx + + def handle(request): + sent.append(request) + return httpx.Response(status, content=json.dumps(reply), headers={"X-Test": "union", "Content-Type": "application/json"}) + + return httpx.Client(transport=httpx.MockTransport(handle)), sent + + import requests + from requests.adapters import HTTPAdapter + + class Adapter(HTTPAdapter): + def send(self, request, **kwargs): + sent.append(request) + response = requests.Response() + response.status_code = status + response._content = json.dumps(reply).encode() + response.headers["X-Test"] = "union" + response.request = request + return response + + session = requests.Session() + session.mount("https://", Adapter()) + return session, sent + + +def sent_json(request): + body = request.content if hasattr(request, "content") else request.body + return json.loads(body) + + +class UnionContracts(unittest.TestCase): + def helper(self, policy, model, direction="from_dict"): + module = importlib.import_module(f"union_{policy}.models.{model}") + return getattr(module, f"{model}_{direction}") + + def test_direct_member_controls(self): + for policy in ("default", "strict", "raw"): + models = importlib.import_module(f"union_{policy}.models") + for name, wire in (("ClientTool", CLIENT), ("SearchTool", SEARCH), ("NumericTwo", NUMERIC)): + with self.subTest(policy=policy, model=name): + self.assertEqual(getattr(models, name).from_dict(wire).to_dict(), wire) + + def test_typed_request_json(self): + for policy in ("default", "strict"): + models = importlib.import_module(f"union_{policy}.models") + with self.subTest(policy=policy): + value = models.Request(tools=[models.ClientTool.from_dict(CLIENT), models.SearchTool.from_dict(SEARCH)]) + self.assertEqual(json.loads(json.dumps(value.to_dict())), {"tools": [CLIENT, SEARCH]}) + + def test_inbound_request_members(self): + for policy in ("default", "strict"): + models = importlib.import_module(f"union_{policy}.models") + with self.subTest(policy=policy): + value = models.Request.from_dict({"tools": [CLIENT, SEARCH]}) + self.assertIsInstance(value.tools[0], models.ClientTool) + self.assertIsInstance(value.tools[1], models.SearchTool) + + def test_nested_scalar_array_map_alias_and_optional(self): + wire = {"choice": NUMERIC, "choice_list": [NUMERIC], "choice_map": {"keep_key": NUMERIC}, "deep": [{"keep_key": [NUMERIC]}]} + for policy in ("default", "strict"): + models = importlib.import_module(f"union_{policy}.models") + with self.subTest(policy=policy): + value = models.Envelope.from_dict(wire) + for member in (value.choice, value.choice_list[0], value.choice_map["keep_key"], value.deep[0]["keep_key"][0]): + self.assertIsInstance(member, models.NumericTwo) + self.assertIsNone(value.optional_choice) + self.assertEqual(json.loads(json.dumps(value.to_dict())), wire) + + def test_disjoint_enums_in_both_directions(self): + for policy in ("default", "strict"): + models = importlib.import_module(f"union_{policy}.models") + with self.subTest(policy=policy): + value = self.helper(policy, "choice")(NUMERIC) + self.assertIsInstance(value, models.NumericTwo) + self.assertEqual(self.helper(policy, "choice", "to_dict")(value), NUMERIC) + + def test_first_match_and_strict_ambiguity(self): + models = importlib.import_module("union_default.models") + self.assertIsInstance(self.helper("default", "ambiguous")(NUMERIC), models.NumericTwo) + for direction in ("from_dict", "to_dict"): + with self.subTest(direction=direction): + with self.assertRaisesRegex(ValueError, "Ambiguous Ambiguous"): + self.helper("strict", "ambiguous", direction)(NUMERIC) + + def test_nested_strict_error_propagates(self): + with self.assertRaisesRegex(ValueError, "Ambiguous Ambiguous"): + self.helper("strict", "nested_ambiguous")(NUMERIC) + + def test_unmatched_default_and_raw_preserve_identity(self): + wire = {"revision": 9, "unknown_key": [1, None, True]} + for policy in ("default", "raw"): + for direction in ("from_dict", "to_dict"): + with self.subTest(policy=policy, direction=direction): + self.assertIs(self.helper(policy, "choice", direction)(wire), wire) + for direction in ("from_dict", "to_dict"): + with self.assertRaisesRegex(ValueError, "No matching Choice"): + self.helper("strict", "choice", direction)(wire) + + def test_open_fallback_does_not_shadow_concrete(self): + for policy in ("default", "strict"): + models = importlib.import_module(f"union_{policy}.models") + with self.subTest(policy=policy): + self.assertIsInstance(self.helper(policy, "open_choice")(NUMERIC), models.NumericTwo) + wire = {"other_key": True} + self.assertIs(self.helper(policy, "open_choice")(wire), wire) + + def test_numeric_matching_is_not_python_boolean_equality(self): + for policy in ("default", "strict"): + for value in (1, 2, True): + self.assertIs(self.helper(policy, "scalar_choice")(value), value) + for value in (False, 3, 1.5): + with self.subTest(value=value): + with self.assertRaises(ValueError): + self.helper("strict", "scalar_choice")(value) + + def test_nullable_union(self): + for policy in ("default", "strict", "raw"): + for direction in ("from_dict", "to_dict"): + self.assertIsNone(self.helper(policy, "nullable_choice", direction)(None)) + + def test_authored_nulls_keep_strict_counts_and_nested_errors(self): + for model in ("inner_null_choice", "wrapped_null_choice"): + for direction in ("from_dict", "to_dict"): + with self.subTest(model=model, direction=direction): + with self.assertRaisesRegex(ValueError, "Ambiguous InnerNullChoice"): + self.helper("strict", model, direction)(None) + models = importlib.import_module("union_strict.models") + with self.assertRaisesRegex(ValueError, "Ambiguous InnerNullChoice"): + models.NullableEnvelope.from_dict({"choice": None, "ambiguous": None}) + + def test_required_nullable_field_preserves_explicit_null(self): + for policy in ("default", "strict", "raw"): + models = importlib.import_module(f"union_{policy}.models") + with self.subTest(policy=policy): + self.assertEqual(models.NullableEnvelope.from_dict({"choice": None}).to_dict(), {"choice": None}) + + def test_nullable_finite_wrapper_restores_its_conversion_boundary(self): + for policy in ("default", "strict", "raw"): + models = importlib.import_module(f"union_{policy}.models") + for value in (None, 1, True): + with self.subTest(policy=policy, value=value): + wire = {"value": value} + self.assertEqual(models.FiniteEnvelope.from_dict(wire).to_dict(), wire) + + def test_recursive_member_conversion_and_imports(self): + wire = {"children": [{"children": ["leaf"]}]} + for policy in ("default", "strict"): + models = importlib.import_module(f"union_{policy}.models") + with self.subTest(policy=policy): + value = self.helper(policy, "recursive")(wire) + self.assertIsInstance(value, models.Branch) + self.assertIsInstance(value.children[0], models.Branch) + self.assertEqual(self.helper(policy, "recursive", "to_dict")(value), wire) + + def test_acyclic_model_dependencies_do_not_gain_helper_import_cycles(self): + wire = {"envelope": {"value": 1}} + for policy in ("default", "strict", "raw"): + models = importlib.import_module(f"union_{policy}.models") + with self.subTest(policy=policy): + envelope = models.AcyclicEnvelope(value=1) + self.assertEqual(envelope.to_dict(), {"value": 1}) + dependent = models.DependentEnvelope.from_dict(wire) + self.assertIsInstance(dependent.envelope, models.AcyclicEnvelope) + self.assertEqual(dependent.to_dict(), wire) + if policy != "raw": + value = self.helper(policy, "dependent_choice")(wire) + self.assertIsInstance(value, models.DependentEnvelope) + self.assertEqual(self.helper(policy, "dependent_choice", "to_dict")(value), wire) + + def test_tagged_members_keep_their_public_payload_representation(self): + for policy in ("default", "strict"): + models = importlib.import_module(f"union_{policy}.models") + for tag, payload, model in (("record", {"name": "weather"}, models.TaggedRecord), ("numeric", NUMERIC, models.NumericTwo)): + wire = {"kind": tag, "payload": payload} + with self.subTest(policy=policy, tag=tag): + value = self.helper(policy, "tagged_member_choice")(wire) + self.assertIsInstance(value, model) + self.assertEqual(self.helper(policy, "adjacent_member", "to_dict")(value), wire) + self.assertEqual(self.helper(policy, "tagged_member_choice", "to_dict")(value), wire) + + def test_tagged_members_match_through_aliases_containers_and_objects(self): + member = {"kind": "record", "payload": {"name": "weather"}} + wire = {"value": member, "values": [member], "mapping": {"keep_key": member}} + for policy in ("default", "strict"): + models = importlib.import_module(f"union_{policy}.models") + with self.subTest(policy=policy): + value = self.helper(policy, "tagged_object_choice")(wire) + self.assertIsInstance(value, models.TaggedMemberEnvelope) + for item in (value.value, value.values[0], value.mapping["keep_key"]): + self.assertIsInstance(item, models.TaggedRecord) + self.assertEqual(json.loads(json.dumps(self.helper(policy, "tagged_object_choice", "to_dict")(value))), wire) + values = self.helper(policy, "tagged_container_choice")([member]) + self.assertIsInstance(values[0], models.TaggedRecord) + self.assertEqual(self.helper(policy, "tagged_container_choice", "to_dict")(values), [member]) + + def test_tagged_member_selection_counts_before_calling_the_delegate(self): + models = importlib.import_module("union_strict.models") + tagged = importlib.import_module("union_strict.models.adjacent_member") + member = models.TaggedRecord(name="weather") + with patch.object(tagged, "adjacent_member_to_dict", wraps=tagged.adjacent_member_to_dict) as encode: + with self.assertRaisesRegex(ValueError, "Ambiguous AmbiguousTaggedMember"): + self.helper("strict", "ambiguous_tagged_member", "to_dict")(member) + encode.assert_not_called() + wire = self.helper("strict", "tagged_member_choice", "to_dict")(member) + self.assertEqual(wire, {"kind": "record", "payload": {"name": "weather"}}) + encode.assert_called_once_with(member) + + def test_raw_tagged_member_subtrees_preserve_wire_identity(self): + models = importlib.import_module("union_raw.models") + member = {"kind": "record", "payload": {"name": "weather"}} + for direction in ("from_dict", "to_dict"): + self.assertIs(self.helper("raw", "tagged_member_choice", direction)(member), member) + wire = {"value": member, "values": [member], "mapping": {"keep_key": member}} + value = models.TaggedMemberEnvelope.from_dict(wire) + self.assertIs(value.value, member) + self.assertEqual(value.to_dict(), wire) + + def test_raw_union_subtree_does_not_disable_parent_conversion(self): + models = importlib.import_module("union_raw.models") + wire = {"choice": NUMERIC, "choice_list": [NUMERIC], "choice_map": {"keep_key": NUMERIC}, "deep": [{"keep_key": [NUMERIC]}]} + value = models.Envelope.from_dict(wire) + self.assertIsInstance(value, models.Envelope) + self.assertIs(value.choice, NUMERIC) + self.assertEqual(value.to_dict(), wire) + + def test_select_before_delegate_and_convert_once(self): + models = importlib.import_module("union_strict.models") + with patch.object(models.NumericTwo, "from_dict", wraps=models.NumericTwo.from_dict) as decode: + with self.assertRaisesRegex(ValueError, "Ambiguous"): + self.helper("strict", "ambiguous")(NUMERIC) + decode.assert_not_called() + self.helper("strict", "choice")(NUMERIC) + decode.assert_called_once_with(NUMERIC) + + def test_http_request_success_and_metadata(self): + for policy in ("default", "strict", "raw"): + models = importlib.import_module(f"union_{policy}.models") + runtime = importlib.import_module(f"union_{policy}.runtime") + api = importlib.import_module(f"union_{policy}.apis.default_api") + http, sent = transport(NUMERIC) + with self.subTest(policy=policy), runtime.Client("https://example.test", http_client=http) as client: + member = NUMERIC if policy == "raw" else models.NumericTwo.from_dict(NUMERIC) + response = api.DefaultApi(client).send_choice_with_http_info(body=member) + self.assertEqual(sent_json(sent[0]), NUMERIC) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.headers["X-Test"], "union") + if policy == "raw": + self.assertEqual(response.data, NUMERIC) + else: + self.assertIsInstance(response.data, models.NumericTwo) + self.assertEqual(response.data.to_dict(), NUMERIC) + + def test_http_array_request_map_response(self): + for policy in ("default", "strict"): + models = importlib.import_module(f"union_{policy}.models") + runtime = importlib.import_module(f"union_{policy}.runtime") + api = importlib.import_module(f"union_{policy}.apis.default_api") + http, sent = transport({"keep_key": NUMERIC}) + with self.subTest(policy=policy), runtime.Client("https://example.test", http_client=http) as client: + value = api.DefaultApi(client).send_choices(body=[models.NumericTwo.from_dict(NUMERIC)]) + self.assertEqual(sent_json(sent[0]), [NUMERIC]) + self.assertIsInstance(value["keep_key"], models.NumericTwo) + + def test_http_error_body_is_typed_lazy_and_cached(self): + for policy in ("default", "strict"): + models = importlib.import_module(f"union_{policy}.models") + runtime = importlib.import_module(f"union_{policy}.runtime") + api = importlib.import_module(f"union_{policy}.apis.default_api") + http, _ = transport(NUMERIC, status=400) + with self.subTest(policy=policy), runtime.Client("https://example.test", http_client=http) as client: + with patch.object(models.NumericTwo, "from_dict", wraps=models.NumericTwo.from_dict) as decode: + member = models.NumericTwo(revision=2, second_value="two") + with self.assertRaises(api.SendChoiceError) as caught: + api.DefaultApi(client).send_choice(body=member) + decode.assert_not_called() + value = caught.exception.detail.body + self.assertIsInstance(value, models.NumericTwo) + self.assertIs(caught.exception.detail.body, value) + decode.assert_called_once_with(NUMERIC) + + def test_http_required_null_is_json_null_not_omitted(self): + for policy in ("default", "strict", "raw"): + runtime = importlib.import_module(f"union_{policy}.runtime") + api = importlib.import_module(f"union_{policy}.apis.default_api") + http, sent = transport(None) + with self.subTest(policy=policy), runtime.Client("https://example.test", http_client=http) as client: + self.assertIsNone(api.DefaultApi(client).send_nullable(body=None)) + self.assertIsNone(sent_json(sent[0])) + + def test_multipart_json_part_uses_the_same_member_codec(self): + for policy in ("default", "strict", "raw"): + models = importlib.import_module(f"union_{policy}.models") + runtime = importlib.import_module(f"union_{policy}.runtime") + api = importlib.import_module(f"union_{policy}.apis.default_api") + http, sent = transport(None, status=204) + with self.subTest(policy=policy), runtime.Client("https://example.test", http_client=http) as client: + member = NUMERIC if policy == "raw" else models.NumericTwo.from_dict(NUMERIC) + body = models.SendMultipartMultipartRequestBody(choice=member) + api.DefaultApi(client).send_multipart(body=body) + request = sent[0] + content = request.content if hasattr(request, "content") else request.body + message = BytesParser(policy=email_policy.default).parsebytes( + ("Content-Type: " + request.headers["Content-Type"] + "\r\nMIME-Version: 1.0\r\n\r\n").encode() + content + ) + parts = {part.get_param("name", header="content-disposition"): part for part in message.iter_parts()} + self.assertEqual(json.loads(parts["choice"].get_payload(decode=True)), NUMERIC) + + def test_tagged_payload_keeps_untagged_conversion_and_policy(self): + wire = {"kind": "choice", "payload": NUMERIC} + for policy in ("default", "strict", "raw"): + models = importlib.import_module(f"union_{policy}.models") + with self.subTest(policy=policy): + value = self.helper(policy, "tagged_choice")(wire) + if policy == "raw": + self.assertIs(value, NUMERIC) + else: + self.assertIsInstance(value, models.NumericTwo) + self.assertEqual(self.helper(policy, "tagged_choice", "to_dict")(value), wire) + with self.assertRaisesRegex(ValueError, "Ambiguous Ambiguous"): + self.helper("strict", "tagged_ambiguous")(wire) + + def test_http_tagged_payload_uses_the_shared_union_boundary(self): + wire = {"kind": "choice", "payload": NUMERIC} + for policy in ("default", "strict", "raw"): + models = importlib.import_module(f"union_{policy}.models") + runtime = importlib.import_module(f"union_{policy}.runtime") + api = importlib.import_module(f"union_{policy}.apis.default_api") + http, sent = transport(wire) + with self.subTest(policy=policy), runtime.Client("https://example.test", http_client=http) as client: + member = NUMERIC if policy == "raw" else models.NumericTwo.from_dict(NUMERIC) + value = api.DefaultApi(client).send_tagged(body=member) + self.assertEqual(sent_json(sent[0]), wire) + if policy == "raw": + self.assertEqual(value, NUMERIC) + else: + self.assertIsInstance(value, models.NumericTwo) + + def test_http_tagged_member_inside_untagged_union(self): + wire = {"kind": "record", "payload": {"name": "weather"}} + for policy in ("default", "strict", "raw"): + models = importlib.import_module(f"union_{policy}.models") + runtime = importlib.import_module(f"union_{policy}.runtime") + api = importlib.import_module(f"union_{policy}.apis.default_api") + http, sent = transport(wire) + with self.subTest(policy=policy), runtime.Client("https://example.test", http_client=http) as client: + member = wire if policy == "raw" else models.TaggedRecord(name="weather") + response = api.DefaultApi(client).send_tagged_member_with_http_info(body=member) + self.assertEqual(sent_json(sent[0]), wire) + if policy == "raw": + self.assertEqual(response.data, wire) + else: + self.assertIsInstance(response.data, models.TaggedRecord) + self.assertEqual(response.data.to_dict(), {"name": "weather"}) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/generated-clients/python/typing_contracts.py b/tests/generated-clients/python/typing_contracts.py new file mode 100644 index 000000000..ba6fd0573 --- /dev/null +++ b/tests/generated-clients/python/typing_contracts.py @@ -0,0 +1,47 @@ +"""Checked-in consumer declarations for generated public Python APIs.""" + +from typing import assert_type + +from union_default.apis.default_api import DefaultApi, SendChoiceBadRequest, SendChoiceError +from union_default.models import AcyclicEnvelope, Branch, Choice, ClientTool, NumericTwo, Request, TaggedMemberChoice, TaggedRecord +from union_default.models.choice import choice_from_dict, choice_to_dict +from union_default.models.tagged_member_choice import tagged_member_choice_from_dict, tagged_member_choice_to_dict +from union_raw.apis.default_api import DefaultApi as RawApi +from union_raw.models import Envelope as RawEnvelope +from union_raw.models.choice import choice_from_dict as raw_choice_from_dict +from union_strict.models.choice import choice_from_dict as strict_choice_from_dict +from union_strict.models import Choice as StrictChoice + + +def typed_models(member: ClientTool, wire: dict[str, object]) -> Request: + request = Request(tools=[member]) + assert_type(request.to_dict(), dict[str, object]) + assert_type(choice_from_dict(wire), Choice) + assert_type(strict_choice_from_dict(wire), StrictChoice) + assert_type(choice_to_dict(NumericTwo(revision=2)), object) + assert_type(Branch.from_dict(wire).children, list[Branch | str]) + return request + + +def typed_http(api: DefaultApi, member: NumericTwo, error: SendChoiceError) -> Choice: + response = api.send_choice_with_http_info(body=member) + assert_type(response.data, Choice) + if isinstance(error.detail, SendChoiceBadRequest): + assert_type(error.detail.body, Choice) + return api.send_choice(body=member) + + +def typed_tagged_member(api: DefaultApi, member: TaggedRecord, wire: dict[str, object]) -> TaggedMemberChoice: + assert_type(AcyclicEnvelope(value=1).value, int | str) + assert_type(tagged_member_choice_from_dict(wire), TaggedMemberChoice) + assert_type(tagged_member_choice_to_dict(member), object) + assert_type(api.send_tagged_member_with_http_info(body=member).data, TaggedMemberChoice) + return api.send_tagged_member(body=member) + + +def raw_http(api: RawApi, value: object, envelope: RawEnvelope) -> object: + assert_type(raw_choice_from_dict(value), object) + assert_type(envelope.choice, object) + assert_type(envelope.choice_list, list[object]) + assert_type(envelope.choice_map, dict[str, object]) + return api.send_choice(body=value) diff --git a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/_union.py.golden b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/_union.py.golden new file mode 100644 index 000000000..e458295c7 --- /dev/null +++ b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/_union.py.golden @@ -0,0 +1,146 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Comprehensive Schema Types Example — 1.0.0 +# Comprehensive test for all OpenAPI v3.1.2 schema types + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if isinstance(value, str): + return value_1_from_dict(value) + + if _union_support.number(value, False): + return value_2_from_dict(value) + + return value + +def value_0_to_dict(value: object) -> object: + if isinstance(value, str): + return value_1_to_dict(value) + + if _union_support.number(value, False): + return value_2_to_dict(value) + + return value + +def value_1_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_1_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (not _union_support.has(value, 'active', 'active', False) or isinstance(_union_support.get(value, 'active', 'active'), bool)) + and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): + return value_4_from_dict(value) + + if (_union_support.is_object(value) + and (not _union_support.has(value, 'metadata', 'metadata', False) or (_union_support.is_object(_union_support.get(value, 'metadata', 'metadata')) + and (not _union_support.has(_union_support.get(value, 'metadata', 'metadata'), 'created', 'created', False) or isinstance(_union_support.get(_union_support.get(value, 'metadata', 'metadata'), 'created', 'created'), str)))) + and (not _union_support.has(value, 'user', 'user', False) or (_union_support.is_object(_union_support.get(value, 'user', 'user')) + and (_union_support.has(_union_support.get(value, 'user', 'user'), 'id', 'id', True) and _union_support.number(_union_support.get(_union_support.get(value, 'user', 'user'), 'id', 'id'), True)) + and (not _union_support.has(_union_support.get(value, 'user', 'user'), 'name', 'name', False) or isinstance(_union_support.get(_union_support.get(value, 'user', 'user'), 'name', 'name'), str))))): + return value_5_from_dict(value) + + return value + +def value_3_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (not _union_support.has(value, 'active', 'active', False) or isinstance(_union_support.get(value, 'active', 'active'), bool)) + and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): + return value_4_to_dict(value) + + if (_union_support.is_object(value) + and (not _union_support.has(value, 'metadata', 'metadata', False) or (_union_support.is_object(_union_support.get(value, 'metadata', 'metadata')) + and (not _union_support.has(_union_support.get(value, 'metadata', 'metadata'), 'created', 'created', False) or isinstance(_union_support.get(_union_support.get(value, 'metadata', 'metadata'), 'created', 'created'), str)))) + and (not _union_support.has(value, 'user', 'user', False) or (_union_support.is_object(_union_support.get(value, 'user', 'user')) + and (_union_support.has(_union_support.get(value, 'user', 'user'), 'id', 'id', True) and _union_support.number(_union_support.get(_union_support.get(value, 'user', 'user'), 'id', 'id'), True)) + and (not _union_support.has(_union_support.get(value, 'user', 'user'), 'name', 'name', False) or isinstance(_union_support.get(_union_support.get(value, 'user', 'user'), 'name', 'name'), str))))): + return value_5_to_dict(value) + + return value + +def value_4_from_dict(value: object) -> object: + from.import basic_object + return basic_object.BasicObject.from_dict(_union_support.as_dict(value)) + +def value_4_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_5_from_dict(value: object) -> object: + from.import nested_object + return nested_object.NestedObject.from_dict(_union_support.as_dict(value)) + +def value_5_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_6_from_dict(value: object) -> object: + if isinstance(value, str): + return value_1_from_dict(value) + + if _union_support.number(value, False): + return value_2_from_dict(value) + + if isinstance(value, bool): + return value_7_from_dict(value) + + return value + +def value_6_to_dict(value: object) -> object: + if isinstance(value, str): + return value_1_to_dict(value) + + if _union_support.number(value, False): + return value_2_to_dict(value) + + if isinstance(value, bool): + return value_7_to_dict(value) + + return value + +def value_7_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_7_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_8_from_dict(value: object) -> object: + if isinstance(value, str): + return value_1_from_dict(value) + + if (_union_support.is_object(value) + and (not _union_support.has(value, 'active', 'active', False) or isinstance(_union_support.get(value, 'active', 'active'), bool)) + and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): + return value_4_from_dict(value) + + if value is None: + return None + + return value + +def value_8_to_dict(value: object) -> object: + if isinstance(value, str): + return value_1_to_dict(value) + + if (_union_support.is_object(value) + and (not _union_support.has(value, 'active', 'active', False) or isinstance(_union_support.get(value, 'active', 'active'), bool)) + and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): + return value_4_to_dict(value) + + if value is None: + return None + + return value diff --git a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/any_of_types.py.golden b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/any_of_types.py.golden index 3cef19651..b9c948909 100644 --- a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/any_of_types.py.golden +++ b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/any_of_types.py.golden @@ -5,9 +5,19 @@ from __future__ import annotations +from typing import cast + +from . import _union + # Any of multiple types type AnyOfTypes = ( str | float ) + +def any_of_types_from_dict(value: object) -> AnyOfTypes: + return cast(AnyOfTypes, _union.value_0_from_dict(value)) + +def any_of_types_to_dict(value: AnyOfTypes) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_objects.py.golden b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_objects.py.golden index dd22f711e..198397c80 100644 --- a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_objects.py.golden +++ b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_objects.py.golden @@ -5,6 +5,9 @@ from __future__ import annotations +from typing import cast + +from . import _union from .basic_object import BasicObject from .nested_object import NestedObject @@ -14,3 +17,9 @@ type OneOfObjects = ( BasicObject | NestedObject ) + +def one_of_objects_from_dict(value: object) -> OneOfObjects: + return cast(OneOfObjects, _union.value_3_from_dict(value)) + +def one_of_objects_to_dict(value: OneOfObjects) -> object: + return _union.value_3_to_dict(value) diff --git a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_types.py.golden b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_types.py.golden index d5e39c344..5e7628744 100644 --- a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_types.py.golden +++ b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_types.py.golden @@ -5,6 +5,10 @@ from __future__ import annotations +from typing import cast + +from . import _union + # Union of multiple primitive types type OneOfTypes = ( @@ -12,3 +16,9 @@ type OneOfTypes = ( | float | bool ) + +def one_of_types_from_dict(value: object) -> OneOfTypes: + return cast(OneOfTypes, _union.value_6_from_dict(value)) + +def one_of_types_to_dict(value: OneOfTypes) -> object: + return _union.value_6_to_dict(value) diff --git a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_with_null.py.golden b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_with_null.py.golden index 79f16d239..fb08327fc 100644 --- a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_with_null.py.golden +++ b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_with_null.py.golden @@ -5,6 +5,9 @@ from __future__ import annotations +from typing import cast + +from . import _union from .basic_object import BasicObject # Union of string, null, and schema reference @@ -14,3 +17,9 @@ type OneOfWithNull = ( | BasicObject | None ) + +def one_of_with_null_from_dict(value: object) -> OneOfWithNull: + return cast(OneOfWithNull, _union.value_8_from_dict(value)) + +def one_of_with_null_to_dict(value: OneOfWithNull) -> object: + return _union.value_8_to_dict(value) diff --git a/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/runtime/_union_support.py.golden new file mode 100644 index 000000000..c24a70570 --- /dev/null +++ b/tests/golden/python/python-httpx/comprehensive-schemas/comprehensive_schema_types_example/runtime/_union_support.py.golden @@ -0,0 +1,91 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Comprehensive Schema Types Example — 1.0.0 +# Comprehensive test for all OpenAPI v3.1.2 schema types + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-httpx/enum-repr/enum_representation_api/apis/enum_repr_api.py.golden b/tests/golden/python/python-httpx/enum-repr/enum_representation_api/apis/enum_repr_api.py.golden index f65600fe1..7d31c3857 100644 --- a/tests/golden/python/python-httpx/enum-repr/enum_representation_api/apis/enum_repr_api.py.golden +++ b/tests/golden/python/python-httpx/enum-repr/enum_representation_api/apis/enum_repr_api.py.golden @@ -5,8 +5,12 @@ from __future__ import annotations +import json + from collections.abc import Mapping +from typing import cast +from ..models import _union from ..models.adjacently_tagged_enum import AdjacentlyTaggedEnum from ..models.externally_tagged_enum import ExternallyTaggedEnum from ..models.internally_tagged_enum import InternallyTaggedEnum @@ -210,14 +214,16 @@ class EnumReprApi: path = "/mixed" headers: dict[str, str] = {} headers["Content-Type"] = "application/json" - response = self._client.request("POST", path, json=body, headers=headers) + _union_body = _union.value_0_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) if not (200 <= response.status_code < 300): if response.status_code == 400: detail = HandleMixedBadRequest(response.status_code, response.headers, response.content) else: detail = HandleMixedUnexpected(response.status_code, response.headers, response.content) raise HandleMixedError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) - data = response.json() # type: ignore[return-value] + _union_data = cast(MixedEnum, _union.value_0_from_dict(response.json())) + data = _union_data return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) def handle_mixed(self, *, body: MixedEnum) -> MixedEnum: @@ -229,14 +235,16 @@ class EnumReprApi: path = "/untagged" headers: dict[str, str] = {} headers["Content-Type"] = "application/json" - response = self._client.request("POST", path, json=body, headers=headers) + _union_body = _union.value_5_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) if not (200 <= response.status_code < 300): if response.status_code == 400: detail = HandleUntaggedBadRequest(response.status_code, response.headers, response.content) else: detail = HandleUntaggedUnexpected(response.status_code, response.headers, response.content) raise HandleUntaggedError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) - data = response.json() # type: ignore[return-value] + _union_data = cast(UntaggedEnum, _union.value_5_from_dict(response.json())) + data = _union_data return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) def handle_untagged(self, *, body: UntaggedEnum) -> UntaggedEnum: diff --git a/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/_union.py.golden b/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/_union.py.golden new file mode 100644 index 000000000..5feee1e72 --- /dev/null +++ b/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/_union.py.golden @@ -0,0 +1,116 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Enum Representation API — 1.0.0 +# This API demonstrates all 4 kinds of enum representation types: Externally Tagged, Internally Tagged, Adjacently Tagged, and Untagged + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if _union_support.equal(value, 'SimpleA'): + return value_1_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'VariantA', 'variant_a', True) and (_union_support.is_object(_union_support.get(value, 'VariantA', 'variant_a')) + and (_union_support.has(_union_support.get(value, 'VariantA', 'variant_a'), 'field1', 'field1', True) and isinstance(_union_support.get(_union_support.get(value, 'VariantA', 'variant_a'), 'field1', 'field1'), str)) + and (_union_support.has(_union_support.get(value, 'VariantA', 'variant_a'), 'field2', 'field2', True) and _union_support.number(_union_support.get(_union_support.get(value, 'VariantA', 'variant_a'), 'field2', 'field2'), True))))): + return value_2_from_dict(value) + + if _union_support.equal(value, 'SimpleB'): + return value_3_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'VariantB', 'variant_b', True) and (_union_support.is_object(_union_support.get(value, 'VariantB', 'variant_b')) + and (_union_support.has(_union_support.get(value, 'VariantB', 'variant_b'), 'field3', 'field3', True) and isinstance(_union_support.get(_union_support.get(value, 'VariantB', 'variant_b'), 'field3', 'field3'), bool)) + and (_union_support.has(_union_support.get(value, 'VariantB', 'variant_b'), 'field4', 'field4', True) and _union_support.number(_union_support.get(_union_support.get(value, 'VariantB', 'variant_b'), 'field4', 'field4'), False))))): + return value_4_from_dict(value) + + return value + +def value_0_to_dict(value: object) -> object: + if _union_support.equal(value, 'SimpleA'): + return value_1_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'VariantA', 'variant_a', True) and (_union_support.is_object(_union_support.get(value, 'VariantA', 'variant_a')) + and (_union_support.has(_union_support.get(value, 'VariantA', 'variant_a'), 'field1', 'field1', True) and isinstance(_union_support.get(_union_support.get(value, 'VariantA', 'variant_a'), 'field1', 'field1'), str)) + and (_union_support.has(_union_support.get(value, 'VariantA', 'variant_a'), 'field2', 'field2', True) and _union_support.number(_union_support.get(_union_support.get(value, 'VariantA', 'variant_a'), 'field2', 'field2'), True))))): + return value_2_to_dict(value) + + if _union_support.equal(value, 'SimpleB'): + return value_3_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'VariantB', 'variant_b', True) and (_union_support.is_object(_union_support.get(value, 'VariantB', 'variant_b')) + and (_union_support.has(_union_support.get(value, 'VariantB', 'variant_b'), 'field3', 'field3', True) and isinstance(_union_support.get(_union_support.get(value, 'VariantB', 'variant_b'), 'field3', 'field3'), bool)) + and (_union_support.has(_union_support.get(value, 'VariantB', 'variant_b'), 'field4', 'field4', True) and _union_support.number(_union_support.get(_union_support.get(value, 'VariantB', 'variant_b'), 'field4', 'field4'), False))))): + return value_4_to_dict(value) + + return value + +def value_1_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_1_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_from_dict(value: object) -> object: + from.import mixed_enum_member2 + return mixed_enum_member2.MixedEnumMember2.from_dict(_union_support.as_dict(value)) + +def value_2_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_3_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_4_from_dict(value: object) -> object: + from.import mixed_enum_member4 + return mixed_enum_member4.MixedEnumMember4.from_dict(_union_support.as_dict(value)) + +def value_4_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_5_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'field1', 'field1', True) and isinstance(_union_support.get(value, 'field1', 'field1'), str)) + and (_union_support.has(value, 'field2', 'field2', True) and _union_support.number(_union_support.get(value, 'field2', 'field2'), True))): + return value_6_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'field3', 'field3', True) and isinstance(_union_support.get(value, 'field3', 'field3'), bool)) + and (_union_support.has(value, 'field4', 'field4', True) and _union_support.number(_union_support.get(value, 'field4', 'field4'), False))): + return value_7_from_dict(value) + + return value + +def value_5_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'field1', 'field1', True) and isinstance(_union_support.get(value, 'field1', 'field1'), str)) + and (_union_support.has(value, 'field2', 'field2', True) and _union_support.number(_union_support.get(value, 'field2', 'field2'), True))): + return value_6_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'field3', 'field3', True) and isinstance(_union_support.get(value, 'field3', 'field3'), bool)) + and (_union_support.has(value, 'field4', 'field4', True) and _union_support.number(_union_support.get(value, 'field4', 'field4'), False))): + return value_7_to_dict(value) + + return value + +def value_6_from_dict(value: object) -> object: + from.import variant_a + return variant_a.VariantA.from_dict(_union_support.as_dict(value)) + +def value_6_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_7_from_dict(value: object) -> object: + from.import variant_b + return variant_b.VariantB.from_dict(_union_support.as_dict(value)) + +def value_7_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) diff --git a/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/adjacently_tagged_enum.py.golden b/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/adjacently_tagged_enum.py.golden index 168416dd2..45ddb5ba8 100644 --- a/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/adjacently_tagged_enum.py.golden +++ b/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/adjacently_tagged_enum.py.golden @@ -37,4 +37,3 @@ def adjacently_tagged_enum_to_dict(obj: AdjacentlyTaggedEnum) -> dict[str, objec elif isinstance(obj, VariantB): return {"ty": "ADJACENTLY_TAGGED_ENUM_VARIANT_B", "data": obj.to_dict()} raise ValueError(f"Unknown variant for AdjacentlyTaggedEnum: {type(obj)}") - diff --git a/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/externally_tagged_enum.py.golden b/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/externally_tagged_enum.py.golden index 9cd0b6442..a4954827a 100644 --- a/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/externally_tagged_enum.py.golden +++ b/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/externally_tagged_enum.py.golden @@ -35,4 +35,3 @@ def externally_tagged_enum_to_dict(obj: ExternallyTaggedEnum) -> dict[str, objec elif isinstance(obj, VariantB): return {"EXTERNALLY_TAGGED_ENUM_VARIANT_B": obj.to_dict()} raise ValueError(f"Unknown variant for ExternallyTaggedEnum: {type(obj)}") - diff --git a/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/internally_tagged_enum.py.golden b/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/internally_tagged_enum.py.golden index 58e40d4f6..a966ef978 100644 --- a/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/internally_tagged_enum.py.golden +++ b/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/internally_tagged_enum.py.golden @@ -40,4 +40,3 @@ def internally_tagged_enum_to_dict(obj: InternallyTaggedEnum) -> dict[str, objec result["ty"] = "INTERNALLY_TAGGED_ENUM_VARIANT_B" return result raise ValueError(f"Unknown variant for InternallyTaggedEnum: {type(obj)}") - diff --git a/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/mixed_enum.py.golden b/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/mixed_enum.py.golden index e27273ea3..bdb54dc3b 100644 --- a/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/mixed_enum.py.golden +++ b/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/mixed_enum.py.golden @@ -5,8 +5,9 @@ from __future__ import annotations -from typing import Literal +from typing import Literal, cast +from . import _union from .mixed_enum_member2 import MixedEnumMember2 from .mixed_enum_member4 import MixedEnumMember4 @@ -18,3 +19,9 @@ type MixedEnum = ( | Literal["SimpleB"] | MixedEnumMember4 ) + +def mixed_enum_from_dict(value: object) -> MixedEnum: + return cast(MixedEnum, _union.value_0_from_dict(value)) + +def mixed_enum_to_dict(value: MixedEnum) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/untagged_enum.py.golden b/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/untagged_enum.py.golden index a16171269..33926d2eb 100644 --- a/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/untagged_enum.py.golden +++ b/tests/golden/python/python-httpx/enum-repr/enum_representation_api/models/untagged_enum.py.golden @@ -5,6 +5,9 @@ from __future__ import annotations +from typing import cast + +from . import _union from .variant_a import VariantA from .variant_b import VariantB @@ -14,3 +17,9 @@ type UntaggedEnum = ( VariantA | VariantB ) + +def untagged_enum_from_dict(value: object) -> UntaggedEnum: + return cast(UntaggedEnum, _union.value_5_from_dict(value)) + +def untagged_enum_to_dict(value: UntaggedEnum) -> object: + return _union.value_5_to_dict(value) diff --git a/tests/golden/python/python-httpx/enum-repr/enum_representation_api/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/enum-repr/enum_representation_api/runtime/_union_support.py.golden new file mode 100644 index 000000000..ee0abbd31 --- /dev/null +++ b/tests/golden/python/python-httpx/enum-repr/enum_representation_api/runtime/_union_support.py.golden @@ -0,0 +1,91 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Enum Representation API — 1.0.0 +# This API demonstrates all 4 kinds of enum representation types: Externally Tagged, Internally Tagged, Adjacently Tagged, and Untagged + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/models/event.py.golden b/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/models/event.py.golden index 1f8b74dd7..12998be35 100644 --- a/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/models/event.py.golden +++ b/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/models/event.py.golden @@ -34,4 +34,3 @@ def event_to_dict(obj: Event) -> dict[str, object]: result["type"] = "completed" return result raise ValueError(f"Unknown variant for Event: {type(obj)}") - diff --git a/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/models/external_event.py.golden b/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/models/external_event.py.golden index 592df2ad8..965f24794 100644 --- a/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/models/external_event.py.golden +++ b/tests/golden/python/python-httpx/open-tagged-additional-properties/open_tagged_additional_properties/models/external_event.py.golden @@ -28,4 +28,3 @@ def external_event_to_dict(obj: ExternalEvent) -> dict[str, object]: elif isinstance(obj, Payload): return {"finished": obj.to_dict()} raise ValueError(f"Unknown variant for ExternalEvent: {type(obj)}") - diff --git a/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/apis/default_api.py.golden b/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/apis/default_api.py.golden index b73636038..ef3535571 100644 --- a/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/apis/default_api.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/apis/default_api.py.golden @@ -5,8 +5,12 @@ from __future__ import annotations +import json + from collections.abc import Mapping +from typing import cast +from ..models import _union from ..models.foo import Foo from ..runtime.client import ApiResponse, Client from ..runtime.errors import ApiError @@ -39,11 +43,13 @@ class DefaultApi: path = "/test" headers: dict[str, str] = {} headers["Content-Type"] = "application/json" - response = self._client.request("POST", path, json=body, headers=headers) + _union_body = _union.value_0_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) if not (200 <= response.status_code < 300): detail = TestComplexUnionUnexpected(response.status_code, response.headers, response.content) raise TestComplexUnionError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) - data = response.json() # type: ignore[return-value] + _union_data = cast(Foo, _union.value_0_from_dict(response.json())) + data = _union_data return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) def test_complex_union(self, *, body: Foo) -> Foo: diff --git a/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/models/_union.py.golden b/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/models/_union.py.golden new file mode 100644 index 000000000..268dd24db --- /dev/null +++ b/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/models/_union.py.golden @@ -0,0 +1,69 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Complex Union Type Test — 1.0.0 +# Test fixture for complex union types + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'bucket', 'bucket', True) and isinstance(_union_support.get(value, 'bucket', 'bucket'), str)) + and (_union_support.has(value, 'endpoint', 'endpoint', True) and isinstance(_union_support.get(value, 'endpoint', 'endpoint'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'bar'))): + return value_1_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'path', 'path', True) and isinstance(_union_support.get(value, 'path', 'path'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'baz'))): + return value_2_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'namespace', 'namespace', True) and isinstance(_union_support.get(value, 'namespace', 'namespace'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'qux')) + and (_union_support.has(value, 'url', 'url', True) and isinstance(_union_support.get(value, 'url', 'url'), str))): + return value_3_from_dict(value) + + return value + +def value_0_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'bucket', 'bucket', True) and isinstance(_union_support.get(value, 'bucket', 'bucket'), str)) + and (_union_support.has(value, 'endpoint', 'endpoint', True) and isinstance(_union_support.get(value, 'endpoint', 'endpoint'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'bar'))): + return value_1_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'path', 'path', True) and isinstance(_union_support.get(value, 'path', 'path'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'baz'))): + return value_2_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'namespace', 'namespace', True) and isinstance(_union_support.get(value, 'namespace', 'namespace'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'qux')) + and (_union_support.has(value, 'url', 'url', True) and isinstance(_union_support.get(value, 'url', 'url'), str))): + return value_3_to_dict(value) + + return value + +def value_1_from_dict(value: object) -> object: + from.import bar + return bar.Bar.from_dict(_union_support.as_dict(value)) + +def value_1_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_2_from_dict(value: object) -> object: + from.import baz + return baz.Baz.from_dict(_union_support.as_dict(value)) + +def value_2_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_3_from_dict(value: object) -> object: + from.import qux + return qux.Qux.from_dict(_union_support.as_dict(value)) + +def value_3_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) diff --git a/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/models/foo.py.golden b/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/models/foo.py.golden index 13e60de67..64cdd8545 100644 --- a/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/models/foo.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/models/foo.py.golden @@ -5,6 +5,9 @@ from __future__ import annotations +from typing import cast + +from . import _union from .bar import Bar from .baz import Baz from .qux import Qux @@ -14,3 +17,9 @@ type Foo = ( | Baz | Qux ) + +def foo_from_dict(value: object) -> Foo: + return cast(Foo, _union.value_0_from_dict(value)) + +def foo_to_dict(value: Foo) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/models/quux.py.golden b/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/models/quux.py.golden index b3ab60f22..a026b22b0 100644 --- a/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/models/quux.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/models/quux.py.golden @@ -6,7 +6,9 @@ from __future__ import annotations from dataclasses import dataclass +from typing import cast +from . import _union from .foo import Foo @dataclass @@ -16,13 +18,13 @@ class Quux: def to_dict(self) -> dict[str, object]: result: dict[str, object] = {} - result["foo"] = self.foo + result['foo'] = _union.value_0_to_dict(self.foo) result["name"] = self.name return result @classmethod def from_dict(cls, data: dict[str, object]) -> Quux: return cls( - foo=data["foo"], # type: ignore[assignment] + foo = cast(Foo, _union.value_0_from_dict(data['foo'])), name=data["name"], # type: ignore[assignment] ) diff --git a/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/runtime/_union_support.py.golden new file mode 100644 index 000000000..1138cbb7b --- /dev/null +++ b/tests/golden/python/python-httpx/type-aliases-complex-union/complex_union_type_test/runtime/_union_support.py.golden @@ -0,0 +1,91 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Complex Union Type Test — 1.0.0 +# Test fixture for complex union types + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event.py.golden b/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event.py.golden index 07feba3ea..9e689f743 100644 --- a/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event.py.golden @@ -52,4 +52,3 @@ def event_to_dict(obj: Event) -> dict[str, object]: result["kind"] = "EVENT_UPDATED" return result raise ValueError(f"Unknown variant for Event: {type(obj)}") - diff --git a/tests/golden/python/python-httpx/type-aliases-discriminated-union-internally-tagged/discriminated_union_internally_tagged_test/models/resource.py.golden b/tests/golden/python/python-httpx/type-aliases-discriminated-union-internally-tagged/discriminated_union_internally_tagged_test/models/resource.py.golden index d76312cc8..ecd0bb5f6 100644 --- a/tests/golden/python/python-httpx/type-aliases-discriminated-union-internally-tagged/discriminated_union_internally_tagged_test/models/resource.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-discriminated-union-internally-tagged/discriminated_union_internally_tagged_test/models/resource.py.golden @@ -45,4 +45,3 @@ def resource_to_dict(obj: Resource) -> dict[str, object]: result["kind"] = "RESOURCE_CUSTOM" return result raise ValueError(f"Unknown variant for Resource: {type(obj)}") - diff --git a/tests/golden/python/python-httpx/type-aliases-discriminated-union-long-names/discriminated_union_with_long_type_names_test/models/very_long_discriminated_union_type_name_for_testing20260630.py.golden b/tests/golden/python/python-httpx/type-aliases-discriminated-union-long-names/discriminated_union_with_long_type_names_test/models/very_long_discriminated_union_type_name_for_testing20260630.py.golden index 4e36d9df7..7988c2457 100644 --- a/tests/golden/python/python-httpx/type-aliases-discriminated-union-long-names/discriminated_union_with_long_type_names_test/models/very_long_discriminated_union_type_name_for_testing20260630.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-discriminated-union-long-names/discriminated_union_with_long_type_names_test/models/very_long_discriminated_union_type_name_for_testing20260630.py.golden @@ -45,4 +45,3 @@ def very_long_discriminated_union_type_name_for_testing20260630_to_dict(obj: Ver result["kind"] = "GAMMA" return result raise ValueError(f"Unknown variant for VeryLongDiscriminatedUnionTypeNameForTesting20260630: {type(obj)}") - diff --git a/tests/golden/python/python-httpx/type-aliases-discriminated-union-mixed-unit-and-allof/discriminated_union_mixed_unit_and_all_of_test/models/setup.py.golden b/tests/golden/python/python-httpx/type-aliases-discriminated-union-mixed-unit-and-allof/discriminated_union_mixed_unit_and_all_of_test/models/setup.py.golden index 8b1a830e9..91e0ca866 100644 --- a/tests/golden/python/python-httpx/type-aliases-discriminated-union-mixed-unit-and-allof/discriminated_union_mixed_unit_and_all_of_test/models/setup.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-discriminated-union-mixed-unit-and-allof/discriminated_union_mixed_unit_and_all_of_test/models/setup.py.golden @@ -50,4 +50,3 @@ def setup_to_dict(obj: Setup) -> dict[str, object]: result["kind"] = "SETUP_CUSTOM" return result raise ValueError(f"Unknown variant for Setup: {type(obj)}") - diff --git a/tests/golden/python/python-httpx/type-aliases-discriminated-union-multiple/multiple_discriminated_unions_test/models/container_kind.py.golden b/tests/golden/python/python-httpx/type-aliases-discriminated-union-multiple/multiple_discriminated_unions_test/models/container_kind.py.golden index 062ea9ceb..b112bc8d3 100644 --- a/tests/golden/python/python-httpx/type-aliases-discriminated-union-multiple/multiple_discriminated_unions_test/models/container_kind.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-discriminated-union-multiple/multiple_discriminated_unions_test/models/container_kind.py.golden @@ -38,4 +38,3 @@ def container_kind_to_dict(obj: ContainerKind) -> dict[str, object]: result["kind"] = "CONTAINER_KIND_CUSTOM" return result raise ValueError(f"Unknown variant for ContainerKind: {type(obj)}") - diff --git a/tests/golden/python/python-httpx/type-aliases-discriminated-union-multiple/multiple_discriminated_unions_test/models/volume_kind.py.golden b/tests/golden/python/python-httpx/type-aliases-discriminated-union-multiple/multiple_discriminated_unions_test/models/volume_kind.py.golden index d8653cb5d..eef24705b 100644 --- a/tests/golden/python/python-httpx/type-aliases-discriminated-union-multiple/multiple_discriminated_unions_test/models/volume_kind.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-discriminated-union-multiple/multiple_discriminated_unions_test/models/volume_kind.py.golden @@ -38,4 +38,3 @@ def volume_kind_to_dict(obj: VolumeKind) -> dict[str, object]: result["kind"] = "VOLUME_KIND_REMOTE" return result raise ValueError(f"Unknown variant for VolumeKind: {type(obj)}") - diff --git a/tests/golden/python/python-httpx/type-aliases-discriminated-union-with-refs/discriminated_union_with_references_test/models/container_image.py.golden b/tests/golden/python/python-httpx/type-aliases-discriminated-union-with-refs/discriminated_union_with_references_test/models/container_image.py.golden index e6fdcfa0c..e2ff20d31 100644 --- a/tests/golden/python/python-httpx/type-aliases-discriminated-union-with-refs/discriminated_union_with_references_test/models/container_image.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-discriminated-union-with-refs/discriminated_union_with_references_test/models/container_image.py.golden @@ -36,4 +36,3 @@ def container_image_to_dict(obj: ContainerImage) -> dict[str, object]: result["kind"] = "CONTAINER_IMAGE_CUSTOM" return result raise ValueError(f"Unknown variant for ContainerImage: {type(obj)}") - diff --git a/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/apis/default_api.py.golden b/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/apis/default_api.py.golden index ce4cfc29e..2c0af5cd2 100644 --- a/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/apis/default_api.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/apis/default_api.py.golden @@ -5,8 +5,12 @@ from __future__ import annotations +import json + from collections.abc import Mapping +from typing import cast +from ..models import _union from ..models.foo import Foo from ..runtime.client import ApiResponse, Client from ..runtime.errors import ApiError @@ -39,11 +43,13 @@ class DefaultApi: path = "/test" headers: dict[str, str] = {} headers["Content-Type"] = "application/json" - response = self._client.request("POST", path, json=body, headers=headers) + _union_body = _union.value_0_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) if not (200 <= response.status_code < 300): detail = TestNestedUnionUnexpected(response.status_code, response.headers, response.content) raise TestNestedUnionError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) - data = response.json() # type: ignore[return-value] + _union_data = cast(Foo, _union.value_0_from_dict(response.json())) + data = _union_data return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) def test_nested_union(self, *, body: Foo) -> Foo: diff --git a/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/models/_union.py.golden b/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/models/_union.py.golden new file mode 100644 index 000000000..7a0183361 --- /dev/null +++ b/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/models/_union.py.golden @@ -0,0 +1,94 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Union Type Test — 1.0.0 +# Test fixture for union types that reference other union types + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'endpoint', 'endpoint', True) and isinstance(_union_support.get(value, 'endpoint', 'endpoint'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'bar'))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'path', 'path', True) and isinstance(_union_support.get(value, 'path', 'path'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'baz')))): + return value_1_from_dict(value) + + if isinstance(value, str): + return value_4_from_dict(value) + + if _union_support.number(value, False): + return value_5_from_dict(value) + + return value + +def value_0_to_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'endpoint', 'endpoint', True) and isinstance(_union_support.get(value, 'endpoint', 'endpoint'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'bar'))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'path', 'path', True) and isinstance(_union_support.get(value, 'path', 'path'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'baz')))): + return value_1_to_dict(value) + + if isinstance(value, str): + return value_4_to_dict(value) + + if _union_support.number(value, False): + return value_5_to_dict(value) + + return value + +def value_1_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'endpoint', 'endpoint', True) and isinstance(_union_support.get(value, 'endpoint', 'endpoint'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'bar'))): + return value_2_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'path', 'path', True) and isinstance(_union_support.get(value, 'path', 'path'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'baz'))): + return value_3_from_dict(value) + + return value + +def value_1_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'endpoint', 'endpoint', True) and isinstance(_union_support.get(value, 'endpoint', 'endpoint'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'bar'))): + return value_2_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'path', 'path', True) and isinstance(_union_support.get(value, 'path', 'path'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'baz'))): + return value_3_to_dict(value) + + return value + +def value_2_from_dict(value: object) -> object: + from.import bar + return bar.Bar.from_dict(_union_support.as_dict(value)) + +def value_2_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_3_from_dict(value: object) -> object: + from.import baz + return baz.Baz.from_dict(_union_support.as_dict(value)) + +def value_3_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_4_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_4_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_5_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_5_to_dict(value: object) -> object: + return _union_support.scalar(value) diff --git a/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/models/foo.py.golden b/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/models/foo.py.golden index e1105b17d..a92ee5d56 100644 --- a/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/models/foo.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/models/foo.py.golden @@ -5,6 +5,9 @@ from __future__ import annotations +from typing import cast + +from . import _union from .qux import Qux type Foo = ( @@ -12,3 +15,9 @@ type Foo = ( | str | float ) + +def foo_from_dict(value: object) -> Foo: + return cast(Foo, _union.value_0_from_dict(value)) + +def foo_to_dict(value: Foo) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/models/qux.py.golden b/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/models/qux.py.golden index c324ed9ec..74b6e838b 100644 --- a/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/models/qux.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/models/qux.py.golden @@ -5,6 +5,9 @@ from __future__ import annotations +from typing import cast + +from . import _union from .bar import Bar from .baz import Baz @@ -12,3 +15,9 @@ type Qux = ( Bar | Baz ) + +def qux_from_dict(value: object) -> Qux: + return cast(Qux, _union.value_1_from_dict(value)) + +def qux_to_dict(value: Qux) -> object: + return _union.value_1_to_dict(value) diff --git a/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/runtime/_union_support.py.golden new file mode 100644 index 000000000..386b49b07 --- /dev/null +++ b/tests/golden/python/python-httpx/type-aliases-nested-union/nested_union_type_test/runtime/_union_support.py.golden @@ -0,0 +1,91 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Union Type Test — 1.0.0 +# Test fixture for union types that reference other union types + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-httpx/type-aliases-union-mixed/union_type_with_mixed_members_test/apis/default_api.py.golden b/tests/golden/python/python-httpx/type-aliases-union-mixed/union_type_with_mixed_members_test/apis/default_api.py.golden index 82a85d2ef..b7db24eb2 100644 --- a/tests/golden/python/python-httpx/type-aliases-union-mixed/union_type_with_mixed_members_test/apis/default_api.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-union-mixed/union_type_with_mixed_members_test/apis/default_api.py.golden @@ -5,8 +5,12 @@ from __future__ import annotations +import json + from collections.abc import Mapping +from typing import cast +from ..models import _union from ..models.foo import Foo from ..runtime.client import ApiResponse, Client from ..runtime.errors import ApiError @@ -39,11 +43,13 @@ class DefaultApi: path = "/test" headers: dict[str, str] = {} headers["Content-Type"] = "application/json" - response = self._client.request("POST", path, json=body, headers=headers) + _union_body = _union.value_0_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) if not (200 <= response.status_code < 300): detail = TestMixedUnionUnexpected(response.status_code, response.headers, response.content) raise TestMixedUnionError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) - data = response.json() # type: ignore[return-value] + _union_data = cast(Foo, _union.value_0_from_dict(response.json())) + data = _union_data return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) def test_mixed_union(self, *, body: Foo) -> Foo: diff --git a/tests/golden/python/python-httpx/type-aliases-union-mixed/union_type_with_mixed_members_test/models/_union.py.golden b/tests/golden/python/python-httpx/type-aliases-union-mixed/union_type_with_mixed_members_test/models/_union.py.golden new file mode 100644 index 000000000..8c5b0d0e0 --- /dev/null +++ b/tests/golden/python/python-httpx/type-aliases-union-mixed/union_type_with_mixed_members_test/models/_union.py.golden @@ -0,0 +1,55 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Mixed Members Test — 1.0.0 +# Test fixture for union types with both interfaces and primitives + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): + return value_1_from_dict(value) + + if isinstance(value, str): + return value_2_from_dict(value) + + if _union_support.number(value, False): + return value_3_from_dict(value) + + return value + +def value_0_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): + return value_1_to_dict(value) + + if isinstance(value, str): + return value_2_to_dict(value) + + if _union_support.number(value, False): + return value_3_to_dict(value) + + return value + +def value_1_from_dict(value: object) -> object: + from.import bar + return bar.Bar.from_dict(_union_support.as_dict(value)) + +def value_1_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_2_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_to_dict(value: object) -> object: + return _union_support.scalar(value) diff --git a/tests/golden/python/python-httpx/type-aliases-union-mixed/union_type_with_mixed_members_test/models/foo.py.golden b/tests/golden/python/python-httpx/type-aliases-union-mixed/union_type_with_mixed_members_test/models/foo.py.golden index 97c56b2dd..28674817a 100644 --- a/tests/golden/python/python-httpx/type-aliases-union-mixed/union_type_with_mixed_members_test/models/foo.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-union-mixed/union_type_with_mixed_members_test/models/foo.py.golden @@ -5,6 +5,9 @@ from __future__ import annotations +from typing import cast + +from . import _union from .bar import Bar type Foo = ( @@ -12,3 +15,9 @@ type Foo = ( | str | float ) + +def foo_from_dict(value: object) -> Foo: + return cast(Foo, _union.value_0_from_dict(value)) + +def foo_to_dict(value: Foo) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-httpx/type-aliases-union-mixed/union_type_with_mixed_members_test/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/type-aliases-union-mixed/union_type_with_mixed_members_test/runtime/_union_support.py.golden new file mode 100644 index 000000000..8a0a49a69 --- /dev/null +++ b/tests/golden/python/python-httpx/type-aliases-union-mixed/union_type_with_mixed_members_test/runtime/_union_support.py.golden @@ -0,0 +1,91 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Mixed Members Test — 1.0.0 +# Test fixture for union types with both interfaces and primitives + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/apis/default_api.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/apis/default_api.py.golden index d1d06568f..cd4b600e8 100644 --- a/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/apis/default_api.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/apis/default_api.py.golden @@ -5,8 +5,11 @@ from __future__ import annotations +import json + from collections.abc import Mapping +from ..models import _union from ..models.config import Config from ..runtime.client import ApiResponse, Client from ..runtime.errors import ApiError @@ -39,11 +42,13 @@ class DefaultApi: path = "/test" headers: dict[str, str] = {} headers["Content-Type"] = "application/json" - response = self._client.request("POST", path, json=body, headers=headers) + _union_body = _union.value_0_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) if not (200 <= response.status_code < 300): detail = TestUnionWithAnyUnexpected(response.status_code, response.headers, response.content) raise TestUnionWithAnyError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) - data = response.json() # type: ignore[return-value] + _union_data = _union.value_0_from_dict(response.json()) + data = _union_data return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) def test_union_with_any(self, *, body: Config) -> Config: diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/models/_union.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/models/_union.py.golden new file mode 100644 index 000000000..db5c526a8 --- /dev/null +++ b/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/models/_union.py.golden @@ -0,0 +1,50 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Any Test — 1.0.0 +# Test fixture for union types that include the any type + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if isinstance(value, str): + return value_1_from_dict(value) + + if _union_support.number(value, False): + return value_2_from_dict(value) + + if True: + return value_3_from_dict(value) + + return value + +def value_0_to_dict(value: object) -> object: + if isinstance(value, str): + return value_1_to_dict(value) + + if _union_support.number(value, False): + return value_2_to_dict(value) + + if True: + return value_3_to_dict(value) + + return value + +def value_1_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_1_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_to_dict(value: object) -> object: + return _union_support.scalar(value) diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/models/config.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/models/config.py.golden index 349fb1c06..4a36e52d5 100644 --- a/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/models/config.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/models/config.py.golden @@ -7,8 +7,16 @@ from __future__ import annotations from typing import Any +from . import _union + type Config = ( str | float | Any ) + +def config_from_dict(value: object) -> Config: + return _union.value_0_from_dict(value) + +def config_to_dict(value: Config) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/runtime/_union_support.py.golden new file mode 100644 index 000000000..aa7278d60 --- /dev/null +++ b/tests/golden/python/python-httpx/type-aliases-union-with-any/union_type_with_any_test/runtime/_union_support.py.golden @@ -0,0 +1,91 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Any Test — 1.0.0 +# Test fixture for union types that include the any type + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/apis/default_api.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/apis/default_api.py.golden index 6b9f7b76d..7975f00cd 100644 --- a/tests/golden/python/python-httpx/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/apis/default_api.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/apis/default_api.py.golden @@ -5,8 +5,12 @@ from __future__ import annotations +import json + from collections.abc import Mapping +from typing import cast +from ..models import _union from ..models.foo import Foo from ..runtime.client import ApiResponse, Client from ..runtime.errors import ApiError @@ -39,11 +43,13 @@ class DefaultApi: path = "/test" headers: dict[str, str] = {} headers["Content-Type"] = "application/json" - response = self._client.request("POST", path, json=body, headers=headers) + _union_body = _union.value_0_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) if not (200 <= response.status_code < 300): detail = TestUnionWithInlineObjectsUnexpected(response.status_code, response.headers, response.content) raise TestUnionWithInlineObjectsError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) - data = response.json() # type: ignore[return-value] + _union_data = cast(Foo, _union.value_0_from_dict(response.json())) + data = _union_data return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) def test_union_with_inline_objects(self, *, body: Foo) -> Foo: diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/models/_union.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/models/_union.py.golden new file mode 100644 index 000000000..cf60881e2 --- /dev/null +++ b/tests/golden/python/python-httpx/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/models/_union.py.golden @@ -0,0 +1,67 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Inline Objects Test — 1.0.0 +# Test fixture for union types (oneOf) with inline object schemas that should generate named interfaces + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'bar', 'bar', True) and (_union_support.is_object(_union_support.get(value, 'bar', 'bar')) + and (_union_support.has(_union_support.get(value, 'bar', 'bar'), 'baz', 'baz', True) and isinstance(_union_support.get(_union_support.get(value, 'bar', 'bar'), 'baz', 'baz'), str)) + and (_union_support.has(_union_support.get(value, 'bar', 'bar'), 'qux', 'qux', True) and isinstance(_union_support.get(_union_support.get(value, 'bar', 'bar'), 'qux', 'qux'), str))))): + return value_1_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'quux', 'quux', True) and (_union_support.is_object(_union_support.get(value, 'quux', 'quux')) + and (_union_support.has(_union_support.get(value, 'quux', 'quux'), 'corge', 'corge', True) and isinstance(_union_support.get(_union_support.get(value, 'quux', 'quux'), 'corge', 'corge'), str))))): + return value_2_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'grault', 'grault', True) and (_union_support.is_object(_union_support.get(value, 'grault', 'grault')) + and (_union_support.has(_union_support.get(value, 'grault', 'grault'), 'garply', 'garply', True) and isinstance(_union_support.get(_union_support.get(value, 'grault', 'grault'), 'garply', 'garply'), str))))): + return value_3_from_dict(value) + + return value + +def value_0_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'bar', 'bar', True) and (_union_support.is_object(_union_support.get(value, 'bar', 'bar')) + and (_union_support.has(_union_support.get(value, 'bar', 'bar'), 'baz', 'baz', True) and isinstance(_union_support.get(_union_support.get(value, 'bar', 'bar'), 'baz', 'baz'), str)) + and (_union_support.has(_union_support.get(value, 'bar', 'bar'), 'qux', 'qux', True) and isinstance(_union_support.get(_union_support.get(value, 'bar', 'bar'), 'qux', 'qux'), str))))): + return value_1_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'quux', 'quux', True) and (_union_support.is_object(_union_support.get(value, 'quux', 'quux')) + and (_union_support.has(_union_support.get(value, 'quux', 'quux'), 'corge', 'corge', True) and isinstance(_union_support.get(_union_support.get(value, 'quux', 'quux'), 'corge', 'corge'), str))))): + return value_2_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'grault', 'grault', True) and (_union_support.is_object(_union_support.get(value, 'grault', 'grault')) + and (_union_support.has(_union_support.get(value, 'grault', 'grault'), 'garply', 'garply', True) and isinstance(_union_support.get(_union_support.get(value, 'grault', 'grault'), 'garply', 'garply'), str))))): + return value_3_to_dict(value) + + return value + +def value_1_from_dict(value: object) -> object: + from.import foo_member1 + return foo_member1.FooMember1.from_dict(_union_support.as_dict(value)) + +def value_1_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_2_from_dict(value: object) -> object: + from.import foo_member2 + return foo_member2.FooMember2.from_dict(_union_support.as_dict(value)) + +def value_2_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_3_from_dict(value: object) -> object: + from.import foo_member3 + return foo_member3.FooMember3.from_dict(_union_support.as_dict(value)) + +def value_3_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/models/foo.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/models/foo.py.golden index aad0b38f5..a9e8d92e9 100644 --- a/tests/golden/python/python-httpx/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/models/foo.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/models/foo.py.golden @@ -5,6 +5,9 @@ from __future__ import annotations +from typing import cast + +from . import _union from .foo_member1 import FooMember1 from .foo_member2 import FooMember2 from .foo_member3 import FooMember3 @@ -16,3 +19,9 @@ type Foo = ( | FooMember2 | FooMember3 ) + +def foo_from_dict(value: object) -> Foo: + return cast(Foo, _union.value_0_from_dict(value)) + +def foo_to_dict(value: Foo) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/runtime/_union_support.py.golden new file mode 100644 index 000000000..20fb2628d --- /dev/null +++ b/tests/golden/python/python-httpx/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/runtime/_union_support.py.golden @@ -0,0 +1,91 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Inline Objects Test — 1.0.0 +# Test fixture for union types (oneOf) with inline object schemas that should generate named interfaces + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-interfaces/union_type_with_interfaces_test/apis/default_api.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-interfaces/union_type_with_interfaces_test/apis/default_api.py.golden index 42731ad22..e2fb2f69e 100644 --- a/tests/golden/python/python-httpx/type-aliases-union-with-interfaces/union_type_with_interfaces_test/apis/default_api.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-union-with-interfaces/union_type_with_interfaces_test/apis/default_api.py.golden @@ -5,8 +5,12 @@ from __future__ import annotations +import json + from collections.abc import Mapping +from typing import cast +from ..models import _union from ..models.foo import Foo from ..runtime.client import ApiResponse, Client from ..runtime.errors import ApiError @@ -39,11 +43,13 @@ class DefaultApi: path = "/test" headers: dict[str, str] = {} headers["Content-Type"] = "application/json" - response = self._client.request("POST", path, json=body, headers=headers) + _union_body = _union.value_0_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) if not (200 <= response.status_code < 300): detail = TestUnionUnexpected(response.status_code, response.headers, response.content) raise TestUnionError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) - data = response.json() # type: ignore[return-value] + _union_data = cast(Foo, _union.value_0_from_dict(response.json())) + data = _union_data return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) def test_union(self, *, body: Foo) -> Foo: diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-interfaces/union_type_with_interfaces_test/models/_union.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-interfaces/union_type_with_interfaces_test/models/_union.py.golden new file mode 100644 index 000000000..de4fcb73a --- /dev/null +++ b/tests/golden/python/python-httpx/type-aliases-union-with-interfaces/union_type_with_interfaces_test/models/_union.py.golden @@ -0,0 +1,69 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Interfaces Test — 1.0.0 +# Test fixture for oneOf/anyOf union types with interface members + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'bucket', 'bucket', True) and isinstance(_union_support.get(value, 'bucket', 'bucket'), str)) + and (_union_support.has(value, 'endpoint', 'endpoint', True) and isinstance(_union_support.get(value, 'endpoint', 'endpoint'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'bar'))): + return value_1_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'path', 'path', True) and isinstance(_union_support.get(value, 'path', 'path'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'baz'))): + return value_2_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'namespace', 'namespace', True) and isinstance(_union_support.get(value, 'namespace', 'namespace'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'qux')) + and (_union_support.has(value, 'url', 'url', True) and isinstance(_union_support.get(value, 'url', 'url'), str))): + return value_3_from_dict(value) + + return value + +def value_0_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'bucket', 'bucket', True) and isinstance(_union_support.get(value, 'bucket', 'bucket'), str)) + and (_union_support.has(value, 'endpoint', 'endpoint', True) and isinstance(_union_support.get(value, 'endpoint', 'endpoint'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'bar'))): + return value_1_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'path', 'path', True) and isinstance(_union_support.get(value, 'path', 'path'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'baz'))): + return value_2_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'namespace', 'namespace', True) and isinstance(_union_support.get(value, 'namespace', 'namespace'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'qux')) + and (_union_support.has(value, 'url', 'url', True) and isinstance(_union_support.get(value, 'url', 'url'), str))): + return value_3_to_dict(value) + + return value + +def value_1_from_dict(value: object) -> object: + from.import bar + return bar.Bar.from_dict(_union_support.as_dict(value)) + +def value_1_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_2_from_dict(value: object) -> object: + from.import baz + return baz.Baz.from_dict(_union_support.as_dict(value)) + +def value_2_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_3_from_dict(value: object) -> object: + from.import qux + return qux.Qux.from_dict(_union_support.as_dict(value)) + +def value_3_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-interfaces/union_type_with_interfaces_test/models/foo.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-interfaces/union_type_with_interfaces_test/models/foo.py.golden index cd6aa5fd0..681e37952 100644 --- a/tests/golden/python/python-httpx/type-aliases-union-with-interfaces/union_type_with_interfaces_test/models/foo.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-union-with-interfaces/union_type_with_interfaces_test/models/foo.py.golden @@ -5,6 +5,9 @@ from __future__ import annotations +from typing import cast + +from . import _union from .bar import Bar from .baz import Baz from .qux import Qux @@ -14,3 +17,9 @@ type Foo = ( | Baz | Qux ) + +def foo_from_dict(value: object) -> Foo: + return cast(Foo, _union.value_0_from_dict(value)) + +def foo_to_dict(value: Foo) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-interfaces/union_type_with_interfaces_test/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-interfaces/union_type_with_interfaces_test/runtime/_union_support.py.golden new file mode 100644 index 000000000..b207b3392 --- /dev/null +++ b/tests/golden/python/python-httpx/type-aliases-union-with-interfaces/union_type_with_interfaces_test/runtime/_union_support.py.golden @@ -0,0 +1,91 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Interfaces Test — 1.0.0 +# Test fixture for oneOf/anyOf union types with interface members + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-primitives/union_type_with_primitives_test/apis/default_api.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-primitives/union_type_with_primitives_test/apis/default_api.py.golden index 598346d91..c1d0536cc 100644 --- a/tests/golden/python/python-httpx/type-aliases-union-with-primitives/union_type_with_primitives_test/apis/default_api.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-union-with-primitives/union_type_with_primitives_test/apis/default_api.py.golden @@ -5,8 +5,12 @@ from __future__ import annotations +import json + from collections.abc import Mapping +from typing import cast +from ..models import _union from ..models.foo import Foo from ..runtime.client import ApiResponse, Client from ..runtime.errors import ApiError @@ -39,11 +43,13 @@ class DefaultApi: path = "/test" headers: dict[str, str] = {} headers["Content-Type"] = "application/json" - response = self._client.request("POST", path, json=body, headers=headers) + _union_body = _union.value_0_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) if not (200 <= response.status_code < 300): detail = TestUnionPrimitivesUnexpected(response.status_code, response.headers, response.content) raise TestUnionPrimitivesError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) - data = response.json() # type: ignore[return-value] + _union_data = cast(Foo, _union.value_0_from_dict(response.json())) + data = _union_data return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) def test_union_primitives(self, *, body: Foo) -> Foo: diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-primitives/union_type_with_primitives_test/models/_union.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-primitives/union_type_with_primitives_test/models/_union.py.golden new file mode 100644 index 000000000..77a6e5ae9 --- /dev/null +++ b/tests/golden/python/python-httpx/type-aliases-union-with-primitives/union_type_with_primitives_test/models/_union.py.golden @@ -0,0 +1,56 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Primitives Test — 1.0.0 +# Test fixture for union types with primitive members + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if isinstance(value, str): + return value_1_from_dict(value) + + if _union_support.number(value, False): + return value_2_from_dict(value) + + if isinstance(value, bool): + return value_3_from_dict(value) + + if value is None: + return None + + return value + +def value_0_to_dict(value: object) -> object: + if isinstance(value, str): + return value_1_to_dict(value) + + if _union_support.number(value, False): + return value_2_to_dict(value) + + if isinstance(value, bool): + return value_3_to_dict(value) + + if value is None: + return None + + return value + +def value_1_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_1_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_to_dict(value: object) -> object: + return _union_support.scalar(value) diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-primitives/union_type_with_primitives_test/models/foo.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-primitives/union_type_with_primitives_test/models/foo.py.golden index c4962a9b9..1da40da0e 100644 --- a/tests/golden/python/python-httpx/type-aliases-union-with-primitives/union_type_with_primitives_test/models/foo.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-union-with-primitives/union_type_with_primitives_test/models/foo.py.golden @@ -5,9 +5,19 @@ from __future__ import annotations +from typing import cast + +from . import _union + type Foo = ( str | float | bool | None ) + +def foo_from_dict(value: object) -> Foo: + return cast(Foo, _union.value_0_from_dict(value)) + +def foo_to_dict(value: Foo) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-httpx/type-aliases-union-with-primitives/union_type_with_primitives_test/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/type-aliases-union-with-primitives/union_type_with_primitives_test/runtime/_union_support.py.golden new file mode 100644 index 000000000..30f44c9b4 --- /dev/null +++ b/tests/golden/python/python-httpx/type-aliases-union-with-primitives/union_type_with_primitives_test/runtime/_union_support.py.golden @@ -0,0 +1,91 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Primitives Test — 1.0.0 +# Test fixture for union types with primitive members + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-httpx/untagged-union-raw/README.md.golden b/tests/golden/python/python-httpx/untagged-union-raw/README.md.golden new file mode 100644 index 000000000..a6f182f18 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/README.md.golden @@ -0,0 +1,7 @@ +# Python union conversion + +Generated Python SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `python_union_conversion`. diff --git a/tests/golden/python/python-httpx/untagged-union-raw/pyproject.toml.golden b/tests/golden/python/python-httpx/untagged-union-raw/pyproject.toml.golden new file mode 100644 index 000000000..01539402d --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/pyproject.toml.golden @@ -0,0 +1,10 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "python_union_conversion" +version = "1.0.0" +description = "Generated Python SDK." +requires-python = ">=3.12" +dependencies = ["httpx>=0.27"] diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/__init__.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/__init__.py.golden new file mode 100644 index 000000000..8f525cedf --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/__init__.py.golden @@ -0,0 +1,11 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .runtime import ApiKeyAuth as ApiKeyAuth +from .runtime import Authenticator as Authenticator +from .runtime import BearerAuth as BearerAuth +from .runtime import ApiResponse as ApiResponse +from .runtime import Client as Client +from .runtime import ApiError as ApiError +from .runtime import UploadFile as UploadFile diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/apis/__init__.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/apis/__init__.py.golden new file mode 100644 index 000000000..8126d3b1a --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/apis/__init__.py.golden @@ -0,0 +1,5 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .default_api import DefaultApi as DefaultApi diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/apis/default_api.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/apis/default_api.py.golden new file mode 100644 index 000000000..b13c5647d --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/apis/default_api.py.golden @@ -0,0 +1,290 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +import json + +from collections.abc import Mapping +from typing import cast + +from ..models import _union +from ..models.choice import Choice +from ..models.envelope import Envelope +from ..models.nullable_choice import NullableChoice +from ..models.send_multipart_multipart_request_body import SendMultipartMultipartRequestBody +from ..models.tagged_choice import TaggedChoice +from ..models.tagged_member_choice import TaggedMemberChoice +from ..runtime.client import ApiResponse, Client +from ..runtime.errors import ApiError + +class SendChoiceBadRequest: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: Choice | None = None + self._body_error: Exception | None = None + @property + def body(self) -> Choice: + if not self._body_loaded: + try: + self._body_value = _union.value_11_from_dict(json.loads(self.raw_body.decode("utf-8"))) + + except Exception as exc: + self._body_error = exc + + self._body_loaded = True + + if self._body_error is not None: + raise self._body_error + + return self._body_value + +class SendChoiceUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendChoiceErrorDetail = ( + SendChoiceBadRequest + | SendChoiceUnexpected +) + +class SendChoiceError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendChoiceErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendChoiceErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendChoicesUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendChoicesErrorDetail = ( + SendChoicesUnexpected +) + +class SendChoicesError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendChoicesErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendChoicesErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendEnvelopeUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendEnvelopeErrorDetail = ( + SendEnvelopeUnexpected +) + +class SendEnvelopeError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendEnvelopeErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendEnvelopeErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendMultipartUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendMultipartErrorDetail = ( + SendMultipartUnexpected +) + +class SendMultipartError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendMultipartErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendMultipartErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendNullableUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendNullableErrorDetail = ( + SendNullableUnexpected +) + +class SendNullableError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendNullableErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendNullableErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendTaggedUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendTaggedErrorDetail = ( + SendTaggedUnexpected +) + +class SendTaggedError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendTaggedErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendTaggedErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendTaggedMemberUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendTaggedMemberErrorDetail = ( + SendTaggedMemberUnexpected +) + +class SendTaggedMemberError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendTaggedMemberErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendTaggedMemberErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + + +class DefaultApi: + def __init__(self, client: Client) -> None: + self._client = client + + def send_choice_with_http_info(self, *, body: Choice) -> ApiResponse[Choice]: + path = "/choice" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_11_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + if response.status_code == 400: + detail = SendChoiceBadRequest(response.status_code, response.headers, response.content) + else: + detail = SendChoiceUnexpected(response.status_code, response.headers, response.content) + raise SendChoiceError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + _union_data = _union.value_11_from_dict(response.json()) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_choice(self, *, body: Choice) -> Choice: + return self.send_choice_with_http_info(body=body).data + + def send_choices_with_http_info(self, *, body: list[Choice]) -> ApiResponse[dict[str, Choice]]: + path = "/choices" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_17_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendChoicesUnexpected(response.status_code, response.headers, response.content) + raise SendChoicesError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + _union_data = cast(dict[str, Choice], _union.value_18_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_choices(self, *, body: list[Choice]) -> dict[str, Choice]: + return self.send_choices_with_http_info(body=body).data + + def send_envelope_with_http_info(self, *, body: Envelope) -> ApiResponse[Envelope]: + path = "/envelope" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + response = self._client.request("POST", path, json=body.to_dict(), headers=headers) + if not (200 <= response.status_code < 300): + detail = SendEnvelopeUnexpected(response.status_code, response.headers, response.content) + raise SendEnvelopeError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + data = Envelope.from_dict(response.json()) + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_envelope(self, *, body: Envelope) -> Envelope: + return self.send_envelope_with_http_info(body=body).data + + def send_multipart_with_http_info(self, *, body: SendMultipartMultipartRequestBody) -> ApiResponse[None]: + path = "/multipart" + files: dict[str, object] = {} + if body.attachment is not None: + files["attachment"] = (body.attachment.filename_or_default("attachment"), body.attachment.data, "application/octet-stream") + + files['choice'] = (None, json.dumps(_union.value_11_to_dict(body.choice)), 'application/json') + response = self._client.request("POST", path, files=files if files else None) + if not (200 <= response.status_code < 300): + detail = SendMultipartUnexpected(response.status_code, response.headers, response.content) + raise SendMultipartError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + data = None + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_multipart(self, *, body: SendMultipartMultipartRequestBody) -> None: + return self.send_multipart_with_http_info(body=body).data + + def send_nullable_with_http_info(self, *, body: NullableChoice) -> ApiResponse[NullableChoice]: + path = "/nullable" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_29_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendNullableUnexpected(response.status_code, response.headers, response.content) + raise SendNullableError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + _union_data = _union.value_29_from_dict(response.json()) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_nullable(self, *, body: NullableChoice) -> NullableChoice: + return self.send_nullable_with_http_info(body=body).data + + def send_tagged_with_http_info(self, *, body: TaggedChoice) -> ApiResponse[TaggedChoice]: + path = "/tagged" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_41_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendTaggedUnexpected(response.status_code, response.headers, response.content) + raise SendTaggedError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + _union_data = _union.value_41_from_dict(response.json()) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_tagged(self, *, body: TaggedChoice) -> TaggedChoice: + return self.send_tagged_with_http_info(body=body).data + + def send_tagged_member_with_http_info(self, *, body: TaggedMemberChoice) -> ApiResponse[TaggedMemberChoice]: + path = "/tagged-member" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_45_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendTaggedMemberUnexpected(response.status_code, response.headers, response.content) + raise SendTaggedMemberError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + _union_data = _union.value_45_from_dict(response.json()) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_tagged_member(self, *, body: TaggedMemberChoice) -> TaggedMemberChoice: + return self.send_tagged_member_with_http_info(body=body).data diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/__init__.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/__init__.py.golden new file mode 100644 index 000000000..30669ee6e --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/__init__.py.golden @@ -0,0 +1,40 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .acyclic_envelope import AcyclicEnvelope as AcyclicEnvelope +from .acyclic_scalar import AcyclicScalar as AcyclicScalar +from .adjacent_member import AdjacentMember as AdjacentMember +from .ambiguous import Ambiguous as Ambiguous +from .ambiguous_tagged_member import AmbiguousTaggedMember as AmbiguousTaggedMember +from .branch import Branch as Branch +from .choice import Choice as Choice +from .choice_alias import ChoiceAlias as ChoiceAlias +from .client_tool import ClientTool as ClientTool +from .dependent_choice import DependentChoice as DependentChoice +from .dependent_envelope import DependentEnvelope as DependentEnvelope +from .envelope import Envelope as Envelope +from .finite_envelope import FiniteEnvelope as FiniteEnvelope +from .inner_null_choice import InnerNullChoice as InnerNullChoice +from .nested_ambiguous import NestedAmbiguous as NestedAmbiguous +from .nullable_choice import NullableChoice as NullableChoice +from .nullable_envelope import NullableEnvelope as NullableEnvelope +from .numeric_one import NumericOne as NumericOne +from .numeric_two import NumericTwo as NumericTwo +from .open_choice import OpenChoice as OpenChoice +from .recursive import Recursive as Recursive +from .request import Request as Request +from .scalar_choice import ScalarChoice as ScalarChoice +from .search_tool import SearchTool as SearchTool +from .send_multipart_multipart_request_body import SendMultipartMultipartRequestBody as SendMultipartMultipartRequestBody +from .send_multipart_request import SendMultipartRequest as SendMultipartRequest +from .tagged_ambiguous import TaggedAmbiguous as TaggedAmbiguous +from .tagged_choice import TaggedChoice as TaggedChoice +from .tagged_container_choice import TaggedContainerChoice as TaggedContainerChoice +from .tagged_member_alias import TaggedMemberAlias as TaggedMemberAlias +from .tagged_member_choice import TaggedMemberChoice as TaggedMemberChoice +from .tagged_member_envelope import TaggedMemberEnvelope as TaggedMemberEnvelope +from .tagged_object_choice import TaggedObjectChoice as TaggedObjectChoice +from .tagged_record import TaggedRecord as TaggedRecord +from .tool import Tool as Tool +from .wrapped_null_choice import WrappedNullChoice as WrappedNullChoice diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/_union.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/_union.py.golden new file mode 100644 index 000000000..3836a8a0c --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/_union.py.golden @@ -0,0 +1,355 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + return value + +def value_0_to_dict(value: object) -> object: + return value + +def value_1_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_1_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_from_dict(value: object) -> object: + return value + +def value_3_to_dict(value: object) -> object: + return value + +def value_4_from_dict(value: object) -> object: + from.import numeric_two + return numeric_two.NumericTwo.from_dict(_union_support.as_dict(value)) + +def value_4_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_5_from_dict(value: object) -> object: + return value + +def value_5_to_dict(value: object) -> object: + return value + +def value_6_from_dict(value: object) -> object: + from.import adjacent_member + return adjacent_member.adjacent_member_from_dict(_union_support.as_dict(value)) + +def value_6_to_dict(value: object) -> object: + from.import adjacent_member + return adjacent_member.adjacent_member_to_dict(cast(adjacent_member.AdjacentMember, value)) + +def value_7_from_dict(value: object) -> object: + from.import tagged_record + return tagged_record.TaggedRecord.from_dict(_union_support.as_dict(value)) + +def value_7_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_8_from_dict(value: object) -> object: + return [value_9_from_dict(item) for item in _union_support.as_list(value)] + +def value_8_to_dict(value: object) -> object: + return [value_9_to_dict(item) for item in _union_support.as_list(value)] + +def value_9_from_dict(value: object) -> object: + return value + +def value_9_to_dict(value: object) -> object: + return value + +def value_10_from_dict(value: object) -> object: + from.import branch + return branch.Branch.from_dict(_union_support.as_dict(value)) + +def value_10_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_11_from_dict(value: object) -> object: + return value + +def value_11_to_dict(value: object) -> object: + return value + +def value_12_from_dict(value: object) -> object: + from.import numeric_one + return numeric_one.NumericOne.from_dict(_union_support.as_dict(value)) + +def value_12_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_13_from_dict(value: object) -> object: + return value_11_from_dict(value) + +def value_13_to_dict(value: object) -> object: + return value_11_to_dict(value) + +def value_14_from_dict(value: object) -> object: + return value + +def value_14_to_dict(value: object) -> object: + return value + +def value_15_from_dict(value: object) -> object: + from.import dependent_envelope + return dependent_envelope.DependentEnvelope.from_dict(_union_support.as_dict(value)) + +def value_15_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_16_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_16_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_17_from_dict(value: object) -> object: + return [value_11_from_dict(item) for item in _union_support.as_list(value)] + +def value_17_to_dict(value: object) -> object: + return [value_11_to_dict(item) for item in _union_support.as_list(value)] + +def value_18_from_dict(value: object) -> object: + return {key: value_11_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_18_to_dict(value: object) -> object: + return {key: value_11_to_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_19_from_dict(value: object) -> object: + return [value_20_from_dict(item) for item in _union_support.as_list(value)] + +def value_19_to_dict(value: object) -> object: + return [value_20_to_dict(item) for item in _union_support.as_list(value)] + +def value_20_from_dict(value: object) -> object: + return {key: value_17_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_20_to_dict(value: object) -> object: + return {key: value_17_to_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_21_from_dict(value: object) -> object: + return value_22_from_dict(value) + +def value_21_to_dict(value: object) -> object: + return value_22_to_dict(value) + +def value_22_from_dict(value: object) -> object: + return None if value is None else value_23_from_dict(value) + +def value_22_to_dict(value: object) -> object: + return None if value is None else value_23_to_dict(value) + +def value_23_from_dict(value: object) -> object: + return value + +def value_23_to_dict(value: object) -> object: + return value + +def value_24_from_dict(value: object) -> object: + return value + +def value_24_to_dict(value: object) -> object: + return value + +def value_25_from_dict(value: object) -> object: + return value_2_from_dict(value) + +def value_25_to_dict(value: object) -> object: + return value_2_to_dict(value) + +def value_26_from_dict(value: object) -> object: + return value_27_from_dict(value) + +def value_26_to_dict(value: object) -> object: + return value_27_to_dict(value) + +def value_27_from_dict(value: object) -> object: + return None if value is None else value_2_from_dict(value) + +def value_27_to_dict(value: object) -> object: + return None if value is None else value_2_to_dict(value) + +def value_28_from_dict(value: object) -> object: + return value + +def value_28_to_dict(value: object) -> object: + return value + +def value_29_from_dict(value: object) -> object: + return value + +def value_29_to_dict(value: object) -> object: + return value + +def value_30_from_dict(value: object) -> object: + return value + +def value_30_to_dict(value: object) -> object: + return value + +def value_31_from_dict(value: object) -> object: + return value + +def value_31_to_dict(value: object) -> object: + return value + +def value_32_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_32_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_33_from_dict(value: object) -> object: + return [value_34_from_dict(item) for item in _union_support.as_list(value)] + +def value_33_to_dict(value: object) -> object: + return [value_34_to_dict(item) for item in _union_support.as_list(value)] + +def value_34_from_dict(value: object) -> object: + return value + +def value_34_to_dict(value: object) -> object: + return value + +def value_35_from_dict(value: object) -> object: + from.import client_tool + return client_tool.ClientTool.from_dict(_union_support.as_dict(value)) + +def value_35_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_36_from_dict(value: object) -> object: + from.import search_tool + return search_tool.SearchTool.from_dict(_union_support.as_dict(value)) + +def value_36_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_37_from_dict(value: object) -> object: + return value + +def value_37_to_dict(value: object) -> object: + return value + +def value_38_from_dict(value: object) -> object: + return value_1_from_dict(value) + +def value_38_to_dict(value: object) -> object: + return value_1_to_dict(value) + +def value_39_from_dict(value: object) -> object: + return value_16_from_dict(value) + +def value_39_to_dict(value: object) -> object: + return value_16_to_dict(value) + +def value_40_from_dict(value: object) -> object: + data = _union_support.as_dict(value) + if data.get('kind') == 'choice': + return value_3_from_dict(data['payload']) + + if data.get('kind') == 'text': + return value_2_from_dict(data['payload']) + + raise ValueError("No matching tagged union variant") + +def value_40_to_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))): + return {'kind': 'choice', 'payload': value_3_to_dict(value)} + + if (isinstance(value, str)): + return {'kind': 'text', 'payload': value_2_to_dict(value)} + + raise ValueError("No matching tagged union variant") + +def value_41_from_dict(value: object) -> object: + data = _union_support.as_dict(value) + if data.get('kind') == 'choice': + return value_11_from_dict(data['payload']) + + if data.get('kind') == 'text': + return value_2_from_dict(data['payload']) + + raise ValueError("No matching tagged union variant") + +def value_41_to_dict(value: object) -> object: + if (((_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))))): + return {'kind': 'choice', 'payload': value_11_to_dict(value)} + + if (isinstance(value, str)): + return {'kind': 'text', 'payload': value_2_to_dict(value)} + + raise ValueError("No matching tagged union variant") + +def value_42_from_dict(value: object) -> object: + return value + +def value_42_to_dict(value: object) -> object: + return value + +def value_43_from_dict(value: object) -> object: + return [value_6_from_dict(item) for item in _union_support.as_list(value)] + +def value_43_to_dict(value: object) -> object: + return [value_6_to_dict(item) for item in _union_support.as_list(value)] + +def value_44_from_dict(value: object) -> object: + return value_45_from_dict(value) + +def value_44_to_dict(value: object) -> object: + return value_45_to_dict(value) + +def value_45_from_dict(value: object) -> object: + return value + +def value_45_to_dict(value: object) -> object: + return value + +def value_46_from_dict(value: object) -> object: + return {key: value_45_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_46_to_dict(value: object) -> object: + return {key: value_45_to_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_47_from_dict(value: object) -> object: + return [value_44_from_dict(item) for item in _union_support.as_list(value)] + +def value_47_to_dict(value: object) -> object: + return [value_44_to_dict(item) for item in _union_support.as_list(value)] + +def value_48_from_dict(value: object) -> object: + return value + +def value_48_to_dict(value: object) -> object: + return value + +def value_49_from_dict(value: object) -> object: + from.import tagged_member_envelope + return tagged_member_envelope.TaggedMemberEnvelope.from_dict(_union_support.as_dict(value)) + +def value_49_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/acyclic_envelope.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/acyclic_envelope.py.golden new file mode 100644 index 000000000..868bba7d5 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/acyclic_envelope.py.golden @@ -0,0 +1,25 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from . import _union +from .acyclic_scalar import AcyclicScalar + +@dataclass +class AcyclicEnvelope: + value: AcyclicScalar + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['value'] = _union.value_0_to_dict(self.value) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> AcyclicEnvelope: + return cls( + value = _union.value_0_from_dict(data['value']), + ) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/acyclic_scalar.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/acyclic_scalar.py.golden new file mode 100644 index 000000000..ab349a251 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/acyclic_scalar.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type AcyclicScalar = (object) + +def acyclic_scalar_from_dict(value: object) -> AcyclicScalar: + return _union.value_0_from_dict(value) + +def acyclic_scalar_to_dict(value: AcyclicScalar) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/adjacent_member.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/adjacent_member.py.golden new file mode 100644 index 000000000..b1c372785 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/adjacent_member.py.golden @@ -0,0 +1,33 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from .numeric_two import NumericTwo +from .tagged_record import TaggedRecord + +# Discriminator: kind / content: payload (adjacent). + +type AdjacentMember = ( + TaggedRecord + | NumericTwo +) + + +def adjacent_member_from_dict(data: dict[str, object]) -> AdjacentMember: + _tag = data["kind"] + _content = data["payload"] # type: ignore[assignment] + if _tag == "record": + return TaggedRecord.from_dict(_content) # type: ignore[arg-type] + elif _tag == "numeric": + return NumericTwo.from_dict(_content) # type: ignore[arg-type] + raise ValueError(f"Unknown discriminator value for AdjacentMember: {data}") + + +def adjacent_member_to_dict(obj: AdjacentMember) -> dict[str, object]: + if isinstance(obj, TaggedRecord): + return {"kind": "record", "payload": obj.to_dict()} + elif isinstance(obj, NumericTwo): + return {"kind": "numeric", "payload": obj.to_dict()} + raise ValueError(f"Unknown variant for AdjacentMember: {type(obj)}") diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/ambiguous.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/ambiguous.py.golden new file mode 100644 index 000000000..4d20794fa --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/ambiguous.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type Ambiguous = (object) + +def ambiguous_from_dict(value: object) -> Ambiguous: + return _union.value_3_from_dict(value) + +def ambiguous_to_dict(value: Ambiguous) -> object: + return _union.value_3_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/ambiguous_tagged_member.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/ambiguous_tagged_member.py.golden new file mode 100644 index 000000000..720326a3a --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/ambiguous_tagged_member.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type AmbiguousTaggedMember = (object) + +def ambiguous_tagged_member_from_dict(value: object) -> AmbiguousTaggedMember: + return _union.value_5_from_dict(value) + +def ambiguous_tagged_member_to_dict(value: AmbiguousTaggedMember) -> object: + return _union.value_5_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/branch.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/branch.py.golden new file mode 100644 index 000000000..fae055beb --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/branch.py.golden @@ -0,0 +1,25 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union, recursive + +@dataclass +class Branch: + children: list[recursive.Recursive] + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['children'] = _union.value_8_to_dict(self.children) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Branch: + return cls( + children = cast(list[recursive.Recursive], _union.value_8_from_dict(data['children'])), + ) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/choice.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/choice.py.golden new file mode 100644 index 000000000..64e4e97d6 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/choice.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type Choice = (object) + +def choice_from_dict(value: object) -> Choice: + return _union.value_11_from_dict(value) + +def choice_to_dict(value: Choice) -> object: + return _union.value_11_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/choice_alias.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/choice_alias.py.golden new file mode 100644 index 000000000..7063a2741 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/choice_alias.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from .choice import Choice + +type ChoiceAlias = (Choice) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/client_tool.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/client_tool.py.golden new file mode 100644 index 000000000..b65bef982 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/client_tool.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +@dataclass +class ClientTool: + input_schema: dict[str, Any] + name: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["input_schema"] = self.input_schema + result["name"] = self.name + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> ClientTool: + return cls( + input_schema=data["input_schema"], # type: ignore[assignment] + name=data["name"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/dependent_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/dependent_choice.py.golden new file mode 100644 index 000000000..dff6c35d0 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/dependent_choice.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type DependentChoice = (object) + +def dependent_choice_from_dict(value: object) -> DependentChoice: + return _union.value_14_from_dict(value) + +def dependent_choice_to_dict(value: DependentChoice) -> object: + return _union.value_14_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/dependent_envelope.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/dependent_envelope.py.golden new file mode 100644 index 000000000..a3e05475d --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/dependent_envelope.py.golden @@ -0,0 +1,24 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from .acyclic_envelope import AcyclicEnvelope + +@dataclass +class DependentEnvelope: + envelope: AcyclicEnvelope + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["envelope"] = self.envelope.to_dict() + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> DependentEnvelope: + return cls( + envelope=AcyclicEnvelope.from_dict(data["envelope"]), # type: ignore[arg-type] + ) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/envelope.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/envelope.py.golden new file mode 100644 index 000000000..b3c1393dc --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/envelope.py.golden @@ -0,0 +1,41 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .choice import Choice +from .choice_alias import ChoiceAlias + +@dataclass +class Envelope: + choice: ChoiceAlias + choice_list: list[Choice] + choice_map: dict[str, Choice] + deep: list[dict[str, list[Choice]]] + optional_choice: Choice | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['choice'] = _union.value_13_to_dict(self.choice) + result['choice_list'] = _union.value_17_to_dict(self.choice_list) + result['choice_map'] = _union.value_18_to_dict(self.choice_map) + result['deep'] = _union.value_19_to_dict(self.deep) + if self.optional_choice is not None: + result['optional_choice'] = _union.value_11_to_dict(self.optional_choice) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Envelope: + return cls( + choice = _union.value_13_from_dict(data['choice']), + choice_list = cast(list[Choice], _union.value_17_from_dict(data['choice_list'])), + choice_map = cast(dict[str, Choice], _union.value_18_from_dict(data['choice_map'])), + deep = cast(list[dict[str, list[Choice]]], _union.value_19_from_dict(data['deep'])), + optional_choice = None if 'optional_choice' not in data else _union.value_11_from_dict(data.get('optional_choice')), + ) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/finite_envelope.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/finite_envelope.py.golden new file mode 100644 index 000000000..38e1f1e98 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/finite_envelope.py.golden @@ -0,0 +1,24 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from . import _union + +@dataclass +class FiniteEnvelope: + value: object | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['value'] = _union.value_21_to_dict(self.value) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> FiniteEnvelope: + return cls( + value = _union.value_21_from_dict(data['value']), + ) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/inner_null_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/inner_null_choice.py.golden new file mode 100644 index 000000000..1a35da1c9 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/inner_null_choice.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type InnerNullChoice = (object) + +def inner_null_choice_from_dict(value: object) -> InnerNullChoice: + return _union.value_24_from_dict(value) + +def inner_null_choice_to_dict(value: InnerNullChoice) -> object: + return _union.value_24_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/nested_ambiguous.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/nested_ambiguous.py.golden new file mode 100644 index 000000000..a0cae8834 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/nested_ambiguous.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type NestedAmbiguous = (object) + +def nested_ambiguous_from_dict(value: object) -> NestedAmbiguous: + return _union.value_28_from_dict(value) + +def nested_ambiguous_to_dict(value: NestedAmbiguous) -> object: + return _union.value_28_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/nullable_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/nullable_choice.py.golden new file mode 100644 index 000000000..dfa314947 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/nullable_choice.py.golden @@ -0,0 +1,18 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type NullableChoice = ( + object + | None +) + +def nullable_choice_from_dict(value: object) -> NullableChoice: + return _union.value_29_from_dict(value) + +def nullable_choice_to_dict(value: NullableChoice) -> object: + return _union.value_29_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/nullable_envelope.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/nullable_envelope.py.golden new file mode 100644 index 000000000..05c9c1d43 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/nullable_envelope.py.golden @@ -0,0 +1,31 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from . import _union +from .nullable_choice import NullableChoice +from .wrapped_null_choice import WrappedNullChoice + +@dataclass +class NullableEnvelope: + choice: NullableChoice + ambiguous: WrappedNullChoice | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['choice'] = _union.value_29_to_dict(self.choice) + if self.ambiguous is not None: + result['ambiguous'] = _union.value_30_to_dict(self.ambiguous) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NullableEnvelope: + return cls( + choice = _union.value_29_from_dict(data['choice']), + ambiguous = None if 'ambiguous' not in data else _union.value_30_from_dict(data.get('ambiguous')), + ) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/numeric_one.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/numeric_one.py.golden new file mode 100644 index 000000000..00917636c --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/numeric_one.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class NumericOne: + revision: int + first_value: str | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["revision"] = self.revision + if self.first_value is not None: + result["first_value"] = self.first_value + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NumericOne: + return cls( + revision=data["revision"], # type: ignore[assignment] + first_value=data.get("first_value"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/numeric_two.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/numeric_two.py.golden new file mode 100644 index 000000000..d042a0d8b --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/numeric_two.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class NumericTwo: + revision: int + second_value: str | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["revision"] = self.revision + if self.second_value is not None: + result["second_value"] = self.second_value + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NumericTwo: + return cls( + revision=data["revision"], # type: ignore[assignment] + second_value=data.get("second_value"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/open_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/open_choice.py.golden new file mode 100644 index 000000000..10f268d49 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/open_choice.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type OpenChoice = (object) + +def open_choice_from_dict(value: object) -> OpenChoice: + return _union.value_31_from_dict(value) + +def open_choice_to_dict(value: OpenChoice) -> object: + return _union.value_31_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/recursive.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/recursive.py.golden new file mode 100644 index 000000000..fb48126b3 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/recursive.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type Recursive = (object) + +def recursive_from_dict(value: object) -> Recursive: + return _union.value_9_from_dict(value) + +def recursive_to_dict(value: Recursive) -> object: + return _union.value_9_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/request.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/request.py.golden new file mode 100644 index 000000000..46530c8d1 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/request.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .tool import Tool + +@dataclass +class Request: + tools: list[Tool] + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['tools'] = _union.value_33_to_dict(self.tools) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Request: + return cls( + tools = cast(list[Tool], _union.value_33_from_dict(data['tools'])), + ) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/scalar_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/scalar_choice.py.golden new file mode 100644 index 000000000..f5d5e897b --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/scalar_choice.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type ScalarChoice = (object) + +def scalar_choice_from_dict(value: object) -> ScalarChoice: + return _union.value_37_from_dict(value) + +def scalar_choice_to_dict(value: ScalarChoice) -> object: + return _union.value_37_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/search_tool.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/search_tool.py.golden new file mode 100644 index 000000000..f6b31466a --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/search_tool.py.golden @@ -0,0 +1,30 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal + +@dataclass +class SearchTool: + name: Literal["web_search"] + type_: Literal["web_search_20250305"] + max_uses: int | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["name"] = self.name + result["type"] = self.type_ + if self.max_uses is not None: + result["max_uses"] = self.max_uses + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> SearchTool: + return cls( + name=data["name"], # type: ignore[assignment] + type_=data["type"], # type: ignore[assignment] + max_uses=data.get("max_uses"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/send_multipart_multipart_request_body.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/send_multipart_multipart_request_body.py.golden new file mode 100644 index 000000000..607318f62 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/send_multipart_multipart_request_body.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from ..runtime import UploadFile +from .choice import Choice + +@dataclass +class SendMultipartMultipartRequestBody: + choice: Choice + attachment: UploadFile | None = None diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/send_multipart_request.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/send_multipart_request.py.golden new file mode 100644 index 000000000..0e2dc62b8 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/send_multipart_request.py.golden @@ -0,0 +1,29 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from . import _union +from .choice import Choice + +@dataclass +class SendMultipartRequest: + choice: Choice + attachment: bytes | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['choice'] = _union.value_11_to_dict(self.choice) + if self.attachment is not None: + result["attachment"] = self.attachment + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> SendMultipartRequest: + return cls( + choice = _union.value_11_from_dict(data['choice']), + attachment=data.get("attachment"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_ambiguous.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_ambiguous.py.golden new file mode 100644 index 000000000..5a6745f21 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_ambiguous.py.golden @@ -0,0 +1,23 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .ambiguous import Ambiguous + +# Discriminator: kind / content: payload (adjacent). + +type TaggedAmbiguous = ( + Ambiguous + | str +) + +def tagged_ambiguous_from_dict(value: object) -> TaggedAmbiguous: + return _union.value_40_from_dict(value) + +def tagged_ambiguous_to_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], _union.value_40_to_dict(value)) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_choice.py.golden new file mode 100644 index 000000000..cb8439aa0 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_choice.py.golden @@ -0,0 +1,23 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .choice import Choice + +# Discriminator: kind / content: payload (adjacent). + +type TaggedChoice = ( + Choice + | str +) + +def tagged_choice_from_dict(value: object) -> TaggedChoice: + return _union.value_41_from_dict(value) + +def tagged_choice_to_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], _union.value_41_to_dict(value)) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_container_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_container_choice.py.golden new file mode 100644 index 000000000..98d20a4f0 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_container_choice.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type TaggedContainerChoice = (object) + +def tagged_container_choice_from_dict(value: object) -> TaggedContainerChoice: + return _union.value_42_from_dict(value) + +def tagged_container_choice_to_dict(value: TaggedContainerChoice) -> object: + return _union.value_42_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_member_alias.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_member_alias.py.golden new file mode 100644 index 000000000..65d4e105c --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_member_alias.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from .tagged_member_choice import TaggedMemberChoice + +type TaggedMemberAlias = (TaggedMemberChoice) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_member_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_member_choice.py.golden new file mode 100644 index 000000000..c045ab07d --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_member_choice.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type TaggedMemberChoice = (object) + +def tagged_member_choice_from_dict(value: object) -> TaggedMemberChoice: + return _union.value_45_from_dict(value) + +def tagged_member_choice_to_dict(value: TaggedMemberChoice) -> object: + return _union.value_45_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_member_envelope.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_member_envelope.py.golden new file mode 100644 index 000000000..32d2ef2a4 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_member_envelope.py.golden @@ -0,0 +1,34 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .tagged_member_alias import TaggedMemberAlias +from .tagged_member_choice import TaggedMemberChoice + +@dataclass +class TaggedMemberEnvelope: + mapping: dict[str, TaggedMemberChoice] + value: TaggedMemberChoice + values: list[TaggedMemberAlias] + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['mapping'] = _union.value_46_to_dict(self.mapping) + result['value'] = _union.value_45_to_dict(self.value) + result['values'] = _union.value_47_to_dict(self.values) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> TaggedMemberEnvelope: + return cls( + mapping = cast(dict[str, + TaggedMemberChoice], _union.value_46_from_dict(data['mapping'])), + value = _union.value_45_from_dict(data['value']), + values = cast(list[TaggedMemberAlias], _union.value_47_from_dict(data['values'])), + ) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_object_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_object_choice.py.golden new file mode 100644 index 000000000..5b623c3f3 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_object_choice.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type TaggedObjectChoice = (object) + +def tagged_object_choice_from_dict(value: object) -> TaggedObjectChoice: + return _union.value_48_from_dict(value) + +def tagged_object_choice_to_dict(value: TaggedObjectChoice) -> object: + return _union.value_48_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_record.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_record.py.golden new file mode 100644 index 000000000..41db830f4 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tagged_record.py.golden @@ -0,0 +1,22 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class TaggedRecord: + name: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["name"] = self.name + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> TaggedRecord: + return cls( + name=data["name"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tool.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tool.py.golden new file mode 100644 index 000000000..c0e8ef1ec --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/tool.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type Tool = (object) + +def tool_from_dict(value: object) -> Tool: + return _union.value_34_from_dict(value) + +def tool_to_dict(value: Tool) -> object: + return _union.value_34_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/wrapped_null_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/wrapped_null_choice.py.golden new file mode 100644 index 000000000..7b36ec1c3 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/models/wrapped_null_choice.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type WrappedNullChoice = (object) + +def wrapped_null_choice_from_dict(value: object) -> WrappedNullChoice: + return _union.value_30_from_dict(value) + +def wrapped_null_choice_to_dict(value: WrappedNullChoice) -> object: + return _union.value_30_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/py.typed.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/py.typed.golden new file mode 100644 index 000000000..e69de29bb diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/__init__.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/__init__.py.golden new file mode 100644 index 000000000..c77ee060c --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/__init__.py.golden @@ -0,0 +1,11 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .auth import ApiKeyAuth as ApiKeyAuth +from .auth import Authenticator as Authenticator +from .auth import BearerAuth as BearerAuth +from .client import ApiResponse as ApiResponse +from .client import Client as Client +from .errors import ApiError as ApiError +from .upload_file import UploadFile as UploadFile diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/_union_support.py.golden new file mode 100644 index 000000000..a4c9e6107 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/_union_support.py.golden @@ -0,0 +1,90 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/auth.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/auth.py.golden new file mode 100644 index 000000000..eaf25448a --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/auth.py.golden @@ -0,0 +1,50 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""Authentication helpers.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Callable + + +class Authenticator(ABC): + """Base class for request authenticators.""" + + @abstractmethod + def auth_headers(self) -> dict[str, str]: + """Return headers to attach to every request.""" + ... + + +class BearerAuth(Authenticator): + """Bearer token authentication. + + Accepts a static token string or a callable that returns the + current token (evaluated on every request). + """ + + def __init__(self, token: str | Callable[[], str]) -> None: + self._token = token + + def auth_headers(self) -> dict[str, str]: + token = self._token() if callable(self._token) else self._token + return {"Authorization": f"Bearer {token}"} + + +class ApiKeyAuth(Authenticator): + """API key authentication via a custom header. + + Accepts a static key string or a callable that returns the + current key (evaluated on every request). + """ + + def __init__(self, header_name: str, api_key: str | Callable[[], str]) -> None: + self._header_name = header_name + self._api_key = api_key + + def auth_headers(self) -> dict[str, str]: + key = self._api_key() if callable(self._api_key) else self._api_key + return {self._header_name: key} diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/client.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/client.py.golden new file mode 100644 index 000000000..1e2fe2176 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/client.py.golden @@ -0,0 +1,84 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""HTTP client wrapping httpx.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any, Generic, TypeVar + +import httpx + +from .auth import Authenticator + +ResponseData = TypeVar("ResponseData") + + +@dataclass(frozen=True) +class ApiResponse(Generic[ResponseData]): + """Decoded response data together with its HTTP metadata.""" + + data: ResponseData + status_code: int + headers: Mapping[str, str] + raw: httpx.Response + + +class Client: + """Synchronous HTTP client for the generated SDK.""" + + def __init__( + self, + base_url: str, + *, + http_client: httpx.Client | None = None, + authenticator: Authenticator | None = None, + ) -> None: + self._base_url = base_url.rstrip("/") + self._http_client = http_client or httpx.Client() + self._authenticator = authenticator + + def request( + self, + method: str, + path: str, + *, + params: dict[str, str] | None = None, + json: Any = None, + content: Any = None, + data: Any = None, + files: Any = None, + headers: dict[str, str] | None = None, + ) -> httpx.Response: + """Send an HTTP request and return the raw response.""" + url = f"{self._base_url}{path}" + req_headers: dict[str, str] = {"Accept": "application/json"} + if json is not None: + req_headers["Content-Type"] = "application/json" + if headers: + req_headers.update(headers) + if self._authenticator is not None: + req_headers.update(self._authenticator.auth_headers()) + return self._http_client.request( + method, + url, + params=params, + json=json, + content=content, + data=data, + files=files, + headers=req_headers, + ) + + def close(self) -> None: + """Close the underlying HTTP client.""" + self._http_client.close() + + def __enter__(self) -> Client: + return self + + def __exit__(self, *args: object) -> None: + self.close() diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/errors.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/errors.py.golden new file mode 100644 index 000000000..3530e64be --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/errors.py.golden @@ -0,0 +1,34 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""API error type.""" + +from __future__ import annotations + +from collections.abc import Mapping + + +class ApiError(Exception): + """Raised when the server returns a non-success status code.""" + + def __init__( + self, + status_code: int, + status: str, + body: bytes, + *, + headers: Mapping[str, str] | None = None, + response: object | None = None, + ) -> None: + self.status_code: int = status_code + self.status: str = status + self.raw_body: bytes = body + self.body: bytes = body + self.headers: Mapping[str, str] | None = headers + self.response: object | None = response + super().__init__(f"{status_code} {status}") + + @property + def text(self) -> str: + return self.raw_body.decode("utf-8", errors="replace") diff --git a/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/upload_file.py.golden b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/upload_file.py.golden new file mode 100644 index 000000000..f218509e4 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-raw/python_union_conversion/runtime/upload_file.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class UploadFile: + data: bytes + filename: str | None = None + + @classmethod + def from_bytes(cls, data: bytes, filename: str | None = None) -> UploadFile: + return cls(data=data, filename=filename) + + def filename_or_default(self, default_name: str) -> str: + return self.filename or default_name diff --git a/tests/golden/python/python-httpx/untagged-union-strict/README.md.golden b/tests/golden/python/python-httpx/untagged-union-strict/README.md.golden new file mode 100644 index 000000000..a6f182f18 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/README.md.golden @@ -0,0 +1,7 @@ +# Python union conversion + +Generated Python SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `python_union_conversion`. diff --git a/tests/golden/python/python-httpx/untagged-union-strict/pyproject.toml.golden b/tests/golden/python/python-httpx/untagged-union-strict/pyproject.toml.golden new file mode 100644 index 000000000..01539402d --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/pyproject.toml.golden @@ -0,0 +1,10 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "python_union_conversion" +version = "1.0.0" +description = "Generated Python SDK." +requires-python = ">=3.12" +dependencies = ["httpx>=0.27"] diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/__init__.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/__init__.py.golden new file mode 100644 index 000000000..8f525cedf --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/__init__.py.golden @@ -0,0 +1,11 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .runtime import ApiKeyAuth as ApiKeyAuth +from .runtime import Authenticator as Authenticator +from .runtime import BearerAuth as BearerAuth +from .runtime import ApiResponse as ApiResponse +from .runtime import Client as Client +from .runtime import ApiError as ApiError +from .runtime import UploadFile as UploadFile diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/apis/__init__.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/apis/__init__.py.golden new file mode 100644 index 000000000..8126d3b1a --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/apis/__init__.py.golden @@ -0,0 +1,5 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .default_api import DefaultApi as DefaultApi diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/apis/default_api.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/apis/default_api.py.golden new file mode 100644 index 000000000..15bdbd85d --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/apis/default_api.py.golden @@ -0,0 +1,290 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +import json + +from collections.abc import Mapping +from typing import cast + +from ..models import _union +from ..models.choice import Choice +from ..models.envelope import Envelope +from ..models.nullable_choice import NullableChoice +from ..models.send_multipart_multipart_request_body import SendMultipartMultipartRequestBody +from ..models.tagged_choice import TaggedChoice +from ..models.tagged_member_choice import TaggedMemberChoice +from ..runtime.client import ApiResponse, Client +from ..runtime.errors import ApiError + +class SendChoiceBadRequest: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: Choice | None = None + self._body_error: Exception | None = None + @property + def body(self) -> Choice: + if not self._body_loaded: + try: + self._body_value = cast(Choice, _union.value_11_from_dict(json.loads(self.raw_body.decode("utf-8")))) + + except Exception as exc: + self._body_error = exc + + self._body_loaded = True + + if self._body_error is not None: + raise self._body_error + + return cast(Choice, self._body_value) + +class SendChoiceUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendChoiceErrorDetail = ( + SendChoiceBadRequest + | SendChoiceUnexpected +) + +class SendChoiceError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendChoiceErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendChoiceErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendChoicesUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendChoicesErrorDetail = ( + SendChoicesUnexpected +) + +class SendChoicesError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendChoicesErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendChoicesErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendEnvelopeUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendEnvelopeErrorDetail = ( + SendEnvelopeUnexpected +) + +class SendEnvelopeError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendEnvelopeErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendEnvelopeErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendMultipartUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendMultipartErrorDetail = ( + SendMultipartUnexpected +) + +class SendMultipartError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendMultipartErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendMultipartErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendNullableUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendNullableErrorDetail = ( + SendNullableUnexpected +) + +class SendNullableError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendNullableErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendNullableErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendTaggedUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendTaggedErrorDetail = ( + SendTaggedUnexpected +) + +class SendTaggedError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendTaggedErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendTaggedErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendTaggedMemberUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendTaggedMemberErrorDetail = ( + SendTaggedMemberUnexpected +) + +class SendTaggedMemberError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendTaggedMemberErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendTaggedMemberErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + + +class DefaultApi: + def __init__(self, client: Client) -> None: + self._client = client + + def send_choice_with_http_info(self, *, body: Choice) -> ApiResponse[Choice]: + path = "/choice" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_11_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + if response.status_code == 400: + detail = SendChoiceBadRequest(response.status_code, response.headers, response.content) + else: + detail = SendChoiceUnexpected(response.status_code, response.headers, response.content) + raise SendChoiceError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + _union_data = cast(Choice, _union.value_11_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_choice(self, *, body: Choice) -> Choice: + return self.send_choice_with_http_info(body=body).data + + def send_choices_with_http_info(self, *, body: list[Choice]) -> ApiResponse[dict[str, Choice]]: + path = "/choices" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_17_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendChoicesUnexpected(response.status_code, response.headers, response.content) + raise SendChoicesError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + _union_data = cast(dict[str, Choice], _union.value_18_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_choices(self, *, body: list[Choice]) -> dict[str, Choice]: + return self.send_choices_with_http_info(body=body).data + + def send_envelope_with_http_info(self, *, body: Envelope) -> ApiResponse[Envelope]: + path = "/envelope" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + response = self._client.request("POST", path, json=body.to_dict(), headers=headers) + if not (200 <= response.status_code < 300): + detail = SendEnvelopeUnexpected(response.status_code, response.headers, response.content) + raise SendEnvelopeError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + data = Envelope.from_dict(response.json()) + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_envelope(self, *, body: Envelope) -> Envelope: + return self.send_envelope_with_http_info(body=body).data + + def send_multipart_with_http_info(self, *, body: SendMultipartMultipartRequestBody) -> ApiResponse[None]: + path = "/multipart" + files: dict[str, object] = {} + if body.attachment is not None: + files["attachment"] = (body.attachment.filename_or_default("attachment"), body.attachment.data, "application/octet-stream") + + files['choice'] = (None, json.dumps(_union.value_11_to_dict(body.choice)), 'application/json') + response = self._client.request("POST", path, files=files if files else None) + if not (200 <= response.status_code < 300): + detail = SendMultipartUnexpected(response.status_code, response.headers, response.content) + raise SendMultipartError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + data = None + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_multipart(self, *, body: SendMultipartMultipartRequestBody) -> None: + return self.send_multipart_with_http_info(body=body).data + + def send_nullable_with_http_info(self, *, body: NullableChoice) -> ApiResponse[NullableChoice]: + path = "/nullable" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_29_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendNullableUnexpected(response.status_code, response.headers, response.content) + raise SendNullableError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + _union_data = cast(NullableChoice, _union.value_29_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_nullable(self, *, body: NullableChoice) -> NullableChoice: + return self.send_nullable_with_http_info(body=body).data + + def send_tagged_with_http_info(self, *, body: TaggedChoice) -> ApiResponse[TaggedChoice]: + path = "/tagged" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_41_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendTaggedUnexpected(response.status_code, response.headers, response.content) + raise SendTaggedError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + _union_data = cast(TaggedChoice, _union.value_41_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_tagged(self, *, body: TaggedChoice) -> TaggedChoice: + return self.send_tagged_with_http_info(body=body).data + + def send_tagged_member_with_http_info(self, *, body: TaggedMemberChoice) -> ApiResponse[TaggedMemberChoice]: + path = "/tagged-member" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_45_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendTaggedMemberUnexpected(response.status_code, response.headers, response.content) + raise SendTaggedMemberError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + _union_data = cast(TaggedMemberChoice, _union.value_45_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_tagged_member(self, *, body: TaggedMemberChoice) -> TaggedMemberChoice: + return self.send_tagged_member_with_http_info(body=body).data diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/__init__.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/__init__.py.golden new file mode 100644 index 000000000..30669ee6e --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/__init__.py.golden @@ -0,0 +1,40 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .acyclic_envelope import AcyclicEnvelope as AcyclicEnvelope +from .acyclic_scalar import AcyclicScalar as AcyclicScalar +from .adjacent_member import AdjacentMember as AdjacentMember +from .ambiguous import Ambiguous as Ambiguous +from .ambiguous_tagged_member import AmbiguousTaggedMember as AmbiguousTaggedMember +from .branch import Branch as Branch +from .choice import Choice as Choice +from .choice_alias import ChoiceAlias as ChoiceAlias +from .client_tool import ClientTool as ClientTool +from .dependent_choice import DependentChoice as DependentChoice +from .dependent_envelope import DependentEnvelope as DependentEnvelope +from .envelope import Envelope as Envelope +from .finite_envelope import FiniteEnvelope as FiniteEnvelope +from .inner_null_choice import InnerNullChoice as InnerNullChoice +from .nested_ambiguous import NestedAmbiguous as NestedAmbiguous +from .nullable_choice import NullableChoice as NullableChoice +from .nullable_envelope import NullableEnvelope as NullableEnvelope +from .numeric_one import NumericOne as NumericOne +from .numeric_two import NumericTwo as NumericTwo +from .open_choice import OpenChoice as OpenChoice +from .recursive import Recursive as Recursive +from .request import Request as Request +from .scalar_choice import ScalarChoice as ScalarChoice +from .search_tool import SearchTool as SearchTool +from .send_multipart_multipart_request_body import SendMultipartMultipartRequestBody as SendMultipartMultipartRequestBody +from .send_multipart_request import SendMultipartRequest as SendMultipartRequest +from .tagged_ambiguous import TaggedAmbiguous as TaggedAmbiguous +from .tagged_choice import TaggedChoice as TaggedChoice +from .tagged_container_choice import TaggedContainerChoice as TaggedContainerChoice +from .tagged_member_alias import TaggedMemberAlias as TaggedMemberAlias +from .tagged_member_choice import TaggedMemberChoice as TaggedMemberChoice +from .tagged_member_envelope import TaggedMemberEnvelope as TaggedMemberEnvelope +from .tagged_object_choice import TaggedObjectChoice as TaggedObjectChoice +from .tagged_record import TaggedRecord as TaggedRecord +from .tool import Tool as Tool +from .wrapped_null_choice import WrappedNullChoice as WrappedNullChoice diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/_union.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/_union.py.golden new file mode 100644 index 000000000..da2f4f470 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/_union.py.golden @@ -0,0 +1,941 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + matches = [_union_support.number(value, True), isinstance(value, str)] + if sum(matches) > 1: + raise ValueError('Ambiguous AcyclicScalar untagged union value') + + if matches[0]: + return value_1_from_dict(value) + + if matches[1]: + return value_2_from_dict(value) + + raise ValueError('No matching AcyclicScalar untagged union member') + +def value_0_to_dict(value: object) -> object: + matches = [_union_support.number(value, True), isinstance(value, str)] + if sum(matches) > 1: + raise ValueError('Ambiguous AcyclicScalar untagged union value') + + if matches[0]: + return value_1_to_dict(value) + + if matches[1]: + return value_2_to_dict(value) + + raise ValueError('No matching AcyclicScalar untagged union member') + +def value_1_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_1_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_from_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))), (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))] + if sum(matches) > 1: + raise ValueError('Ambiguous Ambiguous untagged union value') + + if matches[0]: + return value_4_from_dict(value) + + if matches[1]: + return value_4_from_dict(value) + + raise ValueError('No matching Ambiguous untagged union member') + +def value_3_to_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))), (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))] + if sum(matches) > 1: + raise ValueError('Ambiguous Ambiguous untagged union value') + + if matches[0]: + return value_4_to_dict(value) + + if matches[1]: + return value_4_to_dict(value) + + raise ValueError('No matching Ambiguous untagged union member') + +def value_4_from_dict(value: object) -> object: + from.import numeric_two + return numeric_two.NumericTwo.from_dict(_union_support.as_dict(value)) + +def value_4_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_5_from_dict(value: object) -> object: + matches = [((_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'record')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'numeric')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value'), str)))))), ((_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'record')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'numeric')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value'), str))))))] + if sum(matches) > 1: + raise ValueError('Ambiguous AmbiguousTaggedMember untagged union value') + + if matches[0]: + return value_6_from_dict(value) + + if matches[1]: + return value_6_from_dict(value) + + raise ValueError('No matching AmbiguousTaggedMember untagged union member') + +def value_5_to_dict(value: object) -> object: + matches = [((_union_support.is_object(value) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))), ((_union_support.is_object(value) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))))] + if sum(matches) > 1: + raise ValueError('Ambiguous AmbiguousTaggedMember untagged union value') + + if matches[0]: + return value_6_to_dict(value) + + if matches[1]: + return value_6_to_dict(value) + + raise ValueError('No matching AmbiguousTaggedMember untagged union member') + +def value_6_from_dict(value: object) -> object: + from.import adjacent_member + return adjacent_member.adjacent_member_from_dict(_union_support.as_dict(value)) + +def value_6_to_dict(value: object) -> object: + from.import adjacent_member + return adjacent_member.adjacent_member_to_dict(cast(adjacent_member.AdjacentMember, value)) + +def value_7_from_dict(value: object) -> object: + from.import tagged_record + return tagged_record.TaggedRecord.from_dict(_union_support.as_dict(value)) + +def value_7_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_8_from_dict(value: object) -> object: + return [value_9_from_dict(item) for item in _union_support.as_list(value)] + +def value_8_to_dict(value: object) -> object: + return [value_9_to_dict(item) for item in _union_support.as_list(value)] + +def value_9_from_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_from_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))), isinstance(value, str)] + if sum(matches) > 1: + raise ValueError('Ambiguous Recursive untagged union value') + + if matches[0]: + return value_10_from_dict(value) + + if matches[1]: + return value_2_from_dict(value) + + raise ValueError('No matching Recursive untagged union member') + +def value_9_to_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_to_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))), isinstance(value, str)] + if sum(matches) > 1: + raise ValueError('Ambiguous Recursive untagged union value') + + if matches[0]: + return value_10_to_dict(value) + + if matches[1]: + return value_2_to_dict(value) + + raise ValueError('No matching Recursive untagged union member') + +def value_10_from_dict(value: object) -> object: + from.import branch + return branch.Branch.from_dict(_union_support.as_dict(value)) + +def value_10_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_11_from_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))), (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))] + if sum(matches) > 1: + raise ValueError('Ambiguous Choice untagged union value') + + if matches[0]: + return value_12_from_dict(value) + + if matches[1]: + return value_4_from_dict(value) + + raise ValueError('No matching Choice untagged union member') + +def value_11_to_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))), (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))] + if sum(matches) > 1: + raise ValueError('Ambiguous Choice untagged union value') + + if matches[0]: + return value_12_to_dict(value) + + if matches[1]: + return value_4_to_dict(value) + + raise ValueError('No matching Choice untagged union member') + +def value_12_from_dict(value: object) -> object: + from.import numeric_one + return numeric_one.NumericOne.from_dict(_union_support.as_dict(value)) + +def value_12_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_13_from_dict(value: object) -> object: + return value_11_from_dict(value) + +def value_13_to_dict(value: object) -> object: + return value_11_to_dict(value) + +def value_14_from_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'envelope', 'envelope', True) and (_union_support.is_object(_union_support.get(value, 'envelope', 'envelope')) + and (_union_support.has(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value'), True) + or isinstance(_union_support.get(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value'), str)))))), isinstance(value, bool)] + if sum(matches) > 1: + raise ValueError('Ambiguous DependentChoice untagged union value') + + if matches[0]: + return value_15_from_dict(value) + + if matches[1]: + return value_16_from_dict(value) + + raise ValueError('No matching DependentChoice untagged union member') + +def value_14_to_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'envelope', 'envelope', True) and (_union_support.is_object(_union_support.get(value, 'envelope', 'envelope')) + and (_union_support.has(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value'), True) + or isinstance(_union_support.get(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value'), str)))))), isinstance(value, bool)] + if sum(matches) > 1: + raise ValueError('Ambiguous DependentChoice untagged union value') + + if matches[0]: + return value_15_to_dict(value) + + if matches[1]: + return value_16_to_dict(value) + + raise ValueError('No matching DependentChoice untagged union member') + +def value_15_from_dict(value: object) -> object: + from.import dependent_envelope + return dependent_envelope.DependentEnvelope.from_dict(_union_support.as_dict(value)) + +def value_15_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_16_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_16_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_17_from_dict(value: object) -> object: + return [value_11_from_dict(item) for item in _union_support.as_list(value)] + +def value_17_to_dict(value: object) -> object: + return [value_11_to_dict(item) for item in _union_support.as_list(value)] + +def value_18_from_dict(value: object) -> object: + return {key: value_11_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_18_to_dict(value: object) -> object: + return {key: value_11_to_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_19_from_dict(value: object) -> object: + return [value_20_from_dict(item) for item in _union_support.as_list(value)] + +def value_19_to_dict(value: object) -> object: + return [value_20_to_dict(item) for item in _union_support.as_list(value)] + +def value_20_from_dict(value: object) -> object: + return {key: value_17_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_20_to_dict(value: object) -> object: + return {key: value_17_to_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_21_from_dict(value: object) -> object: + return value_22_from_dict(value) + +def value_21_to_dict(value: object) -> object: + return value_22_to_dict(value) + +def value_22_from_dict(value: object) -> object: + return None if value is None else value_23_from_dict(value) + +def value_22_to_dict(value: object) -> object: + return None if value is None else value_23_to_dict(value) + +def value_23_from_dict(value: object) -> object: + matches = [_union_support.number(value, True), isinstance(value, bool)] + if sum(matches) > 1: + raise ValueError('Ambiguous inline untagged union value') + + if matches[0]: + return value_1_from_dict(value) + + if matches[1]: + return value_16_from_dict(value) + + raise ValueError('No matching inline untagged union member') + +def value_23_to_dict(value: object) -> object: + matches = [_union_support.number(value, True), isinstance(value, bool)] + if sum(matches) > 1: + raise ValueError('Ambiguous inline untagged union value') + + if matches[0]: + return value_1_to_dict(value) + + if matches[1]: + return value_16_to_dict(value) + + raise ValueError('No matching inline untagged union member') + +def value_24_from_dict(value: object) -> object: + matches = [_union_support.equal(value, None), ((value is None or isinstance(value, str)) + and (_union_support.equal(value, 'x') + or _union_support.equal(value, None)) + and (value is None + or isinstance(value, str) + or _union_support.equal(value, 'x') + or _union_support.equal(value, None)))] + if sum(matches) > 1: + raise ValueError('Ambiguous InnerNullChoice untagged union value') + + if matches[0]: + return value_25_from_dict(value) + + if matches[1]: + return value_26_from_dict(value) + + raise ValueError('No matching InnerNullChoice untagged union member') + +def value_24_to_dict(value: object) -> object: + matches = [_union_support.equal(value, None), ((value is None or isinstance(value, str)) + and (_union_support.equal(value, 'x') + or _union_support.equal(value, None)) + and (value is None + or isinstance(value, str) + or _union_support.equal(value, 'x') + or _union_support.equal(value, None)))] + if sum(matches) > 1: + raise ValueError('Ambiguous InnerNullChoice untagged union value') + + if matches[0]: + return value_25_to_dict(value) + + if matches[1]: + return value_26_to_dict(value) + + raise ValueError('No matching InnerNullChoice untagged union member') + +def value_25_from_dict(value: object) -> object: + return value_2_from_dict(value) + +def value_25_to_dict(value: object) -> object: + return value_2_to_dict(value) + +def value_26_from_dict(value: object) -> object: + return value_27_from_dict(value) + +def value_26_to_dict(value: object) -> object: + return value_27_to_dict(value) + +def value_27_from_dict(value: object) -> object: + return None if value is None else value_2_from_dict(value) + +def value_27_to_dict(value: object) -> object: + return None if value is None else value_2_to_dict(value) + +def value_28_from_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))), isinstance(value, str)] + if sum(matches) > 1: + raise ValueError('Ambiguous NestedAmbiguous untagged union value') + + if matches[0]: + return value_3_from_dict(value) + + if matches[1]: + return value_2_from_dict(value) + + raise ValueError('No matching NestedAmbiguous untagged union member') + +def value_28_to_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))), isinstance(value, str)] + if sum(matches) > 1: + raise ValueError('Ambiguous NestedAmbiguous untagged union value') + + if matches[0]: + return value_3_to_dict(value) + + if matches[1]: + return value_2_to_dict(value) + + raise ValueError('No matching NestedAmbiguous untagged union member') + +def value_29_from_dict(value: object) -> object: + matches = [((_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))))] + if sum(matches) > 1: + raise ValueError('Ambiguous NullableChoice untagged union value') + + if matches[0]: + return value_11_from_dict(value) + + if value is None: + return None + + raise ValueError('No matching NullableChoice untagged union member') + +def value_29_to_dict(value: object) -> object: + matches = [((_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))))] + if sum(matches) > 1: + raise ValueError('Ambiguous NullableChoice untagged union value') + + if matches[0]: + return value_11_to_dict(value) + + if value is None: + return None + + raise ValueError('No matching NullableChoice untagged union member') + +def value_30_from_dict(value: object) -> object: + matches = [(_union_support.equal(value, None) + or ((value is None or isinstance(value, str)) + and (_union_support.equal(value, 'x') + or _union_support.equal(value, None)) + and (value is None + or isinstance(value, str) + or _union_support.equal(value, 'x') + or _union_support.equal(value, None)))), _union_support.number(value, True)] + if sum(matches) > 1: + raise ValueError('Ambiguous WrappedNullChoice untagged union value') + + if matches[0]: + return value_24_from_dict(value) + + if matches[1]: + return value_1_from_dict(value) + + raise ValueError('No matching WrappedNullChoice untagged union member') + +def value_30_to_dict(value: object) -> object: + matches = [(_union_support.equal(value, None) + or ((value is None or isinstance(value, str)) + and (_union_support.equal(value, 'x') + or _union_support.equal(value, None)) + and (value is None + or isinstance(value, str) + or _union_support.equal(value, 'x') + or _union_support.equal(value, None)))), _union_support.number(value, True)] + if sum(matches) > 1: + raise ValueError('Ambiguous WrappedNullChoice untagged union value') + + if matches[0]: + return value_24_to_dict(value) + + if matches[1]: + return value_1_to_dict(value) + + raise ValueError('No matching WrappedNullChoice untagged union member') + +def value_31_from_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))] + if sum(matches) > 1: + raise ValueError('Ambiguous OpenChoice untagged union value') + + if matches[0]: + return value_4_from_dict(value) + + if True: + return value_32_from_dict(value) + + raise ValueError('No matching OpenChoice untagged union member') + +def value_31_to_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))] + if sum(matches) > 1: + raise ValueError('Ambiguous OpenChoice untagged union value') + + if matches[0]: + return value_4_to_dict(value) + + if True: + return value_32_to_dict(value) + + raise ValueError('No matching OpenChoice untagged union member') + +def value_32_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_32_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_33_from_dict(value: object) -> object: + return [value_34_from_dict(item) for item in _union_support.as_list(value)] + +def value_33_to_dict(value: object) -> object: + return [value_34_to_dict(item) for item in _union_support.as_list(value)] + +def value_34_from_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'input_schema', 'input_schema', True) and (_union_support.is_dict(_union_support.get(value, 'input_schema', 'input_schema')) and all(True for _item in _union_support.as_dict(_union_support.get(value, 'input_schema', 'input_schema')).values()))) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))), (_union_support.is_object(value) + and (not _union_support.has(value, 'max_uses', 'max_uses', False) or _union_support.number(_union_support.get(value, 'max_uses', 'max_uses'), True)) + and (_union_support.has(value, 'name', 'name', True) and _union_support.equal(_union_support.get(value, 'name', 'name'), 'web_search')) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'web_search_20250305')))] + if sum(matches) > 1: + raise ValueError('Ambiguous Tool untagged union value') + + if matches[0]: + return value_35_from_dict(value) + + if matches[1]: + return value_36_from_dict(value) + + raise ValueError('No matching Tool untagged union member') + +def value_34_to_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'input_schema', 'input_schema', True) and (_union_support.is_dict(_union_support.get(value, 'input_schema', 'input_schema')) and all(True for _item in _union_support.as_dict(_union_support.get(value, 'input_schema', 'input_schema')).values()))) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))), (_union_support.is_object(value) + and (not _union_support.has(value, 'max_uses', 'max_uses', False) or _union_support.number(_union_support.get(value, 'max_uses', 'max_uses'), True)) + and (_union_support.has(value, 'name', 'name', True) and _union_support.equal(_union_support.get(value, 'name', 'name'), 'web_search')) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'web_search_20250305')))] + if sum(matches) > 1: + raise ValueError('Ambiguous Tool untagged union value') + + if matches[0]: + return value_35_to_dict(value) + + if matches[1]: + return value_36_to_dict(value) + + raise ValueError('No matching Tool untagged union member') + +def value_35_from_dict(value: object) -> object: + from.import client_tool + return client_tool.ClientTool.from_dict(_union_support.as_dict(value)) + +def value_35_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_36_from_dict(value: object) -> object: + from.import search_tool + return search_tool.SearchTool.from_dict(_union_support.as_dict(value)) + +def value_36_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_37_from_dict(value: object) -> object: + matches = [(_union_support.number(value, True) + and (_union_support.equal(value, 1) + or _union_support.equal(value, 2)) + and (_union_support.number(value, True) + or _union_support.equal(value, 1) + or _union_support.equal(value, 2))), (isinstance(value, bool) + and _union_support.equal(value, True) + and (isinstance(value, bool) + or _union_support.equal(value, True)))] + if sum(matches) > 1: + raise ValueError('Ambiguous ScalarChoice untagged union value') + + if matches[0]: + return value_38_from_dict(value) + + if matches[1]: + return value_39_from_dict(value) + + raise ValueError('No matching ScalarChoice untagged union member') + +def value_37_to_dict(value: object) -> object: + matches = [(_union_support.number(value, True) + and (_union_support.equal(value, 1) + or _union_support.equal(value, 2)) + and (_union_support.number(value, True) + or _union_support.equal(value, 1) + or _union_support.equal(value, 2))), (isinstance(value, bool) + and _union_support.equal(value, True) + and (isinstance(value, bool) + or _union_support.equal(value, True)))] + if sum(matches) > 1: + raise ValueError('Ambiguous ScalarChoice untagged union value') + + if matches[0]: + return value_38_to_dict(value) + + if matches[1]: + return value_39_to_dict(value) + + raise ValueError('No matching ScalarChoice untagged union member') + +def value_38_from_dict(value: object) -> object: + return value_1_from_dict(value) + +def value_38_to_dict(value: object) -> object: + return value_1_to_dict(value) + +def value_39_from_dict(value: object) -> object: + return value_16_from_dict(value) + +def value_39_to_dict(value: object) -> object: + return value_16_to_dict(value) + +def value_40_from_dict(value: object) -> object: + data = _union_support.as_dict(value) + if data.get('kind') == 'choice': + return value_3_from_dict(data['payload']) + + if data.get('kind') == 'text': + return value_2_from_dict(data['payload']) + + raise ValueError("No matching tagged union variant") + +def value_40_to_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))): + return {'kind': 'choice', 'payload': value_3_to_dict(value)} + + if (isinstance(value, str)): + return {'kind': 'text', 'payload': value_2_to_dict(value)} + + raise ValueError("No matching tagged union variant") + +def value_41_from_dict(value: object) -> object: + data = _union_support.as_dict(value) + if data.get('kind') == 'choice': + return value_11_from_dict(data['payload']) + + if data.get('kind') == 'text': + return value_2_from_dict(data['payload']) + + raise ValueError("No matching tagged union variant") + +def value_41_to_dict(value: object) -> object: + if (((_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))))): + return {'kind': 'choice', 'payload': value_11_to_dict(value)} + + if (isinstance(value, str)): + return {'kind': 'text', 'payload': value_2_to_dict(value)} + + raise ValueError("No matching tagged union variant") + +def value_42_from_dict(value: object) -> object: + matches = [(_union_support.is_list(value) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'record')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'numeric')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value'), str)))))) for _item in _union_support.as_list(value))), isinstance(value, str)] + if sum(matches) > 1: + raise ValueError('Ambiguous TaggedContainerChoice untagged union value') + + if matches[0]: + return value_43_from_dict(value) + + if matches[1]: + return value_2_from_dict(value) + + raise ValueError('No matching TaggedContainerChoice untagged union member') + +def value_42_to_dict(value: object) -> object: + matches = [(_union_support.is_list(value) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'name', 'name', True) and isinstance(_union_support.get(_item, 'name', 'name'), str))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'revision', 'revision', True) and (_union_support.number(_union_support.get(_item, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_item, 'revision', 'revision'), 2))) + and (not _union_support.has(_item, 'second_value', 'second_value', False) or isinstance(_union_support.get(_item, 'second_value', 'second_value'), str)))) for _item in _union_support.as_list(value))), isinstance(value, str)] + if sum(matches) > 1: + raise ValueError('Ambiguous TaggedContainerChoice untagged union value') + + if matches[0]: + return value_43_to_dict(value) + + if matches[1]: + return value_2_to_dict(value) + + raise ValueError('No matching TaggedContainerChoice untagged union member') + +def value_43_from_dict(value: object) -> object: + return [value_6_from_dict(item) for item in _union_support.as_list(value)] + +def value_43_to_dict(value: object) -> object: + return [value_6_to_dict(item) for item in _union_support.as_list(value)] + +def value_44_from_dict(value: object) -> object: + return value_45_from_dict(value) + +def value_44_to_dict(value: object) -> object: + return value_45_to_dict(value) + +def value_45_from_dict(value: object) -> object: + matches = [((_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'record')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'numeric')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value'), str)))))), _union_support.number(value, True)] + if sum(matches) > 1: + raise ValueError('Ambiguous TaggedMemberChoice untagged union value') + + if matches[0]: + return value_6_from_dict(value) + + if matches[1]: + return value_1_from_dict(value) + + raise ValueError('No matching TaggedMemberChoice untagged union member') + +def value_45_to_dict(value: object) -> object: + matches = [((_union_support.is_object(value) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))), _union_support.number(value, True)] + if sum(matches) > 1: + raise ValueError('Ambiguous TaggedMemberChoice untagged union value') + + if matches[0]: + return value_6_to_dict(value) + + if matches[1]: + return value_1_to_dict(value) + + raise ValueError('No matching TaggedMemberChoice untagged union member') + +def value_46_from_dict(value: object) -> object: + return {key: value_45_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_46_to_dict(value: object) -> object: + return {key: value_45_to_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_47_from_dict(value: object) -> object: + return [value_44_from_dict(item) for item in _union_support.as_list(value)] + +def value_47_to_dict(value: object) -> object: + return [value_44_to_dict(item) for item in _union_support.as_list(value)] + +def value_48_from_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'mapping', 'mapping', True) and (_union_support.is_dict(_union_support.get(value, 'mapping', 'mapping')) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'record')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'numeric')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value'), str))))) + or _union_support.number(_item, True)) for _item in _union_support.as_dict(_union_support.get(value, 'mapping', 'mapping')).values()))) + and (_union_support.has(value, 'value', 'value', True) and ((_union_support.is_object(_union_support.get(value, 'value', 'value')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'kind', 'kind', True) and _union_support.equal(_union_support.get(_union_support.get(value, 'value', 'value'), 'kind', 'kind'), 'record')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload')) + and (_union_support.has(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(_union_support.get(value, 'value', 'value')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'kind', 'kind', True) and _union_support.equal(_union_support.get(_union_support.get(value, 'value', 'value'), 'kind', 'kind'), 'numeric')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload')) + and (_union_support.has(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'second_value', 'second_value'), str))))) + or _union_support.number(_union_support.get(value, 'value', 'value'), True))) + and (_union_support.has(value, 'values', 'values', True) and (_union_support.is_list(_union_support.get(value, 'values', 'values')) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'record')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'numeric')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value'), str))))) + or _union_support.number(_item, True)) for _item in _union_support.as_list(_union_support.get(value, 'values', 'values')))))), _union_support.number(value, True)] + if sum(matches) > 1: + raise ValueError('Ambiguous TaggedObjectChoice untagged union value') + + if matches[0]: + return value_49_from_dict(value) + + if matches[1]: + return value_1_from_dict(value) + + raise ValueError('No matching TaggedObjectChoice untagged union member') + +def value_48_to_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'mapping', 'mapping', True) and (_union_support.is_dict(_union_support.get(value, 'mapping', 'mapping')) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'name', 'name', True) and isinstance(_union_support.get(_item, 'name', 'name'), str))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'revision', 'revision', True) and (_union_support.number(_union_support.get(_item, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_item, 'revision', 'revision'), 2))) + and (not _union_support.has(_item, 'second_value', 'second_value', False) or isinstance(_union_support.get(_item, 'second_value', 'second_value'), str))) + or _union_support.number(_item, True)) for _item in _union_support.as_dict(_union_support.get(value, 'mapping', 'mapping')).values()))) + and (_union_support.has(value, 'value', 'value', True) and ((_union_support.is_object(_union_support.get(value, 'value', 'value')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'value', 'value'), 'name', 'name'), str))) + or (_union_support.is_object(_union_support.get(value, 'value', 'value')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'value', 'value'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(value, 'value', 'value'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(value, 'value', 'value'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(value, 'value', 'value'), 'second_value', 'second_value'), str))) + or _union_support.number(_union_support.get(value, 'value', 'value'), True))) + and (_union_support.has(value, 'values', 'values', True) and (_union_support.is_list(_union_support.get(value, 'values', 'values')) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'name', 'name', True) and isinstance(_union_support.get(_item, 'name', 'name'), str))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'revision', 'revision', True) and (_union_support.number(_union_support.get(_item, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_item, 'revision', 'revision'), 2))) + and (not _union_support.has(_item, 'second_value', 'second_value', False) or isinstance(_union_support.get(_item, 'second_value', 'second_value'), str))) + or _union_support.number(_item, True)) for _item in _union_support.as_list(_union_support.get(value, 'values', 'values')))))), _union_support.number(value, True)] + if sum(matches) > 1: + raise ValueError('Ambiguous TaggedObjectChoice untagged union value') + + if matches[0]: + return value_49_to_dict(value) + + if matches[1]: + return value_1_to_dict(value) + + raise ValueError('No matching TaggedObjectChoice untagged union member') + +def value_49_from_dict(value: object) -> object: + from.import tagged_member_envelope + return tagged_member_envelope.TaggedMemberEnvelope.from_dict(_union_support.as_dict(value)) + +def value_49_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def match_recursive_any_from_dict(value: object) -> bool: + return ((_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_from_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))) + or isinstance(value, str)) + +def match_recursive_concrete_from_dict(value: object) -> bool: + return ((_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_from_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))) + or isinstance(value, str)) + +def match_recursive_fallback_from_dict(value: object) -> bool: + return False + +def match_recursive_any_to_dict(value: object) -> bool: + return ((_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_to_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))) + or isinstance(value, str)) + +def match_recursive_concrete_to_dict(value: object) -> bool: + return ((_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_to_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))) + or isinstance(value, str)) + +def match_recursive_fallback_to_dict(value: object) -> bool: + return False diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/acyclic_envelope.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/acyclic_envelope.py.golden new file mode 100644 index 000000000..57ded1581 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/acyclic_envelope.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .acyclic_scalar import AcyclicScalar + +@dataclass +class AcyclicEnvelope: + value: AcyclicScalar + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['value'] = _union.value_0_to_dict(self.value) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> AcyclicEnvelope: + return cls( + value = cast(AcyclicScalar, _union.value_0_from_dict(data['value'])), + ) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/acyclic_scalar.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/acyclic_scalar.py.golden new file mode 100644 index 000000000..e7448aa38 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/acyclic_scalar.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union + +type AcyclicScalar = ( + int + | str +) + +def acyclic_scalar_from_dict(value: object) -> AcyclicScalar: + return cast(AcyclicScalar, _union.value_0_from_dict(value)) + +def acyclic_scalar_to_dict(value: AcyclicScalar) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/adjacent_member.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/adjacent_member.py.golden new file mode 100644 index 000000000..b1c372785 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/adjacent_member.py.golden @@ -0,0 +1,33 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from .numeric_two import NumericTwo +from .tagged_record import TaggedRecord + +# Discriminator: kind / content: payload (adjacent). + +type AdjacentMember = ( + TaggedRecord + | NumericTwo +) + + +def adjacent_member_from_dict(data: dict[str, object]) -> AdjacentMember: + _tag = data["kind"] + _content = data["payload"] # type: ignore[assignment] + if _tag == "record": + return TaggedRecord.from_dict(_content) # type: ignore[arg-type] + elif _tag == "numeric": + return NumericTwo.from_dict(_content) # type: ignore[arg-type] + raise ValueError(f"Unknown discriminator value for AdjacentMember: {data}") + + +def adjacent_member_to_dict(obj: AdjacentMember) -> dict[str, object]: + if isinstance(obj, TaggedRecord): + return {"kind": "record", "payload": obj.to_dict()} + elif isinstance(obj, NumericTwo): + return {"kind": "numeric", "payload": obj.to_dict()} + raise ValueError(f"Unknown variant for AdjacentMember: {type(obj)}") diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/ambiguous.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/ambiguous.py.golden new file mode 100644 index 000000000..540aa34d8 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/ambiguous.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .numeric_two import NumericTwo + +type Ambiguous = ( + NumericTwo + | NumericTwo +) + +def ambiguous_from_dict(value: object) -> Ambiguous: + return cast(Ambiguous, _union.value_3_from_dict(value)) + +def ambiguous_to_dict(value: Ambiguous) -> object: + return _union.value_3_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/ambiguous_tagged_member.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/ambiguous_tagged_member.py.golden new file mode 100644 index 000000000..bcc1f745a --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/ambiguous_tagged_member.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .adjacent_member import AdjacentMember + +type AmbiguousTaggedMember = ( + AdjacentMember + | AdjacentMember +) + +def ambiguous_tagged_member_from_dict(value: object) -> AmbiguousTaggedMember: + return cast(AmbiguousTaggedMember, _union.value_5_from_dict(value)) + +def ambiguous_tagged_member_to_dict(value: AmbiguousTaggedMember) -> object: + return _union.value_5_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/branch.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/branch.py.golden new file mode 100644 index 000000000..fae055beb --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/branch.py.golden @@ -0,0 +1,25 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union, recursive + +@dataclass +class Branch: + children: list[recursive.Recursive] + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['children'] = _union.value_8_to_dict(self.children) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Branch: + return cls( + children = cast(list[recursive.Recursive], _union.value_8_from_dict(data['children'])), + ) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/choice.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/choice.py.golden new file mode 100644 index 000000000..dbbb3f910 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/choice.py.golden @@ -0,0 +1,22 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .numeric_one import NumericOne +from .numeric_two import NumericTwo + +type Choice = ( + NumericOne + | NumericTwo +) + +def choice_from_dict(value: object) -> Choice: + return cast(Choice, _union.value_11_from_dict(value)) + +def choice_to_dict(value: Choice) -> object: + return _union.value_11_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/choice_alias.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/choice_alias.py.golden new file mode 100644 index 000000000..7063a2741 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/choice_alias.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from .choice import Choice + +type ChoiceAlias = (Choice) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/client_tool.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/client_tool.py.golden new file mode 100644 index 000000000..b65bef982 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/client_tool.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +@dataclass +class ClientTool: + input_schema: dict[str, Any] + name: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["input_schema"] = self.input_schema + result["name"] = self.name + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> ClientTool: + return cls( + input_schema=data["input_schema"], # type: ignore[assignment] + name=data["name"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/dependent_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/dependent_choice.py.golden new file mode 100644 index 000000000..76ce3e523 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/dependent_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .dependent_envelope import DependentEnvelope + +type DependentChoice = ( + DependentEnvelope + | bool +) + +def dependent_choice_from_dict(value: object) -> DependentChoice: + return cast(DependentChoice, _union.value_14_from_dict(value)) + +def dependent_choice_to_dict(value: DependentChoice) -> object: + return _union.value_14_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/dependent_envelope.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/dependent_envelope.py.golden new file mode 100644 index 000000000..a3e05475d --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/dependent_envelope.py.golden @@ -0,0 +1,24 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from .acyclic_envelope import AcyclicEnvelope + +@dataclass +class DependentEnvelope: + envelope: AcyclicEnvelope + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["envelope"] = self.envelope.to_dict() + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> DependentEnvelope: + return cls( + envelope=AcyclicEnvelope.from_dict(data["envelope"]), # type: ignore[arg-type] + ) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/envelope.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/envelope.py.golden new file mode 100644 index 000000000..85977ed31 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/envelope.py.golden @@ -0,0 +1,42 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .choice import Choice +from .choice_alias import ChoiceAlias + +@dataclass +class Envelope: + choice: ChoiceAlias + choice_list: list[Choice] + choice_map: dict[str, Choice] + deep: list[dict[str, list[Choice]]] + optional_choice: Choice | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['choice'] = _union.value_13_to_dict(self.choice) + result['choice_list'] = _union.value_17_to_dict(self.choice_list) + result['choice_map'] = _union.value_18_to_dict(self.choice_map) + result['deep'] = _union.value_19_to_dict(self.deep) + if self.optional_choice is not None: + result['optional_choice'] = _union.value_11_to_dict(self.optional_choice) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Envelope: + return cls( + choice = cast(ChoiceAlias, _union.value_13_from_dict(data['choice'])), + choice_list = cast(list[Choice], _union.value_17_from_dict(data['choice_list'])), + choice_map = cast(dict[str, Choice], _union.value_18_from_dict(data['choice_map'])), + deep = cast(list[dict[str, list[Choice]]], _union.value_19_from_dict(data['deep'])), + optional_choice = cast(Choice + | None, None if 'optional_choice' not in data else _union.value_11_from_dict(data.get('optional_choice'))), + ) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/finite_envelope.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/finite_envelope.py.golden new file mode 100644 index 000000000..d1c6f6d9f --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/finite_envelope.py.golden @@ -0,0 +1,25 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union + +@dataclass +class FiniteEnvelope: + value: int | bool | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['value'] = _union.value_21_to_dict(self.value) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> FiniteEnvelope: + return cls( + value = cast(int | bool | None, _union.value_21_from_dict(data['value'])), + ) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/inner_null_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/inner_null_choice.py.golden new file mode 100644 index 000000000..83addfac4 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/inner_null_choice.py.golden @@ -0,0 +1,18 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type InnerNullChoice = ( + object + | str | None +) + +def inner_null_choice_from_dict(value: object) -> InnerNullChoice: + return _union.value_24_from_dict(value) + +def inner_null_choice_to_dict(value: InnerNullChoice) -> object: + return _union.value_24_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/nested_ambiguous.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/nested_ambiguous.py.golden new file mode 100644 index 000000000..199893d53 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/nested_ambiguous.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .ambiguous import Ambiguous + +type NestedAmbiguous = ( + Ambiguous + | str +) + +def nested_ambiguous_from_dict(value: object) -> NestedAmbiguous: + return cast(NestedAmbiguous, _union.value_28_from_dict(value)) + +def nested_ambiguous_to_dict(value: NestedAmbiguous) -> object: + return _union.value_28_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/nullable_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/nullable_choice.py.golden new file mode 100644 index 000000000..94eb7ca64 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/nullable_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .choice import Choice + +type NullableChoice = ( + Choice + | None +) + +def nullable_choice_from_dict(value: object) -> NullableChoice: + return cast(NullableChoice, _union.value_29_from_dict(value)) + +def nullable_choice_to_dict(value: NullableChoice) -> object: + return _union.value_29_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/nullable_envelope.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/nullable_envelope.py.golden new file mode 100644 index 000000000..a039cdb7f --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/nullable_envelope.py.golden @@ -0,0 +1,32 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .nullable_choice import NullableChoice +from .wrapped_null_choice import WrappedNullChoice + +@dataclass +class NullableEnvelope: + choice: NullableChoice + ambiguous: WrappedNullChoice | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['choice'] = _union.value_29_to_dict(self.choice) + if self.ambiguous is not None: + result['ambiguous'] = _union.value_30_to_dict(self.ambiguous) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NullableEnvelope: + return cls( + choice = cast(NullableChoice, _union.value_29_from_dict(data['choice'])), + ambiguous = None if 'ambiguous' not in data else _union.value_30_from_dict(data.get('ambiguous')), + ) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/numeric_one.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/numeric_one.py.golden new file mode 100644 index 000000000..00917636c --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/numeric_one.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class NumericOne: + revision: int + first_value: str | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["revision"] = self.revision + if self.first_value is not None: + result["first_value"] = self.first_value + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NumericOne: + return cls( + revision=data["revision"], # type: ignore[assignment] + first_value=data.get("first_value"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/numeric_two.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/numeric_two.py.golden new file mode 100644 index 000000000..d042a0d8b --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/numeric_two.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class NumericTwo: + revision: int + second_value: str | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["revision"] = self.revision + if self.second_value is not None: + result["second_value"] = self.second_value + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NumericTwo: + return cls( + revision=data["revision"], # type: ignore[assignment] + second_value=data.get("second_value"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/open_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/open_choice.py.golden new file mode 100644 index 000000000..3bcd71d81 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/open_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import Any + +from . import _union +from .numeric_two import NumericTwo + +type OpenChoice = ( + Any + | NumericTwo +) + +def open_choice_from_dict(value: object) -> OpenChoice: + return _union.value_31_from_dict(value) + +def open_choice_to_dict(value: OpenChoice) -> object: + return _union.value_31_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/recursive.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/recursive.py.golden new file mode 100644 index 000000000..79aeec558 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/recursive.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union, branch + +type Recursive = ( + branch.Branch + | str +) + +def recursive_from_dict(value: object) -> Recursive: + return cast(Recursive, _union.value_9_from_dict(value)) + +def recursive_to_dict(value: Recursive) -> object: + return _union.value_9_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/request.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/request.py.golden new file mode 100644 index 000000000..46530c8d1 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/request.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .tool import Tool + +@dataclass +class Request: + tools: list[Tool] + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['tools'] = _union.value_33_to_dict(self.tools) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Request: + return cls( + tools = cast(list[Tool], _union.value_33_from_dict(data['tools'])), + ) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/scalar_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/scalar_choice.py.golden new file mode 100644 index 000000000..a055918c9 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/scalar_choice.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union + +type ScalarChoice = ( + int + | bool +) + +def scalar_choice_from_dict(value: object) -> ScalarChoice: + return cast(ScalarChoice, _union.value_37_from_dict(value)) + +def scalar_choice_to_dict(value: ScalarChoice) -> object: + return _union.value_37_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/search_tool.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/search_tool.py.golden new file mode 100644 index 000000000..f6b31466a --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/search_tool.py.golden @@ -0,0 +1,30 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal + +@dataclass +class SearchTool: + name: Literal["web_search"] + type_: Literal["web_search_20250305"] + max_uses: int | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["name"] = self.name + result["type"] = self.type_ + if self.max_uses is not None: + result["max_uses"] = self.max_uses + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> SearchTool: + return cls( + name=data["name"], # type: ignore[assignment] + type_=data["type"], # type: ignore[assignment] + max_uses=data.get("max_uses"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/send_multipart_multipart_request_body.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/send_multipart_multipart_request_body.py.golden new file mode 100644 index 000000000..607318f62 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/send_multipart_multipart_request_body.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from ..runtime import UploadFile +from .choice import Choice + +@dataclass +class SendMultipartMultipartRequestBody: + choice: Choice + attachment: UploadFile | None = None diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/send_multipart_request.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/send_multipart_request.py.golden new file mode 100644 index 000000000..0e9e53f29 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/send_multipart_request.py.golden @@ -0,0 +1,30 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .choice import Choice + +@dataclass +class SendMultipartRequest: + choice: Choice + attachment: bytes | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['choice'] = _union.value_11_to_dict(self.choice) + if self.attachment is not None: + result["attachment"] = self.attachment + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> SendMultipartRequest: + return cls( + choice = cast(Choice, _union.value_11_from_dict(data['choice'])), + attachment=data.get("attachment"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_ambiguous.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_ambiguous.py.golden new file mode 100644 index 000000000..d4fd8dc37 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_ambiguous.py.golden @@ -0,0 +1,23 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .ambiguous import Ambiguous + +# Discriminator: kind / content: payload (adjacent). + +type TaggedAmbiguous = ( + Ambiguous + | str +) + +def tagged_ambiguous_from_dict(value: object) -> TaggedAmbiguous: + return cast(TaggedAmbiguous, _union.value_40_from_dict(value)) + +def tagged_ambiguous_to_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], _union.value_40_to_dict(value)) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_choice.py.golden new file mode 100644 index 000000000..4bbaf7ad5 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_choice.py.golden @@ -0,0 +1,23 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .choice import Choice + +# Discriminator: kind / content: payload (adjacent). + +type TaggedChoice = ( + Choice + | str +) + +def tagged_choice_from_dict(value: object) -> TaggedChoice: + return cast(TaggedChoice, _union.value_41_from_dict(value)) + +def tagged_choice_to_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], _union.value_41_to_dict(value)) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_container_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_container_choice.py.golden new file mode 100644 index 000000000..d8a108327 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_container_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .adjacent_member import AdjacentMember + +type TaggedContainerChoice = ( + list[AdjacentMember] + | str +) + +def tagged_container_choice_from_dict(value: object) -> TaggedContainerChoice: + return cast(TaggedContainerChoice, _union.value_42_from_dict(value)) + +def tagged_container_choice_to_dict(value: TaggedContainerChoice) -> object: + return _union.value_42_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_member_alias.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_member_alias.py.golden new file mode 100644 index 000000000..65d4e105c --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_member_alias.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from .tagged_member_choice import TaggedMemberChoice + +type TaggedMemberAlias = (TaggedMemberChoice) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_member_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_member_choice.py.golden new file mode 100644 index 000000000..b1a85c387 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_member_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .adjacent_member import AdjacentMember + +type TaggedMemberChoice = ( + AdjacentMember + | int +) + +def tagged_member_choice_from_dict(value: object) -> TaggedMemberChoice: + return cast(TaggedMemberChoice, _union.value_45_from_dict(value)) + +def tagged_member_choice_to_dict(value: TaggedMemberChoice) -> object: + return _union.value_45_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_member_envelope.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_member_envelope.py.golden new file mode 100644 index 000000000..6d7d76561 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_member_envelope.py.golden @@ -0,0 +1,34 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .tagged_member_alias import TaggedMemberAlias +from .tagged_member_choice import TaggedMemberChoice + +@dataclass +class TaggedMemberEnvelope: + mapping: dict[str, TaggedMemberChoice] + value: TaggedMemberChoice + values: list[TaggedMemberAlias] + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['mapping'] = _union.value_46_to_dict(self.mapping) + result['value'] = _union.value_45_to_dict(self.value) + result['values'] = _union.value_47_to_dict(self.values) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> TaggedMemberEnvelope: + return cls( + mapping = cast(dict[str, + TaggedMemberChoice], _union.value_46_from_dict(data['mapping'])), + value = cast(TaggedMemberChoice, _union.value_45_from_dict(data['value'])), + values = cast(list[TaggedMemberAlias], _union.value_47_from_dict(data['values'])), + ) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_object_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_object_choice.py.golden new file mode 100644 index 000000000..052030fcf --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_object_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .tagged_member_envelope import TaggedMemberEnvelope + +type TaggedObjectChoice = ( + TaggedMemberEnvelope + | int +) + +def tagged_object_choice_from_dict(value: object) -> TaggedObjectChoice: + return cast(TaggedObjectChoice, _union.value_48_from_dict(value)) + +def tagged_object_choice_to_dict(value: TaggedObjectChoice) -> object: + return _union.value_48_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_record.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_record.py.golden new file mode 100644 index 000000000..41db830f4 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tagged_record.py.golden @@ -0,0 +1,22 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class TaggedRecord: + name: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["name"] = self.name + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> TaggedRecord: + return cls( + name=data["name"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tool.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tool.py.golden new file mode 100644 index 000000000..42944e010 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/tool.py.golden @@ -0,0 +1,22 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .client_tool import ClientTool +from .search_tool import SearchTool + +type Tool = ( + ClientTool + | SearchTool +) + +def tool_from_dict(value: object) -> Tool: + return cast(Tool, _union.value_34_from_dict(value)) + +def tool_to_dict(value: Tool) -> object: + return _union.value_34_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/wrapped_null_choice.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/wrapped_null_choice.py.golden new file mode 100644 index 000000000..e47e6b7dc --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/models/wrapped_null_choice.py.golden @@ -0,0 +1,19 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union +from .inner_null_choice import InnerNullChoice + +type WrappedNullChoice = ( + InnerNullChoice + | int +) + +def wrapped_null_choice_from_dict(value: object) -> WrappedNullChoice: + return _union.value_30_from_dict(value) + +def wrapped_null_choice_to_dict(value: WrappedNullChoice) -> object: + return _union.value_30_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/py.typed.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/py.typed.golden new file mode 100644 index 000000000..e69de29bb diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/__init__.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/__init__.py.golden new file mode 100644 index 000000000..c77ee060c --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/__init__.py.golden @@ -0,0 +1,11 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .auth import ApiKeyAuth as ApiKeyAuth +from .auth import Authenticator as Authenticator +from .auth import BearerAuth as BearerAuth +from .client import ApiResponse as ApiResponse +from .client import Client as Client +from .errors import ApiError as ApiError +from .upload_file import UploadFile as UploadFile diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/_union_support.py.golden new file mode 100644 index 000000000..a4c9e6107 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/_union_support.py.golden @@ -0,0 +1,90 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/auth.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/auth.py.golden new file mode 100644 index 000000000..eaf25448a --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/auth.py.golden @@ -0,0 +1,50 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""Authentication helpers.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Callable + + +class Authenticator(ABC): + """Base class for request authenticators.""" + + @abstractmethod + def auth_headers(self) -> dict[str, str]: + """Return headers to attach to every request.""" + ... + + +class BearerAuth(Authenticator): + """Bearer token authentication. + + Accepts a static token string or a callable that returns the + current token (evaluated on every request). + """ + + def __init__(self, token: str | Callable[[], str]) -> None: + self._token = token + + def auth_headers(self) -> dict[str, str]: + token = self._token() if callable(self._token) else self._token + return {"Authorization": f"Bearer {token}"} + + +class ApiKeyAuth(Authenticator): + """API key authentication via a custom header. + + Accepts a static key string or a callable that returns the + current key (evaluated on every request). + """ + + def __init__(self, header_name: str, api_key: str | Callable[[], str]) -> None: + self._header_name = header_name + self._api_key = api_key + + def auth_headers(self) -> dict[str, str]: + key = self._api_key() if callable(self._api_key) else self._api_key + return {self._header_name: key} diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/client.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/client.py.golden new file mode 100644 index 000000000..1e2fe2176 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/client.py.golden @@ -0,0 +1,84 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""HTTP client wrapping httpx.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any, Generic, TypeVar + +import httpx + +from .auth import Authenticator + +ResponseData = TypeVar("ResponseData") + + +@dataclass(frozen=True) +class ApiResponse(Generic[ResponseData]): + """Decoded response data together with its HTTP metadata.""" + + data: ResponseData + status_code: int + headers: Mapping[str, str] + raw: httpx.Response + + +class Client: + """Synchronous HTTP client for the generated SDK.""" + + def __init__( + self, + base_url: str, + *, + http_client: httpx.Client | None = None, + authenticator: Authenticator | None = None, + ) -> None: + self._base_url = base_url.rstrip("/") + self._http_client = http_client or httpx.Client() + self._authenticator = authenticator + + def request( + self, + method: str, + path: str, + *, + params: dict[str, str] | None = None, + json: Any = None, + content: Any = None, + data: Any = None, + files: Any = None, + headers: dict[str, str] | None = None, + ) -> httpx.Response: + """Send an HTTP request and return the raw response.""" + url = f"{self._base_url}{path}" + req_headers: dict[str, str] = {"Accept": "application/json"} + if json is not None: + req_headers["Content-Type"] = "application/json" + if headers: + req_headers.update(headers) + if self._authenticator is not None: + req_headers.update(self._authenticator.auth_headers()) + return self._http_client.request( + method, + url, + params=params, + json=json, + content=content, + data=data, + files=files, + headers=req_headers, + ) + + def close(self) -> None: + """Close the underlying HTTP client.""" + self._http_client.close() + + def __enter__(self) -> Client: + return self + + def __exit__(self, *args: object) -> None: + self.close() diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/errors.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/errors.py.golden new file mode 100644 index 000000000..3530e64be --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/errors.py.golden @@ -0,0 +1,34 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""API error type.""" + +from __future__ import annotations + +from collections.abc import Mapping + + +class ApiError(Exception): + """Raised when the server returns a non-success status code.""" + + def __init__( + self, + status_code: int, + status: str, + body: bytes, + *, + headers: Mapping[str, str] | None = None, + response: object | None = None, + ) -> None: + self.status_code: int = status_code + self.status: str = status + self.raw_body: bytes = body + self.body: bytes = body + self.headers: Mapping[str, str] | None = headers + self.response: object | None = response + super().__init__(f"{status_code} {status}") + + @property + def text(self) -> str: + return self.raw_body.decode("utf-8", errors="replace") diff --git a/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/upload_file.py.golden b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/upload_file.py.golden new file mode 100644 index 000000000..f218509e4 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union-strict/python_union_conversion/runtime/upload_file.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class UploadFile: + data: bytes + filename: str | None = None + + @classmethod + def from_bytes(cls, data: bytes, filename: str | None = None) -> UploadFile: + return cls(data=data, filename=filename) + + def filename_or_default(self, default_name: str) -> str: + return self.filename or default_name diff --git a/tests/golden/python/python-httpx/untagged-union/README.md.golden b/tests/golden/python/python-httpx/untagged-union/README.md.golden new file mode 100644 index 000000000..a6f182f18 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/README.md.golden @@ -0,0 +1,7 @@ +# Python union conversion + +Generated Python SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `python_union_conversion`. diff --git a/tests/golden/python/python-httpx/untagged-union/pyproject.toml.golden b/tests/golden/python/python-httpx/untagged-union/pyproject.toml.golden new file mode 100644 index 000000000..01539402d --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/pyproject.toml.golden @@ -0,0 +1,10 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "python_union_conversion" +version = "1.0.0" +description = "Generated Python SDK." +requires-python = ">=3.12" +dependencies = ["httpx>=0.27"] diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/__init__.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/__init__.py.golden new file mode 100644 index 000000000..8f525cedf --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/__init__.py.golden @@ -0,0 +1,11 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .runtime import ApiKeyAuth as ApiKeyAuth +from .runtime import Authenticator as Authenticator +from .runtime import BearerAuth as BearerAuth +from .runtime import ApiResponse as ApiResponse +from .runtime import Client as Client +from .runtime import ApiError as ApiError +from .runtime import UploadFile as UploadFile diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/apis/__init__.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/apis/__init__.py.golden new file mode 100644 index 000000000..8126d3b1a --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/apis/__init__.py.golden @@ -0,0 +1,5 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .default_api import DefaultApi as DefaultApi diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/apis/default_api.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/apis/default_api.py.golden new file mode 100644 index 000000000..15bdbd85d --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/apis/default_api.py.golden @@ -0,0 +1,290 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +import json + +from collections.abc import Mapping +from typing import cast + +from ..models import _union +from ..models.choice import Choice +from ..models.envelope import Envelope +from ..models.nullable_choice import NullableChoice +from ..models.send_multipart_multipart_request_body import SendMultipartMultipartRequestBody +from ..models.tagged_choice import TaggedChoice +from ..models.tagged_member_choice import TaggedMemberChoice +from ..runtime.client import ApiResponse, Client +from ..runtime.errors import ApiError + +class SendChoiceBadRequest: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: Choice | None = None + self._body_error: Exception | None = None + @property + def body(self) -> Choice: + if not self._body_loaded: + try: + self._body_value = cast(Choice, _union.value_11_from_dict(json.loads(self.raw_body.decode("utf-8")))) + + except Exception as exc: + self._body_error = exc + + self._body_loaded = True + + if self._body_error is not None: + raise self._body_error + + return cast(Choice, self._body_value) + +class SendChoiceUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendChoiceErrorDetail = ( + SendChoiceBadRequest + | SendChoiceUnexpected +) + +class SendChoiceError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendChoiceErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendChoiceErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendChoicesUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendChoicesErrorDetail = ( + SendChoicesUnexpected +) + +class SendChoicesError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendChoicesErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendChoicesErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendEnvelopeUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendEnvelopeErrorDetail = ( + SendEnvelopeUnexpected +) + +class SendEnvelopeError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendEnvelopeErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendEnvelopeErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendMultipartUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendMultipartErrorDetail = ( + SendMultipartUnexpected +) + +class SendMultipartError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendMultipartErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendMultipartErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendNullableUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendNullableErrorDetail = ( + SendNullableUnexpected +) + +class SendNullableError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendNullableErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendNullableErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendTaggedUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendTaggedErrorDetail = ( + SendTaggedUnexpected +) + +class SendTaggedError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendTaggedErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendTaggedErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendTaggedMemberUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendTaggedMemberErrorDetail = ( + SendTaggedMemberUnexpected +) + +class SendTaggedMemberError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendTaggedMemberErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendTaggedMemberErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + + +class DefaultApi: + def __init__(self, client: Client) -> None: + self._client = client + + def send_choice_with_http_info(self, *, body: Choice) -> ApiResponse[Choice]: + path = "/choice" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_11_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + if response.status_code == 400: + detail = SendChoiceBadRequest(response.status_code, response.headers, response.content) + else: + detail = SendChoiceUnexpected(response.status_code, response.headers, response.content) + raise SendChoiceError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + _union_data = cast(Choice, _union.value_11_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_choice(self, *, body: Choice) -> Choice: + return self.send_choice_with_http_info(body=body).data + + def send_choices_with_http_info(self, *, body: list[Choice]) -> ApiResponse[dict[str, Choice]]: + path = "/choices" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_17_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendChoicesUnexpected(response.status_code, response.headers, response.content) + raise SendChoicesError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + _union_data = cast(dict[str, Choice], _union.value_18_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_choices(self, *, body: list[Choice]) -> dict[str, Choice]: + return self.send_choices_with_http_info(body=body).data + + def send_envelope_with_http_info(self, *, body: Envelope) -> ApiResponse[Envelope]: + path = "/envelope" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + response = self._client.request("POST", path, json=body.to_dict(), headers=headers) + if not (200 <= response.status_code < 300): + detail = SendEnvelopeUnexpected(response.status_code, response.headers, response.content) + raise SendEnvelopeError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + data = Envelope.from_dict(response.json()) + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_envelope(self, *, body: Envelope) -> Envelope: + return self.send_envelope_with_http_info(body=body).data + + def send_multipart_with_http_info(self, *, body: SendMultipartMultipartRequestBody) -> ApiResponse[None]: + path = "/multipart" + files: dict[str, object] = {} + if body.attachment is not None: + files["attachment"] = (body.attachment.filename_or_default("attachment"), body.attachment.data, "application/octet-stream") + + files['choice'] = (None, json.dumps(_union.value_11_to_dict(body.choice)), 'application/json') + response = self._client.request("POST", path, files=files if files else None) + if not (200 <= response.status_code < 300): + detail = SendMultipartUnexpected(response.status_code, response.headers, response.content) + raise SendMultipartError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + data = None + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_multipart(self, *, body: SendMultipartMultipartRequestBody) -> None: + return self.send_multipart_with_http_info(body=body).data + + def send_nullable_with_http_info(self, *, body: NullableChoice) -> ApiResponse[NullableChoice]: + path = "/nullable" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_29_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendNullableUnexpected(response.status_code, response.headers, response.content) + raise SendNullableError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + _union_data = cast(NullableChoice, _union.value_29_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_nullable(self, *, body: NullableChoice) -> NullableChoice: + return self.send_nullable_with_http_info(body=body).data + + def send_tagged_with_http_info(self, *, body: TaggedChoice) -> ApiResponse[TaggedChoice]: + path = "/tagged" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_41_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendTaggedUnexpected(response.status_code, response.headers, response.content) + raise SendTaggedError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + _union_data = cast(TaggedChoice, _union.value_41_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_tagged(self, *, body: TaggedChoice) -> TaggedChoice: + return self.send_tagged_with_http_info(body=body).data + + def send_tagged_member_with_http_info(self, *, body: TaggedMemberChoice) -> ApiResponse[TaggedMemberChoice]: + path = "/tagged-member" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_45_to_dict(body) + response = self._client.request("POST", path, headers=headers, content = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendTaggedMemberUnexpected(response.status_code, response.headers, response.content) + raise SendTaggedMemberError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + _union_data = cast(TaggedMemberChoice, _union.value_45_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_tagged_member(self, *, body: TaggedMemberChoice) -> TaggedMemberChoice: + return self.send_tagged_member_with_http_info(body=body).data diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/__init__.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/__init__.py.golden new file mode 100644 index 000000000..30669ee6e --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/__init__.py.golden @@ -0,0 +1,40 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .acyclic_envelope import AcyclicEnvelope as AcyclicEnvelope +from .acyclic_scalar import AcyclicScalar as AcyclicScalar +from .adjacent_member import AdjacentMember as AdjacentMember +from .ambiguous import Ambiguous as Ambiguous +from .ambiguous_tagged_member import AmbiguousTaggedMember as AmbiguousTaggedMember +from .branch import Branch as Branch +from .choice import Choice as Choice +from .choice_alias import ChoiceAlias as ChoiceAlias +from .client_tool import ClientTool as ClientTool +from .dependent_choice import DependentChoice as DependentChoice +from .dependent_envelope import DependentEnvelope as DependentEnvelope +from .envelope import Envelope as Envelope +from .finite_envelope import FiniteEnvelope as FiniteEnvelope +from .inner_null_choice import InnerNullChoice as InnerNullChoice +from .nested_ambiguous import NestedAmbiguous as NestedAmbiguous +from .nullable_choice import NullableChoice as NullableChoice +from .nullable_envelope import NullableEnvelope as NullableEnvelope +from .numeric_one import NumericOne as NumericOne +from .numeric_two import NumericTwo as NumericTwo +from .open_choice import OpenChoice as OpenChoice +from .recursive import Recursive as Recursive +from .request import Request as Request +from .scalar_choice import ScalarChoice as ScalarChoice +from .search_tool import SearchTool as SearchTool +from .send_multipart_multipart_request_body import SendMultipartMultipartRequestBody as SendMultipartMultipartRequestBody +from .send_multipart_request import SendMultipartRequest as SendMultipartRequest +from .tagged_ambiguous import TaggedAmbiguous as TaggedAmbiguous +from .tagged_choice import TaggedChoice as TaggedChoice +from .tagged_container_choice import TaggedContainerChoice as TaggedContainerChoice +from .tagged_member_alias import TaggedMemberAlias as TaggedMemberAlias +from .tagged_member_choice import TaggedMemberChoice as TaggedMemberChoice +from .tagged_member_envelope import TaggedMemberEnvelope as TaggedMemberEnvelope +from .tagged_object_choice import TaggedObjectChoice as TaggedObjectChoice +from .tagged_record import TaggedRecord as TaggedRecord +from .tool import Tool as Tool +from .wrapped_null_choice import WrappedNullChoice as WrappedNullChoice diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/_union.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/_union.py.golden new file mode 100644 index 000000000..b537ed00a --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/_union.py.golden @@ -0,0 +1,805 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if _union_support.number(value, True): + return value_1_from_dict(value) + + if isinstance(value, str): + return value_2_from_dict(value) + + return value + +def value_0_to_dict(value: object) -> object: + if _union_support.number(value, True): + return value_1_to_dict(value) + + if isinstance(value, str): + return value_2_to_dict(value) + + return value + +def value_1_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_1_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_4_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_4_from_dict(value) + + return value + +def value_3_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_4_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_4_to_dict(value) + + return value + +def value_4_from_dict(value: object) -> object: + from.import numeric_two + return numeric_two.NumericTwo.from_dict(_union_support.as_dict(value)) + +def value_4_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_5_from_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'record')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'numeric')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value'), str)))))): + return value_6_from_dict(value) + + if ((_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'record')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'numeric')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value'), str)))))): + return value_6_from_dict(value) + + return value + +def value_5_to_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))): + return value_6_to_dict(value) + + if ((_union_support.is_object(value) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))): + return value_6_to_dict(value) + + return value + +def value_6_from_dict(value: object) -> object: + from.import adjacent_member + return adjacent_member.adjacent_member_from_dict(_union_support.as_dict(value)) + +def value_6_to_dict(value: object) -> object: + from.import adjacent_member + return adjacent_member.adjacent_member_to_dict(cast(adjacent_member.AdjacentMember, value)) + +def value_7_from_dict(value: object) -> object: + from.import tagged_record + return tagged_record.TaggedRecord.from_dict(_union_support.as_dict(value)) + +def value_7_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_8_from_dict(value: object) -> object: + return [value_9_from_dict(item) for item in _union_support.as_list(value)] + +def value_8_to_dict(value: object) -> object: + return [value_9_to_dict(item) for item in _union_support.as_list(value)] + +def value_9_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_from_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))): + return value_10_from_dict(value) + + if isinstance(value, str): + return value_2_from_dict(value) + + return value + +def value_9_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_to_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))): + return value_10_to_dict(value) + + if isinstance(value, str): + return value_2_to_dict(value) + + return value + +def value_10_from_dict(value: object) -> object: + from.import branch + return branch.Branch.from_dict(_union_support.as_dict(value)) + +def value_10_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_11_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))): + return value_12_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_4_from_dict(value) + + return value + +def value_11_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))): + return value_12_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_4_to_dict(value) + + return value + +def value_12_from_dict(value: object) -> object: + from.import numeric_one + return numeric_one.NumericOne.from_dict(_union_support.as_dict(value)) + +def value_12_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_13_from_dict(value: object) -> object: + return value_11_from_dict(value) + +def value_13_to_dict(value: object) -> object: + return value_11_to_dict(value) + +def value_14_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'envelope', 'envelope', True) and (_union_support.is_object(_union_support.get(value, 'envelope', 'envelope')) + and (_union_support.has(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value'), True) + or isinstance(_union_support.get(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value'), str)))))): + return value_15_from_dict(value) + + if isinstance(value, bool): + return value_16_from_dict(value) + + return value + +def value_14_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'envelope', 'envelope', True) and (_union_support.is_object(_union_support.get(value, 'envelope', 'envelope')) + and (_union_support.has(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value'), True) + or isinstance(_union_support.get(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value'), str)))))): + return value_15_to_dict(value) + + if isinstance(value, bool): + return value_16_to_dict(value) + + return value + +def value_15_from_dict(value: object) -> object: + from.import dependent_envelope + return dependent_envelope.DependentEnvelope.from_dict(_union_support.as_dict(value)) + +def value_15_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_16_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_16_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_17_from_dict(value: object) -> object: + return [value_11_from_dict(item) for item in _union_support.as_list(value)] + +def value_17_to_dict(value: object) -> object: + return [value_11_to_dict(item) for item in _union_support.as_list(value)] + +def value_18_from_dict(value: object) -> object: + return {key: value_11_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_18_to_dict(value: object) -> object: + return {key: value_11_to_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_19_from_dict(value: object) -> object: + return [value_20_from_dict(item) for item in _union_support.as_list(value)] + +def value_19_to_dict(value: object) -> object: + return [value_20_to_dict(item) for item in _union_support.as_list(value)] + +def value_20_from_dict(value: object) -> object: + return {key: value_17_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_20_to_dict(value: object) -> object: + return {key: value_17_to_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_21_from_dict(value: object) -> object: + return value_22_from_dict(value) + +def value_21_to_dict(value: object) -> object: + return value_22_to_dict(value) + +def value_22_from_dict(value: object) -> object: + return None if value is None else value_23_from_dict(value) + +def value_22_to_dict(value: object) -> object: + return None if value is None else value_23_to_dict(value) + +def value_23_from_dict(value: object) -> object: + if _union_support.number(value, True): + return value_1_from_dict(value) + + if isinstance(value, bool): + return value_16_from_dict(value) + + return value + +def value_23_to_dict(value: object) -> object: + if _union_support.number(value, True): + return value_1_to_dict(value) + + if isinstance(value, bool): + return value_16_to_dict(value) + + return value + +def value_24_from_dict(value: object) -> object: + if _union_support.equal(value, None): + return value_25_from_dict(value) + + if ((value is None or isinstance(value, str)) + and (_union_support.equal(value, 'x') + or _union_support.equal(value, None)) + and (value is None + or isinstance(value, str) + or _union_support.equal(value, 'x') + or _union_support.equal(value, None))): + return value_26_from_dict(value) + + return value + +def value_24_to_dict(value: object) -> object: + if _union_support.equal(value, None): + return value_25_to_dict(value) + + if ((value is None or isinstance(value, str)) + and (_union_support.equal(value, 'x') + or _union_support.equal(value, None)) + and (value is None + or isinstance(value, str) + or _union_support.equal(value, 'x') + or _union_support.equal(value, None))): + return value_26_to_dict(value) + + return value + +def value_25_from_dict(value: object) -> object: + return value_2_from_dict(value) + +def value_25_to_dict(value: object) -> object: + return value_2_to_dict(value) + +def value_26_from_dict(value: object) -> object: + return value_27_from_dict(value) + +def value_26_to_dict(value: object) -> object: + return value_27_to_dict(value) + +def value_27_from_dict(value: object) -> object: + return None if value is None else value_2_from_dict(value) + +def value_27_to_dict(value: object) -> object: + return None if value is None else value_2_to_dict(value) + +def value_28_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_3_from_dict(value) + + if isinstance(value, str): + return value_2_from_dict(value) + + return value + +def value_28_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_3_to_dict(value) + + if isinstance(value, str): + return value_2_to_dict(value) + + return value + +def value_29_from_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))): + return value_11_from_dict(value) + + if value is None: + return None + + return value + +def value_29_to_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))): + return value_11_to_dict(value) + + if value is None: + return None + + return value + +def value_30_from_dict(value: object) -> object: + if (_union_support.equal(value, None) + or ((value is None or isinstance(value, str)) + and (_union_support.equal(value, 'x') + or _union_support.equal(value, None)) + and (value is None + or isinstance(value, str) + or _union_support.equal(value, 'x') + or _union_support.equal(value, None)))): + return value_24_from_dict(value) + + if _union_support.number(value, True): + return value_1_from_dict(value) + + return value + +def value_30_to_dict(value: object) -> object: + if (_union_support.equal(value, None) + or ((value is None or isinstance(value, str)) + and (_union_support.equal(value, 'x') + or _union_support.equal(value, None)) + and (value is None + or isinstance(value, str) + or _union_support.equal(value, 'x') + or _union_support.equal(value, None)))): + return value_24_to_dict(value) + + if _union_support.number(value, True): + return value_1_to_dict(value) + + return value + +def value_31_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_4_from_dict(value) + + if True: + return value_32_from_dict(value) + + return value + +def value_31_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_4_to_dict(value) + + if True: + return value_32_to_dict(value) + + return value + +def value_32_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_32_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_33_from_dict(value: object) -> object: + return [value_34_from_dict(item) for item in _union_support.as_list(value)] + +def value_33_to_dict(value: object) -> object: + return [value_34_to_dict(item) for item in _union_support.as_list(value)] + +def value_34_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'input_schema', 'input_schema', True) and (_union_support.is_dict(_union_support.get(value, 'input_schema', 'input_schema')) and all(True for _item in _union_support.as_dict(_union_support.get(value, 'input_schema', 'input_schema')).values()))) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): + return value_35_from_dict(value) + + if (_union_support.is_object(value) + and (not _union_support.has(value, 'max_uses', 'max_uses', False) or _union_support.number(_union_support.get(value, 'max_uses', 'max_uses'), True)) + and (_union_support.has(value, 'name', 'name', True) and _union_support.equal(_union_support.get(value, 'name', 'name'), 'web_search')) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'web_search_20250305'))): + return value_36_from_dict(value) + + return value + +def value_34_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'input_schema', 'input_schema', True) and (_union_support.is_dict(_union_support.get(value, 'input_schema', 'input_schema')) and all(True for _item in _union_support.as_dict(_union_support.get(value, 'input_schema', 'input_schema')).values()))) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): + return value_35_to_dict(value) + + if (_union_support.is_object(value) + and (not _union_support.has(value, 'max_uses', 'max_uses', False) or _union_support.number(_union_support.get(value, 'max_uses', 'max_uses'), True)) + and (_union_support.has(value, 'name', 'name', True) and _union_support.equal(_union_support.get(value, 'name', 'name'), 'web_search')) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'web_search_20250305'))): + return value_36_to_dict(value) + + return value + +def value_35_from_dict(value: object) -> object: + from.import client_tool + return client_tool.ClientTool.from_dict(_union_support.as_dict(value)) + +def value_35_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_36_from_dict(value: object) -> object: + from.import search_tool + return search_tool.SearchTool.from_dict(_union_support.as_dict(value)) + +def value_36_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_37_from_dict(value: object) -> object: + if (_union_support.number(value, True) + and (_union_support.equal(value, 1) + or _union_support.equal(value, 2)) + and (_union_support.number(value, True) + or _union_support.equal(value, 1) + or _union_support.equal(value, 2))): + return value_38_from_dict(value) + + if (isinstance(value, bool) + and _union_support.equal(value, True) + and (isinstance(value, bool) + or _union_support.equal(value, True))): + return value_39_from_dict(value) + + return value + +def value_37_to_dict(value: object) -> object: + if (_union_support.number(value, True) + and (_union_support.equal(value, 1) + or _union_support.equal(value, 2)) + and (_union_support.number(value, True) + or _union_support.equal(value, 1) + or _union_support.equal(value, 2))): + return value_38_to_dict(value) + + if (isinstance(value, bool) + and _union_support.equal(value, True) + and (isinstance(value, bool) + or _union_support.equal(value, True))): + return value_39_to_dict(value) + + return value + +def value_38_from_dict(value: object) -> object: + return value_1_from_dict(value) + +def value_38_to_dict(value: object) -> object: + return value_1_to_dict(value) + +def value_39_from_dict(value: object) -> object: + return value_16_from_dict(value) + +def value_39_to_dict(value: object) -> object: + return value_16_to_dict(value) + +def value_40_from_dict(value: object) -> object: + data = _union_support.as_dict(value) + if data.get('kind') == 'choice': + return value_3_from_dict(data['payload']) + + if data.get('kind') == 'text': + return value_2_from_dict(data['payload']) + + raise ValueError("No matching tagged union variant") + +def value_40_to_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))): + return {'kind': 'choice', 'payload': value_3_to_dict(value)} + + if (isinstance(value, str)): + return {'kind': 'text', 'payload': value_2_to_dict(value)} + + raise ValueError("No matching tagged union variant") + +def value_41_from_dict(value: object) -> object: + data = _union_support.as_dict(value) + if data.get('kind') == 'choice': + return value_11_from_dict(data['payload']) + + if data.get('kind') == 'text': + return value_2_from_dict(data['payload']) + + raise ValueError("No matching tagged union variant") + +def value_41_to_dict(value: object) -> object: + if (((_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))))): + return {'kind': 'choice', 'payload': value_11_to_dict(value)} + + if (isinstance(value, str)): + return {'kind': 'text', 'payload': value_2_to_dict(value)} + + raise ValueError("No matching tagged union variant") + +def value_42_from_dict(value: object) -> object: + if (_union_support.is_list(value) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'record')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'numeric')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value'), str)))))) for _item in _union_support.as_list(value))): + return value_43_from_dict(value) + + if isinstance(value, str): + return value_2_from_dict(value) + + return value + +def value_42_to_dict(value: object) -> object: + if (_union_support.is_list(value) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'name', 'name', True) and isinstance(_union_support.get(_item, 'name', 'name'), str))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'revision', 'revision', True) and (_union_support.number(_union_support.get(_item, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_item, 'revision', 'revision'), 2))) + and (not _union_support.has(_item, 'second_value', 'second_value', False) or isinstance(_union_support.get(_item, 'second_value', 'second_value'), str)))) for _item in _union_support.as_list(value))): + return value_43_to_dict(value) + + if isinstance(value, str): + return value_2_to_dict(value) + + return value + +def value_43_from_dict(value: object) -> object: + return [value_6_from_dict(item) for item in _union_support.as_list(value)] + +def value_43_to_dict(value: object) -> object: + return [value_6_to_dict(item) for item in _union_support.as_list(value)] + +def value_44_from_dict(value: object) -> object: + return value_45_from_dict(value) + +def value_44_to_dict(value: object) -> object: + return value_45_to_dict(value) + +def value_45_from_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'record')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'numeric')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value'), str)))))): + return value_6_from_dict(value) + + if _union_support.number(value, True): + return value_1_from_dict(value) + + return value + +def value_45_to_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))): + return value_6_to_dict(value) + + if _union_support.number(value, True): + return value_1_to_dict(value) + + return value + +def value_46_from_dict(value: object) -> object: + return {key: value_45_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_46_to_dict(value: object) -> object: + return {key: value_45_to_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_47_from_dict(value: object) -> object: + return [value_44_from_dict(item) for item in _union_support.as_list(value)] + +def value_47_to_dict(value: object) -> object: + return [value_44_to_dict(item) for item in _union_support.as_list(value)] + +def value_48_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'mapping', 'mapping', True) and (_union_support.is_dict(_union_support.get(value, 'mapping', 'mapping')) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'record')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'numeric')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value'), str))))) + or _union_support.number(_item, True)) for _item in _union_support.as_dict(_union_support.get(value, 'mapping', 'mapping')).values()))) + and (_union_support.has(value, 'value', 'value', True) and ((_union_support.is_object(_union_support.get(value, 'value', 'value')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'kind', 'kind', True) and _union_support.equal(_union_support.get(_union_support.get(value, 'value', 'value'), 'kind', 'kind'), 'record')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload')) + and (_union_support.has(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(_union_support.get(value, 'value', 'value')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'kind', 'kind', True) and _union_support.equal(_union_support.get(_union_support.get(value, 'value', 'value'), 'kind', 'kind'), 'numeric')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload')) + and (_union_support.has(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'second_value', 'second_value'), str))))) + or _union_support.number(_union_support.get(value, 'value', 'value'), True))) + and (_union_support.has(value, 'values', 'values', True) and (_union_support.is_list(_union_support.get(value, 'values', 'values')) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'record')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'numeric')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value'), str))))) + or _union_support.number(_item, True)) for _item in _union_support.as_list(_union_support.get(value, 'values', 'values')))))): + return value_49_from_dict(value) + + if _union_support.number(value, True): + return value_1_from_dict(value) + + return value + +def value_48_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'mapping', 'mapping', True) and (_union_support.is_dict(_union_support.get(value, 'mapping', 'mapping')) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'name', 'name', True) and isinstance(_union_support.get(_item, 'name', 'name'), str))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'revision', 'revision', True) and (_union_support.number(_union_support.get(_item, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_item, 'revision', 'revision'), 2))) + and (not _union_support.has(_item, 'second_value', 'second_value', False) or isinstance(_union_support.get(_item, 'second_value', 'second_value'), str))) + or _union_support.number(_item, True)) for _item in _union_support.as_dict(_union_support.get(value, 'mapping', 'mapping')).values()))) + and (_union_support.has(value, 'value', 'value', True) and ((_union_support.is_object(_union_support.get(value, 'value', 'value')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'value', 'value'), 'name', 'name'), str))) + or (_union_support.is_object(_union_support.get(value, 'value', 'value')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'value', 'value'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(value, 'value', 'value'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(value, 'value', 'value'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(value, 'value', 'value'), 'second_value', 'second_value'), str))) + or _union_support.number(_union_support.get(value, 'value', 'value'), True))) + and (_union_support.has(value, 'values', 'values', True) and (_union_support.is_list(_union_support.get(value, 'values', 'values')) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'name', 'name', True) and isinstance(_union_support.get(_item, 'name', 'name'), str))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'revision', 'revision', True) and (_union_support.number(_union_support.get(_item, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_item, 'revision', 'revision'), 2))) + and (not _union_support.has(_item, 'second_value', 'second_value', False) or isinstance(_union_support.get(_item, 'second_value', 'second_value'), str))) + or _union_support.number(_item, True)) for _item in _union_support.as_list(_union_support.get(value, 'values', 'values')))))): + return value_49_to_dict(value) + + if _union_support.number(value, True): + return value_1_to_dict(value) + + return value + +def value_49_from_dict(value: object) -> object: + from.import tagged_member_envelope + return tagged_member_envelope.TaggedMemberEnvelope.from_dict(_union_support.as_dict(value)) + +def value_49_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def match_recursive_any_from_dict(value: object) -> bool: + return ((_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_from_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))) + or isinstance(value, str)) + +def match_recursive_concrete_from_dict(value: object) -> bool: + return ((_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_from_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))) + or isinstance(value, str)) + +def match_recursive_fallback_from_dict(value: object) -> bool: + return False + +def match_recursive_any_to_dict(value: object) -> bool: + return ((_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_to_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))) + or isinstance(value, str)) + +def match_recursive_concrete_to_dict(value: object) -> bool: + return ((_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_to_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))) + or isinstance(value, str)) + +def match_recursive_fallback_to_dict(value: object) -> bool: + return False diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/acyclic_envelope.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/acyclic_envelope.py.golden new file mode 100644 index 000000000..57ded1581 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/acyclic_envelope.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .acyclic_scalar import AcyclicScalar + +@dataclass +class AcyclicEnvelope: + value: AcyclicScalar + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['value'] = _union.value_0_to_dict(self.value) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> AcyclicEnvelope: + return cls( + value = cast(AcyclicScalar, _union.value_0_from_dict(data['value'])), + ) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/acyclic_scalar.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/acyclic_scalar.py.golden new file mode 100644 index 000000000..e7448aa38 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/acyclic_scalar.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union + +type AcyclicScalar = ( + int + | str +) + +def acyclic_scalar_from_dict(value: object) -> AcyclicScalar: + return cast(AcyclicScalar, _union.value_0_from_dict(value)) + +def acyclic_scalar_to_dict(value: AcyclicScalar) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/adjacent_member.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/adjacent_member.py.golden new file mode 100644 index 000000000..b1c372785 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/adjacent_member.py.golden @@ -0,0 +1,33 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from .numeric_two import NumericTwo +from .tagged_record import TaggedRecord + +# Discriminator: kind / content: payload (adjacent). + +type AdjacentMember = ( + TaggedRecord + | NumericTwo +) + + +def adjacent_member_from_dict(data: dict[str, object]) -> AdjacentMember: + _tag = data["kind"] + _content = data["payload"] # type: ignore[assignment] + if _tag == "record": + return TaggedRecord.from_dict(_content) # type: ignore[arg-type] + elif _tag == "numeric": + return NumericTwo.from_dict(_content) # type: ignore[arg-type] + raise ValueError(f"Unknown discriminator value for AdjacentMember: {data}") + + +def adjacent_member_to_dict(obj: AdjacentMember) -> dict[str, object]: + if isinstance(obj, TaggedRecord): + return {"kind": "record", "payload": obj.to_dict()} + elif isinstance(obj, NumericTwo): + return {"kind": "numeric", "payload": obj.to_dict()} + raise ValueError(f"Unknown variant for AdjacentMember: {type(obj)}") diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/ambiguous.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/ambiguous.py.golden new file mode 100644 index 000000000..540aa34d8 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/ambiguous.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .numeric_two import NumericTwo + +type Ambiguous = ( + NumericTwo + | NumericTwo +) + +def ambiguous_from_dict(value: object) -> Ambiguous: + return cast(Ambiguous, _union.value_3_from_dict(value)) + +def ambiguous_to_dict(value: Ambiguous) -> object: + return _union.value_3_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/ambiguous_tagged_member.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/ambiguous_tagged_member.py.golden new file mode 100644 index 000000000..bcc1f745a --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/ambiguous_tagged_member.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .adjacent_member import AdjacentMember + +type AmbiguousTaggedMember = ( + AdjacentMember + | AdjacentMember +) + +def ambiguous_tagged_member_from_dict(value: object) -> AmbiguousTaggedMember: + return cast(AmbiguousTaggedMember, _union.value_5_from_dict(value)) + +def ambiguous_tagged_member_to_dict(value: AmbiguousTaggedMember) -> object: + return _union.value_5_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/branch.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/branch.py.golden new file mode 100644 index 000000000..fae055beb --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/branch.py.golden @@ -0,0 +1,25 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union, recursive + +@dataclass +class Branch: + children: list[recursive.Recursive] + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['children'] = _union.value_8_to_dict(self.children) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Branch: + return cls( + children = cast(list[recursive.Recursive], _union.value_8_from_dict(data['children'])), + ) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/choice.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/choice.py.golden new file mode 100644 index 000000000..dbbb3f910 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/choice.py.golden @@ -0,0 +1,22 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .numeric_one import NumericOne +from .numeric_two import NumericTwo + +type Choice = ( + NumericOne + | NumericTwo +) + +def choice_from_dict(value: object) -> Choice: + return cast(Choice, _union.value_11_from_dict(value)) + +def choice_to_dict(value: Choice) -> object: + return _union.value_11_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/choice_alias.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/choice_alias.py.golden new file mode 100644 index 000000000..7063a2741 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/choice_alias.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from .choice import Choice + +type ChoiceAlias = (Choice) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/client_tool.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/client_tool.py.golden new file mode 100644 index 000000000..b65bef982 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/client_tool.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +@dataclass +class ClientTool: + input_schema: dict[str, Any] + name: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["input_schema"] = self.input_schema + result["name"] = self.name + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> ClientTool: + return cls( + input_schema=data["input_schema"], # type: ignore[assignment] + name=data["name"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/dependent_choice.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/dependent_choice.py.golden new file mode 100644 index 000000000..76ce3e523 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/dependent_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .dependent_envelope import DependentEnvelope + +type DependentChoice = ( + DependentEnvelope + | bool +) + +def dependent_choice_from_dict(value: object) -> DependentChoice: + return cast(DependentChoice, _union.value_14_from_dict(value)) + +def dependent_choice_to_dict(value: DependentChoice) -> object: + return _union.value_14_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/dependent_envelope.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/dependent_envelope.py.golden new file mode 100644 index 000000000..a3e05475d --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/dependent_envelope.py.golden @@ -0,0 +1,24 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from .acyclic_envelope import AcyclicEnvelope + +@dataclass +class DependentEnvelope: + envelope: AcyclicEnvelope + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["envelope"] = self.envelope.to_dict() + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> DependentEnvelope: + return cls( + envelope=AcyclicEnvelope.from_dict(data["envelope"]), # type: ignore[arg-type] + ) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/envelope.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/envelope.py.golden new file mode 100644 index 000000000..85977ed31 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/envelope.py.golden @@ -0,0 +1,42 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .choice import Choice +from .choice_alias import ChoiceAlias + +@dataclass +class Envelope: + choice: ChoiceAlias + choice_list: list[Choice] + choice_map: dict[str, Choice] + deep: list[dict[str, list[Choice]]] + optional_choice: Choice | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['choice'] = _union.value_13_to_dict(self.choice) + result['choice_list'] = _union.value_17_to_dict(self.choice_list) + result['choice_map'] = _union.value_18_to_dict(self.choice_map) + result['deep'] = _union.value_19_to_dict(self.deep) + if self.optional_choice is not None: + result['optional_choice'] = _union.value_11_to_dict(self.optional_choice) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Envelope: + return cls( + choice = cast(ChoiceAlias, _union.value_13_from_dict(data['choice'])), + choice_list = cast(list[Choice], _union.value_17_from_dict(data['choice_list'])), + choice_map = cast(dict[str, Choice], _union.value_18_from_dict(data['choice_map'])), + deep = cast(list[dict[str, list[Choice]]], _union.value_19_from_dict(data['deep'])), + optional_choice = cast(Choice + | None, None if 'optional_choice' not in data else _union.value_11_from_dict(data.get('optional_choice'))), + ) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/finite_envelope.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/finite_envelope.py.golden new file mode 100644 index 000000000..d1c6f6d9f --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/finite_envelope.py.golden @@ -0,0 +1,25 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union + +@dataclass +class FiniteEnvelope: + value: int | bool | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['value'] = _union.value_21_to_dict(self.value) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> FiniteEnvelope: + return cls( + value = cast(int | bool | None, _union.value_21_from_dict(data['value'])), + ) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/inner_null_choice.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/inner_null_choice.py.golden new file mode 100644 index 000000000..83addfac4 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/inner_null_choice.py.golden @@ -0,0 +1,18 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type InnerNullChoice = ( + object + | str | None +) + +def inner_null_choice_from_dict(value: object) -> InnerNullChoice: + return _union.value_24_from_dict(value) + +def inner_null_choice_to_dict(value: InnerNullChoice) -> object: + return _union.value_24_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/nested_ambiguous.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/nested_ambiguous.py.golden new file mode 100644 index 000000000..199893d53 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/nested_ambiguous.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .ambiguous import Ambiguous + +type NestedAmbiguous = ( + Ambiguous + | str +) + +def nested_ambiguous_from_dict(value: object) -> NestedAmbiguous: + return cast(NestedAmbiguous, _union.value_28_from_dict(value)) + +def nested_ambiguous_to_dict(value: NestedAmbiguous) -> object: + return _union.value_28_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/nullable_choice.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/nullable_choice.py.golden new file mode 100644 index 000000000..94eb7ca64 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/nullable_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .choice import Choice + +type NullableChoice = ( + Choice + | None +) + +def nullable_choice_from_dict(value: object) -> NullableChoice: + return cast(NullableChoice, _union.value_29_from_dict(value)) + +def nullable_choice_to_dict(value: NullableChoice) -> object: + return _union.value_29_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/nullable_envelope.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/nullable_envelope.py.golden new file mode 100644 index 000000000..a039cdb7f --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/nullable_envelope.py.golden @@ -0,0 +1,32 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .nullable_choice import NullableChoice +from .wrapped_null_choice import WrappedNullChoice + +@dataclass +class NullableEnvelope: + choice: NullableChoice + ambiguous: WrappedNullChoice | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['choice'] = _union.value_29_to_dict(self.choice) + if self.ambiguous is not None: + result['ambiguous'] = _union.value_30_to_dict(self.ambiguous) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NullableEnvelope: + return cls( + choice = cast(NullableChoice, _union.value_29_from_dict(data['choice'])), + ambiguous = None if 'ambiguous' not in data else _union.value_30_from_dict(data.get('ambiguous')), + ) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/numeric_one.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/numeric_one.py.golden new file mode 100644 index 000000000..00917636c --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/numeric_one.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class NumericOne: + revision: int + first_value: str | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["revision"] = self.revision + if self.first_value is not None: + result["first_value"] = self.first_value + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NumericOne: + return cls( + revision=data["revision"], # type: ignore[assignment] + first_value=data.get("first_value"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/numeric_two.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/numeric_two.py.golden new file mode 100644 index 000000000..d042a0d8b --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/numeric_two.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class NumericTwo: + revision: int + second_value: str | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["revision"] = self.revision + if self.second_value is not None: + result["second_value"] = self.second_value + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NumericTwo: + return cls( + revision=data["revision"], # type: ignore[assignment] + second_value=data.get("second_value"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/open_choice.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/open_choice.py.golden new file mode 100644 index 000000000..3bcd71d81 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/open_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import Any + +from . import _union +from .numeric_two import NumericTwo + +type OpenChoice = ( + Any + | NumericTwo +) + +def open_choice_from_dict(value: object) -> OpenChoice: + return _union.value_31_from_dict(value) + +def open_choice_to_dict(value: OpenChoice) -> object: + return _union.value_31_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/recursive.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/recursive.py.golden new file mode 100644 index 000000000..79aeec558 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/recursive.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union, branch + +type Recursive = ( + branch.Branch + | str +) + +def recursive_from_dict(value: object) -> Recursive: + return cast(Recursive, _union.value_9_from_dict(value)) + +def recursive_to_dict(value: Recursive) -> object: + return _union.value_9_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/request.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/request.py.golden new file mode 100644 index 000000000..46530c8d1 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/request.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .tool import Tool + +@dataclass +class Request: + tools: list[Tool] + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['tools'] = _union.value_33_to_dict(self.tools) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Request: + return cls( + tools = cast(list[Tool], _union.value_33_from_dict(data['tools'])), + ) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/scalar_choice.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/scalar_choice.py.golden new file mode 100644 index 000000000..a055918c9 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/scalar_choice.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union + +type ScalarChoice = ( + int + | bool +) + +def scalar_choice_from_dict(value: object) -> ScalarChoice: + return cast(ScalarChoice, _union.value_37_from_dict(value)) + +def scalar_choice_to_dict(value: ScalarChoice) -> object: + return _union.value_37_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/search_tool.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/search_tool.py.golden new file mode 100644 index 000000000..f6b31466a --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/search_tool.py.golden @@ -0,0 +1,30 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal + +@dataclass +class SearchTool: + name: Literal["web_search"] + type_: Literal["web_search_20250305"] + max_uses: int | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["name"] = self.name + result["type"] = self.type_ + if self.max_uses is not None: + result["max_uses"] = self.max_uses + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> SearchTool: + return cls( + name=data["name"], # type: ignore[assignment] + type_=data["type"], # type: ignore[assignment] + max_uses=data.get("max_uses"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/send_multipart_multipart_request_body.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/send_multipart_multipart_request_body.py.golden new file mode 100644 index 000000000..607318f62 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/send_multipart_multipart_request_body.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from ..runtime import UploadFile +from .choice import Choice + +@dataclass +class SendMultipartMultipartRequestBody: + choice: Choice + attachment: UploadFile | None = None diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/send_multipart_request.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/send_multipart_request.py.golden new file mode 100644 index 000000000..0e9e53f29 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/send_multipart_request.py.golden @@ -0,0 +1,30 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .choice import Choice + +@dataclass +class SendMultipartRequest: + choice: Choice + attachment: bytes | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['choice'] = _union.value_11_to_dict(self.choice) + if self.attachment is not None: + result["attachment"] = self.attachment + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> SendMultipartRequest: + return cls( + choice = cast(Choice, _union.value_11_from_dict(data['choice'])), + attachment=data.get("attachment"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_ambiguous.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_ambiguous.py.golden new file mode 100644 index 000000000..d4fd8dc37 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_ambiguous.py.golden @@ -0,0 +1,23 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .ambiguous import Ambiguous + +# Discriminator: kind / content: payload (adjacent). + +type TaggedAmbiguous = ( + Ambiguous + | str +) + +def tagged_ambiguous_from_dict(value: object) -> TaggedAmbiguous: + return cast(TaggedAmbiguous, _union.value_40_from_dict(value)) + +def tagged_ambiguous_to_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], _union.value_40_to_dict(value)) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_choice.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_choice.py.golden new file mode 100644 index 000000000..4bbaf7ad5 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_choice.py.golden @@ -0,0 +1,23 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .choice import Choice + +# Discriminator: kind / content: payload (adjacent). + +type TaggedChoice = ( + Choice + | str +) + +def tagged_choice_from_dict(value: object) -> TaggedChoice: + return cast(TaggedChoice, _union.value_41_from_dict(value)) + +def tagged_choice_to_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], _union.value_41_to_dict(value)) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_container_choice.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_container_choice.py.golden new file mode 100644 index 000000000..d8a108327 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_container_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .adjacent_member import AdjacentMember + +type TaggedContainerChoice = ( + list[AdjacentMember] + | str +) + +def tagged_container_choice_from_dict(value: object) -> TaggedContainerChoice: + return cast(TaggedContainerChoice, _union.value_42_from_dict(value)) + +def tagged_container_choice_to_dict(value: TaggedContainerChoice) -> object: + return _union.value_42_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_member_alias.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_member_alias.py.golden new file mode 100644 index 000000000..65d4e105c --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_member_alias.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from .tagged_member_choice import TaggedMemberChoice + +type TaggedMemberAlias = (TaggedMemberChoice) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_member_choice.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_member_choice.py.golden new file mode 100644 index 000000000..b1a85c387 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_member_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .adjacent_member import AdjacentMember + +type TaggedMemberChoice = ( + AdjacentMember + | int +) + +def tagged_member_choice_from_dict(value: object) -> TaggedMemberChoice: + return cast(TaggedMemberChoice, _union.value_45_from_dict(value)) + +def tagged_member_choice_to_dict(value: TaggedMemberChoice) -> object: + return _union.value_45_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_member_envelope.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_member_envelope.py.golden new file mode 100644 index 000000000..6d7d76561 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_member_envelope.py.golden @@ -0,0 +1,34 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .tagged_member_alias import TaggedMemberAlias +from .tagged_member_choice import TaggedMemberChoice + +@dataclass +class TaggedMemberEnvelope: + mapping: dict[str, TaggedMemberChoice] + value: TaggedMemberChoice + values: list[TaggedMemberAlias] + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['mapping'] = _union.value_46_to_dict(self.mapping) + result['value'] = _union.value_45_to_dict(self.value) + result['values'] = _union.value_47_to_dict(self.values) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> TaggedMemberEnvelope: + return cls( + mapping = cast(dict[str, + TaggedMemberChoice], _union.value_46_from_dict(data['mapping'])), + value = cast(TaggedMemberChoice, _union.value_45_from_dict(data['value'])), + values = cast(list[TaggedMemberAlias], _union.value_47_from_dict(data['values'])), + ) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_object_choice.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_object_choice.py.golden new file mode 100644 index 000000000..052030fcf --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_object_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .tagged_member_envelope import TaggedMemberEnvelope + +type TaggedObjectChoice = ( + TaggedMemberEnvelope + | int +) + +def tagged_object_choice_from_dict(value: object) -> TaggedObjectChoice: + return cast(TaggedObjectChoice, _union.value_48_from_dict(value)) + +def tagged_object_choice_to_dict(value: TaggedObjectChoice) -> object: + return _union.value_48_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_record.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_record.py.golden new file mode 100644 index 000000000..41db830f4 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tagged_record.py.golden @@ -0,0 +1,22 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class TaggedRecord: + name: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["name"] = self.name + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> TaggedRecord: + return cls( + name=data["name"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tool.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tool.py.golden new file mode 100644 index 000000000..42944e010 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/tool.py.golden @@ -0,0 +1,22 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .client_tool import ClientTool +from .search_tool import SearchTool + +type Tool = ( + ClientTool + | SearchTool +) + +def tool_from_dict(value: object) -> Tool: + return cast(Tool, _union.value_34_from_dict(value)) + +def tool_to_dict(value: Tool) -> object: + return _union.value_34_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/wrapped_null_choice.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/wrapped_null_choice.py.golden new file mode 100644 index 000000000..e47e6b7dc --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/models/wrapped_null_choice.py.golden @@ -0,0 +1,19 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union +from .inner_null_choice import InnerNullChoice + +type WrappedNullChoice = ( + InnerNullChoice + | int +) + +def wrapped_null_choice_from_dict(value: object) -> WrappedNullChoice: + return _union.value_30_from_dict(value) + +def wrapped_null_choice_to_dict(value: WrappedNullChoice) -> object: + return _union.value_30_to_dict(value) diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/py.typed.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/py.typed.golden new file mode 100644 index 000000000..e69de29bb diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/__init__.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/__init__.py.golden new file mode 100644 index 000000000..c77ee060c --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/__init__.py.golden @@ -0,0 +1,11 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .auth import ApiKeyAuth as ApiKeyAuth +from .auth import Authenticator as Authenticator +from .auth import BearerAuth as BearerAuth +from .client import ApiResponse as ApiResponse +from .client import Client as Client +from .errors import ApiError as ApiError +from .upload_file import UploadFile as UploadFile diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/_union_support.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/_union_support.py.golden new file mode 100644 index 000000000..a4c9e6107 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/_union_support.py.golden @@ -0,0 +1,90 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/auth.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/auth.py.golden new file mode 100644 index 000000000..eaf25448a --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/auth.py.golden @@ -0,0 +1,50 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""Authentication helpers.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Callable + + +class Authenticator(ABC): + """Base class for request authenticators.""" + + @abstractmethod + def auth_headers(self) -> dict[str, str]: + """Return headers to attach to every request.""" + ... + + +class BearerAuth(Authenticator): + """Bearer token authentication. + + Accepts a static token string or a callable that returns the + current token (evaluated on every request). + """ + + def __init__(self, token: str | Callable[[], str]) -> None: + self._token = token + + def auth_headers(self) -> dict[str, str]: + token = self._token() if callable(self._token) else self._token + return {"Authorization": f"Bearer {token}"} + + +class ApiKeyAuth(Authenticator): + """API key authentication via a custom header. + + Accepts a static key string or a callable that returns the + current key (evaluated on every request). + """ + + def __init__(self, header_name: str, api_key: str | Callable[[], str]) -> None: + self._header_name = header_name + self._api_key = api_key + + def auth_headers(self) -> dict[str, str]: + key = self._api_key() if callable(self._api_key) else self._api_key + return {self._header_name: key} diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/client.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/client.py.golden new file mode 100644 index 000000000..1e2fe2176 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/client.py.golden @@ -0,0 +1,84 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""HTTP client wrapping httpx.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any, Generic, TypeVar + +import httpx + +from .auth import Authenticator + +ResponseData = TypeVar("ResponseData") + + +@dataclass(frozen=True) +class ApiResponse(Generic[ResponseData]): + """Decoded response data together with its HTTP metadata.""" + + data: ResponseData + status_code: int + headers: Mapping[str, str] + raw: httpx.Response + + +class Client: + """Synchronous HTTP client for the generated SDK.""" + + def __init__( + self, + base_url: str, + *, + http_client: httpx.Client | None = None, + authenticator: Authenticator | None = None, + ) -> None: + self._base_url = base_url.rstrip("/") + self._http_client = http_client or httpx.Client() + self._authenticator = authenticator + + def request( + self, + method: str, + path: str, + *, + params: dict[str, str] | None = None, + json: Any = None, + content: Any = None, + data: Any = None, + files: Any = None, + headers: dict[str, str] | None = None, + ) -> httpx.Response: + """Send an HTTP request and return the raw response.""" + url = f"{self._base_url}{path}" + req_headers: dict[str, str] = {"Accept": "application/json"} + if json is not None: + req_headers["Content-Type"] = "application/json" + if headers: + req_headers.update(headers) + if self._authenticator is not None: + req_headers.update(self._authenticator.auth_headers()) + return self._http_client.request( + method, + url, + params=params, + json=json, + content=content, + data=data, + files=files, + headers=req_headers, + ) + + def close(self) -> None: + """Close the underlying HTTP client.""" + self._http_client.close() + + def __enter__(self) -> Client: + return self + + def __exit__(self, *args: object) -> None: + self.close() diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/errors.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/errors.py.golden new file mode 100644 index 000000000..3530e64be --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/errors.py.golden @@ -0,0 +1,34 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""API error type.""" + +from __future__ import annotations + +from collections.abc import Mapping + + +class ApiError(Exception): + """Raised when the server returns a non-success status code.""" + + def __init__( + self, + status_code: int, + status: str, + body: bytes, + *, + headers: Mapping[str, str] | None = None, + response: object | None = None, + ) -> None: + self.status_code: int = status_code + self.status: str = status + self.raw_body: bytes = body + self.body: bytes = body + self.headers: Mapping[str, str] | None = headers + self.response: object | None = response + super().__init__(f"{status_code} {status}") + + @property + def text(self) -> str: + return self.raw_body.decode("utf-8", errors="replace") diff --git a/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/upload_file.py.golden b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/upload_file.py.golden new file mode 100644 index 000000000..f218509e4 --- /dev/null +++ b/tests/golden/python/python-httpx/untagged-union/python_union_conversion/runtime/upload_file.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class UploadFile: + data: bytes + filename: str | None = None + + @classmethod + def from_bytes(cls, data: bytes, filename: str | None = None) -> UploadFile: + return cls(data=data, filename=filename) + + def filename_or_default(self, default_name: str) -> str: + return self.filename or default_name diff --git a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/_union.py.golden b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/_union.py.golden new file mode 100644 index 000000000..e458295c7 --- /dev/null +++ b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/_union.py.golden @@ -0,0 +1,146 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Comprehensive Schema Types Example — 1.0.0 +# Comprehensive test for all OpenAPI v3.1.2 schema types + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if isinstance(value, str): + return value_1_from_dict(value) + + if _union_support.number(value, False): + return value_2_from_dict(value) + + return value + +def value_0_to_dict(value: object) -> object: + if isinstance(value, str): + return value_1_to_dict(value) + + if _union_support.number(value, False): + return value_2_to_dict(value) + + return value + +def value_1_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_1_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (not _union_support.has(value, 'active', 'active', False) or isinstance(_union_support.get(value, 'active', 'active'), bool)) + and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): + return value_4_from_dict(value) + + if (_union_support.is_object(value) + and (not _union_support.has(value, 'metadata', 'metadata', False) or (_union_support.is_object(_union_support.get(value, 'metadata', 'metadata')) + and (not _union_support.has(_union_support.get(value, 'metadata', 'metadata'), 'created', 'created', False) or isinstance(_union_support.get(_union_support.get(value, 'metadata', 'metadata'), 'created', 'created'), str)))) + and (not _union_support.has(value, 'user', 'user', False) or (_union_support.is_object(_union_support.get(value, 'user', 'user')) + and (_union_support.has(_union_support.get(value, 'user', 'user'), 'id', 'id', True) and _union_support.number(_union_support.get(_union_support.get(value, 'user', 'user'), 'id', 'id'), True)) + and (not _union_support.has(_union_support.get(value, 'user', 'user'), 'name', 'name', False) or isinstance(_union_support.get(_union_support.get(value, 'user', 'user'), 'name', 'name'), str))))): + return value_5_from_dict(value) + + return value + +def value_3_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (not _union_support.has(value, 'active', 'active', False) or isinstance(_union_support.get(value, 'active', 'active'), bool)) + and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): + return value_4_to_dict(value) + + if (_union_support.is_object(value) + and (not _union_support.has(value, 'metadata', 'metadata', False) or (_union_support.is_object(_union_support.get(value, 'metadata', 'metadata')) + and (not _union_support.has(_union_support.get(value, 'metadata', 'metadata'), 'created', 'created', False) or isinstance(_union_support.get(_union_support.get(value, 'metadata', 'metadata'), 'created', 'created'), str)))) + and (not _union_support.has(value, 'user', 'user', False) or (_union_support.is_object(_union_support.get(value, 'user', 'user')) + and (_union_support.has(_union_support.get(value, 'user', 'user'), 'id', 'id', True) and _union_support.number(_union_support.get(_union_support.get(value, 'user', 'user'), 'id', 'id'), True)) + and (not _union_support.has(_union_support.get(value, 'user', 'user'), 'name', 'name', False) or isinstance(_union_support.get(_union_support.get(value, 'user', 'user'), 'name', 'name'), str))))): + return value_5_to_dict(value) + + return value + +def value_4_from_dict(value: object) -> object: + from.import basic_object + return basic_object.BasicObject.from_dict(_union_support.as_dict(value)) + +def value_4_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_5_from_dict(value: object) -> object: + from.import nested_object + return nested_object.NestedObject.from_dict(_union_support.as_dict(value)) + +def value_5_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_6_from_dict(value: object) -> object: + if isinstance(value, str): + return value_1_from_dict(value) + + if _union_support.number(value, False): + return value_2_from_dict(value) + + if isinstance(value, bool): + return value_7_from_dict(value) + + return value + +def value_6_to_dict(value: object) -> object: + if isinstance(value, str): + return value_1_to_dict(value) + + if _union_support.number(value, False): + return value_2_to_dict(value) + + if isinstance(value, bool): + return value_7_to_dict(value) + + return value + +def value_7_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_7_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_8_from_dict(value: object) -> object: + if isinstance(value, str): + return value_1_from_dict(value) + + if (_union_support.is_object(value) + and (not _union_support.has(value, 'active', 'active', False) or isinstance(_union_support.get(value, 'active', 'active'), bool)) + and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): + return value_4_from_dict(value) + + if value is None: + return None + + return value + +def value_8_to_dict(value: object) -> object: + if isinstance(value, str): + return value_1_to_dict(value) + + if (_union_support.is_object(value) + and (not _union_support.has(value, 'active', 'active', False) or isinstance(_union_support.get(value, 'active', 'active'), bool)) + and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): + return value_4_to_dict(value) + + if value is None: + return None + + return value diff --git a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/any_of_types.py.golden b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/any_of_types.py.golden index 3cef19651..b9c948909 100644 --- a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/any_of_types.py.golden +++ b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/any_of_types.py.golden @@ -5,9 +5,19 @@ from __future__ import annotations +from typing import cast + +from . import _union + # Any of multiple types type AnyOfTypes = ( str | float ) + +def any_of_types_from_dict(value: object) -> AnyOfTypes: + return cast(AnyOfTypes, _union.value_0_from_dict(value)) + +def any_of_types_to_dict(value: AnyOfTypes) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_objects.py.golden b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_objects.py.golden index dd22f711e..198397c80 100644 --- a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_objects.py.golden +++ b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_objects.py.golden @@ -5,6 +5,9 @@ from __future__ import annotations +from typing import cast + +from . import _union from .basic_object import BasicObject from .nested_object import NestedObject @@ -14,3 +17,9 @@ type OneOfObjects = ( BasicObject | NestedObject ) + +def one_of_objects_from_dict(value: object) -> OneOfObjects: + return cast(OneOfObjects, _union.value_3_from_dict(value)) + +def one_of_objects_to_dict(value: OneOfObjects) -> object: + return _union.value_3_to_dict(value) diff --git a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_types.py.golden b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_types.py.golden index d5e39c344..5e7628744 100644 --- a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_types.py.golden +++ b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_types.py.golden @@ -5,6 +5,10 @@ from __future__ import annotations +from typing import cast + +from . import _union + # Union of multiple primitive types type OneOfTypes = ( @@ -12,3 +16,9 @@ type OneOfTypes = ( | float | bool ) + +def one_of_types_from_dict(value: object) -> OneOfTypes: + return cast(OneOfTypes, _union.value_6_from_dict(value)) + +def one_of_types_to_dict(value: OneOfTypes) -> object: + return _union.value_6_to_dict(value) diff --git a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_with_null.py.golden b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_with_null.py.golden index 79f16d239..fb08327fc 100644 --- a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_with_null.py.golden +++ b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/models/one_of_with_null.py.golden @@ -5,6 +5,9 @@ from __future__ import annotations +from typing import cast + +from . import _union from .basic_object import BasicObject # Union of string, null, and schema reference @@ -14,3 +17,9 @@ type OneOfWithNull = ( | BasicObject | None ) + +def one_of_with_null_from_dict(value: object) -> OneOfWithNull: + return cast(OneOfWithNull, _union.value_8_from_dict(value)) + +def one_of_with_null_to_dict(value: OneOfWithNull) -> object: + return _union.value_8_to_dict(value) diff --git a/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/runtime/_union_support.py.golden b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/runtime/_union_support.py.golden new file mode 100644 index 000000000..c24a70570 --- /dev/null +++ b/tests/golden/python/python-requests/comprehensive-schemas/comprehensive_schema_types_example/runtime/_union_support.py.golden @@ -0,0 +1,91 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Comprehensive Schema Types Example — 1.0.0 +# Comprehensive test for all OpenAPI v3.1.2 schema types + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-requests/enum-repr/enum_representation_api/apis/enum_repr_api.py.golden b/tests/golden/python/python-requests/enum-repr/enum_representation_api/apis/enum_repr_api.py.golden index 8c9fd5405..c2f80ed5c 100644 --- a/tests/golden/python/python-requests/enum-repr/enum_representation_api/apis/enum_repr_api.py.golden +++ b/tests/golden/python/python-requests/enum-repr/enum_representation_api/apis/enum_repr_api.py.golden @@ -5,8 +5,12 @@ from __future__ import annotations +import json + from collections.abc import Mapping +from typing import cast +from ..models import _union from ..models.adjacently_tagged_enum import AdjacentlyTaggedEnum from ..models.externally_tagged_enum import ExternallyTaggedEnum from ..models.internally_tagged_enum import InternallyTaggedEnum @@ -210,14 +214,16 @@ class EnumReprApi: path = "/mixed" headers: dict[str, str] = {} headers["Content-Type"] = "application/json" - response = self._client.request("POST", path, json=body, headers=headers) + _union_body = _union.value_0_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) if not (200 <= response.status_code < 300): if response.status_code == 400: detail = HandleMixedBadRequest(response.status_code, response.headers, response.content) else: detail = HandleMixedUnexpected(response.status_code, response.headers, response.content) raise HandleMixedError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) - data = response.json() # type: ignore[return-value] + _union_data = cast(MixedEnum, _union.value_0_from_dict(response.json())) + data = _union_data return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) def handle_mixed(self, *, body: MixedEnum) -> MixedEnum: @@ -229,14 +235,16 @@ class EnumReprApi: path = "/untagged" headers: dict[str, str] = {} headers["Content-Type"] = "application/json" - response = self._client.request("POST", path, json=body, headers=headers) + _union_body = _union.value_5_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) if not (200 <= response.status_code < 300): if response.status_code == 400: detail = HandleUntaggedBadRequest(response.status_code, response.headers, response.content) else: detail = HandleUntaggedUnexpected(response.status_code, response.headers, response.content) raise HandleUntaggedError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) - data = response.json() # type: ignore[return-value] + _union_data = cast(UntaggedEnum, _union.value_5_from_dict(response.json())) + data = _union_data return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) def handle_untagged(self, *, body: UntaggedEnum) -> UntaggedEnum: diff --git a/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/_union.py.golden b/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/_union.py.golden new file mode 100644 index 000000000..5feee1e72 --- /dev/null +++ b/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/_union.py.golden @@ -0,0 +1,116 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Enum Representation API — 1.0.0 +# This API demonstrates all 4 kinds of enum representation types: Externally Tagged, Internally Tagged, Adjacently Tagged, and Untagged + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if _union_support.equal(value, 'SimpleA'): + return value_1_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'VariantA', 'variant_a', True) and (_union_support.is_object(_union_support.get(value, 'VariantA', 'variant_a')) + and (_union_support.has(_union_support.get(value, 'VariantA', 'variant_a'), 'field1', 'field1', True) and isinstance(_union_support.get(_union_support.get(value, 'VariantA', 'variant_a'), 'field1', 'field1'), str)) + and (_union_support.has(_union_support.get(value, 'VariantA', 'variant_a'), 'field2', 'field2', True) and _union_support.number(_union_support.get(_union_support.get(value, 'VariantA', 'variant_a'), 'field2', 'field2'), True))))): + return value_2_from_dict(value) + + if _union_support.equal(value, 'SimpleB'): + return value_3_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'VariantB', 'variant_b', True) and (_union_support.is_object(_union_support.get(value, 'VariantB', 'variant_b')) + and (_union_support.has(_union_support.get(value, 'VariantB', 'variant_b'), 'field3', 'field3', True) and isinstance(_union_support.get(_union_support.get(value, 'VariantB', 'variant_b'), 'field3', 'field3'), bool)) + and (_union_support.has(_union_support.get(value, 'VariantB', 'variant_b'), 'field4', 'field4', True) and _union_support.number(_union_support.get(_union_support.get(value, 'VariantB', 'variant_b'), 'field4', 'field4'), False))))): + return value_4_from_dict(value) + + return value + +def value_0_to_dict(value: object) -> object: + if _union_support.equal(value, 'SimpleA'): + return value_1_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'VariantA', 'variant_a', True) and (_union_support.is_object(_union_support.get(value, 'VariantA', 'variant_a')) + and (_union_support.has(_union_support.get(value, 'VariantA', 'variant_a'), 'field1', 'field1', True) and isinstance(_union_support.get(_union_support.get(value, 'VariantA', 'variant_a'), 'field1', 'field1'), str)) + and (_union_support.has(_union_support.get(value, 'VariantA', 'variant_a'), 'field2', 'field2', True) and _union_support.number(_union_support.get(_union_support.get(value, 'VariantA', 'variant_a'), 'field2', 'field2'), True))))): + return value_2_to_dict(value) + + if _union_support.equal(value, 'SimpleB'): + return value_3_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'VariantB', 'variant_b', True) and (_union_support.is_object(_union_support.get(value, 'VariantB', 'variant_b')) + and (_union_support.has(_union_support.get(value, 'VariantB', 'variant_b'), 'field3', 'field3', True) and isinstance(_union_support.get(_union_support.get(value, 'VariantB', 'variant_b'), 'field3', 'field3'), bool)) + and (_union_support.has(_union_support.get(value, 'VariantB', 'variant_b'), 'field4', 'field4', True) and _union_support.number(_union_support.get(_union_support.get(value, 'VariantB', 'variant_b'), 'field4', 'field4'), False))))): + return value_4_to_dict(value) + + return value + +def value_1_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_1_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_from_dict(value: object) -> object: + from.import mixed_enum_member2 + return mixed_enum_member2.MixedEnumMember2.from_dict(_union_support.as_dict(value)) + +def value_2_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_3_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_4_from_dict(value: object) -> object: + from.import mixed_enum_member4 + return mixed_enum_member4.MixedEnumMember4.from_dict(_union_support.as_dict(value)) + +def value_4_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_5_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'field1', 'field1', True) and isinstance(_union_support.get(value, 'field1', 'field1'), str)) + and (_union_support.has(value, 'field2', 'field2', True) and _union_support.number(_union_support.get(value, 'field2', 'field2'), True))): + return value_6_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'field3', 'field3', True) and isinstance(_union_support.get(value, 'field3', 'field3'), bool)) + and (_union_support.has(value, 'field4', 'field4', True) and _union_support.number(_union_support.get(value, 'field4', 'field4'), False))): + return value_7_from_dict(value) + + return value + +def value_5_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'field1', 'field1', True) and isinstance(_union_support.get(value, 'field1', 'field1'), str)) + and (_union_support.has(value, 'field2', 'field2', True) and _union_support.number(_union_support.get(value, 'field2', 'field2'), True))): + return value_6_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'field3', 'field3', True) and isinstance(_union_support.get(value, 'field3', 'field3'), bool)) + and (_union_support.has(value, 'field4', 'field4', True) and _union_support.number(_union_support.get(value, 'field4', 'field4'), False))): + return value_7_to_dict(value) + + return value + +def value_6_from_dict(value: object) -> object: + from.import variant_a + return variant_a.VariantA.from_dict(_union_support.as_dict(value)) + +def value_6_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_7_from_dict(value: object) -> object: + from.import variant_b + return variant_b.VariantB.from_dict(_union_support.as_dict(value)) + +def value_7_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) diff --git a/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/adjacently_tagged_enum.py.golden b/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/adjacently_tagged_enum.py.golden index 168416dd2..45ddb5ba8 100644 --- a/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/adjacently_tagged_enum.py.golden +++ b/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/adjacently_tagged_enum.py.golden @@ -37,4 +37,3 @@ def adjacently_tagged_enum_to_dict(obj: AdjacentlyTaggedEnum) -> dict[str, objec elif isinstance(obj, VariantB): return {"ty": "ADJACENTLY_TAGGED_ENUM_VARIANT_B", "data": obj.to_dict()} raise ValueError(f"Unknown variant for AdjacentlyTaggedEnum: {type(obj)}") - diff --git a/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/externally_tagged_enum.py.golden b/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/externally_tagged_enum.py.golden index 9cd0b6442..a4954827a 100644 --- a/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/externally_tagged_enum.py.golden +++ b/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/externally_tagged_enum.py.golden @@ -35,4 +35,3 @@ def externally_tagged_enum_to_dict(obj: ExternallyTaggedEnum) -> dict[str, objec elif isinstance(obj, VariantB): return {"EXTERNALLY_TAGGED_ENUM_VARIANT_B": obj.to_dict()} raise ValueError(f"Unknown variant for ExternallyTaggedEnum: {type(obj)}") - diff --git a/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/internally_tagged_enum.py.golden b/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/internally_tagged_enum.py.golden index 58e40d4f6..a966ef978 100644 --- a/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/internally_tagged_enum.py.golden +++ b/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/internally_tagged_enum.py.golden @@ -40,4 +40,3 @@ def internally_tagged_enum_to_dict(obj: InternallyTaggedEnum) -> dict[str, objec result["ty"] = "INTERNALLY_TAGGED_ENUM_VARIANT_B" return result raise ValueError(f"Unknown variant for InternallyTaggedEnum: {type(obj)}") - diff --git a/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/mixed_enum.py.golden b/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/mixed_enum.py.golden index e27273ea3..bdb54dc3b 100644 --- a/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/mixed_enum.py.golden +++ b/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/mixed_enum.py.golden @@ -5,8 +5,9 @@ from __future__ import annotations -from typing import Literal +from typing import Literal, cast +from . import _union from .mixed_enum_member2 import MixedEnumMember2 from .mixed_enum_member4 import MixedEnumMember4 @@ -18,3 +19,9 @@ type MixedEnum = ( | Literal["SimpleB"] | MixedEnumMember4 ) + +def mixed_enum_from_dict(value: object) -> MixedEnum: + return cast(MixedEnum, _union.value_0_from_dict(value)) + +def mixed_enum_to_dict(value: MixedEnum) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/untagged_enum.py.golden b/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/untagged_enum.py.golden index a16171269..33926d2eb 100644 --- a/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/untagged_enum.py.golden +++ b/tests/golden/python/python-requests/enum-repr/enum_representation_api/models/untagged_enum.py.golden @@ -5,6 +5,9 @@ from __future__ import annotations +from typing import cast + +from . import _union from .variant_a import VariantA from .variant_b import VariantB @@ -14,3 +17,9 @@ type UntaggedEnum = ( VariantA | VariantB ) + +def untagged_enum_from_dict(value: object) -> UntaggedEnum: + return cast(UntaggedEnum, _union.value_5_from_dict(value)) + +def untagged_enum_to_dict(value: UntaggedEnum) -> object: + return _union.value_5_to_dict(value) diff --git a/tests/golden/python/python-requests/enum-repr/enum_representation_api/runtime/_union_support.py.golden b/tests/golden/python/python-requests/enum-repr/enum_representation_api/runtime/_union_support.py.golden new file mode 100644 index 000000000..ee0abbd31 --- /dev/null +++ b/tests/golden/python/python-requests/enum-repr/enum_representation_api/runtime/_union_support.py.golden @@ -0,0 +1,91 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Enum Representation API — 1.0.0 +# This API demonstrates all 4 kinds of enum representation types: Externally Tagged, Internally Tagged, Adjacently Tagged, and Untagged + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/models/event.py.golden b/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/models/event.py.golden index 1f8b74dd7..12998be35 100644 --- a/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/models/event.py.golden +++ b/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/models/event.py.golden @@ -34,4 +34,3 @@ def event_to_dict(obj: Event) -> dict[str, object]: result["type"] = "completed" return result raise ValueError(f"Unknown variant for Event: {type(obj)}") - diff --git a/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/models/external_event.py.golden b/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/models/external_event.py.golden index 592df2ad8..965f24794 100644 --- a/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/models/external_event.py.golden +++ b/tests/golden/python/python-requests/open-tagged-additional-properties/open_tagged_additional_properties/models/external_event.py.golden @@ -28,4 +28,3 @@ def external_event_to_dict(obj: ExternalEvent) -> dict[str, object]: elif isinstance(obj, Payload): return {"finished": obj.to_dict()} raise ValueError(f"Unknown variant for ExternalEvent: {type(obj)}") - diff --git a/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/apis/default_api.py.golden b/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/apis/default_api.py.golden index 7f020a264..0b5435108 100644 --- a/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/apis/default_api.py.golden +++ b/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/apis/default_api.py.golden @@ -5,8 +5,12 @@ from __future__ import annotations +import json + from collections.abc import Mapping +from typing import cast +from ..models import _union from ..models.foo import Foo from ..runtime.client import ApiResponse, Client from ..runtime.errors import ApiError @@ -39,11 +43,13 @@ class DefaultApi: path = "/test" headers: dict[str, str] = {} headers["Content-Type"] = "application/json" - response = self._client.request("POST", path, json=body, headers=headers) + _union_body = _union.value_0_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) if not (200 <= response.status_code < 300): detail = TestComplexUnionUnexpected(response.status_code, response.headers, response.content) raise TestComplexUnionError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) - data = response.json() # type: ignore[return-value] + _union_data = cast(Foo, _union.value_0_from_dict(response.json())) + data = _union_data return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) def test_complex_union(self, *, body: Foo) -> Foo: diff --git a/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/models/_union.py.golden b/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/models/_union.py.golden new file mode 100644 index 000000000..268dd24db --- /dev/null +++ b/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/models/_union.py.golden @@ -0,0 +1,69 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Complex Union Type Test — 1.0.0 +# Test fixture for complex union types + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'bucket', 'bucket', True) and isinstance(_union_support.get(value, 'bucket', 'bucket'), str)) + and (_union_support.has(value, 'endpoint', 'endpoint', True) and isinstance(_union_support.get(value, 'endpoint', 'endpoint'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'bar'))): + return value_1_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'path', 'path', True) and isinstance(_union_support.get(value, 'path', 'path'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'baz'))): + return value_2_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'namespace', 'namespace', True) and isinstance(_union_support.get(value, 'namespace', 'namespace'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'qux')) + and (_union_support.has(value, 'url', 'url', True) and isinstance(_union_support.get(value, 'url', 'url'), str))): + return value_3_from_dict(value) + + return value + +def value_0_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'bucket', 'bucket', True) and isinstance(_union_support.get(value, 'bucket', 'bucket'), str)) + and (_union_support.has(value, 'endpoint', 'endpoint', True) and isinstance(_union_support.get(value, 'endpoint', 'endpoint'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'bar'))): + return value_1_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'path', 'path', True) and isinstance(_union_support.get(value, 'path', 'path'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'baz'))): + return value_2_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'namespace', 'namespace', True) and isinstance(_union_support.get(value, 'namespace', 'namespace'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'qux')) + and (_union_support.has(value, 'url', 'url', True) and isinstance(_union_support.get(value, 'url', 'url'), str))): + return value_3_to_dict(value) + + return value + +def value_1_from_dict(value: object) -> object: + from.import bar + return bar.Bar.from_dict(_union_support.as_dict(value)) + +def value_1_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_2_from_dict(value: object) -> object: + from.import baz + return baz.Baz.from_dict(_union_support.as_dict(value)) + +def value_2_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_3_from_dict(value: object) -> object: + from.import qux + return qux.Qux.from_dict(_union_support.as_dict(value)) + +def value_3_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) diff --git a/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/models/foo.py.golden b/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/models/foo.py.golden index 13e60de67..64cdd8545 100644 --- a/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/models/foo.py.golden +++ b/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/models/foo.py.golden @@ -5,6 +5,9 @@ from __future__ import annotations +from typing import cast + +from . import _union from .bar import Bar from .baz import Baz from .qux import Qux @@ -14,3 +17,9 @@ type Foo = ( | Baz | Qux ) + +def foo_from_dict(value: object) -> Foo: + return cast(Foo, _union.value_0_from_dict(value)) + +def foo_to_dict(value: Foo) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/models/quux.py.golden b/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/models/quux.py.golden index b3ab60f22..a026b22b0 100644 --- a/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/models/quux.py.golden +++ b/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/models/quux.py.golden @@ -6,7 +6,9 @@ from __future__ import annotations from dataclasses import dataclass +from typing import cast +from . import _union from .foo import Foo @dataclass @@ -16,13 +18,13 @@ class Quux: def to_dict(self) -> dict[str, object]: result: dict[str, object] = {} - result["foo"] = self.foo + result['foo'] = _union.value_0_to_dict(self.foo) result["name"] = self.name return result @classmethod def from_dict(cls, data: dict[str, object]) -> Quux: return cls( - foo=data["foo"], # type: ignore[assignment] + foo = cast(Foo, _union.value_0_from_dict(data['foo'])), name=data["name"], # type: ignore[assignment] ) diff --git a/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/runtime/_union_support.py.golden b/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/runtime/_union_support.py.golden new file mode 100644 index 000000000..1138cbb7b --- /dev/null +++ b/tests/golden/python/python-requests/type-aliases-complex-union/complex_union_type_test/runtime/_union_support.py.golden @@ -0,0 +1,91 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Complex Union Type Test — 1.0.0 +# Test fixture for complex union types + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event.py.golden b/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event.py.golden index 07feba3ea..9e689f743 100644 --- a/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event.py.golden +++ b/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event.py.golden @@ -52,4 +52,3 @@ def event_to_dict(obj: Event) -> dict[str, object]: result["kind"] = "EVENT_UPDATED" return result raise ValueError(f"Unknown variant for Event: {type(obj)}") - diff --git a/tests/golden/python/python-requests/type-aliases-discriminated-union-internally-tagged/discriminated_union_internally_tagged_test/models/resource.py.golden b/tests/golden/python/python-requests/type-aliases-discriminated-union-internally-tagged/discriminated_union_internally_tagged_test/models/resource.py.golden index d76312cc8..ecd0bb5f6 100644 --- a/tests/golden/python/python-requests/type-aliases-discriminated-union-internally-tagged/discriminated_union_internally_tagged_test/models/resource.py.golden +++ b/tests/golden/python/python-requests/type-aliases-discriminated-union-internally-tagged/discriminated_union_internally_tagged_test/models/resource.py.golden @@ -45,4 +45,3 @@ def resource_to_dict(obj: Resource) -> dict[str, object]: result["kind"] = "RESOURCE_CUSTOM" return result raise ValueError(f"Unknown variant for Resource: {type(obj)}") - diff --git a/tests/golden/python/python-requests/type-aliases-discriminated-union-long-names/discriminated_union_with_long_type_names_test/models/very_long_discriminated_union_type_name_for_testing20260630.py.golden b/tests/golden/python/python-requests/type-aliases-discriminated-union-long-names/discriminated_union_with_long_type_names_test/models/very_long_discriminated_union_type_name_for_testing20260630.py.golden index 4e36d9df7..7988c2457 100644 --- a/tests/golden/python/python-requests/type-aliases-discriminated-union-long-names/discriminated_union_with_long_type_names_test/models/very_long_discriminated_union_type_name_for_testing20260630.py.golden +++ b/tests/golden/python/python-requests/type-aliases-discriminated-union-long-names/discriminated_union_with_long_type_names_test/models/very_long_discriminated_union_type_name_for_testing20260630.py.golden @@ -45,4 +45,3 @@ def very_long_discriminated_union_type_name_for_testing20260630_to_dict(obj: Ver result["kind"] = "GAMMA" return result raise ValueError(f"Unknown variant for VeryLongDiscriminatedUnionTypeNameForTesting20260630: {type(obj)}") - diff --git a/tests/golden/python/python-requests/type-aliases-discriminated-union-mixed-unit-and-allof/discriminated_union_mixed_unit_and_all_of_test/models/setup.py.golden b/tests/golden/python/python-requests/type-aliases-discriminated-union-mixed-unit-and-allof/discriminated_union_mixed_unit_and_all_of_test/models/setup.py.golden index 8b1a830e9..91e0ca866 100644 --- a/tests/golden/python/python-requests/type-aliases-discriminated-union-mixed-unit-and-allof/discriminated_union_mixed_unit_and_all_of_test/models/setup.py.golden +++ b/tests/golden/python/python-requests/type-aliases-discriminated-union-mixed-unit-and-allof/discriminated_union_mixed_unit_and_all_of_test/models/setup.py.golden @@ -50,4 +50,3 @@ def setup_to_dict(obj: Setup) -> dict[str, object]: result["kind"] = "SETUP_CUSTOM" return result raise ValueError(f"Unknown variant for Setup: {type(obj)}") - diff --git a/tests/golden/python/python-requests/type-aliases-discriminated-union-multiple/multiple_discriminated_unions_test/models/container_kind.py.golden b/tests/golden/python/python-requests/type-aliases-discriminated-union-multiple/multiple_discriminated_unions_test/models/container_kind.py.golden index 062ea9ceb..b112bc8d3 100644 --- a/tests/golden/python/python-requests/type-aliases-discriminated-union-multiple/multiple_discriminated_unions_test/models/container_kind.py.golden +++ b/tests/golden/python/python-requests/type-aliases-discriminated-union-multiple/multiple_discriminated_unions_test/models/container_kind.py.golden @@ -38,4 +38,3 @@ def container_kind_to_dict(obj: ContainerKind) -> dict[str, object]: result["kind"] = "CONTAINER_KIND_CUSTOM" return result raise ValueError(f"Unknown variant for ContainerKind: {type(obj)}") - diff --git a/tests/golden/python/python-requests/type-aliases-discriminated-union-multiple/multiple_discriminated_unions_test/models/volume_kind.py.golden b/tests/golden/python/python-requests/type-aliases-discriminated-union-multiple/multiple_discriminated_unions_test/models/volume_kind.py.golden index d8653cb5d..eef24705b 100644 --- a/tests/golden/python/python-requests/type-aliases-discriminated-union-multiple/multiple_discriminated_unions_test/models/volume_kind.py.golden +++ b/tests/golden/python/python-requests/type-aliases-discriminated-union-multiple/multiple_discriminated_unions_test/models/volume_kind.py.golden @@ -38,4 +38,3 @@ def volume_kind_to_dict(obj: VolumeKind) -> dict[str, object]: result["kind"] = "VOLUME_KIND_REMOTE" return result raise ValueError(f"Unknown variant for VolumeKind: {type(obj)}") - diff --git a/tests/golden/python/python-requests/type-aliases-discriminated-union-with-refs/discriminated_union_with_references_test/models/container_image.py.golden b/tests/golden/python/python-requests/type-aliases-discriminated-union-with-refs/discriminated_union_with_references_test/models/container_image.py.golden index e6fdcfa0c..e2ff20d31 100644 --- a/tests/golden/python/python-requests/type-aliases-discriminated-union-with-refs/discriminated_union_with_references_test/models/container_image.py.golden +++ b/tests/golden/python/python-requests/type-aliases-discriminated-union-with-refs/discriminated_union_with_references_test/models/container_image.py.golden @@ -36,4 +36,3 @@ def container_image_to_dict(obj: ContainerImage) -> dict[str, object]: result["kind"] = "CONTAINER_IMAGE_CUSTOM" return result raise ValueError(f"Unknown variant for ContainerImage: {type(obj)}") - diff --git a/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/apis/default_api.py.golden b/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/apis/default_api.py.golden index 9c3dd0205..80fee28b9 100644 --- a/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/apis/default_api.py.golden +++ b/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/apis/default_api.py.golden @@ -5,8 +5,12 @@ from __future__ import annotations +import json + from collections.abc import Mapping +from typing import cast +from ..models import _union from ..models.foo import Foo from ..runtime.client import ApiResponse, Client from ..runtime.errors import ApiError @@ -39,11 +43,13 @@ class DefaultApi: path = "/test" headers: dict[str, str] = {} headers["Content-Type"] = "application/json" - response = self._client.request("POST", path, json=body, headers=headers) + _union_body = _union.value_0_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) if not (200 <= response.status_code < 300): detail = TestNestedUnionUnexpected(response.status_code, response.headers, response.content) raise TestNestedUnionError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) - data = response.json() # type: ignore[return-value] + _union_data = cast(Foo, _union.value_0_from_dict(response.json())) + data = _union_data return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) def test_nested_union(self, *, body: Foo) -> Foo: diff --git a/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/models/_union.py.golden b/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/models/_union.py.golden new file mode 100644 index 000000000..7a0183361 --- /dev/null +++ b/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/models/_union.py.golden @@ -0,0 +1,94 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Union Type Test — 1.0.0 +# Test fixture for union types that reference other union types + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'endpoint', 'endpoint', True) and isinstance(_union_support.get(value, 'endpoint', 'endpoint'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'bar'))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'path', 'path', True) and isinstance(_union_support.get(value, 'path', 'path'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'baz')))): + return value_1_from_dict(value) + + if isinstance(value, str): + return value_4_from_dict(value) + + if _union_support.number(value, False): + return value_5_from_dict(value) + + return value + +def value_0_to_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'endpoint', 'endpoint', True) and isinstance(_union_support.get(value, 'endpoint', 'endpoint'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'bar'))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'path', 'path', True) and isinstance(_union_support.get(value, 'path', 'path'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'baz')))): + return value_1_to_dict(value) + + if isinstance(value, str): + return value_4_to_dict(value) + + if _union_support.number(value, False): + return value_5_to_dict(value) + + return value + +def value_1_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'endpoint', 'endpoint', True) and isinstance(_union_support.get(value, 'endpoint', 'endpoint'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'bar'))): + return value_2_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'path', 'path', True) and isinstance(_union_support.get(value, 'path', 'path'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'baz'))): + return value_3_from_dict(value) + + return value + +def value_1_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'endpoint', 'endpoint', True) and isinstance(_union_support.get(value, 'endpoint', 'endpoint'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'bar'))): + return value_2_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'path', 'path', True) and isinstance(_union_support.get(value, 'path', 'path'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'baz'))): + return value_3_to_dict(value) + + return value + +def value_2_from_dict(value: object) -> object: + from.import bar + return bar.Bar.from_dict(_union_support.as_dict(value)) + +def value_2_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_3_from_dict(value: object) -> object: + from.import baz + return baz.Baz.from_dict(_union_support.as_dict(value)) + +def value_3_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_4_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_4_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_5_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_5_to_dict(value: object) -> object: + return _union_support.scalar(value) diff --git a/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/models/foo.py.golden b/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/models/foo.py.golden index e1105b17d..a92ee5d56 100644 --- a/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/models/foo.py.golden +++ b/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/models/foo.py.golden @@ -5,6 +5,9 @@ from __future__ import annotations +from typing import cast + +from . import _union from .qux import Qux type Foo = ( @@ -12,3 +15,9 @@ type Foo = ( | str | float ) + +def foo_from_dict(value: object) -> Foo: + return cast(Foo, _union.value_0_from_dict(value)) + +def foo_to_dict(value: Foo) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/models/qux.py.golden b/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/models/qux.py.golden index c324ed9ec..74b6e838b 100644 --- a/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/models/qux.py.golden +++ b/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/models/qux.py.golden @@ -5,6 +5,9 @@ from __future__ import annotations +from typing import cast + +from . import _union from .bar import Bar from .baz import Baz @@ -12,3 +15,9 @@ type Qux = ( Bar | Baz ) + +def qux_from_dict(value: object) -> Qux: + return cast(Qux, _union.value_1_from_dict(value)) + +def qux_to_dict(value: Qux) -> object: + return _union.value_1_to_dict(value) diff --git a/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/runtime/_union_support.py.golden b/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/runtime/_union_support.py.golden new file mode 100644 index 000000000..386b49b07 --- /dev/null +++ b/tests/golden/python/python-requests/type-aliases-nested-union/nested_union_type_test/runtime/_union_support.py.golden @@ -0,0 +1,91 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Union Type Test — 1.0.0 +# Test fixture for union types that reference other union types + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-requests/type-aliases-union-mixed/union_type_with_mixed_members_test/apis/default_api.py.golden b/tests/golden/python/python-requests/type-aliases-union-mixed/union_type_with_mixed_members_test/apis/default_api.py.golden index bf59ab172..2bfea2561 100644 --- a/tests/golden/python/python-requests/type-aliases-union-mixed/union_type_with_mixed_members_test/apis/default_api.py.golden +++ b/tests/golden/python/python-requests/type-aliases-union-mixed/union_type_with_mixed_members_test/apis/default_api.py.golden @@ -5,8 +5,12 @@ from __future__ import annotations +import json + from collections.abc import Mapping +from typing import cast +from ..models import _union from ..models.foo import Foo from ..runtime.client import ApiResponse, Client from ..runtime.errors import ApiError @@ -39,11 +43,13 @@ class DefaultApi: path = "/test" headers: dict[str, str] = {} headers["Content-Type"] = "application/json" - response = self._client.request("POST", path, json=body, headers=headers) + _union_body = _union.value_0_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) if not (200 <= response.status_code < 300): detail = TestMixedUnionUnexpected(response.status_code, response.headers, response.content) raise TestMixedUnionError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) - data = response.json() # type: ignore[return-value] + _union_data = cast(Foo, _union.value_0_from_dict(response.json())) + data = _union_data return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) def test_mixed_union(self, *, body: Foo) -> Foo: diff --git a/tests/golden/python/python-requests/type-aliases-union-mixed/union_type_with_mixed_members_test/models/_union.py.golden b/tests/golden/python/python-requests/type-aliases-union-mixed/union_type_with_mixed_members_test/models/_union.py.golden new file mode 100644 index 000000000..8c5b0d0e0 --- /dev/null +++ b/tests/golden/python/python-requests/type-aliases-union-mixed/union_type_with_mixed_members_test/models/_union.py.golden @@ -0,0 +1,55 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Mixed Members Test — 1.0.0 +# Test fixture for union types with both interfaces and primitives + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): + return value_1_from_dict(value) + + if isinstance(value, str): + return value_2_from_dict(value) + + if _union_support.number(value, False): + return value_3_from_dict(value) + + return value + +def value_0_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'id', 'id', True) and _union_support.number(_union_support.get(value, 'id', 'id'), True)) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): + return value_1_to_dict(value) + + if isinstance(value, str): + return value_2_to_dict(value) + + if _union_support.number(value, False): + return value_3_to_dict(value) + + return value + +def value_1_from_dict(value: object) -> object: + from.import bar + return bar.Bar.from_dict(_union_support.as_dict(value)) + +def value_1_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_2_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_to_dict(value: object) -> object: + return _union_support.scalar(value) diff --git a/tests/golden/python/python-requests/type-aliases-union-mixed/union_type_with_mixed_members_test/models/foo.py.golden b/tests/golden/python/python-requests/type-aliases-union-mixed/union_type_with_mixed_members_test/models/foo.py.golden index 97c56b2dd..28674817a 100644 --- a/tests/golden/python/python-requests/type-aliases-union-mixed/union_type_with_mixed_members_test/models/foo.py.golden +++ b/tests/golden/python/python-requests/type-aliases-union-mixed/union_type_with_mixed_members_test/models/foo.py.golden @@ -5,6 +5,9 @@ from __future__ import annotations +from typing import cast + +from . import _union from .bar import Bar type Foo = ( @@ -12,3 +15,9 @@ type Foo = ( | str | float ) + +def foo_from_dict(value: object) -> Foo: + return cast(Foo, _union.value_0_from_dict(value)) + +def foo_to_dict(value: Foo) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-requests/type-aliases-union-mixed/union_type_with_mixed_members_test/runtime/_union_support.py.golden b/tests/golden/python/python-requests/type-aliases-union-mixed/union_type_with_mixed_members_test/runtime/_union_support.py.golden new file mode 100644 index 000000000..8a0a49a69 --- /dev/null +++ b/tests/golden/python/python-requests/type-aliases-union-mixed/union_type_with_mixed_members_test/runtime/_union_support.py.golden @@ -0,0 +1,91 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Mixed Members Test — 1.0.0 +# Test fixture for union types with both interfaces and primitives + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/apis/default_api.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/apis/default_api.py.golden index 11d997bde..a2ad39000 100644 --- a/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/apis/default_api.py.golden +++ b/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/apis/default_api.py.golden @@ -5,8 +5,11 @@ from __future__ import annotations +import json + from collections.abc import Mapping +from ..models import _union from ..models.config import Config from ..runtime.client import ApiResponse, Client from ..runtime.errors import ApiError @@ -39,11 +42,13 @@ class DefaultApi: path = "/test" headers: dict[str, str] = {} headers["Content-Type"] = "application/json" - response = self._client.request("POST", path, json=body, headers=headers) + _union_body = _union.value_0_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) if not (200 <= response.status_code < 300): detail = TestUnionWithAnyUnexpected(response.status_code, response.headers, response.content) raise TestUnionWithAnyError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) - data = response.json() # type: ignore[return-value] + _union_data = _union.value_0_from_dict(response.json()) + data = _union_data return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) def test_union_with_any(self, *, body: Config) -> Config: diff --git a/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/models/_union.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/models/_union.py.golden new file mode 100644 index 000000000..db5c526a8 --- /dev/null +++ b/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/models/_union.py.golden @@ -0,0 +1,50 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Any Test — 1.0.0 +# Test fixture for union types that include the any type + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if isinstance(value, str): + return value_1_from_dict(value) + + if _union_support.number(value, False): + return value_2_from_dict(value) + + if True: + return value_3_from_dict(value) + + return value + +def value_0_to_dict(value: object) -> object: + if isinstance(value, str): + return value_1_to_dict(value) + + if _union_support.number(value, False): + return value_2_to_dict(value) + + if True: + return value_3_to_dict(value) + + return value + +def value_1_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_1_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_to_dict(value: object) -> object: + return _union_support.scalar(value) diff --git a/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/models/config.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/models/config.py.golden index 349fb1c06..4a36e52d5 100644 --- a/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/models/config.py.golden +++ b/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/models/config.py.golden @@ -7,8 +7,16 @@ from __future__ import annotations from typing import Any +from . import _union + type Config = ( str | float | Any ) + +def config_from_dict(value: object) -> Config: + return _union.value_0_from_dict(value) + +def config_to_dict(value: Config) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/runtime/_union_support.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/runtime/_union_support.py.golden new file mode 100644 index 000000000..aa7278d60 --- /dev/null +++ b/tests/golden/python/python-requests/type-aliases-union-with-any/union_type_with_any_test/runtime/_union_support.py.golden @@ -0,0 +1,91 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Any Test — 1.0.0 +# Test fixture for union types that include the any type + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-requests/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/apis/default_api.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/apis/default_api.py.golden index 21aa19ee5..a28cd43d2 100644 --- a/tests/golden/python/python-requests/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/apis/default_api.py.golden +++ b/tests/golden/python/python-requests/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/apis/default_api.py.golden @@ -5,8 +5,12 @@ from __future__ import annotations +import json + from collections.abc import Mapping +from typing import cast +from ..models import _union from ..models.foo import Foo from ..runtime.client import ApiResponse, Client from ..runtime.errors import ApiError @@ -39,11 +43,13 @@ class DefaultApi: path = "/test" headers: dict[str, str] = {} headers["Content-Type"] = "application/json" - response = self._client.request("POST", path, json=body, headers=headers) + _union_body = _union.value_0_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) if not (200 <= response.status_code < 300): detail = TestUnionWithInlineObjectsUnexpected(response.status_code, response.headers, response.content) raise TestUnionWithInlineObjectsError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) - data = response.json() # type: ignore[return-value] + _union_data = cast(Foo, _union.value_0_from_dict(response.json())) + data = _union_data return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) def test_union_with_inline_objects(self, *, body: Foo) -> Foo: diff --git a/tests/golden/python/python-requests/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/models/_union.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/models/_union.py.golden new file mode 100644 index 000000000..cf60881e2 --- /dev/null +++ b/tests/golden/python/python-requests/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/models/_union.py.golden @@ -0,0 +1,67 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Inline Objects Test — 1.0.0 +# Test fixture for union types (oneOf) with inline object schemas that should generate named interfaces + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'bar', 'bar', True) and (_union_support.is_object(_union_support.get(value, 'bar', 'bar')) + and (_union_support.has(_union_support.get(value, 'bar', 'bar'), 'baz', 'baz', True) and isinstance(_union_support.get(_union_support.get(value, 'bar', 'bar'), 'baz', 'baz'), str)) + and (_union_support.has(_union_support.get(value, 'bar', 'bar'), 'qux', 'qux', True) and isinstance(_union_support.get(_union_support.get(value, 'bar', 'bar'), 'qux', 'qux'), str))))): + return value_1_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'quux', 'quux', True) and (_union_support.is_object(_union_support.get(value, 'quux', 'quux')) + and (_union_support.has(_union_support.get(value, 'quux', 'quux'), 'corge', 'corge', True) and isinstance(_union_support.get(_union_support.get(value, 'quux', 'quux'), 'corge', 'corge'), str))))): + return value_2_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'grault', 'grault', True) and (_union_support.is_object(_union_support.get(value, 'grault', 'grault')) + and (_union_support.has(_union_support.get(value, 'grault', 'grault'), 'garply', 'garply', True) and isinstance(_union_support.get(_union_support.get(value, 'grault', 'grault'), 'garply', 'garply'), str))))): + return value_3_from_dict(value) + + return value + +def value_0_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'bar', 'bar', True) and (_union_support.is_object(_union_support.get(value, 'bar', 'bar')) + and (_union_support.has(_union_support.get(value, 'bar', 'bar'), 'baz', 'baz', True) and isinstance(_union_support.get(_union_support.get(value, 'bar', 'bar'), 'baz', 'baz'), str)) + and (_union_support.has(_union_support.get(value, 'bar', 'bar'), 'qux', 'qux', True) and isinstance(_union_support.get(_union_support.get(value, 'bar', 'bar'), 'qux', 'qux'), str))))): + return value_1_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'quux', 'quux', True) and (_union_support.is_object(_union_support.get(value, 'quux', 'quux')) + and (_union_support.has(_union_support.get(value, 'quux', 'quux'), 'corge', 'corge', True) and isinstance(_union_support.get(_union_support.get(value, 'quux', 'quux'), 'corge', 'corge'), str))))): + return value_2_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'grault', 'grault', True) and (_union_support.is_object(_union_support.get(value, 'grault', 'grault')) + and (_union_support.has(_union_support.get(value, 'grault', 'grault'), 'garply', 'garply', True) and isinstance(_union_support.get(_union_support.get(value, 'grault', 'grault'), 'garply', 'garply'), str))))): + return value_3_to_dict(value) + + return value + +def value_1_from_dict(value: object) -> object: + from.import foo_member1 + return foo_member1.FooMember1.from_dict(_union_support.as_dict(value)) + +def value_1_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_2_from_dict(value: object) -> object: + from.import foo_member2 + return foo_member2.FooMember2.from_dict(_union_support.as_dict(value)) + +def value_2_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_3_from_dict(value: object) -> object: + from.import foo_member3 + return foo_member3.FooMember3.from_dict(_union_support.as_dict(value)) + +def value_3_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) diff --git a/tests/golden/python/python-requests/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/models/foo.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/models/foo.py.golden index aad0b38f5..a9e8d92e9 100644 --- a/tests/golden/python/python-requests/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/models/foo.py.golden +++ b/tests/golden/python/python-requests/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/models/foo.py.golden @@ -5,6 +5,9 @@ from __future__ import annotations +from typing import cast + +from . import _union from .foo_member1 import FooMember1 from .foo_member2 import FooMember2 from .foo_member3 import FooMember3 @@ -16,3 +19,9 @@ type Foo = ( | FooMember2 | FooMember3 ) + +def foo_from_dict(value: object) -> Foo: + return cast(Foo, _union.value_0_from_dict(value)) + +def foo_to_dict(value: Foo) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-requests/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/runtime/_union_support.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/runtime/_union_support.py.golden new file mode 100644 index 000000000..20fb2628d --- /dev/null +++ b/tests/golden/python/python-requests/type-aliases-union-with-inline-objects/union_type_with_inline_objects_test/runtime/_union_support.py.golden @@ -0,0 +1,91 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Inline Objects Test — 1.0.0 +# Test fixture for union types (oneOf) with inline object schemas that should generate named interfaces + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-requests/type-aliases-union-with-interfaces/union_type_with_interfaces_test/apis/default_api.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-interfaces/union_type_with_interfaces_test/apis/default_api.py.golden index 93bcb3c95..a69f9d78c 100644 --- a/tests/golden/python/python-requests/type-aliases-union-with-interfaces/union_type_with_interfaces_test/apis/default_api.py.golden +++ b/tests/golden/python/python-requests/type-aliases-union-with-interfaces/union_type_with_interfaces_test/apis/default_api.py.golden @@ -5,8 +5,12 @@ from __future__ import annotations +import json + from collections.abc import Mapping +from typing import cast +from ..models import _union from ..models.foo import Foo from ..runtime.client import ApiResponse, Client from ..runtime.errors import ApiError @@ -39,11 +43,13 @@ class DefaultApi: path = "/test" headers: dict[str, str] = {} headers["Content-Type"] = "application/json" - response = self._client.request("POST", path, json=body, headers=headers) + _union_body = _union.value_0_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) if not (200 <= response.status_code < 300): detail = TestUnionUnexpected(response.status_code, response.headers, response.content) raise TestUnionError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) - data = response.json() # type: ignore[return-value] + _union_data = cast(Foo, _union.value_0_from_dict(response.json())) + data = _union_data return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) def test_union(self, *, body: Foo) -> Foo: diff --git a/tests/golden/python/python-requests/type-aliases-union-with-interfaces/union_type_with_interfaces_test/models/_union.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-interfaces/union_type_with_interfaces_test/models/_union.py.golden new file mode 100644 index 000000000..de4fcb73a --- /dev/null +++ b/tests/golden/python/python-requests/type-aliases-union-with-interfaces/union_type_with_interfaces_test/models/_union.py.golden @@ -0,0 +1,69 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Interfaces Test — 1.0.0 +# Test fixture for oneOf/anyOf union types with interface members + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'bucket', 'bucket', True) and isinstance(_union_support.get(value, 'bucket', 'bucket'), str)) + and (_union_support.has(value, 'endpoint', 'endpoint', True) and isinstance(_union_support.get(value, 'endpoint', 'endpoint'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'bar'))): + return value_1_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'path', 'path', True) and isinstance(_union_support.get(value, 'path', 'path'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'baz'))): + return value_2_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'namespace', 'namespace', True) and isinstance(_union_support.get(value, 'namespace', 'namespace'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'qux')) + and (_union_support.has(value, 'url', 'url', True) and isinstance(_union_support.get(value, 'url', 'url'), str))): + return value_3_from_dict(value) + + return value + +def value_0_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'bucket', 'bucket', True) and isinstance(_union_support.get(value, 'bucket', 'bucket'), str)) + and (_union_support.has(value, 'endpoint', 'endpoint', True) and isinstance(_union_support.get(value, 'endpoint', 'endpoint'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'bar'))): + return value_1_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'path', 'path', True) and isinstance(_union_support.get(value, 'path', 'path'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'baz'))): + return value_2_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'namespace', 'namespace', True) and isinstance(_union_support.get(value, 'namespace', 'namespace'), str)) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'qux')) + and (_union_support.has(value, 'url', 'url', True) and isinstance(_union_support.get(value, 'url', 'url'), str))): + return value_3_to_dict(value) + + return value + +def value_1_from_dict(value: object) -> object: + from.import bar + return bar.Bar.from_dict(_union_support.as_dict(value)) + +def value_1_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_2_from_dict(value: object) -> object: + from.import baz + return baz.Baz.from_dict(_union_support.as_dict(value)) + +def value_2_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_3_from_dict(value: object) -> object: + from.import qux + return qux.Qux.from_dict(_union_support.as_dict(value)) + +def value_3_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) diff --git a/tests/golden/python/python-requests/type-aliases-union-with-interfaces/union_type_with_interfaces_test/models/foo.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-interfaces/union_type_with_interfaces_test/models/foo.py.golden index cd6aa5fd0..681e37952 100644 --- a/tests/golden/python/python-requests/type-aliases-union-with-interfaces/union_type_with_interfaces_test/models/foo.py.golden +++ b/tests/golden/python/python-requests/type-aliases-union-with-interfaces/union_type_with_interfaces_test/models/foo.py.golden @@ -5,6 +5,9 @@ from __future__ import annotations +from typing import cast + +from . import _union from .bar import Bar from .baz import Baz from .qux import Qux @@ -14,3 +17,9 @@ type Foo = ( | Baz | Qux ) + +def foo_from_dict(value: object) -> Foo: + return cast(Foo, _union.value_0_from_dict(value)) + +def foo_to_dict(value: Foo) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-requests/type-aliases-union-with-interfaces/union_type_with_interfaces_test/runtime/_union_support.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-interfaces/union_type_with_interfaces_test/runtime/_union_support.py.golden new file mode 100644 index 000000000..b207b3392 --- /dev/null +++ b/tests/golden/python/python-requests/type-aliases-union-with-interfaces/union_type_with_interfaces_test/runtime/_union_support.py.golden @@ -0,0 +1,91 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Interfaces Test — 1.0.0 +# Test fixture for oneOf/anyOf union types with interface members + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-requests/type-aliases-union-with-primitives/union_type_with_primitives_test/apis/default_api.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-primitives/union_type_with_primitives_test/apis/default_api.py.golden index 6af90911e..0439141d4 100644 --- a/tests/golden/python/python-requests/type-aliases-union-with-primitives/union_type_with_primitives_test/apis/default_api.py.golden +++ b/tests/golden/python/python-requests/type-aliases-union-with-primitives/union_type_with_primitives_test/apis/default_api.py.golden @@ -5,8 +5,12 @@ from __future__ import annotations +import json + from collections.abc import Mapping +from typing import cast +from ..models import _union from ..models.foo import Foo from ..runtime.client import ApiResponse, Client from ..runtime.errors import ApiError @@ -39,11 +43,13 @@ class DefaultApi: path = "/test" headers: dict[str, str] = {} headers["Content-Type"] = "application/json" - response = self._client.request("POST", path, json=body, headers=headers) + _union_body = _union.value_0_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) if not (200 <= response.status_code < 300): detail = TestUnionPrimitivesUnexpected(response.status_code, response.headers, response.content) raise TestUnionPrimitivesError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) - data = response.json() # type: ignore[return-value] + _union_data = cast(Foo, _union.value_0_from_dict(response.json())) + data = _union_data return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) def test_union_primitives(self, *, body: Foo) -> Foo: diff --git a/tests/golden/python/python-requests/type-aliases-union-with-primitives/union_type_with_primitives_test/models/_union.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-primitives/union_type_with_primitives_test/models/_union.py.golden new file mode 100644 index 000000000..77a6e5ae9 --- /dev/null +++ b/tests/golden/python/python-requests/type-aliases-union-with-primitives/union_type_with_primitives_test/models/_union.py.golden @@ -0,0 +1,56 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Primitives Test — 1.0.0 +# Test fixture for union types with primitive members + +from __future__ import annotations + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if isinstance(value, str): + return value_1_from_dict(value) + + if _union_support.number(value, False): + return value_2_from_dict(value) + + if isinstance(value, bool): + return value_3_from_dict(value) + + if value is None: + return None + + return value + +def value_0_to_dict(value: object) -> object: + if isinstance(value, str): + return value_1_to_dict(value) + + if _union_support.number(value, False): + return value_2_to_dict(value) + + if isinstance(value, bool): + return value_3_to_dict(value) + + if value is None: + return None + + return value + +def value_1_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_1_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_to_dict(value: object) -> object: + return _union_support.scalar(value) diff --git a/tests/golden/python/python-requests/type-aliases-union-with-primitives/union_type_with_primitives_test/models/foo.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-primitives/union_type_with_primitives_test/models/foo.py.golden index c4962a9b9..1da40da0e 100644 --- a/tests/golden/python/python-requests/type-aliases-union-with-primitives/union_type_with_primitives_test/models/foo.py.golden +++ b/tests/golden/python/python-requests/type-aliases-union-with-primitives/union_type_with_primitives_test/models/foo.py.golden @@ -5,9 +5,19 @@ from __future__ import annotations +from typing import cast + +from . import _union + type Foo = ( str | float | bool | None ) + +def foo_from_dict(value: object) -> Foo: + return cast(Foo, _union.value_0_from_dict(value)) + +def foo_to_dict(value: Foo) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-requests/type-aliases-union-with-primitives/union_type_with_primitives_test/runtime/_union_support.py.golden b/tests/golden/python/python-requests/type-aliases-union-with-primitives/union_type_with_primitives_test/runtime/_union_support.py.golden new file mode 100644 index 000000000..30f44c9b4 --- /dev/null +++ b/tests/golden/python/python-requests/type-aliases-union-with-primitives/union_type_with_primitives_test/runtime/_union_support.py.golden @@ -0,0 +1,91 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Union Type with Primitives Test — 1.0.0 +# Test fixture for union types with primitive members + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-requests/untagged-union-raw/README.md.golden b/tests/golden/python/python-requests/untagged-union-raw/README.md.golden new file mode 100644 index 000000000..a6f182f18 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/README.md.golden @@ -0,0 +1,7 @@ +# Python union conversion + +Generated Python SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `python_union_conversion`. diff --git a/tests/golden/python/python-requests/untagged-union-raw/pyproject.toml.golden b/tests/golden/python/python-requests/untagged-union-raw/pyproject.toml.golden new file mode 100644 index 000000000..03f2a11f9 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/pyproject.toml.golden @@ -0,0 +1,10 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "python_union_conversion" +version = "1.0.0" +description = "Generated Python SDK." +requires-python = ">=3.12" +dependencies = ["requests>=2.32"] diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/__init__.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/__init__.py.golden new file mode 100644 index 000000000..8f525cedf --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/__init__.py.golden @@ -0,0 +1,11 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .runtime import ApiKeyAuth as ApiKeyAuth +from .runtime import Authenticator as Authenticator +from .runtime import BearerAuth as BearerAuth +from .runtime import ApiResponse as ApiResponse +from .runtime import Client as Client +from .runtime import ApiError as ApiError +from .runtime import UploadFile as UploadFile diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/apis/__init__.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/apis/__init__.py.golden new file mode 100644 index 000000000..8126d3b1a --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/apis/__init__.py.golden @@ -0,0 +1,5 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .default_api import DefaultApi as DefaultApi diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/apis/default_api.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/apis/default_api.py.golden new file mode 100644 index 000000000..46218b49c --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/apis/default_api.py.golden @@ -0,0 +1,290 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +import json + +from collections.abc import Mapping +from typing import cast + +from ..models import _union +from ..models.choice import Choice +from ..models.envelope import Envelope +from ..models.nullable_choice import NullableChoice +from ..models.send_multipart_multipart_request_body import SendMultipartMultipartRequestBody +from ..models.tagged_choice import TaggedChoice +from ..models.tagged_member_choice import TaggedMemberChoice +from ..runtime.client import ApiResponse, Client +from ..runtime.errors import ApiError + +class SendChoiceBadRequest: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: Choice | None = None + self._body_error: Exception | None = None + @property + def body(self) -> Choice: + if not self._body_loaded: + try: + self._body_value = _union.value_11_from_dict(json.loads(self.raw_body.decode("utf-8"))) + + except Exception as exc: + self._body_error = exc + + self._body_loaded = True + + if self._body_error is not None: + raise self._body_error + + return self._body_value + +class SendChoiceUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendChoiceErrorDetail = ( + SendChoiceBadRequest + | SendChoiceUnexpected +) + +class SendChoiceError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendChoiceErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendChoiceErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendChoicesUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendChoicesErrorDetail = ( + SendChoicesUnexpected +) + +class SendChoicesError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendChoicesErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendChoicesErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendEnvelopeUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendEnvelopeErrorDetail = ( + SendEnvelopeUnexpected +) + +class SendEnvelopeError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendEnvelopeErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendEnvelopeErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendMultipartUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendMultipartErrorDetail = ( + SendMultipartUnexpected +) + +class SendMultipartError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendMultipartErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendMultipartErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendNullableUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendNullableErrorDetail = ( + SendNullableUnexpected +) + +class SendNullableError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendNullableErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendNullableErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendTaggedUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendTaggedErrorDetail = ( + SendTaggedUnexpected +) + +class SendTaggedError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendTaggedErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendTaggedErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendTaggedMemberUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendTaggedMemberErrorDetail = ( + SendTaggedMemberUnexpected +) + +class SendTaggedMemberError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendTaggedMemberErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendTaggedMemberErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + + +class DefaultApi: + def __init__(self, client: Client) -> None: + self._client = client + + def send_choice_with_http_info(self, *, body: Choice) -> ApiResponse[Choice]: + path = "/choice" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_11_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + if response.status_code == 400: + detail = SendChoiceBadRequest(response.status_code, response.headers, response.content) + else: + detail = SendChoiceUnexpected(response.status_code, response.headers, response.content) + raise SendChoiceError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + _union_data = _union.value_11_from_dict(response.json()) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_choice(self, *, body: Choice) -> Choice: + return self.send_choice_with_http_info(body=body).data + + def send_choices_with_http_info(self, *, body: list[Choice]) -> ApiResponse[dict[str, Choice]]: + path = "/choices" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_17_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendChoicesUnexpected(response.status_code, response.headers, response.content) + raise SendChoicesError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + _union_data = cast(dict[str, Choice], _union.value_18_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_choices(self, *, body: list[Choice]) -> dict[str, Choice]: + return self.send_choices_with_http_info(body=body).data + + def send_envelope_with_http_info(self, *, body: Envelope) -> ApiResponse[Envelope]: + path = "/envelope" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + response = self._client.request("POST", path, json=body.to_dict(), headers=headers) + if not (200 <= response.status_code < 300): + detail = SendEnvelopeUnexpected(response.status_code, response.headers, response.content) + raise SendEnvelopeError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + data = Envelope.from_dict(response.json()) + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_envelope(self, *, body: Envelope) -> Envelope: + return self.send_envelope_with_http_info(body=body).data + + def send_multipart_with_http_info(self, *, body: SendMultipartMultipartRequestBody) -> ApiResponse[None]: + path = "/multipart" + files: dict[str, object] = {} + if body.attachment is not None: + files["attachment"] = (body.attachment.filename_or_default("attachment"), body.attachment.data, "application/octet-stream") + + files['choice'] = (None, json.dumps(_union.value_11_to_dict(body.choice)), 'application/json') + response = self._client.request("POST", path, files=files if files else None) + if not (200 <= response.status_code < 300): + detail = SendMultipartUnexpected(response.status_code, response.headers, response.content) + raise SendMultipartError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + data = None + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_multipart(self, *, body: SendMultipartMultipartRequestBody) -> None: + return self.send_multipart_with_http_info(body=body).data + + def send_nullable_with_http_info(self, *, body: NullableChoice) -> ApiResponse[NullableChoice]: + path = "/nullable" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_29_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendNullableUnexpected(response.status_code, response.headers, response.content) + raise SendNullableError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + _union_data = _union.value_29_from_dict(response.json()) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_nullable(self, *, body: NullableChoice) -> NullableChoice: + return self.send_nullable_with_http_info(body=body).data + + def send_tagged_with_http_info(self, *, body: TaggedChoice) -> ApiResponse[TaggedChoice]: + path = "/tagged" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_41_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendTaggedUnexpected(response.status_code, response.headers, response.content) + raise SendTaggedError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + _union_data = _union.value_41_from_dict(response.json()) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_tagged(self, *, body: TaggedChoice) -> TaggedChoice: + return self.send_tagged_with_http_info(body=body).data + + def send_tagged_member_with_http_info(self, *, body: TaggedMemberChoice) -> ApiResponse[TaggedMemberChoice]: + path = "/tagged-member" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_45_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendTaggedMemberUnexpected(response.status_code, response.headers, response.content) + raise SendTaggedMemberError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + _union_data = _union.value_45_from_dict(response.json()) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_tagged_member(self, *, body: TaggedMemberChoice) -> TaggedMemberChoice: + return self.send_tagged_member_with_http_info(body=body).data diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/__init__.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/__init__.py.golden new file mode 100644 index 000000000..30669ee6e --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/__init__.py.golden @@ -0,0 +1,40 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .acyclic_envelope import AcyclicEnvelope as AcyclicEnvelope +from .acyclic_scalar import AcyclicScalar as AcyclicScalar +from .adjacent_member import AdjacentMember as AdjacentMember +from .ambiguous import Ambiguous as Ambiguous +from .ambiguous_tagged_member import AmbiguousTaggedMember as AmbiguousTaggedMember +from .branch import Branch as Branch +from .choice import Choice as Choice +from .choice_alias import ChoiceAlias as ChoiceAlias +from .client_tool import ClientTool as ClientTool +from .dependent_choice import DependentChoice as DependentChoice +from .dependent_envelope import DependentEnvelope as DependentEnvelope +from .envelope import Envelope as Envelope +from .finite_envelope import FiniteEnvelope as FiniteEnvelope +from .inner_null_choice import InnerNullChoice as InnerNullChoice +from .nested_ambiguous import NestedAmbiguous as NestedAmbiguous +from .nullable_choice import NullableChoice as NullableChoice +from .nullable_envelope import NullableEnvelope as NullableEnvelope +from .numeric_one import NumericOne as NumericOne +from .numeric_two import NumericTwo as NumericTwo +from .open_choice import OpenChoice as OpenChoice +from .recursive import Recursive as Recursive +from .request import Request as Request +from .scalar_choice import ScalarChoice as ScalarChoice +from .search_tool import SearchTool as SearchTool +from .send_multipart_multipart_request_body import SendMultipartMultipartRequestBody as SendMultipartMultipartRequestBody +from .send_multipart_request import SendMultipartRequest as SendMultipartRequest +from .tagged_ambiguous import TaggedAmbiguous as TaggedAmbiguous +from .tagged_choice import TaggedChoice as TaggedChoice +from .tagged_container_choice import TaggedContainerChoice as TaggedContainerChoice +from .tagged_member_alias import TaggedMemberAlias as TaggedMemberAlias +from .tagged_member_choice import TaggedMemberChoice as TaggedMemberChoice +from .tagged_member_envelope import TaggedMemberEnvelope as TaggedMemberEnvelope +from .tagged_object_choice import TaggedObjectChoice as TaggedObjectChoice +from .tagged_record import TaggedRecord as TaggedRecord +from .tool import Tool as Tool +from .wrapped_null_choice import WrappedNullChoice as WrappedNullChoice diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/_union.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/_union.py.golden new file mode 100644 index 000000000..3836a8a0c --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/_union.py.golden @@ -0,0 +1,355 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + return value + +def value_0_to_dict(value: object) -> object: + return value + +def value_1_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_1_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_from_dict(value: object) -> object: + return value + +def value_3_to_dict(value: object) -> object: + return value + +def value_4_from_dict(value: object) -> object: + from.import numeric_two + return numeric_two.NumericTwo.from_dict(_union_support.as_dict(value)) + +def value_4_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_5_from_dict(value: object) -> object: + return value + +def value_5_to_dict(value: object) -> object: + return value + +def value_6_from_dict(value: object) -> object: + from.import adjacent_member + return adjacent_member.adjacent_member_from_dict(_union_support.as_dict(value)) + +def value_6_to_dict(value: object) -> object: + from.import adjacent_member + return adjacent_member.adjacent_member_to_dict(cast(adjacent_member.AdjacentMember, value)) + +def value_7_from_dict(value: object) -> object: + from.import tagged_record + return tagged_record.TaggedRecord.from_dict(_union_support.as_dict(value)) + +def value_7_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_8_from_dict(value: object) -> object: + return [value_9_from_dict(item) for item in _union_support.as_list(value)] + +def value_8_to_dict(value: object) -> object: + return [value_9_to_dict(item) for item in _union_support.as_list(value)] + +def value_9_from_dict(value: object) -> object: + return value + +def value_9_to_dict(value: object) -> object: + return value + +def value_10_from_dict(value: object) -> object: + from.import branch + return branch.Branch.from_dict(_union_support.as_dict(value)) + +def value_10_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_11_from_dict(value: object) -> object: + return value + +def value_11_to_dict(value: object) -> object: + return value + +def value_12_from_dict(value: object) -> object: + from.import numeric_one + return numeric_one.NumericOne.from_dict(_union_support.as_dict(value)) + +def value_12_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_13_from_dict(value: object) -> object: + return value_11_from_dict(value) + +def value_13_to_dict(value: object) -> object: + return value_11_to_dict(value) + +def value_14_from_dict(value: object) -> object: + return value + +def value_14_to_dict(value: object) -> object: + return value + +def value_15_from_dict(value: object) -> object: + from.import dependent_envelope + return dependent_envelope.DependentEnvelope.from_dict(_union_support.as_dict(value)) + +def value_15_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_16_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_16_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_17_from_dict(value: object) -> object: + return [value_11_from_dict(item) for item in _union_support.as_list(value)] + +def value_17_to_dict(value: object) -> object: + return [value_11_to_dict(item) for item in _union_support.as_list(value)] + +def value_18_from_dict(value: object) -> object: + return {key: value_11_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_18_to_dict(value: object) -> object: + return {key: value_11_to_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_19_from_dict(value: object) -> object: + return [value_20_from_dict(item) for item in _union_support.as_list(value)] + +def value_19_to_dict(value: object) -> object: + return [value_20_to_dict(item) for item in _union_support.as_list(value)] + +def value_20_from_dict(value: object) -> object: + return {key: value_17_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_20_to_dict(value: object) -> object: + return {key: value_17_to_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_21_from_dict(value: object) -> object: + return value_22_from_dict(value) + +def value_21_to_dict(value: object) -> object: + return value_22_to_dict(value) + +def value_22_from_dict(value: object) -> object: + return None if value is None else value_23_from_dict(value) + +def value_22_to_dict(value: object) -> object: + return None if value is None else value_23_to_dict(value) + +def value_23_from_dict(value: object) -> object: + return value + +def value_23_to_dict(value: object) -> object: + return value + +def value_24_from_dict(value: object) -> object: + return value + +def value_24_to_dict(value: object) -> object: + return value + +def value_25_from_dict(value: object) -> object: + return value_2_from_dict(value) + +def value_25_to_dict(value: object) -> object: + return value_2_to_dict(value) + +def value_26_from_dict(value: object) -> object: + return value_27_from_dict(value) + +def value_26_to_dict(value: object) -> object: + return value_27_to_dict(value) + +def value_27_from_dict(value: object) -> object: + return None if value is None else value_2_from_dict(value) + +def value_27_to_dict(value: object) -> object: + return None if value is None else value_2_to_dict(value) + +def value_28_from_dict(value: object) -> object: + return value + +def value_28_to_dict(value: object) -> object: + return value + +def value_29_from_dict(value: object) -> object: + return value + +def value_29_to_dict(value: object) -> object: + return value + +def value_30_from_dict(value: object) -> object: + return value + +def value_30_to_dict(value: object) -> object: + return value + +def value_31_from_dict(value: object) -> object: + return value + +def value_31_to_dict(value: object) -> object: + return value + +def value_32_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_32_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_33_from_dict(value: object) -> object: + return [value_34_from_dict(item) for item in _union_support.as_list(value)] + +def value_33_to_dict(value: object) -> object: + return [value_34_to_dict(item) for item in _union_support.as_list(value)] + +def value_34_from_dict(value: object) -> object: + return value + +def value_34_to_dict(value: object) -> object: + return value + +def value_35_from_dict(value: object) -> object: + from.import client_tool + return client_tool.ClientTool.from_dict(_union_support.as_dict(value)) + +def value_35_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_36_from_dict(value: object) -> object: + from.import search_tool + return search_tool.SearchTool.from_dict(_union_support.as_dict(value)) + +def value_36_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_37_from_dict(value: object) -> object: + return value + +def value_37_to_dict(value: object) -> object: + return value + +def value_38_from_dict(value: object) -> object: + return value_1_from_dict(value) + +def value_38_to_dict(value: object) -> object: + return value_1_to_dict(value) + +def value_39_from_dict(value: object) -> object: + return value_16_from_dict(value) + +def value_39_to_dict(value: object) -> object: + return value_16_to_dict(value) + +def value_40_from_dict(value: object) -> object: + data = _union_support.as_dict(value) + if data.get('kind') == 'choice': + return value_3_from_dict(data['payload']) + + if data.get('kind') == 'text': + return value_2_from_dict(data['payload']) + + raise ValueError("No matching tagged union variant") + +def value_40_to_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))): + return {'kind': 'choice', 'payload': value_3_to_dict(value)} + + if (isinstance(value, str)): + return {'kind': 'text', 'payload': value_2_to_dict(value)} + + raise ValueError("No matching tagged union variant") + +def value_41_from_dict(value: object) -> object: + data = _union_support.as_dict(value) + if data.get('kind') == 'choice': + return value_11_from_dict(data['payload']) + + if data.get('kind') == 'text': + return value_2_from_dict(data['payload']) + + raise ValueError("No matching tagged union variant") + +def value_41_to_dict(value: object) -> object: + if (((_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))))): + return {'kind': 'choice', 'payload': value_11_to_dict(value)} + + if (isinstance(value, str)): + return {'kind': 'text', 'payload': value_2_to_dict(value)} + + raise ValueError("No matching tagged union variant") + +def value_42_from_dict(value: object) -> object: + return value + +def value_42_to_dict(value: object) -> object: + return value + +def value_43_from_dict(value: object) -> object: + return [value_6_from_dict(item) for item in _union_support.as_list(value)] + +def value_43_to_dict(value: object) -> object: + return [value_6_to_dict(item) for item in _union_support.as_list(value)] + +def value_44_from_dict(value: object) -> object: + return value_45_from_dict(value) + +def value_44_to_dict(value: object) -> object: + return value_45_to_dict(value) + +def value_45_from_dict(value: object) -> object: + return value + +def value_45_to_dict(value: object) -> object: + return value + +def value_46_from_dict(value: object) -> object: + return {key: value_45_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_46_to_dict(value: object) -> object: + return {key: value_45_to_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_47_from_dict(value: object) -> object: + return [value_44_from_dict(item) for item in _union_support.as_list(value)] + +def value_47_to_dict(value: object) -> object: + return [value_44_to_dict(item) for item in _union_support.as_list(value)] + +def value_48_from_dict(value: object) -> object: + return value + +def value_48_to_dict(value: object) -> object: + return value + +def value_49_from_dict(value: object) -> object: + from.import tagged_member_envelope + return tagged_member_envelope.TaggedMemberEnvelope.from_dict(_union_support.as_dict(value)) + +def value_49_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/acyclic_envelope.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/acyclic_envelope.py.golden new file mode 100644 index 000000000..868bba7d5 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/acyclic_envelope.py.golden @@ -0,0 +1,25 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from . import _union +from .acyclic_scalar import AcyclicScalar + +@dataclass +class AcyclicEnvelope: + value: AcyclicScalar + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['value'] = _union.value_0_to_dict(self.value) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> AcyclicEnvelope: + return cls( + value = _union.value_0_from_dict(data['value']), + ) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/acyclic_scalar.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/acyclic_scalar.py.golden new file mode 100644 index 000000000..ab349a251 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/acyclic_scalar.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type AcyclicScalar = (object) + +def acyclic_scalar_from_dict(value: object) -> AcyclicScalar: + return _union.value_0_from_dict(value) + +def acyclic_scalar_to_dict(value: AcyclicScalar) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/adjacent_member.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/adjacent_member.py.golden new file mode 100644 index 000000000..b1c372785 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/adjacent_member.py.golden @@ -0,0 +1,33 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from .numeric_two import NumericTwo +from .tagged_record import TaggedRecord + +# Discriminator: kind / content: payload (adjacent). + +type AdjacentMember = ( + TaggedRecord + | NumericTwo +) + + +def adjacent_member_from_dict(data: dict[str, object]) -> AdjacentMember: + _tag = data["kind"] + _content = data["payload"] # type: ignore[assignment] + if _tag == "record": + return TaggedRecord.from_dict(_content) # type: ignore[arg-type] + elif _tag == "numeric": + return NumericTwo.from_dict(_content) # type: ignore[arg-type] + raise ValueError(f"Unknown discriminator value for AdjacentMember: {data}") + + +def adjacent_member_to_dict(obj: AdjacentMember) -> dict[str, object]: + if isinstance(obj, TaggedRecord): + return {"kind": "record", "payload": obj.to_dict()} + elif isinstance(obj, NumericTwo): + return {"kind": "numeric", "payload": obj.to_dict()} + raise ValueError(f"Unknown variant for AdjacentMember: {type(obj)}") diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/ambiguous.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/ambiguous.py.golden new file mode 100644 index 000000000..4d20794fa --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/ambiguous.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type Ambiguous = (object) + +def ambiguous_from_dict(value: object) -> Ambiguous: + return _union.value_3_from_dict(value) + +def ambiguous_to_dict(value: Ambiguous) -> object: + return _union.value_3_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/ambiguous_tagged_member.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/ambiguous_tagged_member.py.golden new file mode 100644 index 000000000..720326a3a --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/ambiguous_tagged_member.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type AmbiguousTaggedMember = (object) + +def ambiguous_tagged_member_from_dict(value: object) -> AmbiguousTaggedMember: + return _union.value_5_from_dict(value) + +def ambiguous_tagged_member_to_dict(value: AmbiguousTaggedMember) -> object: + return _union.value_5_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/branch.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/branch.py.golden new file mode 100644 index 000000000..fae055beb --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/branch.py.golden @@ -0,0 +1,25 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union, recursive + +@dataclass +class Branch: + children: list[recursive.Recursive] + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['children'] = _union.value_8_to_dict(self.children) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Branch: + return cls( + children = cast(list[recursive.Recursive], _union.value_8_from_dict(data['children'])), + ) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/choice.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/choice.py.golden new file mode 100644 index 000000000..64e4e97d6 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/choice.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type Choice = (object) + +def choice_from_dict(value: object) -> Choice: + return _union.value_11_from_dict(value) + +def choice_to_dict(value: Choice) -> object: + return _union.value_11_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/choice_alias.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/choice_alias.py.golden new file mode 100644 index 000000000..7063a2741 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/choice_alias.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from .choice import Choice + +type ChoiceAlias = (Choice) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/client_tool.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/client_tool.py.golden new file mode 100644 index 000000000..b65bef982 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/client_tool.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +@dataclass +class ClientTool: + input_schema: dict[str, Any] + name: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["input_schema"] = self.input_schema + result["name"] = self.name + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> ClientTool: + return cls( + input_schema=data["input_schema"], # type: ignore[assignment] + name=data["name"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/dependent_choice.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/dependent_choice.py.golden new file mode 100644 index 000000000..dff6c35d0 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/dependent_choice.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type DependentChoice = (object) + +def dependent_choice_from_dict(value: object) -> DependentChoice: + return _union.value_14_from_dict(value) + +def dependent_choice_to_dict(value: DependentChoice) -> object: + return _union.value_14_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/dependent_envelope.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/dependent_envelope.py.golden new file mode 100644 index 000000000..a3e05475d --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/dependent_envelope.py.golden @@ -0,0 +1,24 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from .acyclic_envelope import AcyclicEnvelope + +@dataclass +class DependentEnvelope: + envelope: AcyclicEnvelope + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["envelope"] = self.envelope.to_dict() + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> DependentEnvelope: + return cls( + envelope=AcyclicEnvelope.from_dict(data["envelope"]), # type: ignore[arg-type] + ) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/envelope.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/envelope.py.golden new file mode 100644 index 000000000..b3c1393dc --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/envelope.py.golden @@ -0,0 +1,41 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .choice import Choice +from .choice_alias import ChoiceAlias + +@dataclass +class Envelope: + choice: ChoiceAlias + choice_list: list[Choice] + choice_map: dict[str, Choice] + deep: list[dict[str, list[Choice]]] + optional_choice: Choice | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['choice'] = _union.value_13_to_dict(self.choice) + result['choice_list'] = _union.value_17_to_dict(self.choice_list) + result['choice_map'] = _union.value_18_to_dict(self.choice_map) + result['deep'] = _union.value_19_to_dict(self.deep) + if self.optional_choice is not None: + result['optional_choice'] = _union.value_11_to_dict(self.optional_choice) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Envelope: + return cls( + choice = _union.value_13_from_dict(data['choice']), + choice_list = cast(list[Choice], _union.value_17_from_dict(data['choice_list'])), + choice_map = cast(dict[str, Choice], _union.value_18_from_dict(data['choice_map'])), + deep = cast(list[dict[str, list[Choice]]], _union.value_19_from_dict(data['deep'])), + optional_choice = None if 'optional_choice' not in data else _union.value_11_from_dict(data.get('optional_choice')), + ) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/finite_envelope.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/finite_envelope.py.golden new file mode 100644 index 000000000..38e1f1e98 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/finite_envelope.py.golden @@ -0,0 +1,24 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from . import _union + +@dataclass +class FiniteEnvelope: + value: object | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['value'] = _union.value_21_to_dict(self.value) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> FiniteEnvelope: + return cls( + value = _union.value_21_from_dict(data['value']), + ) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/inner_null_choice.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/inner_null_choice.py.golden new file mode 100644 index 000000000..1a35da1c9 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/inner_null_choice.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type InnerNullChoice = (object) + +def inner_null_choice_from_dict(value: object) -> InnerNullChoice: + return _union.value_24_from_dict(value) + +def inner_null_choice_to_dict(value: InnerNullChoice) -> object: + return _union.value_24_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/nested_ambiguous.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/nested_ambiguous.py.golden new file mode 100644 index 000000000..a0cae8834 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/nested_ambiguous.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type NestedAmbiguous = (object) + +def nested_ambiguous_from_dict(value: object) -> NestedAmbiguous: + return _union.value_28_from_dict(value) + +def nested_ambiguous_to_dict(value: NestedAmbiguous) -> object: + return _union.value_28_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/nullable_choice.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/nullable_choice.py.golden new file mode 100644 index 000000000..dfa314947 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/nullable_choice.py.golden @@ -0,0 +1,18 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type NullableChoice = ( + object + | None +) + +def nullable_choice_from_dict(value: object) -> NullableChoice: + return _union.value_29_from_dict(value) + +def nullable_choice_to_dict(value: NullableChoice) -> object: + return _union.value_29_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/nullable_envelope.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/nullable_envelope.py.golden new file mode 100644 index 000000000..05c9c1d43 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/nullable_envelope.py.golden @@ -0,0 +1,31 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from . import _union +from .nullable_choice import NullableChoice +from .wrapped_null_choice import WrappedNullChoice + +@dataclass +class NullableEnvelope: + choice: NullableChoice + ambiguous: WrappedNullChoice | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['choice'] = _union.value_29_to_dict(self.choice) + if self.ambiguous is not None: + result['ambiguous'] = _union.value_30_to_dict(self.ambiguous) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NullableEnvelope: + return cls( + choice = _union.value_29_from_dict(data['choice']), + ambiguous = None if 'ambiguous' not in data else _union.value_30_from_dict(data.get('ambiguous')), + ) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/numeric_one.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/numeric_one.py.golden new file mode 100644 index 000000000..00917636c --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/numeric_one.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class NumericOne: + revision: int + first_value: str | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["revision"] = self.revision + if self.first_value is not None: + result["first_value"] = self.first_value + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NumericOne: + return cls( + revision=data["revision"], # type: ignore[assignment] + first_value=data.get("first_value"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/numeric_two.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/numeric_two.py.golden new file mode 100644 index 000000000..d042a0d8b --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/numeric_two.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class NumericTwo: + revision: int + second_value: str | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["revision"] = self.revision + if self.second_value is not None: + result["second_value"] = self.second_value + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NumericTwo: + return cls( + revision=data["revision"], # type: ignore[assignment] + second_value=data.get("second_value"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/open_choice.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/open_choice.py.golden new file mode 100644 index 000000000..10f268d49 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/open_choice.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type OpenChoice = (object) + +def open_choice_from_dict(value: object) -> OpenChoice: + return _union.value_31_from_dict(value) + +def open_choice_to_dict(value: OpenChoice) -> object: + return _union.value_31_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/recursive.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/recursive.py.golden new file mode 100644 index 000000000..fb48126b3 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/recursive.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type Recursive = (object) + +def recursive_from_dict(value: object) -> Recursive: + return _union.value_9_from_dict(value) + +def recursive_to_dict(value: Recursive) -> object: + return _union.value_9_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/request.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/request.py.golden new file mode 100644 index 000000000..46530c8d1 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/request.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .tool import Tool + +@dataclass +class Request: + tools: list[Tool] + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['tools'] = _union.value_33_to_dict(self.tools) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Request: + return cls( + tools = cast(list[Tool], _union.value_33_from_dict(data['tools'])), + ) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/scalar_choice.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/scalar_choice.py.golden new file mode 100644 index 000000000..f5d5e897b --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/scalar_choice.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type ScalarChoice = (object) + +def scalar_choice_from_dict(value: object) -> ScalarChoice: + return _union.value_37_from_dict(value) + +def scalar_choice_to_dict(value: ScalarChoice) -> object: + return _union.value_37_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/search_tool.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/search_tool.py.golden new file mode 100644 index 000000000..f6b31466a --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/search_tool.py.golden @@ -0,0 +1,30 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal + +@dataclass +class SearchTool: + name: Literal["web_search"] + type_: Literal["web_search_20250305"] + max_uses: int | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["name"] = self.name + result["type"] = self.type_ + if self.max_uses is not None: + result["max_uses"] = self.max_uses + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> SearchTool: + return cls( + name=data["name"], # type: ignore[assignment] + type_=data["type"], # type: ignore[assignment] + max_uses=data.get("max_uses"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/send_multipart_multipart_request_body.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/send_multipart_multipart_request_body.py.golden new file mode 100644 index 000000000..607318f62 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/send_multipart_multipart_request_body.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from ..runtime import UploadFile +from .choice import Choice + +@dataclass +class SendMultipartMultipartRequestBody: + choice: Choice + attachment: UploadFile | None = None diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/send_multipart_request.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/send_multipart_request.py.golden new file mode 100644 index 000000000..0e2dc62b8 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/send_multipart_request.py.golden @@ -0,0 +1,29 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from . import _union +from .choice import Choice + +@dataclass +class SendMultipartRequest: + choice: Choice + attachment: bytes | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['choice'] = _union.value_11_to_dict(self.choice) + if self.attachment is not None: + result["attachment"] = self.attachment + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> SendMultipartRequest: + return cls( + choice = _union.value_11_from_dict(data['choice']), + attachment=data.get("attachment"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_ambiguous.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_ambiguous.py.golden new file mode 100644 index 000000000..5a6745f21 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_ambiguous.py.golden @@ -0,0 +1,23 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .ambiguous import Ambiguous + +# Discriminator: kind / content: payload (adjacent). + +type TaggedAmbiguous = ( + Ambiguous + | str +) + +def tagged_ambiguous_from_dict(value: object) -> TaggedAmbiguous: + return _union.value_40_from_dict(value) + +def tagged_ambiguous_to_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], _union.value_40_to_dict(value)) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_choice.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_choice.py.golden new file mode 100644 index 000000000..cb8439aa0 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_choice.py.golden @@ -0,0 +1,23 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .choice import Choice + +# Discriminator: kind / content: payload (adjacent). + +type TaggedChoice = ( + Choice + | str +) + +def tagged_choice_from_dict(value: object) -> TaggedChoice: + return _union.value_41_from_dict(value) + +def tagged_choice_to_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], _union.value_41_to_dict(value)) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_container_choice.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_container_choice.py.golden new file mode 100644 index 000000000..98d20a4f0 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_container_choice.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type TaggedContainerChoice = (object) + +def tagged_container_choice_from_dict(value: object) -> TaggedContainerChoice: + return _union.value_42_from_dict(value) + +def tagged_container_choice_to_dict(value: TaggedContainerChoice) -> object: + return _union.value_42_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_member_alias.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_member_alias.py.golden new file mode 100644 index 000000000..65d4e105c --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_member_alias.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from .tagged_member_choice import TaggedMemberChoice + +type TaggedMemberAlias = (TaggedMemberChoice) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_member_choice.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_member_choice.py.golden new file mode 100644 index 000000000..c045ab07d --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_member_choice.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type TaggedMemberChoice = (object) + +def tagged_member_choice_from_dict(value: object) -> TaggedMemberChoice: + return _union.value_45_from_dict(value) + +def tagged_member_choice_to_dict(value: TaggedMemberChoice) -> object: + return _union.value_45_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_member_envelope.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_member_envelope.py.golden new file mode 100644 index 000000000..32d2ef2a4 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_member_envelope.py.golden @@ -0,0 +1,34 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .tagged_member_alias import TaggedMemberAlias +from .tagged_member_choice import TaggedMemberChoice + +@dataclass +class TaggedMemberEnvelope: + mapping: dict[str, TaggedMemberChoice] + value: TaggedMemberChoice + values: list[TaggedMemberAlias] + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['mapping'] = _union.value_46_to_dict(self.mapping) + result['value'] = _union.value_45_to_dict(self.value) + result['values'] = _union.value_47_to_dict(self.values) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> TaggedMemberEnvelope: + return cls( + mapping = cast(dict[str, + TaggedMemberChoice], _union.value_46_from_dict(data['mapping'])), + value = _union.value_45_from_dict(data['value']), + values = cast(list[TaggedMemberAlias], _union.value_47_from_dict(data['values'])), + ) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_object_choice.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_object_choice.py.golden new file mode 100644 index 000000000..5b623c3f3 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_object_choice.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type TaggedObjectChoice = (object) + +def tagged_object_choice_from_dict(value: object) -> TaggedObjectChoice: + return _union.value_48_from_dict(value) + +def tagged_object_choice_to_dict(value: TaggedObjectChoice) -> object: + return _union.value_48_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_record.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_record.py.golden new file mode 100644 index 000000000..41db830f4 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tagged_record.py.golden @@ -0,0 +1,22 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class TaggedRecord: + name: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["name"] = self.name + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> TaggedRecord: + return cls( + name=data["name"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tool.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tool.py.golden new file mode 100644 index 000000000..c0e8ef1ec --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/tool.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type Tool = (object) + +def tool_from_dict(value: object) -> Tool: + return _union.value_34_from_dict(value) + +def tool_to_dict(value: Tool) -> object: + return _union.value_34_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/wrapped_null_choice.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/wrapped_null_choice.py.golden new file mode 100644 index 000000000..7b36ec1c3 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/models/wrapped_null_choice.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type WrappedNullChoice = (object) + +def wrapped_null_choice_from_dict(value: object) -> WrappedNullChoice: + return _union.value_30_from_dict(value) + +def wrapped_null_choice_to_dict(value: WrappedNullChoice) -> object: + return _union.value_30_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/py.typed.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/py.typed.golden new file mode 100644 index 000000000..e69de29bb diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/__init__.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/__init__.py.golden new file mode 100644 index 000000000..c77ee060c --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/__init__.py.golden @@ -0,0 +1,11 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .auth import ApiKeyAuth as ApiKeyAuth +from .auth import Authenticator as Authenticator +from .auth import BearerAuth as BearerAuth +from .client import ApiResponse as ApiResponse +from .client import Client as Client +from .errors import ApiError as ApiError +from .upload_file import UploadFile as UploadFile diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/_union_support.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/_union_support.py.golden new file mode 100644 index 000000000..a4c9e6107 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/_union_support.py.golden @@ -0,0 +1,90 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/auth.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/auth.py.golden new file mode 100644 index 000000000..eaf25448a --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/auth.py.golden @@ -0,0 +1,50 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""Authentication helpers.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Callable + + +class Authenticator(ABC): + """Base class for request authenticators.""" + + @abstractmethod + def auth_headers(self) -> dict[str, str]: + """Return headers to attach to every request.""" + ... + + +class BearerAuth(Authenticator): + """Bearer token authentication. + + Accepts a static token string or a callable that returns the + current token (evaluated on every request). + """ + + def __init__(self, token: str | Callable[[], str]) -> None: + self._token = token + + def auth_headers(self) -> dict[str, str]: + token = self._token() if callable(self._token) else self._token + return {"Authorization": f"Bearer {token}"} + + +class ApiKeyAuth(Authenticator): + """API key authentication via a custom header. + + Accepts a static key string or a callable that returns the + current key (evaluated on every request). + """ + + def __init__(self, header_name: str, api_key: str | Callable[[], str]) -> None: + self._header_name = header_name + self._api_key = api_key + + def auth_headers(self) -> dict[str, str]: + key = self._api_key() if callable(self._api_key) else self._api_key + return {self._header_name: key} diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/client.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/client.py.golden new file mode 100644 index 000000000..e11bded82 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/client.py.golden @@ -0,0 +1,116 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""HTTP client wrapping requests.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any, Generic, TypeVar, cast + +import requests + +from .auth import Authenticator + +ResponseData = TypeVar("ResponseData") + + +class Response: + """Thin wrapper over requests.Response with strict types.""" + + def __init__(self, raw: requests.Response) -> None: + self._raw = raw + + @property + def status_code(self) -> int: + return self._raw.status_code # type: ignore[return-value] + + @property + def reason(self) -> str: + return self._raw.reason or "" + + @property + def content(self) -> bytes: + return self._raw.content # type: ignore[return-value] + + @property + def headers(self) -> Mapping[str, str]: + return cast(Mapping[str, str], self._raw.headers) + + @property + def text(self) -> str: + return self._raw.text + + def json(self) -> Any: # type: ignore[explicit-override] + return self._raw.json() # type: ignore[reportUnknownMemberType] + + +@dataclass(frozen=True) +class ApiResponse(Generic[ResponseData]): + """Decoded response data together with its HTTP metadata.""" + + data: ResponseData + status_code: int + headers: Mapping[str, str] + raw: Response + + +class Client: + """Synchronous HTTP client for the generated SDK.""" + + def __init__( + self, + base_url: str, + *, + http_client: requests.Session | None = None, + authenticator: Authenticator | None = None, + ) -> None: + self._base_url = base_url.rstrip("/") + self._http_client = http_client or requests.Session() + self._authenticator = authenticator + + def request( + self, + method: str, + path: str, + *, + params: dict[str, str] | None = None, + json: Any = None, + data: Any = None, + files: Any = None, + headers: dict[str, str] | None = None, + ) -> Response: + """Send an HTTP request and return the raw response.""" + url = f"{self._base_url}{path}" + req_headers: dict[str, str] = {"Accept": "application/json"} + if json is not None: + req_headers["Content-Type"] = "application/json" + if headers: + req_headers.update(headers) + if self._authenticator is not None: + req_headers.update(self._authenticator.auth_headers()) + raw = cast( + requests.Response, + self._http_client.request( # type: ignore[reportUnknownMemberType] + method, + url, + params=params, + json=json, + data=data, + files=files, + headers=req_headers, + ), + ) + return Response(raw) + + def close(self) -> None: + """Close the underlying HTTP client.""" + self._http_client.close() + + def __enter__(self) -> Client: + return self + + def __exit__(self, *args: object) -> None: + self.close() diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/errors.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/errors.py.golden new file mode 100644 index 000000000..3530e64be --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/errors.py.golden @@ -0,0 +1,34 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""API error type.""" + +from __future__ import annotations + +from collections.abc import Mapping + + +class ApiError(Exception): + """Raised when the server returns a non-success status code.""" + + def __init__( + self, + status_code: int, + status: str, + body: bytes, + *, + headers: Mapping[str, str] | None = None, + response: object | None = None, + ) -> None: + self.status_code: int = status_code + self.status: str = status + self.raw_body: bytes = body + self.body: bytes = body + self.headers: Mapping[str, str] | None = headers + self.response: object | None = response + super().__init__(f"{status_code} {status}") + + @property + def text(self) -> str: + return self.raw_body.decode("utf-8", errors="replace") diff --git a/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/upload_file.py.golden b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/upload_file.py.golden new file mode 100644 index 000000000..f218509e4 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-raw/python_union_conversion/runtime/upload_file.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class UploadFile: + data: bytes + filename: str | None = None + + @classmethod + def from_bytes(cls, data: bytes, filename: str | None = None) -> UploadFile: + return cls(data=data, filename=filename) + + def filename_or_default(self, default_name: str) -> str: + return self.filename or default_name diff --git a/tests/golden/python/python-requests/untagged-union-strict/README.md.golden b/tests/golden/python/python-requests/untagged-union-strict/README.md.golden new file mode 100644 index 000000000..a6f182f18 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/README.md.golden @@ -0,0 +1,7 @@ +# Python union conversion + +Generated Python SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `python_union_conversion`. diff --git a/tests/golden/python/python-requests/untagged-union-strict/pyproject.toml.golden b/tests/golden/python/python-requests/untagged-union-strict/pyproject.toml.golden new file mode 100644 index 000000000..03f2a11f9 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/pyproject.toml.golden @@ -0,0 +1,10 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "python_union_conversion" +version = "1.0.0" +description = "Generated Python SDK." +requires-python = ">=3.12" +dependencies = ["requests>=2.32"] diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/__init__.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/__init__.py.golden new file mode 100644 index 000000000..8f525cedf --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/__init__.py.golden @@ -0,0 +1,11 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .runtime import ApiKeyAuth as ApiKeyAuth +from .runtime import Authenticator as Authenticator +from .runtime import BearerAuth as BearerAuth +from .runtime import ApiResponse as ApiResponse +from .runtime import Client as Client +from .runtime import ApiError as ApiError +from .runtime import UploadFile as UploadFile diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/apis/__init__.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/apis/__init__.py.golden new file mode 100644 index 000000000..8126d3b1a --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/apis/__init__.py.golden @@ -0,0 +1,5 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .default_api import DefaultApi as DefaultApi diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/apis/default_api.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/apis/default_api.py.golden new file mode 100644 index 000000000..56f2c8d66 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/apis/default_api.py.golden @@ -0,0 +1,290 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +import json + +from collections.abc import Mapping +from typing import cast + +from ..models import _union +from ..models.choice import Choice +from ..models.envelope import Envelope +from ..models.nullable_choice import NullableChoice +from ..models.send_multipart_multipart_request_body import SendMultipartMultipartRequestBody +from ..models.tagged_choice import TaggedChoice +from ..models.tagged_member_choice import TaggedMemberChoice +from ..runtime.client import ApiResponse, Client +from ..runtime.errors import ApiError + +class SendChoiceBadRequest: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: Choice | None = None + self._body_error: Exception | None = None + @property + def body(self) -> Choice: + if not self._body_loaded: + try: + self._body_value = cast(Choice, _union.value_11_from_dict(json.loads(self.raw_body.decode("utf-8")))) + + except Exception as exc: + self._body_error = exc + + self._body_loaded = True + + if self._body_error is not None: + raise self._body_error + + return cast(Choice, self._body_value) + +class SendChoiceUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendChoiceErrorDetail = ( + SendChoiceBadRequest + | SendChoiceUnexpected +) + +class SendChoiceError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendChoiceErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendChoiceErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendChoicesUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendChoicesErrorDetail = ( + SendChoicesUnexpected +) + +class SendChoicesError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendChoicesErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendChoicesErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendEnvelopeUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendEnvelopeErrorDetail = ( + SendEnvelopeUnexpected +) + +class SendEnvelopeError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendEnvelopeErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendEnvelopeErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendMultipartUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendMultipartErrorDetail = ( + SendMultipartUnexpected +) + +class SendMultipartError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendMultipartErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendMultipartErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendNullableUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendNullableErrorDetail = ( + SendNullableUnexpected +) + +class SendNullableError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendNullableErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendNullableErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendTaggedUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendTaggedErrorDetail = ( + SendTaggedUnexpected +) + +class SendTaggedError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendTaggedErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendTaggedErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendTaggedMemberUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendTaggedMemberErrorDetail = ( + SendTaggedMemberUnexpected +) + +class SendTaggedMemberError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendTaggedMemberErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendTaggedMemberErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + + +class DefaultApi: + def __init__(self, client: Client) -> None: + self._client = client + + def send_choice_with_http_info(self, *, body: Choice) -> ApiResponse[Choice]: + path = "/choice" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_11_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + if response.status_code == 400: + detail = SendChoiceBadRequest(response.status_code, response.headers, response.content) + else: + detail = SendChoiceUnexpected(response.status_code, response.headers, response.content) + raise SendChoiceError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + _union_data = cast(Choice, _union.value_11_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_choice(self, *, body: Choice) -> Choice: + return self.send_choice_with_http_info(body=body).data + + def send_choices_with_http_info(self, *, body: list[Choice]) -> ApiResponse[dict[str, Choice]]: + path = "/choices" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_17_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendChoicesUnexpected(response.status_code, response.headers, response.content) + raise SendChoicesError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + _union_data = cast(dict[str, Choice], _union.value_18_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_choices(self, *, body: list[Choice]) -> dict[str, Choice]: + return self.send_choices_with_http_info(body=body).data + + def send_envelope_with_http_info(self, *, body: Envelope) -> ApiResponse[Envelope]: + path = "/envelope" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + response = self._client.request("POST", path, json=body.to_dict(), headers=headers) + if not (200 <= response.status_code < 300): + detail = SendEnvelopeUnexpected(response.status_code, response.headers, response.content) + raise SendEnvelopeError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + data = Envelope.from_dict(response.json()) + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_envelope(self, *, body: Envelope) -> Envelope: + return self.send_envelope_with_http_info(body=body).data + + def send_multipart_with_http_info(self, *, body: SendMultipartMultipartRequestBody) -> ApiResponse[None]: + path = "/multipart" + files: dict[str, object] = {} + if body.attachment is not None: + files["attachment"] = (body.attachment.filename_or_default("attachment"), body.attachment.data, "application/octet-stream") + + files['choice'] = (None, json.dumps(_union.value_11_to_dict(body.choice)), 'application/json') + response = self._client.request("POST", path, files=files if files else None) + if not (200 <= response.status_code < 300): + detail = SendMultipartUnexpected(response.status_code, response.headers, response.content) + raise SendMultipartError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + data = None + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_multipart(self, *, body: SendMultipartMultipartRequestBody) -> None: + return self.send_multipart_with_http_info(body=body).data + + def send_nullable_with_http_info(self, *, body: NullableChoice) -> ApiResponse[NullableChoice]: + path = "/nullable" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_29_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendNullableUnexpected(response.status_code, response.headers, response.content) + raise SendNullableError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + _union_data = cast(NullableChoice, _union.value_29_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_nullable(self, *, body: NullableChoice) -> NullableChoice: + return self.send_nullable_with_http_info(body=body).data + + def send_tagged_with_http_info(self, *, body: TaggedChoice) -> ApiResponse[TaggedChoice]: + path = "/tagged" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_41_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendTaggedUnexpected(response.status_code, response.headers, response.content) + raise SendTaggedError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + _union_data = cast(TaggedChoice, _union.value_41_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_tagged(self, *, body: TaggedChoice) -> TaggedChoice: + return self.send_tagged_with_http_info(body=body).data + + def send_tagged_member_with_http_info(self, *, body: TaggedMemberChoice) -> ApiResponse[TaggedMemberChoice]: + path = "/tagged-member" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_45_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendTaggedMemberUnexpected(response.status_code, response.headers, response.content) + raise SendTaggedMemberError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + _union_data = cast(TaggedMemberChoice, _union.value_45_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_tagged_member(self, *, body: TaggedMemberChoice) -> TaggedMemberChoice: + return self.send_tagged_member_with_http_info(body=body).data diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/__init__.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/__init__.py.golden new file mode 100644 index 000000000..30669ee6e --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/__init__.py.golden @@ -0,0 +1,40 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .acyclic_envelope import AcyclicEnvelope as AcyclicEnvelope +from .acyclic_scalar import AcyclicScalar as AcyclicScalar +from .adjacent_member import AdjacentMember as AdjacentMember +from .ambiguous import Ambiguous as Ambiguous +from .ambiguous_tagged_member import AmbiguousTaggedMember as AmbiguousTaggedMember +from .branch import Branch as Branch +from .choice import Choice as Choice +from .choice_alias import ChoiceAlias as ChoiceAlias +from .client_tool import ClientTool as ClientTool +from .dependent_choice import DependentChoice as DependentChoice +from .dependent_envelope import DependentEnvelope as DependentEnvelope +from .envelope import Envelope as Envelope +from .finite_envelope import FiniteEnvelope as FiniteEnvelope +from .inner_null_choice import InnerNullChoice as InnerNullChoice +from .nested_ambiguous import NestedAmbiguous as NestedAmbiguous +from .nullable_choice import NullableChoice as NullableChoice +from .nullable_envelope import NullableEnvelope as NullableEnvelope +from .numeric_one import NumericOne as NumericOne +from .numeric_two import NumericTwo as NumericTwo +from .open_choice import OpenChoice as OpenChoice +from .recursive import Recursive as Recursive +from .request import Request as Request +from .scalar_choice import ScalarChoice as ScalarChoice +from .search_tool import SearchTool as SearchTool +from .send_multipart_multipart_request_body import SendMultipartMultipartRequestBody as SendMultipartMultipartRequestBody +from .send_multipart_request import SendMultipartRequest as SendMultipartRequest +from .tagged_ambiguous import TaggedAmbiguous as TaggedAmbiguous +from .tagged_choice import TaggedChoice as TaggedChoice +from .tagged_container_choice import TaggedContainerChoice as TaggedContainerChoice +from .tagged_member_alias import TaggedMemberAlias as TaggedMemberAlias +from .tagged_member_choice import TaggedMemberChoice as TaggedMemberChoice +from .tagged_member_envelope import TaggedMemberEnvelope as TaggedMemberEnvelope +from .tagged_object_choice import TaggedObjectChoice as TaggedObjectChoice +from .tagged_record import TaggedRecord as TaggedRecord +from .tool import Tool as Tool +from .wrapped_null_choice import WrappedNullChoice as WrappedNullChoice diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/_union.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/_union.py.golden new file mode 100644 index 000000000..da2f4f470 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/_union.py.golden @@ -0,0 +1,941 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + matches = [_union_support.number(value, True), isinstance(value, str)] + if sum(matches) > 1: + raise ValueError('Ambiguous AcyclicScalar untagged union value') + + if matches[0]: + return value_1_from_dict(value) + + if matches[1]: + return value_2_from_dict(value) + + raise ValueError('No matching AcyclicScalar untagged union member') + +def value_0_to_dict(value: object) -> object: + matches = [_union_support.number(value, True), isinstance(value, str)] + if sum(matches) > 1: + raise ValueError('Ambiguous AcyclicScalar untagged union value') + + if matches[0]: + return value_1_to_dict(value) + + if matches[1]: + return value_2_to_dict(value) + + raise ValueError('No matching AcyclicScalar untagged union member') + +def value_1_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_1_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_from_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))), (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))] + if sum(matches) > 1: + raise ValueError('Ambiguous Ambiguous untagged union value') + + if matches[0]: + return value_4_from_dict(value) + + if matches[1]: + return value_4_from_dict(value) + + raise ValueError('No matching Ambiguous untagged union member') + +def value_3_to_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))), (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))] + if sum(matches) > 1: + raise ValueError('Ambiguous Ambiguous untagged union value') + + if matches[0]: + return value_4_to_dict(value) + + if matches[1]: + return value_4_to_dict(value) + + raise ValueError('No matching Ambiguous untagged union member') + +def value_4_from_dict(value: object) -> object: + from.import numeric_two + return numeric_two.NumericTwo.from_dict(_union_support.as_dict(value)) + +def value_4_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_5_from_dict(value: object) -> object: + matches = [((_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'record')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'numeric')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value'), str)))))), ((_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'record')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'numeric')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value'), str))))))] + if sum(matches) > 1: + raise ValueError('Ambiguous AmbiguousTaggedMember untagged union value') + + if matches[0]: + return value_6_from_dict(value) + + if matches[1]: + return value_6_from_dict(value) + + raise ValueError('No matching AmbiguousTaggedMember untagged union member') + +def value_5_to_dict(value: object) -> object: + matches = [((_union_support.is_object(value) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))), ((_union_support.is_object(value) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))))] + if sum(matches) > 1: + raise ValueError('Ambiguous AmbiguousTaggedMember untagged union value') + + if matches[0]: + return value_6_to_dict(value) + + if matches[1]: + return value_6_to_dict(value) + + raise ValueError('No matching AmbiguousTaggedMember untagged union member') + +def value_6_from_dict(value: object) -> object: + from.import adjacent_member + return adjacent_member.adjacent_member_from_dict(_union_support.as_dict(value)) + +def value_6_to_dict(value: object) -> object: + from.import adjacent_member + return adjacent_member.adjacent_member_to_dict(cast(adjacent_member.AdjacentMember, value)) + +def value_7_from_dict(value: object) -> object: + from.import tagged_record + return tagged_record.TaggedRecord.from_dict(_union_support.as_dict(value)) + +def value_7_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_8_from_dict(value: object) -> object: + return [value_9_from_dict(item) for item in _union_support.as_list(value)] + +def value_8_to_dict(value: object) -> object: + return [value_9_to_dict(item) for item in _union_support.as_list(value)] + +def value_9_from_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_from_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))), isinstance(value, str)] + if sum(matches) > 1: + raise ValueError('Ambiguous Recursive untagged union value') + + if matches[0]: + return value_10_from_dict(value) + + if matches[1]: + return value_2_from_dict(value) + + raise ValueError('No matching Recursive untagged union member') + +def value_9_to_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_to_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))), isinstance(value, str)] + if sum(matches) > 1: + raise ValueError('Ambiguous Recursive untagged union value') + + if matches[0]: + return value_10_to_dict(value) + + if matches[1]: + return value_2_to_dict(value) + + raise ValueError('No matching Recursive untagged union member') + +def value_10_from_dict(value: object) -> object: + from.import branch + return branch.Branch.from_dict(_union_support.as_dict(value)) + +def value_10_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_11_from_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))), (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))] + if sum(matches) > 1: + raise ValueError('Ambiguous Choice untagged union value') + + if matches[0]: + return value_12_from_dict(value) + + if matches[1]: + return value_4_from_dict(value) + + raise ValueError('No matching Choice untagged union member') + +def value_11_to_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))), (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))] + if sum(matches) > 1: + raise ValueError('Ambiguous Choice untagged union value') + + if matches[0]: + return value_12_to_dict(value) + + if matches[1]: + return value_4_to_dict(value) + + raise ValueError('No matching Choice untagged union member') + +def value_12_from_dict(value: object) -> object: + from.import numeric_one + return numeric_one.NumericOne.from_dict(_union_support.as_dict(value)) + +def value_12_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_13_from_dict(value: object) -> object: + return value_11_from_dict(value) + +def value_13_to_dict(value: object) -> object: + return value_11_to_dict(value) + +def value_14_from_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'envelope', 'envelope', True) and (_union_support.is_object(_union_support.get(value, 'envelope', 'envelope')) + and (_union_support.has(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value'), True) + or isinstance(_union_support.get(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value'), str)))))), isinstance(value, bool)] + if sum(matches) > 1: + raise ValueError('Ambiguous DependentChoice untagged union value') + + if matches[0]: + return value_15_from_dict(value) + + if matches[1]: + return value_16_from_dict(value) + + raise ValueError('No matching DependentChoice untagged union member') + +def value_14_to_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'envelope', 'envelope', True) and (_union_support.is_object(_union_support.get(value, 'envelope', 'envelope')) + and (_union_support.has(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value'), True) + or isinstance(_union_support.get(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value'), str)))))), isinstance(value, bool)] + if sum(matches) > 1: + raise ValueError('Ambiguous DependentChoice untagged union value') + + if matches[0]: + return value_15_to_dict(value) + + if matches[1]: + return value_16_to_dict(value) + + raise ValueError('No matching DependentChoice untagged union member') + +def value_15_from_dict(value: object) -> object: + from.import dependent_envelope + return dependent_envelope.DependentEnvelope.from_dict(_union_support.as_dict(value)) + +def value_15_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_16_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_16_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_17_from_dict(value: object) -> object: + return [value_11_from_dict(item) for item in _union_support.as_list(value)] + +def value_17_to_dict(value: object) -> object: + return [value_11_to_dict(item) for item in _union_support.as_list(value)] + +def value_18_from_dict(value: object) -> object: + return {key: value_11_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_18_to_dict(value: object) -> object: + return {key: value_11_to_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_19_from_dict(value: object) -> object: + return [value_20_from_dict(item) for item in _union_support.as_list(value)] + +def value_19_to_dict(value: object) -> object: + return [value_20_to_dict(item) for item in _union_support.as_list(value)] + +def value_20_from_dict(value: object) -> object: + return {key: value_17_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_20_to_dict(value: object) -> object: + return {key: value_17_to_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_21_from_dict(value: object) -> object: + return value_22_from_dict(value) + +def value_21_to_dict(value: object) -> object: + return value_22_to_dict(value) + +def value_22_from_dict(value: object) -> object: + return None if value is None else value_23_from_dict(value) + +def value_22_to_dict(value: object) -> object: + return None if value is None else value_23_to_dict(value) + +def value_23_from_dict(value: object) -> object: + matches = [_union_support.number(value, True), isinstance(value, bool)] + if sum(matches) > 1: + raise ValueError('Ambiguous inline untagged union value') + + if matches[0]: + return value_1_from_dict(value) + + if matches[1]: + return value_16_from_dict(value) + + raise ValueError('No matching inline untagged union member') + +def value_23_to_dict(value: object) -> object: + matches = [_union_support.number(value, True), isinstance(value, bool)] + if sum(matches) > 1: + raise ValueError('Ambiguous inline untagged union value') + + if matches[0]: + return value_1_to_dict(value) + + if matches[1]: + return value_16_to_dict(value) + + raise ValueError('No matching inline untagged union member') + +def value_24_from_dict(value: object) -> object: + matches = [_union_support.equal(value, None), ((value is None or isinstance(value, str)) + and (_union_support.equal(value, 'x') + or _union_support.equal(value, None)) + and (value is None + or isinstance(value, str) + or _union_support.equal(value, 'x') + or _union_support.equal(value, None)))] + if sum(matches) > 1: + raise ValueError('Ambiguous InnerNullChoice untagged union value') + + if matches[0]: + return value_25_from_dict(value) + + if matches[1]: + return value_26_from_dict(value) + + raise ValueError('No matching InnerNullChoice untagged union member') + +def value_24_to_dict(value: object) -> object: + matches = [_union_support.equal(value, None), ((value is None or isinstance(value, str)) + and (_union_support.equal(value, 'x') + or _union_support.equal(value, None)) + and (value is None + or isinstance(value, str) + or _union_support.equal(value, 'x') + or _union_support.equal(value, None)))] + if sum(matches) > 1: + raise ValueError('Ambiguous InnerNullChoice untagged union value') + + if matches[0]: + return value_25_to_dict(value) + + if matches[1]: + return value_26_to_dict(value) + + raise ValueError('No matching InnerNullChoice untagged union member') + +def value_25_from_dict(value: object) -> object: + return value_2_from_dict(value) + +def value_25_to_dict(value: object) -> object: + return value_2_to_dict(value) + +def value_26_from_dict(value: object) -> object: + return value_27_from_dict(value) + +def value_26_to_dict(value: object) -> object: + return value_27_to_dict(value) + +def value_27_from_dict(value: object) -> object: + return None if value is None else value_2_from_dict(value) + +def value_27_to_dict(value: object) -> object: + return None if value is None else value_2_to_dict(value) + +def value_28_from_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))), isinstance(value, str)] + if sum(matches) > 1: + raise ValueError('Ambiguous NestedAmbiguous untagged union value') + + if matches[0]: + return value_3_from_dict(value) + + if matches[1]: + return value_2_from_dict(value) + + raise ValueError('No matching NestedAmbiguous untagged union member') + +def value_28_to_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))), isinstance(value, str)] + if sum(matches) > 1: + raise ValueError('Ambiguous NestedAmbiguous untagged union value') + + if matches[0]: + return value_3_to_dict(value) + + if matches[1]: + return value_2_to_dict(value) + + raise ValueError('No matching NestedAmbiguous untagged union member') + +def value_29_from_dict(value: object) -> object: + matches = [((_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))))] + if sum(matches) > 1: + raise ValueError('Ambiguous NullableChoice untagged union value') + + if matches[0]: + return value_11_from_dict(value) + + if value is None: + return None + + raise ValueError('No matching NullableChoice untagged union member') + +def value_29_to_dict(value: object) -> object: + matches = [((_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))))] + if sum(matches) > 1: + raise ValueError('Ambiguous NullableChoice untagged union value') + + if matches[0]: + return value_11_to_dict(value) + + if value is None: + return None + + raise ValueError('No matching NullableChoice untagged union member') + +def value_30_from_dict(value: object) -> object: + matches = [(_union_support.equal(value, None) + or ((value is None or isinstance(value, str)) + and (_union_support.equal(value, 'x') + or _union_support.equal(value, None)) + and (value is None + or isinstance(value, str) + or _union_support.equal(value, 'x') + or _union_support.equal(value, None)))), _union_support.number(value, True)] + if sum(matches) > 1: + raise ValueError('Ambiguous WrappedNullChoice untagged union value') + + if matches[0]: + return value_24_from_dict(value) + + if matches[1]: + return value_1_from_dict(value) + + raise ValueError('No matching WrappedNullChoice untagged union member') + +def value_30_to_dict(value: object) -> object: + matches = [(_union_support.equal(value, None) + or ((value is None or isinstance(value, str)) + and (_union_support.equal(value, 'x') + or _union_support.equal(value, None)) + and (value is None + or isinstance(value, str) + or _union_support.equal(value, 'x') + or _union_support.equal(value, None)))), _union_support.number(value, True)] + if sum(matches) > 1: + raise ValueError('Ambiguous WrappedNullChoice untagged union value') + + if matches[0]: + return value_24_to_dict(value) + + if matches[1]: + return value_1_to_dict(value) + + raise ValueError('No matching WrappedNullChoice untagged union member') + +def value_31_from_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))] + if sum(matches) > 1: + raise ValueError('Ambiguous OpenChoice untagged union value') + + if matches[0]: + return value_4_from_dict(value) + + if True: + return value_32_from_dict(value) + + raise ValueError('No matching OpenChoice untagged union member') + +def value_31_to_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))] + if sum(matches) > 1: + raise ValueError('Ambiguous OpenChoice untagged union value') + + if matches[0]: + return value_4_to_dict(value) + + if True: + return value_32_to_dict(value) + + raise ValueError('No matching OpenChoice untagged union member') + +def value_32_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_32_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_33_from_dict(value: object) -> object: + return [value_34_from_dict(item) for item in _union_support.as_list(value)] + +def value_33_to_dict(value: object) -> object: + return [value_34_to_dict(item) for item in _union_support.as_list(value)] + +def value_34_from_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'input_schema', 'input_schema', True) and (_union_support.is_dict(_union_support.get(value, 'input_schema', 'input_schema')) and all(True for _item in _union_support.as_dict(_union_support.get(value, 'input_schema', 'input_schema')).values()))) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))), (_union_support.is_object(value) + and (not _union_support.has(value, 'max_uses', 'max_uses', False) or _union_support.number(_union_support.get(value, 'max_uses', 'max_uses'), True)) + and (_union_support.has(value, 'name', 'name', True) and _union_support.equal(_union_support.get(value, 'name', 'name'), 'web_search')) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'web_search_20250305')))] + if sum(matches) > 1: + raise ValueError('Ambiguous Tool untagged union value') + + if matches[0]: + return value_35_from_dict(value) + + if matches[1]: + return value_36_from_dict(value) + + raise ValueError('No matching Tool untagged union member') + +def value_34_to_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'input_schema', 'input_schema', True) and (_union_support.is_dict(_union_support.get(value, 'input_schema', 'input_schema')) and all(True for _item in _union_support.as_dict(_union_support.get(value, 'input_schema', 'input_schema')).values()))) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))), (_union_support.is_object(value) + and (not _union_support.has(value, 'max_uses', 'max_uses', False) or _union_support.number(_union_support.get(value, 'max_uses', 'max_uses'), True)) + and (_union_support.has(value, 'name', 'name', True) and _union_support.equal(_union_support.get(value, 'name', 'name'), 'web_search')) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'web_search_20250305')))] + if sum(matches) > 1: + raise ValueError('Ambiguous Tool untagged union value') + + if matches[0]: + return value_35_to_dict(value) + + if matches[1]: + return value_36_to_dict(value) + + raise ValueError('No matching Tool untagged union member') + +def value_35_from_dict(value: object) -> object: + from.import client_tool + return client_tool.ClientTool.from_dict(_union_support.as_dict(value)) + +def value_35_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_36_from_dict(value: object) -> object: + from.import search_tool + return search_tool.SearchTool.from_dict(_union_support.as_dict(value)) + +def value_36_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_37_from_dict(value: object) -> object: + matches = [(_union_support.number(value, True) + and (_union_support.equal(value, 1) + or _union_support.equal(value, 2)) + and (_union_support.number(value, True) + or _union_support.equal(value, 1) + or _union_support.equal(value, 2))), (isinstance(value, bool) + and _union_support.equal(value, True) + and (isinstance(value, bool) + or _union_support.equal(value, True)))] + if sum(matches) > 1: + raise ValueError('Ambiguous ScalarChoice untagged union value') + + if matches[0]: + return value_38_from_dict(value) + + if matches[1]: + return value_39_from_dict(value) + + raise ValueError('No matching ScalarChoice untagged union member') + +def value_37_to_dict(value: object) -> object: + matches = [(_union_support.number(value, True) + and (_union_support.equal(value, 1) + or _union_support.equal(value, 2)) + and (_union_support.number(value, True) + or _union_support.equal(value, 1) + or _union_support.equal(value, 2))), (isinstance(value, bool) + and _union_support.equal(value, True) + and (isinstance(value, bool) + or _union_support.equal(value, True)))] + if sum(matches) > 1: + raise ValueError('Ambiguous ScalarChoice untagged union value') + + if matches[0]: + return value_38_to_dict(value) + + if matches[1]: + return value_39_to_dict(value) + + raise ValueError('No matching ScalarChoice untagged union member') + +def value_38_from_dict(value: object) -> object: + return value_1_from_dict(value) + +def value_38_to_dict(value: object) -> object: + return value_1_to_dict(value) + +def value_39_from_dict(value: object) -> object: + return value_16_from_dict(value) + +def value_39_to_dict(value: object) -> object: + return value_16_to_dict(value) + +def value_40_from_dict(value: object) -> object: + data = _union_support.as_dict(value) + if data.get('kind') == 'choice': + return value_3_from_dict(data['payload']) + + if data.get('kind') == 'text': + return value_2_from_dict(data['payload']) + + raise ValueError("No matching tagged union variant") + +def value_40_to_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))): + return {'kind': 'choice', 'payload': value_3_to_dict(value)} + + if (isinstance(value, str)): + return {'kind': 'text', 'payload': value_2_to_dict(value)} + + raise ValueError("No matching tagged union variant") + +def value_41_from_dict(value: object) -> object: + data = _union_support.as_dict(value) + if data.get('kind') == 'choice': + return value_11_from_dict(data['payload']) + + if data.get('kind') == 'text': + return value_2_from_dict(data['payload']) + + raise ValueError("No matching tagged union variant") + +def value_41_to_dict(value: object) -> object: + if (((_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))))): + return {'kind': 'choice', 'payload': value_11_to_dict(value)} + + if (isinstance(value, str)): + return {'kind': 'text', 'payload': value_2_to_dict(value)} + + raise ValueError("No matching tagged union variant") + +def value_42_from_dict(value: object) -> object: + matches = [(_union_support.is_list(value) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'record')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'numeric')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value'), str)))))) for _item in _union_support.as_list(value))), isinstance(value, str)] + if sum(matches) > 1: + raise ValueError('Ambiguous TaggedContainerChoice untagged union value') + + if matches[0]: + return value_43_from_dict(value) + + if matches[1]: + return value_2_from_dict(value) + + raise ValueError('No matching TaggedContainerChoice untagged union member') + +def value_42_to_dict(value: object) -> object: + matches = [(_union_support.is_list(value) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'name', 'name', True) and isinstance(_union_support.get(_item, 'name', 'name'), str))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'revision', 'revision', True) and (_union_support.number(_union_support.get(_item, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_item, 'revision', 'revision'), 2))) + and (not _union_support.has(_item, 'second_value', 'second_value', False) or isinstance(_union_support.get(_item, 'second_value', 'second_value'), str)))) for _item in _union_support.as_list(value))), isinstance(value, str)] + if sum(matches) > 1: + raise ValueError('Ambiguous TaggedContainerChoice untagged union value') + + if matches[0]: + return value_43_to_dict(value) + + if matches[1]: + return value_2_to_dict(value) + + raise ValueError('No matching TaggedContainerChoice untagged union member') + +def value_43_from_dict(value: object) -> object: + return [value_6_from_dict(item) for item in _union_support.as_list(value)] + +def value_43_to_dict(value: object) -> object: + return [value_6_to_dict(item) for item in _union_support.as_list(value)] + +def value_44_from_dict(value: object) -> object: + return value_45_from_dict(value) + +def value_44_to_dict(value: object) -> object: + return value_45_to_dict(value) + +def value_45_from_dict(value: object) -> object: + matches = [((_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'record')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'numeric')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value'), str)))))), _union_support.number(value, True)] + if sum(matches) > 1: + raise ValueError('Ambiguous TaggedMemberChoice untagged union value') + + if matches[0]: + return value_6_from_dict(value) + + if matches[1]: + return value_1_from_dict(value) + + raise ValueError('No matching TaggedMemberChoice untagged union member') + +def value_45_to_dict(value: object) -> object: + matches = [((_union_support.is_object(value) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))), _union_support.number(value, True)] + if sum(matches) > 1: + raise ValueError('Ambiguous TaggedMemberChoice untagged union value') + + if matches[0]: + return value_6_to_dict(value) + + if matches[1]: + return value_1_to_dict(value) + + raise ValueError('No matching TaggedMemberChoice untagged union member') + +def value_46_from_dict(value: object) -> object: + return {key: value_45_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_46_to_dict(value: object) -> object: + return {key: value_45_to_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_47_from_dict(value: object) -> object: + return [value_44_from_dict(item) for item in _union_support.as_list(value)] + +def value_47_to_dict(value: object) -> object: + return [value_44_to_dict(item) for item in _union_support.as_list(value)] + +def value_48_from_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'mapping', 'mapping', True) and (_union_support.is_dict(_union_support.get(value, 'mapping', 'mapping')) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'record')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'numeric')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value'), str))))) + or _union_support.number(_item, True)) for _item in _union_support.as_dict(_union_support.get(value, 'mapping', 'mapping')).values()))) + and (_union_support.has(value, 'value', 'value', True) and ((_union_support.is_object(_union_support.get(value, 'value', 'value')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'kind', 'kind', True) and _union_support.equal(_union_support.get(_union_support.get(value, 'value', 'value'), 'kind', 'kind'), 'record')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload')) + and (_union_support.has(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(_union_support.get(value, 'value', 'value')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'kind', 'kind', True) and _union_support.equal(_union_support.get(_union_support.get(value, 'value', 'value'), 'kind', 'kind'), 'numeric')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload')) + and (_union_support.has(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'second_value', 'second_value'), str))))) + or _union_support.number(_union_support.get(value, 'value', 'value'), True))) + and (_union_support.has(value, 'values', 'values', True) and (_union_support.is_list(_union_support.get(value, 'values', 'values')) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'record')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'numeric')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value'), str))))) + or _union_support.number(_item, True)) for _item in _union_support.as_list(_union_support.get(value, 'values', 'values')))))), _union_support.number(value, True)] + if sum(matches) > 1: + raise ValueError('Ambiguous TaggedObjectChoice untagged union value') + + if matches[0]: + return value_49_from_dict(value) + + if matches[1]: + return value_1_from_dict(value) + + raise ValueError('No matching TaggedObjectChoice untagged union member') + +def value_48_to_dict(value: object) -> object: + matches = [(_union_support.is_object(value) + and (_union_support.has(value, 'mapping', 'mapping', True) and (_union_support.is_dict(_union_support.get(value, 'mapping', 'mapping')) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'name', 'name', True) and isinstance(_union_support.get(_item, 'name', 'name'), str))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'revision', 'revision', True) and (_union_support.number(_union_support.get(_item, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_item, 'revision', 'revision'), 2))) + and (not _union_support.has(_item, 'second_value', 'second_value', False) or isinstance(_union_support.get(_item, 'second_value', 'second_value'), str))) + or _union_support.number(_item, True)) for _item in _union_support.as_dict(_union_support.get(value, 'mapping', 'mapping')).values()))) + and (_union_support.has(value, 'value', 'value', True) and ((_union_support.is_object(_union_support.get(value, 'value', 'value')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'value', 'value'), 'name', 'name'), str))) + or (_union_support.is_object(_union_support.get(value, 'value', 'value')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'value', 'value'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(value, 'value', 'value'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(value, 'value', 'value'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(value, 'value', 'value'), 'second_value', 'second_value'), str))) + or _union_support.number(_union_support.get(value, 'value', 'value'), True))) + and (_union_support.has(value, 'values', 'values', True) and (_union_support.is_list(_union_support.get(value, 'values', 'values')) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'name', 'name', True) and isinstance(_union_support.get(_item, 'name', 'name'), str))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'revision', 'revision', True) and (_union_support.number(_union_support.get(_item, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_item, 'revision', 'revision'), 2))) + and (not _union_support.has(_item, 'second_value', 'second_value', False) or isinstance(_union_support.get(_item, 'second_value', 'second_value'), str))) + or _union_support.number(_item, True)) for _item in _union_support.as_list(_union_support.get(value, 'values', 'values')))))), _union_support.number(value, True)] + if sum(matches) > 1: + raise ValueError('Ambiguous TaggedObjectChoice untagged union value') + + if matches[0]: + return value_49_to_dict(value) + + if matches[1]: + return value_1_to_dict(value) + + raise ValueError('No matching TaggedObjectChoice untagged union member') + +def value_49_from_dict(value: object) -> object: + from.import tagged_member_envelope + return tagged_member_envelope.TaggedMemberEnvelope.from_dict(_union_support.as_dict(value)) + +def value_49_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def match_recursive_any_from_dict(value: object) -> bool: + return ((_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_from_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))) + or isinstance(value, str)) + +def match_recursive_concrete_from_dict(value: object) -> bool: + return ((_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_from_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))) + or isinstance(value, str)) + +def match_recursive_fallback_from_dict(value: object) -> bool: + return False + +def match_recursive_any_to_dict(value: object) -> bool: + return ((_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_to_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))) + or isinstance(value, str)) + +def match_recursive_concrete_to_dict(value: object) -> bool: + return ((_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_to_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))) + or isinstance(value, str)) + +def match_recursive_fallback_to_dict(value: object) -> bool: + return False diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/acyclic_envelope.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/acyclic_envelope.py.golden new file mode 100644 index 000000000..57ded1581 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/acyclic_envelope.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .acyclic_scalar import AcyclicScalar + +@dataclass +class AcyclicEnvelope: + value: AcyclicScalar + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['value'] = _union.value_0_to_dict(self.value) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> AcyclicEnvelope: + return cls( + value = cast(AcyclicScalar, _union.value_0_from_dict(data['value'])), + ) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/acyclic_scalar.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/acyclic_scalar.py.golden new file mode 100644 index 000000000..e7448aa38 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/acyclic_scalar.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union + +type AcyclicScalar = ( + int + | str +) + +def acyclic_scalar_from_dict(value: object) -> AcyclicScalar: + return cast(AcyclicScalar, _union.value_0_from_dict(value)) + +def acyclic_scalar_to_dict(value: AcyclicScalar) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/adjacent_member.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/adjacent_member.py.golden new file mode 100644 index 000000000..b1c372785 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/adjacent_member.py.golden @@ -0,0 +1,33 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from .numeric_two import NumericTwo +from .tagged_record import TaggedRecord + +# Discriminator: kind / content: payload (adjacent). + +type AdjacentMember = ( + TaggedRecord + | NumericTwo +) + + +def adjacent_member_from_dict(data: dict[str, object]) -> AdjacentMember: + _tag = data["kind"] + _content = data["payload"] # type: ignore[assignment] + if _tag == "record": + return TaggedRecord.from_dict(_content) # type: ignore[arg-type] + elif _tag == "numeric": + return NumericTwo.from_dict(_content) # type: ignore[arg-type] + raise ValueError(f"Unknown discriminator value for AdjacentMember: {data}") + + +def adjacent_member_to_dict(obj: AdjacentMember) -> dict[str, object]: + if isinstance(obj, TaggedRecord): + return {"kind": "record", "payload": obj.to_dict()} + elif isinstance(obj, NumericTwo): + return {"kind": "numeric", "payload": obj.to_dict()} + raise ValueError(f"Unknown variant for AdjacentMember: {type(obj)}") diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/ambiguous.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/ambiguous.py.golden new file mode 100644 index 000000000..540aa34d8 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/ambiguous.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .numeric_two import NumericTwo + +type Ambiguous = ( + NumericTwo + | NumericTwo +) + +def ambiguous_from_dict(value: object) -> Ambiguous: + return cast(Ambiguous, _union.value_3_from_dict(value)) + +def ambiguous_to_dict(value: Ambiguous) -> object: + return _union.value_3_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/ambiguous_tagged_member.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/ambiguous_tagged_member.py.golden new file mode 100644 index 000000000..bcc1f745a --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/ambiguous_tagged_member.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .adjacent_member import AdjacentMember + +type AmbiguousTaggedMember = ( + AdjacentMember + | AdjacentMember +) + +def ambiguous_tagged_member_from_dict(value: object) -> AmbiguousTaggedMember: + return cast(AmbiguousTaggedMember, _union.value_5_from_dict(value)) + +def ambiguous_tagged_member_to_dict(value: AmbiguousTaggedMember) -> object: + return _union.value_5_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/branch.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/branch.py.golden new file mode 100644 index 000000000..fae055beb --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/branch.py.golden @@ -0,0 +1,25 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union, recursive + +@dataclass +class Branch: + children: list[recursive.Recursive] + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['children'] = _union.value_8_to_dict(self.children) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Branch: + return cls( + children = cast(list[recursive.Recursive], _union.value_8_from_dict(data['children'])), + ) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/choice.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/choice.py.golden new file mode 100644 index 000000000..dbbb3f910 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/choice.py.golden @@ -0,0 +1,22 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .numeric_one import NumericOne +from .numeric_two import NumericTwo + +type Choice = ( + NumericOne + | NumericTwo +) + +def choice_from_dict(value: object) -> Choice: + return cast(Choice, _union.value_11_from_dict(value)) + +def choice_to_dict(value: Choice) -> object: + return _union.value_11_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/choice_alias.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/choice_alias.py.golden new file mode 100644 index 000000000..7063a2741 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/choice_alias.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from .choice import Choice + +type ChoiceAlias = (Choice) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/client_tool.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/client_tool.py.golden new file mode 100644 index 000000000..b65bef982 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/client_tool.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +@dataclass +class ClientTool: + input_schema: dict[str, Any] + name: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["input_schema"] = self.input_schema + result["name"] = self.name + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> ClientTool: + return cls( + input_schema=data["input_schema"], # type: ignore[assignment] + name=data["name"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/dependent_choice.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/dependent_choice.py.golden new file mode 100644 index 000000000..76ce3e523 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/dependent_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .dependent_envelope import DependentEnvelope + +type DependentChoice = ( + DependentEnvelope + | bool +) + +def dependent_choice_from_dict(value: object) -> DependentChoice: + return cast(DependentChoice, _union.value_14_from_dict(value)) + +def dependent_choice_to_dict(value: DependentChoice) -> object: + return _union.value_14_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/dependent_envelope.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/dependent_envelope.py.golden new file mode 100644 index 000000000..a3e05475d --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/dependent_envelope.py.golden @@ -0,0 +1,24 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from .acyclic_envelope import AcyclicEnvelope + +@dataclass +class DependentEnvelope: + envelope: AcyclicEnvelope + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["envelope"] = self.envelope.to_dict() + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> DependentEnvelope: + return cls( + envelope=AcyclicEnvelope.from_dict(data["envelope"]), # type: ignore[arg-type] + ) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/envelope.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/envelope.py.golden new file mode 100644 index 000000000..85977ed31 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/envelope.py.golden @@ -0,0 +1,42 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .choice import Choice +from .choice_alias import ChoiceAlias + +@dataclass +class Envelope: + choice: ChoiceAlias + choice_list: list[Choice] + choice_map: dict[str, Choice] + deep: list[dict[str, list[Choice]]] + optional_choice: Choice | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['choice'] = _union.value_13_to_dict(self.choice) + result['choice_list'] = _union.value_17_to_dict(self.choice_list) + result['choice_map'] = _union.value_18_to_dict(self.choice_map) + result['deep'] = _union.value_19_to_dict(self.deep) + if self.optional_choice is not None: + result['optional_choice'] = _union.value_11_to_dict(self.optional_choice) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Envelope: + return cls( + choice = cast(ChoiceAlias, _union.value_13_from_dict(data['choice'])), + choice_list = cast(list[Choice], _union.value_17_from_dict(data['choice_list'])), + choice_map = cast(dict[str, Choice], _union.value_18_from_dict(data['choice_map'])), + deep = cast(list[dict[str, list[Choice]]], _union.value_19_from_dict(data['deep'])), + optional_choice = cast(Choice + | None, None if 'optional_choice' not in data else _union.value_11_from_dict(data.get('optional_choice'))), + ) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/finite_envelope.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/finite_envelope.py.golden new file mode 100644 index 000000000..d1c6f6d9f --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/finite_envelope.py.golden @@ -0,0 +1,25 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union + +@dataclass +class FiniteEnvelope: + value: int | bool | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['value'] = _union.value_21_to_dict(self.value) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> FiniteEnvelope: + return cls( + value = cast(int | bool | None, _union.value_21_from_dict(data['value'])), + ) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/inner_null_choice.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/inner_null_choice.py.golden new file mode 100644 index 000000000..83addfac4 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/inner_null_choice.py.golden @@ -0,0 +1,18 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type InnerNullChoice = ( + object + | str | None +) + +def inner_null_choice_from_dict(value: object) -> InnerNullChoice: + return _union.value_24_from_dict(value) + +def inner_null_choice_to_dict(value: InnerNullChoice) -> object: + return _union.value_24_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/nested_ambiguous.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/nested_ambiguous.py.golden new file mode 100644 index 000000000..199893d53 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/nested_ambiguous.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .ambiguous import Ambiguous + +type NestedAmbiguous = ( + Ambiguous + | str +) + +def nested_ambiguous_from_dict(value: object) -> NestedAmbiguous: + return cast(NestedAmbiguous, _union.value_28_from_dict(value)) + +def nested_ambiguous_to_dict(value: NestedAmbiguous) -> object: + return _union.value_28_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/nullable_choice.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/nullable_choice.py.golden new file mode 100644 index 000000000..94eb7ca64 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/nullable_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .choice import Choice + +type NullableChoice = ( + Choice + | None +) + +def nullable_choice_from_dict(value: object) -> NullableChoice: + return cast(NullableChoice, _union.value_29_from_dict(value)) + +def nullable_choice_to_dict(value: NullableChoice) -> object: + return _union.value_29_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/nullable_envelope.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/nullable_envelope.py.golden new file mode 100644 index 000000000..a039cdb7f --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/nullable_envelope.py.golden @@ -0,0 +1,32 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .nullable_choice import NullableChoice +from .wrapped_null_choice import WrappedNullChoice + +@dataclass +class NullableEnvelope: + choice: NullableChoice + ambiguous: WrappedNullChoice | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['choice'] = _union.value_29_to_dict(self.choice) + if self.ambiguous is not None: + result['ambiguous'] = _union.value_30_to_dict(self.ambiguous) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NullableEnvelope: + return cls( + choice = cast(NullableChoice, _union.value_29_from_dict(data['choice'])), + ambiguous = None if 'ambiguous' not in data else _union.value_30_from_dict(data.get('ambiguous')), + ) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/numeric_one.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/numeric_one.py.golden new file mode 100644 index 000000000..00917636c --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/numeric_one.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class NumericOne: + revision: int + first_value: str | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["revision"] = self.revision + if self.first_value is not None: + result["first_value"] = self.first_value + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NumericOne: + return cls( + revision=data["revision"], # type: ignore[assignment] + first_value=data.get("first_value"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/numeric_two.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/numeric_two.py.golden new file mode 100644 index 000000000..d042a0d8b --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/numeric_two.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class NumericTwo: + revision: int + second_value: str | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["revision"] = self.revision + if self.second_value is not None: + result["second_value"] = self.second_value + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NumericTwo: + return cls( + revision=data["revision"], # type: ignore[assignment] + second_value=data.get("second_value"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/open_choice.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/open_choice.py.golden new file mode 100644 index 000000000..3bcd71d81 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/open_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import Any + +from . import _union +from .numeric_two import NumericTwo + +type OpenChoice = ( + Any + | NumericTwo +) + +def open_choice_from_dict(value: object) -> OpenChoice: + return _union.value_31_from_dict(value) + +def open_choice_to_dict(value: OpenChoice) -> object: + return _union.value_31_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/recursive.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/recursive.py.golden new file mode 100644 index 000000000..79aeec558 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/recursive.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union, branch + +type Recursive = ( + branch.Branch + | str +) + +def recursive_from_dict(value: object) -> Recursive: + return cast(Recursive, _union.value_9_from_dict(value)) + +def recursive_to_dict(value: Recursive) -> object: + return _union.value_9_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/request.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/request.py.golden new file mode 100644 index 000000000..46530c8d1 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/request.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .tool import Tool + +@dataclass +class Request: + tools: list[Tool] + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['tools'] = _union.value_33_to_dict(self.tools) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Request: + return cls( + tools = cast(list[Tool], _union.value_33_from_dict(data['tools'])), + ) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/scalar_choice.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/scalar_choice.py.golden new file mode 100644 index 000000000..a055918c9 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/scalar_choice.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union + +type ScalarChoice = ( + int + | bool +) + +def scalar_choice_from_dict(value: object) -> ScalarChoice: + return cast(ScalarChoice, _union.value_37_from_dict(value)) + +def scalar_choice_to_dict(value: ScalarChoice) -> object: + return _union.value_37_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/search_tool.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/search_tool.py.golden new file mode 100644 index 000000000..f6b31466a --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/search_tool.py.golden @@ -0,0 +1,30 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal + +@dataclass +class SearchTool: + name: Literal["web_search"] + type_: Literal["web_search_20250305"] + max_uses: int | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["name"] = self.name + result["type"] = self.type_ + if self.max_uses is not None: + result["max_uses"] = self.max_uses + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> SearchTool: + return cls( + name=data["name"], # type: ignore[assignment] + type_=data["type"], # type: ignore[assignment] + max_uses=data.get("max_uses"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/send_multipart_multipart_request_body.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/send_multipart_multipart_request_body.py.golden new file mode 100644 index 000000000..607318f62 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/send_multipart_multipart_request_body.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from ..runtime import UploadFile +from .choice import Choice + +@dataclass +class SendMultipartMultipartRequestBody: + choice: Choice + attachment: UploadFile | None = None diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/send_multipart_request.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/send_multipart_request.py.golden new file mode 100644 index 000000000..0e9e53f29 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/send_multipart_request.py.golden @@ -0,0 +1,30 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .choice import Choice + +@dataclass +class SendMultipartRequest: + choice: Choice + attachment: bytes | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['choice'] = _union.value_11_to_dict(self.choice) + if self.attachment is not None: + result["attachment"] = self.attachment + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> SendMultipartRequest: + return cls( + choice = cast(Choice, _union.value_11_from_dict(data['choice'])), + attachment=data.get("attachment"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_ambiguous.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_ambiguous.py.golden new file mode 100644 index 000000000..d4fd8dc37 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_ambiguous.py.golden @@ -0,0 +1,23 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .ambiguous import Ambiguous + +# Discriminator: kind / content: payload (adjacent). + +type TaggedAmbiguous = ( + Ambiguous + | str +) + +def tagged_ambiguous_from_dict(value: object) -> TaggedAmbiguous: + return cast(TaggedAmbiguous, _union.value_40_from_dict(value)) + +def tagged_ambiguous_to_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], _union.value_40_to_dict(value)) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_choice.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_choice.py.golden new file mode 100644 index 000000000..4bbaf7ad5 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_choice.py.golden @@ -0,0 +1,23 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .choice import Choice + +# Discriminator: kind / content: payload (adjacent). + +type TaggedChoice = ( + Choice + | str +) + +def tagged_choice_from_dict(value: object) -> TaggedChoice: + return cast(TaggedChoice, _union.value_41_from_dict(value)) + +def tagged_choice_to_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], _union.value_41_to_dict(value)) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_container_choice.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_container_choice.py.golden new file mode 100644 index 000000000..d8a108327 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_container_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .adjacent_member import AdjacentMember + +type TaggedContainerChoice = ( + list[AdjacentMember] + | str +) + +def tagged_container_choice_from_dict(value: object) -> TaggedContainerChoice: + return cast(TaggedContainerChoice, _union.value_42_from_dict(value)) + +def tagged_container_choice_to_dict(value: TaggedContainerChoice) -> object: + return _union.value_42_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_member_alias.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_member_alias.py.golden new file mode 100644 index 000000000..65d4e105c --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_member_alias.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from .tagged_member_choice import TaggedMemberChoice + +type TaggedMemberAlias = (TaggedMemberChoice) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_member_choice.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_member_choice.py.golden new file mode 100644 index 000000000..b1a85c387 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_member_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .adjacent_member import AdjacentMember + +type TaggedMemberChoice = ( + AdjacentMember + | int +) + +def tagged_member_choice_from_dict(value: object) -> TaggedMemberChoice: + return cast(TaggedMemberChoice, _union.value_45_from_dict(value)) + +def tagged_member_choice_to_dict(value: TaggedMemberChoice) -> object: + return _union.value_45_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_member_envelope.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_member_envelope.py.golden new file mode 100644 index 000000000..6d7d76561 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_member_envelope.py.golden @@ -0,0 +1,34 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .tagged_member_alias import TaggedMemberAlias +from .tagged_member_choice import TaggedMemberChoice + +@dataclass +class TaggedMemberEnvelope: + mapping: dict[str, TaggedMemberChoice] + value: TaggedMemberChoice + values: list[TaggedMemberAlias] + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['mapping'] = _union.value_46_to_dict(self.mapping) + result['value'] = _union.value_45_to_dict(self.value) + result['values'] = _union.value_47_to_dict(self.values) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> TaggedMemberEnvelope: + return cls( + mapping = cast(dict[str, + TaggedMemberChoice], _union.value_46_from_dict(data['mapping'])), + value = cast(TaggedMemberChoice, _union.value_45_from_dict(data['value'])), + values = cast(list[TaggedMemberAlias], _union.value_47_from_dict(data['values'])), + ) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_object_choice.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_object_choice.py.golden new file mode 100644 index 000000000..052030fcf --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_object_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .tagged_member_envelope import TaggedMemberEnvelope + +type TaggedObjectChoice = ( + TaggedMemberEnvelope + | int +) + +def tagged_object_choice_from_dict(value: object) -> TaggedObjectChoice: + return cast(TaggedObjectChoice, _union.value_48_from_dict(value)) + +def tagged_object_choice_to_dict(value: TaggedObjectChoice) -> object: + return _union.value_48_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_record.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_record.py.golden new file mode 100644 index 000000000..41db830f4 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tagged_record.py.golden @@ -0,0 +1,22 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class TaggedRecord: + name: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["name"] = self.name + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> TaggedRecord: + return cls( + name=data["name"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tool.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tool.py.golden new file mode 100644 index 000000000..42944e010 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/tool.py.golden @@ -0,0 +1,22 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .client_tool import ClientTool +from .search_tool import SearchTool + +type Tool = ( + ClientTool + | SearchTool +) + +def tool_from_dict(value: object) -> Tool: + return cast(Tool, _union.value_34_from_dict(value)) + +def tool_to_dict(value: Tool) -> object: + return _union.value_34_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/wrapped_null_choice.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/wrapped_null_choice.py.golden new file mode 100644 index 000000000..e47e6b7dc --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/models/wrapped_null_choice.py.golden @@ -0,0 +1,19 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union +from .inner_null_choice import InnerNullChoice + +type WrappedNullChoice = ( + InnerNullChoice + | int +) + +def wrapped_null_choice_from_dict(value: object) -> WrappedNullChoice: + return _union.value_30_from_dict(value) + +def wrapped_null_choice_to_dict(value: WrappedNullChoice) -> object: + return _union.value_30_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/py.typed.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/py.typed.golden new file mode 100644 index 000000000..e69de29bb diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/__init__.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/__init__.py.golden new file mode 100644 index 000000000..c77ee060c --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/__init__.py.golden @@ -0,0 +1,11 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .auth import ApiKeyAuth as ApiKeyAuth +from .auth import Authenticator as Authenticator +from .auth import BearerAuth as BearerAuth +from .client import ApiResponse as ApiResponse +from .client import Client as Client +from .errors import ApiError as ApiError +from .upload_file import UploadFile as UploadFile diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/_union_support.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/_union_support.py.golden new file mode 100644 index 000000000..a4c9e6107 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/_union_support.py.golden @@ -0,0 +1,90 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/auth.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/auth.py.golden new file mode 100644 index 000000000..eaf25448a --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/auth.py.golden @@ -0,0 +1,50 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""Authentication helpers.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Callable + + +class Authenticator(ABC): + """Base class for request authenticators.""" + + @abstractmethod + def auth_headers(self) -> dict[str, str]: + """Return headers to attach to every request.""" + ... + + +class BearerAuth(Authenticator): + """Bearer token authentication. + + Accepts a static token string or a callable that returns the + current token (evaluated on every request). + """ + + def __init__(self, token: str | Callable[[], str]) -> None: + self._token = token + + def auth_headers(self) -> dict[str, str]: + token = self._token() if callable(self._token) else self._token + return {"Authorization": f"Bearer {token}"} + + +class ApiKeyAuth(Authenticator): + """API key authentication via a custom header. + + Accepts a static key string or a callable that returns the + current key (evaluated on every request). + """ + + def __init__(self, header_name: str, api_key: str | Callable[[], str]) -> None: + self._header_name = header_name + self._api_key = api_key + + def auth_headers(self) -> dict[str, str]: + key = self._api_key() if callable(self._api_key) else self._api_key + return {self._header_name: key} diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/client.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/client.py.golden new file mode 100644 index 000000000..e11bded82 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/client.py.golden @@ -0,0 +1,116 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""HTTP client wrapping requests.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any, Generic, TypeVar, cast + +import requests + +from .auth import Authenticator + +ResponseData = TypeVar("ResponseData") + + +class Response: + """Thin wrapper over requests.Response with strict types.""" + + def __init__(self, raw: requests.Response) -> None: + self._raw = raw + + @property + def status_code(self) -> int: + return self._raw.status_code # type: ignore[return-value] + + @property + def reason(self) -> str: + return self._raw.reason or "" + + @property + def content(self) -> bytes: + return self._raw.content # type: ignore[return-value] + + @property + def headers(self) -> Mapping[str, str]: + return cast(Mapping[str, str], self._raw.headers) + + @property + def text(self) -> str: + return self._raw.text + + def json(self) -> Any: # type: ignore[explicit-override] + return self._raw.json() # type: ignore[reportUnknownMemberType] + + +@dataclass(frozen=True) +class ApiResponse(Generic[ResponseData]): + """Decoded response data together with its HTTP metadata.""" + + data: ResponseData + status_code: int + headers: Mapping[str, str] + raw: Response + + +class Client: + """Synchronous HTTP client for the generated SDK.""" + + def __init__( + self, + base_url: str, + *, + http_client: requests.Session | None = None, + authenticator: Authenticator | None = None, + ) -> None: + self._base_url = base_url.rstrip("/") + self._http_client = http_client or requests.Session() + self._authenticator = authenticator + + def request( + self, + method: str, + path: str, + *, + params: dict[str, str] | None = None, + json: Any = None, + data: Any = None, + files: Any = None, + headers: dict[str, str] | None = None, + ) -> Response: + """Send an HTTP request and return the raw response.""" + url = f"{self._base_url}{path}" + req_headers: dict[str, str] = {"Accept": "application/json"} + if json is not None: + req_headers["Content-Type"] = "application/json" + if headers: + req_headers.update(headers) + if self._authenticator is not None: + req_headers.update(self._authenticator.auth_headers()) + raw = cast( + requests.Response, + self._http_client.request( # type: ignore[reportUnknownMemberType] + method, + url, + params=params, + json=json, + data=data, + files=files, + headers=req_headers, + ), + ) + return Response(raw) + + def close(self) -> None: + """Close the underlying HTTP client.""" + self._http_client.close() + + def __enter__(self) -> Client: + return self + + def __exit__(self, *args: object) -> None: + self.close() diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/errors.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/errors.py.golden new file mode 100644 index 000000000..3530e64be --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/errors.py.golden @@ -0,0 +1,34 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""API error type.""" + +from __future__ import annotations + +from collections.abc import Mapping + + +class ApiError(Exception): + """Raised when the server returns a non-success status code.""" + + def __init__( + self, + status_code: int, + status: str, + body: bytes, + *, + headers: Mapping[str, str] | None = None, + response: object | None = None, + ) -> None: + self.status_code: int = status_code + self.status: str = status + self.raw_body: bytes = body + self.body: bytes = body + self.headers: Mapping[str, str] | None = headers + self.response: object | None = response + super().__init__(f"{status_code} {status}") + + @property + def text(self) -> str: + return self.raw_body.decode("utf-8", errors="replace") diff --git a/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/upload_file.py.golden b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/upload_file.py.golden new file mode 100644 index 000000000..f218509e4 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union-strict/python_union_conversion/runtime/upload_file.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class UploadFile: + data: bytes + filename: str | None = None + + @classmethod + def from_bytes(cls, data: bytes, filename: str | None = None) -> UploadFile: + return cls(data=data, filename=filename) + + def filename_or_default(self, default_name: str) -> str: + return self.filename or default_name diff --git a/tests/golden/python/python-requests/untagged-union/README.md.golden b/tests/golden/python/python-requests/untagged-union/README.md.golden new file mode 100644 index 000000000..a6f182f18 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/README.md.golden @@ -0,0 +1,7 @@ +# Python union conversion + +Generated Python SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `python_union_conversion`. diff --git a/tests/golden/python/python-requests/untagged-union/pyproject.toml.golden b/tests/golden/python/python-requests/untagged-union/pyproject.toml.golden new file mode 100644 index 000000000..03f2a11f9 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/pyproject.toml.golden @@ -0,0 +1,10 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "python_union_conversion" +version = "1.0.0" +description = "Generated Python SDK." +requires-python = ">=3.12" +dependencies = ["requests>=2.32"] diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/__init__.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/__init__.py.golden new file mode 100644 index 000000000..8f525cedf --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/__init__.py.golden @@ -0,0 +1,11 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .runtime import ApiKeyAuth as ApiKeyAuth +from .runtime import Authenticator as Authenticator +from .runtime import BearerAuth as BearerAuth +from .runtime import ApiResponse as ApiResponse +from .runtime import Client as Client +from .runtime import ApiError as ApiError +from .runtime import UploadFile as UploadFile diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/apis/__init__.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/apis/__init__.py.golden new file mode 100644 index 000000000..8126d3b1a --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/apis/__init__.py.golden @@ -0,0 +1,5 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .default_api import DefaultApi as DefaultApi diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/apis/default_api.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/apis/default_api.py.golden new file mode 100644 index 000000000..56f2c8d66 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/apis/default_api.py.golden @@ -0,0 +1,290 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +import json + +from collections.abc import Mapping +from typing import cast + +from ..models import _union +from ..models.choice import Choice +from ..models.envelope import Envelope +from ..models.nullable_choice import NullableChoice +from ..models.send_multipart_multipart_request_body import SendMultipartMultipartRequestBody +from ..models.tagged_choice import TaggedChoice +from ..models.tagged_member_choice import TaggedMemberChoice +from ..runtime.client import ApiResponse, Client +from ..runtime.errors import ApiError + +class SendChoiceBadRequest: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: Choice | None = None + self._body_error: Exception | None = None + @property + def body(self) -> Choice: + if not self._body_loaded: + try: + self._body_value = cast(Choice, _union.value_11_from_dict(json.loads(self.raw_body.decode("utf-8")))) + + except Exception as exc: + self._body_error = exc + + self._body_loaded = True + + if self._body_error is not None: + raise self._body_error + + return cast(Choice, self._body_value) + +class SendChoiceUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendChoiceErrorDetail = ( + SendChoiceBadRequest + | SendChoiceUnexpected +) + +class SendChoiceError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendChoiceErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendChoiceErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendChoicesUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendChoicesErrorDetail = ( + SendChoicesUnexpected +) + +class SendChoicesError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendChoicesErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendChoicesErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendEnvelopeUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendEnvelopeErrorDetail = ( + SendEnvelopeUnexpected +) + +class SendEnvelopeError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendEnvelopeErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendEnvelopeErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendMultipartUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendMultipartErrorDetail = ( + SendMultipartUnexpected +) + +class SendMultipartError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendMultipartErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendMultipartErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendNullableUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendNullableErrorDetail = ( + SendNullableUnexpected +) + +class SendNullableError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendNullableErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendNullableErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendTaggedUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendTaggedErrorDetail = ( + SendTaggedUnexpected +) + +class SendTaggedError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendTaggedErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendTaggedErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + +class SendTaggedMemberUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type SendTaggedMemberErrorDetail = ( + SendTaggedMemberUnexpected +) + +class SendTaggedMemberError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: SendTaggedMemberErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: SendTaggedMemberErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + + +class DefaultApi: + def __init__(self, client: Client) -> None: + self._client = client + + def send_choice_with_http_info(self, *, body: Choice) -> ApiResponse[Choice]: + path = "/choice" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_11_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + if response.status_code == 400: + detail = SendChoiceBadRequest(response.status_code, response.headers, response.content) + else: + detail = SendChoiceUnexpected(response.status_code, response.headers, response.content) + raise SendChoiceError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + _union_data = cast(Choice, _union.value_11_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_choice(self, *, body: Choice) -> Choice: + return self.send_choice_with_http_info(body=body).data + + def send_choices_with_http_info(self, *, body: list[Choice]) -> ApiResponse[dict[str, Choice]]: + path = "/choices" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_17_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendChoicesUnexpected(response.status_code, response.headers, response.content) + raise SendChoicesError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + _union_data = cast(dict[str, Choice], _union.value_18_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_choices(self, *, body: list[Choice]) -> dict[str, Choice]: + return self.send_choices_with_http_info(body=body).data + + def send_envelope_with_http_info(self, *, body: Envelope) -> ApiResponse[Envelope]: + path = "/envelope" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + response = self._client.request("POST", path, json=body.to_dict(), headers=headers) + if not (200 <= response.status_code < 300): + detail = SendEnvelopeUnexpected(response.status_code, response.headers, response.content) + raise SendEnvelopeError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + data = Envelope.from_dict(response.json()) + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_envelope(self, *, body: Envelope) -> Envelope: + return self.send_envelope_with_http_info(body=body).data + + def send_multipart_with_http_info(self, *, body: SendMultipartMultipartRequestBody) -> ApiResponse[None]: + path = "/multipart" + files: dict[str, object] = {} + if body.attachment is not None: + files["attachment"] = (body.attachment.filename_or_default("attachment"), body.attachment.data, "application/octet-stream") + + files['choice'] = (None, json.dumps(_union.value_11_to_dict(body.choice)), 'application/json') + response = self._client.request("POST", path, files=files if files else None) + if not (200 <= response.status_code < 300): + detail = SendMultipartUnexpected(response.status_code, response.headers, response.content) + raise SendMultipartError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + data = None + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_multipart(self, *, body: SendMultipartMultipartRequestBody) -> None: + return self.send_multipart_with_http_info(body=body).data + + def send_nullable_with_http_info(self, *, body: NullableChoice) -> ApiResponse[NullableChoice]: + path = "/nullable" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_29_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendNullableUnexpected(response.status_code, response.headers, response.content) + raise SendNullableError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + _union_data = cast(NullableChoice, _union.value_29_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_nullable(self, *, body: NullableChoice) -> NullableChoice: + return self.send_nullable_with_http_info(body=body).data + + def send_tagged_with_http_info(self, *, body: TaggedChoice) -> ApiResponse[TaggedChoice]: + path = "/tagged" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_41_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendTaggedUnexpected(response.status_code, response.headers, response.content) + raise SendTaggedError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + _union_data = cast(TaggedChoice, _union.value_41_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_tagged(self, *, body: TaggedChoice) -> TaggedChoice: + return self.send_tagged_with_http_info(body=body).data + + def send_tagged_member_with_http_info(self, *, body: TaggedMemberChoice) -> ApiResponse[TaggedMemberChoice]: + path = "/tagged-member" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + _union_body = _union.value_45_to_dict(body) + response = self._client.request("POST", path, headers=headers, data = json.dumps(_union_body, allow_nan = False)) + if not (200 <= response.status_code < 300): + detail = SendTaggedMemberUnexpected(response.status_code, response.headers, response.content) + raise SendTaggedMemberError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + _union_data = cast(TaggedMemberChoice, _union.value_45_from_dict(response.json())) + data = _union_data + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def send_tagged_member(self, *, body: TaggedMemberChoice) -> TaggedMemberChoice: + return self.send_tagged_member_with_http_info(body=body).data diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/__init__.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/__init__.py.golden new file mode 100644 index 000000000..30669ee6e --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/__init__.py.golden @@ -0,0 +1,40 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .acyclic_envelope import AcyclicEnvelope as AcyclicEnvelope +from .acyclic_scalar import AcyclicScalar as AcyclicScalar +from .adjacent_member import AdjacentMember as AdjacentMember +from .ambiguous import Ambiguous as Ambiguous +from .ambiguous_tagged_member import AmbiguousTaggedMember as AmbiguousTaggedMember +from .branch import Branch as Branch +from .choice import Choice as Choice +from .choice_alias import ChoiceAlias as ChoiceAlias +from .client_tool import ClientTool as ClientTool +from .dependent_choice import DependentChoice as DependentChoice +from .dependent_envelope import DependentEnvelope as DependentEnvelope +from .envelope import Envelope as Envelope +from .finite_envelope import FiniteEnvelope as FiniteEnvelope +from .inner_null_choice import InnerNullChoice as InnerNullChoice +from .nested_ambiguous import NestedAmbiguous as NestedAmbiguous +from .nullable_choice import NullableChoice as NullableChoice +from .nullable_envelope import NullableEnvelope as NullableEnvelope +from .numeric_one import NumericOne as NumericOne +from .numeric_two import NumericTwo as NumericTwo +from .open_choice import OpenChoice as OpenChoice +from .recursive import Recursive as Recursive +from .request import Request as Request +from .scalar_choice import ScalarChoice as ScalarChoice +from .search_tool import SearchTool as SearchTool +from .send_multipart_multipart_request_body import SendMultipartMultipartRequestBody as SendMultipartMultipartRequestBody +from .send_multipart_request import SendMultipartRequest as SendMultipartRequest +from .tagged_ambiguous import TaggedAmbiguous as TaggedAmbiguous +from .tagged_choice import TaggedChoice as TaggedChoice +from .tagged_container_choice import TaggedContainerChoice as TaggedContainerChoice +from .tagged_member_alias import TaggedMemberAlias as TaggedMemberAlias +from .tagged_member_choice import TaggedMemberChoice as TaggedMemberChoice +from .tagged_member_envelope import TaggedMemberEnvelope as TaggedMemberEnvelope +from .tagged_object_choice import TaggedObjectChoice as TaggedObjectChoice +from .tagged_record import TaggedRecord as TaggedRecord +from .tool import Tool as Tool +from .wrapped_null_choice import WrappedNullChoice as WrappedNullChoice diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/_union.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/_union.py.golden new file mode 100644 index 000000000..b537ed00a --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/_union.py.golden @@ -0,0 +1,805 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from ..runtime import _union_support + +def value_0_from_dict(value: object) -> object: + if _union_support.number(value, True): + return value_1_from_dict(value) + + if isinstance(value, str): + return value_2_from_dict(value) + + return value + +def value_0_to_dict(value: object) -> object: + if _union_support.number(value, True): + return value_1_to_dict(value) + + if isinstance(value, str): + return value_2_to_dict(value) + + return value + +def value_1_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_1_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_2_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_3_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_4_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_4_from_dict(value) + + return value + +def value_3_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_4_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_4_to_dict(value) + + return value + +def value_4_from_dict(value: object) -> object: + from.import numeric_two + return numeric_two.NumericTwo.from_dict(_union_support.as_dict(value)) + +def value_4_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_5_from_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'record')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'numeric')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value'), str)))))): + return value_6_from_dict(value) + + if ((_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'record')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'numeric')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value'), str)))))): + return value_6_from_dict(value) + + return value + +def value_5_to_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))): + return value_6_to_dict(value) + + if ((_union_support.is_object(value) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))): + return value_6_to_dict(value) + + return value + +def value_6_from_dict(value: object) -> object: + from.import adjacent_member + return adjacent_member.adjacent_member_from_dict(_union_support.as_dict(value)) + +def value_6_to_dict(value: object) -> object: + from.import adjacent_member + return adjacent_member.adjacent_member_to_dict(cast(adjacent_member.AdjacentMember, value)) + +def value_7_from_dict(value: object) -> object: + from.import tagged_record + return tagged_record.TaggedRecord.from_dict(_union_support.as_dict(value)) + +def value_7_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_8_from_dict(value: object) -> object: + return [value_9_from_dict(item) for item in _union_support.as_list(value)] + +def value_8_to_dict(value: object) -> object: + return [value_9_to_dict(item) for item in _union_support.as_list(value)] + +def value_9_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_from_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))): + return value_10_from_dict(value) + + if isinstance(value, str): + return value_2_from_dict(value) + + return value + +def value_9_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_to_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))): + return value_10_to_dict(value) + + if isinstance(value, str): + return value_2_to_dict(value) + + return value + +def value_10_from_dict(value: object) -> object: + from.import branch + return branch.Branch.from_dict(_union_support.as_dict(value)) + +def value_10_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_11_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))): + return value_12_from_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_4_from_dict(value) + + return value + +def value_11_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))): + return value_12_to_dict(value) + + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_4_to_dict(value) + + return value + +def value_12_from_dict(value: object) -> object: + from.import numeric_one + return numeric_one.NumericOne.from_dict(_union_support.as_dict(value)) + +def value_12_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_13_from_dict(value: object) -> object: + return value_11_from_dict(value) + +def value_13_to_dict(value: object) -> object: + return value_11_to_dict(value) + +def value_14_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'envelope', 'envelope', True) and (_union_support.is_object(_union_support.get(value, 'envelope', 'envelope')) + and (_union_support.has(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value'), True) + or isinstance(_union_support.get(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value'), str)))))): + return value_15_from_dict(value) + + if isinstance(value, bool): + return value_16_from_dict(value) + + return value + +def value_14_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'envelope', 'envelope', True) and (_union_support.is_object(_union_support.get(value, 'envelope', 'envelope')) + and (_union_support.has(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value'), True) + or isinstance(_union_support.get(_union_support.get(value, 'envelope', 'envelope'), 'value', 'value'), str)))))): + return value_15_to_dict(value) + + if isinstance(value, bool): + return value_16_to_dict(value) + + return value + +def value_15_from_dict(value: object) -> object: + from.import dependent_envelope + return dependent_envelope.DependentEnvelope.from_dict(_union_support.as_dict(value)) + +def value_15_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_16_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_16_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_17_from_dict(value: object) -> object: + return [value_11_from_dict(item) for item in _union_support.as_list(value)] + +def value_17_to_dict(value: object) -> object: + return [value_11_to_dict(item) for item in _union_support.as_list(value)] + +def value_18_from_dict(value: object) -> object: + return {key: value_11_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_18_to_dict(value: object) -> object: + return {key: value_11_to_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_19_from_dict(value: object) -> object: + return [value_20_from_dict(item) for item in _union_support.as_list(value)] + +def value_19_to_dict(value: object) -> object: + return [value_20_to_dict(item) for item in _union_support.as_list(value)] + +def value_20_from_dict(value: object) -> object: + return {key: value_17_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_20_to_dict(value: object) -> object: + return {key: value_17_to_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_21_from_dict(value: object) -> object: + return value_22_from_dict(value) + +def value_21_to_dict(value: object) -> object: + return value_22_to_dict(value) + +def value_22_from_dict(value: object) -> object: + return None if value is None else value_23_from_dict(value) + +def value_22_to_dict(value: object) -> object: + return None if value is None else value_23_to_dict(value) + +def value_23_from_dict(value: object) -> object: + if _union_support.number(value, True): + return value_1_from_dict(value) + + if isinstance(value, bool): + return value_16_from_dict(value) + + return value + +def value_23_to_dict(value: object) -> object: + if _union_support.number(value, True): + return value_1_to_dict(value) + + if isinstance(value, bool): + return value_16_to_dict(value) + + return value + +def value_24_from_dict(value: object) -> object: + if _union_support.equal(value, None): + return value_25_from_dict(value) + + if ((value is None or isinstance(value, str)) + and (_union_support.equal(value, 'x') + or _union_support.equal(value, None)) + and (value is None + or isinstance(value, str) + or _union_support.equal(value, 'x') + or _union_support.equal(value, None))): + return value_26_from_dict(value) + + return value + +def value_24_to_dict(value: object) -> object: + if _union_support.equal(value, None): + return value_25_to_dict(value) + + if ((value is None or isinstance(value, str)) + and (_union_support.equal(value, 'x') + or _union_support.equal(value, None)) + and (value is None + or isinstance(value, str) + or _union_support.equal(value, 'x') + or _union_support.equal(value, None))): + return value_26_to_dict(value) + + return value + +def value_25_from_dict(value: object) -> object: + return value_2_from_dict(value) + +def value_25_to_dict(value: object) -> object: + return value_2_to_dict(value) + +def value_26_from_dict(value: object) -> object: + return value_27_from_dict(value) + +def value_26_to_dict(value: object) -> object: + return value_27_to_dict(value) + +def value_27_from_dict(value: object) -> object: + return None if value is None else value_2_from_dict(value) + +def value_27_to_dict(value: object) -> object: + return None if value is None else value_2_to_dict(value) + +def value_28_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_3_from_dict(value) + + if isinstance(value, str): + return value_2_from_dict(value) + + return value + +def value_28_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_3_to_dict(value) + + if isinstance(value, str): + return value_2_to_dict(value) + + return value + +def value_29_from_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))): + return value_11_from_dict(value) + + if value is None: + return None + + return value + +def value_29_to_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))): + return value_11_to_dict(value) + + if value is None: + return None + + return value + +def value_30_from_dict(value: object) -> object: + if (_union_support.equal(value, None) + or ((value is None or isinstance(value, str)) + and (_union_support.equal(value, 'x') + or _union_support.equal(value, None)) + and (value is None + or isinstance(value, str) + or _union_support.equal(value, 'x') + or _union_support.equal(value, None)))): + return value_24_from_dict(value) + + if _union_support.number(value, True): + return value_1_from_dict(value) + + return value + +def value_30_to_dict(value: object) -> object: + if (_union_support.equal(value, None) + or ((value is None or isinstance(value, str)) + and (_union_support.equal(value, 'x') + or _union_support.equal(value, None)) + and (value is None + or isinstance(value, str) + or _union_support.equal(value, 'x') + or _union_support.equal(value, None)))): + return value_24_to_dict(value) + + if _union_support.number(value, True): + return value_1_to_dict(value) + + return value + +def value_31_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_4_from_dict(value) + + if True: + return value_32_from_dict(value) + + return value + +def value_31_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))): + return value_4_to_dict(value) + + if True: + return value_32_to_dict(value) + + return value + +def value_32_from_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_32_to_dict(value: object) -> object: + return _union_support.scalar(value) + +def value_33_from_dict(value: object) -> object: + return [value_34_from_dict(item) for item in _union_support.as_list(value)] + +def value_33_to_dict(value: object) -> object: + return [value_34_to_dict(item) for item in _union_support.as_list(value)] + +def value_34_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'input_schema', 'input_schema', True) and (_union_support.is_dict(_union_support.get(value, 'input_schema', 'input_schema')) and all(True for _item in _union_support.as_dict(_union_support.get(value, 'input_schema', 'input_schema')).values()))) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): + return value_35_from_dict(value) + + if (_union_support.is_object(value) + and (not _union_support.has(value, 'max_uses', 'max_uses', False) or _union_support.number(_union_support.get(value, 'max_uses', 'max_uses'), True)) + and (_union_support.has(value, 'name', 'name', True) and _union_support.equal(_union_support.get(value, 'name', 'name'), 'web_search')) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'web_search_20250305'))): + return value_36_from_dict(value) + + return value + +def value_34_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'input_schema', 'input_schema', True) and (_union_support.is_dict(_union_support.get(value, 'input_schema', 'input_schema')) and all(True for _item in _union_support.as_dict(_union_support.get(value, 'input_schema', 'input_schema')).values()))) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))): + return value_35_to_dict(value) + + if (_union_support.is_object(value) + and (not _union_support.has(value, 'max_uses', 'max_uses', False) or _union_support.number(_union_support.get(value, 'max_uses', 'max_uses'), True)) + and (_union_support.has(value, 'name', 'name', True) and _union_support.equal(_union_support.get(value, 'name', 'name'), 'web_search')) + and (_union_support.has(value, 'type', 'type_', True) and _union_support.equal(_union_support.get(value, 'type', 'type_'), 'web_search_20250305'))): + return value_36_to_dict(value) + + return value + +def value_35_from_dict(value: object) -> object: + from.import client_tool + return client_tool.ClientTool.from_dict(_union_support.as_dict(value)) + +def value_35_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_36_from_dict(value: object) -> object: + from.import search_tool + return search_tool.SearchTool.from_dict(_union_support.as_dict(value)) + +def value_36_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def value_37_from_dict(value: object) -> object: + if (_union_support.number(value, True) + and (_union_support.equal(value, 1) + or _union_support.equal(value, 2)) + and (_union_support.number(value, True) + or _union_support.equal(value, 1) + or _union_support.equal(value, 2))): + return value_38_from_dict(value) + + if (isinstance(value, bool) + and _union_support.equal(value, True) + and (isinstance(value, bool) + or _union_support.equal(value, True))): + return value_39_from_dict(value) + + return value + +def value_37_to_dict(value: object) -> object: + if (_union_support.number(value, True) + and (_union_support.equal(value, 1) + or _union_support.equal(value, 2)) + and (_union_support.number(value, True) + or _union_support.equal(value, 1) + or _union_support.equal(value, 2))): + return value_38_to_dict(value) + + if (isinstance(value, bool) + and _union_support.equal(value, True) + and (isinstance(value, bool) + or _union_support.equal(value, True))): + return value_39_to_dict(value) + + return value + +def value_38_from_dict(value: object) -> object: + return value_1_from_dict(value) + +def value_38_to_dict(value: object) -> object: + return value_1_to_dict(value) + +def value_39_from_dict(value: object) -> object: + return value_16_from_dict(value) + +def value_39_to_dict(value: object) -> object: + return value_16_to_dict(value) + +def value_40_from_dict(value: object) -> object: + data = _union_support.as_dict(value) + if data.get('kind') == 'choice': + return value_3_from_dict(data['payload']) + + if data.get('kind') == 'text': + return value_2_from_dict(data['payload']) + + raise ValueError("No matching tagged union variant") + +def value_40_to_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))): + return {'kind': 'choice', 'payload': value_3_to_dict(value)} + + if (isinstance(value, str)): + return {'kind': 'text', 'payload': value_2_to_dict(value)} + + raise ValueError("No matching tagged union variant") + +def value_41_from_dict(value: object) -> object: + data = _union_support.as_dict(value) + if data.get('kind') == 'choice': + return value_11_from_dict(data['payload']) + + if data.get('kind') == 'text': + return value_2_from_dict(data['payload']) + + raise ValueError("No matching tagged union variant") + +def value_41_to_dict(value: object) -> object: + if (((_union_support.is_object(value) + and (not _union_support.has(value, 'first_value', 'first_value', False) or isinstance(_union_support.get(value, 'first_value', 'first_value'), str)) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 1)))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str))))): + return {'kind': 'choice', 'payload': value_11_to_dict(value)} + + if (isinstance(value, str)): + return {'kind': 'text', 'payload': value_2_to_dict(value)} + + raise ValueError("No matching tagged union variant") + +def value_42_from_dict(value: object) -> object: + if (_union_support.is_list(value) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'record')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'numeric')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value'), str)))))) for _item in _union_support.as_list(value))): + return value_43_from_dict(value) + + if isinstance(value, str): + return value_2_from_dict(value) + + return value + +def value_42_to_dict(value: object) -> object: + if (_union_support.is_list(value) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'name', 'name', True) and isinstance(_union_support.get(_item, 'name', 'name'), str))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'revision', 'revision', True) and (_union_support.number(_union_support.get(_item, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_item, 'revision', 'revision'), 2))) + and (not _union_support.has(_item, 'second_value', 'second_value', False) or isinstance(_union_support.get(_item, 'second_value', 'second_value'), str)))) for _item in _union_support.as_list(value))): + return value_43_to_dict(value) + + if isinstance(value, str): + return value_2_to_dict(value) + + return value + +def value_43_from_dict(value: object) -> object: + return [value_6_from_dict(item) for item in _union_support.as_list(value)] + +def value_43_to_dict(value: object) -> object: + return [value_6_to_dict(item) for item in _union_support.as_list(value)] + +def value_44_from_dict(value: object) -> object: + return value_45_from_dict(value) + +def value_44_to_dict(value: object) -> object: + return value_45_to_dict(value) + +def value_45_from_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'record')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'kind', 'kind', True) and _union_support.equal(_union_support.get(value, 'kind', 'kind'), 'numeric')) + and (_union_support.has(value, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(value, 'payload', 'payload')) + and (_union_support.has(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(value, 'payload', 'payload'), 'second_value', 'second_value'), str)))))): + return value_6_from_dict(value) + + if _union_support.number(value, True): + return value_1_from_dict(value) + + return value + +def value_45_to_dict(value: object) -> object: + if ((_union_support.is_object(value) + and (_union_support.has(value, 'name', 'name', True) and isinstance(_union_support.get(value, 'name', 'name'), str))) + or (_union_support.is_object(value) + and (_union_support.has(value, 'revision', 'revision', True) and (_union_support.number(_union_support.get(value, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(value, 'revision', 'revision'), 2))) + and (not _union_support.has(value, 'second_value', 'second_value', False) or isinstance(_union_support.get(value, 'second_value', 'second_value'), str)))): + return value_6_to_dict(value) + + if _union_support.number(value, True): + return value_1_to_dict(value) + + return value + +def value_46_from_dict(value: object) -> object: + return {key: value_45_from_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_46_to_dict(value: object) -> object: + return {key: value_45_to_dict(item) for key, item in _union_support.as_dict(value).items()} + +def value_47_from_dict(value: object) -> object: + return [value_44_from_dict(item) for item in _union_support.as_list(value)] + +def value_47_to_dict(value: object) -> object: + return [value_44_to_dict(item) for item in _union_support.as_list(value)] + +def value_48_from_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'mapping', 'mapping', True) and (_union_support.is_dict(_union_support.get(value, 'mapping', 'mapping')) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'record')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'numeric')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value'), str))))) + or _union_support.number(_item, True)) for _item in _union_support.as_dict(_union_support.get(value, 'mapping', 'mapping')).values()))) + and (_union_support.has(value, 'value', 'value', True) and ((_union_support.is_object(_union_support.get(value, 'value', 'value')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'kind', 'kind', True) and _union_support.equal(_union_support.get(_union_support.get(value, 'value', 'value'), 'kind', 'kind'), 'record')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload')) + and (_union_support.has(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(_union_support.get(value, 'value', 'value')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'kind', 'kind', True) and _union_support.equal(_union_support.get(_union_support.get(value, 'value', 'value'), 'kind', 'kind'), 'numeric')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload')) + and (_union_support.has(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(_union_support.get(value, 'value', 'value'), 'payload', 'payload'), 'second_value', 'second_value'), str))))) + or _union_support.number(_union_support.get(value, 'value', 'value'), True))) + and (_union_support.has(value, 'values', 'values', True) and (_union_support.is_list(_union_support.get(value, 'values', 'values')) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'record')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'name', 'name'), str))))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'kind', 'kind', True) and _union_support.equal(_union_support.get(_item, 'kind', 'kind'), 'numeric')) + and (_union_support.has(_item, 'payload', 'payload', True) and (_union_support.is_object(_union_support.get(_item, 'payload', 'payload')) + and (_union_support.has(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(_item, 'payload', 'payload'), 'second_value', 'second_value'), str))))) + or _union_support.number(_item, True)) for _item in _union_support.as_list(_union_support.get(value, 'values', 'values')))))): + return value_49_from_dict(value) + + if _union_support.number(value, True): + return value_1_from_dict(value) + + return value + +def value_48_to_dict(value: object) -> object: + if (_union_support.is_object(value) + and (_union_support.has(value, 'mapping', 'mapping', True) and (_union_support.is_dict(_union_support.get(value, 'mapping', 'mapping')) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'name', 'name', True) and isinstance(_union_support.get(_item, 'name', 'name'), str))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'revision', 'revision', True) and (_union_support.number(_union_support.get(_item, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_item, 'revision', 'revision'), 2))) + and (not _union_support.has(_item, 'second_value', 'second_value', False) or isinstance(_union_support.get(_item, 'second_value', 'second_value'), str))) + or _union_support.number(_item, True)) for _item in _union_support.as_dict(_union_support.get(value, 'mapping', 'mapping')).values()))) + and (_union_support.has(value, 'value', 'value', True) and ((_union_support.is_object(_union_support.get(value, 'value', 'value')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'name', 'name', True) and isinstance(_union_support.get(_union_support.get(value, 'value', 'value'), 'name', 'name'), str))) + or (_union_support.is_object(_union_support.get(value, 'value', 'value')) + and (_union_support.has(_union_support.get(value, 'value', 'value'), 'revision', 'revision', True) and (_union_support.number(_union_support.get(_union_support.get(value, 'value', 'value'), 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_union_support.get(value, 'value', 'value'), 'revision', 'revision'), 2))) + and (not _union_support.has(_union_support.get(value, 'value', 'value'), 'second_value', 'second_value', False) or isinstance(_union_support.get(_union_support.get(value, 'value', 'value'), 'second_value', 'second_value'), str))) + or _union_support.number(_union_support.get(value, 'value', 'value'), True))) + and (_union_support.has(value, 'values', 'values', True) and (_union_support.is_list(_union_support.get(value, 'values', 'values')) and all(((_union_support.is_object(_item) + and (_union_support.has(_item, 'name', 'name', True) and isinstance(_union_support.get(_item, 'name', 'name'), str))) + or (_union_support.is_object(_item) + and (_union_support.has(_item, 'revision', 'revision', True) and (_union_support.number(_union_support.get(_item, 'revision', 'revision'), True) + and _union_support.equal(_union_support.get(_item, 'revision', 'revision'), 2))) + and (not _union_support.has(_item, 'second_value', 'second_value', False) or isinstance(_union_support.get(_item, 'second_value', 'second_value'), str))) + or _union_support.number(_item, True)) for _item in _union_support.as_list(_union_support.get(value, 'values', 'values')))))): + return value_49_to_dict(value) + + if _union_support.number(value, True): + return value_1_to_dict(value) + + return value + +def value_49_from_dict(value: object) -> object: + from.import tagged_member_envelope + return tagged_member_envelope.TaggedMemberEnvelope.from_dict(_union_support.as_dict(value)) + +def value_49_to_dict(value: object) -> object: + return _union_support.model_to_dict(value) + +def match_recursive_any_from_dict(value: object) -> bool: + return ((_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_from_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))) + or isinstance(value, str)) + +def match_recursive_concrete_from_dict(value: object) -> bool: + return ((_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_from_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))) + or isinstance(value, str)) + +def match_recursive_fallback_from_dict(value: object) -> bool: + return False + +def match_recursive_any_to_dict(value: object) -> bool: + return ((_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_to_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))) + or isinstance(value, str)) + +def match_recursive_concrete_to_dict(value: object) -> bool: + return ((_union_support.is_object(value) + and (_union_support.has(value, 'children', 'children', True) and (_union_support.is_list(_union_support.get(value, 'children', 'children')) and all(match_recursive_any_to_dict(_item) for _item in _union_support.as_list(_union_support.get(value, 'children', 'children')))))) + or isinstance(value, str)) + +def match_recursive_fallback_to_dict(value: object) -> bool: + return False diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/acyclic_envelope.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/acyclic_envelope.py.golden new file mode 100644 index 000000000..57ded1581 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/acyclic_envelope.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .acyclic_scalar import AcyclicScalar + +@dataclass +class AcyclicEnvelope: + value: AcyclicScalar + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['value'] = _union.value_0_to_dict(self.value) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> AcyclicEnvelope: + return cls( + value = cast(AcyclicScalar, _union.value_0_from_dict(data['value'])), + ) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/acyclic_scalar.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/acyclic_scalar.py.golden new file mode 100644 index 000000000..e7448aa38 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/acyclic_scalar.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union + +type AcyclicScalar = ( + int + | str +) + +def acyclic_scalar_from_dict(value: object) -> AcyclicScalar: + return cast(AcyclicScalar, _union.value_0_from_dict(value)) + +def acyclic_scalar_to_dict(value: AcyclicScalar) -> object: + return _union.value_0_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/adjacent_member.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/adjacent_member.py.golden new file mode 100644 index 000000000..b1c372785 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/adjacent_member.py.golden @@ -0,0 +1,33 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from .numeric_two import NumericTwo +from .tagged_record import TaggedRecord + +# Discriminator: kind / content: payload (adjacent). + +type AdjacentMember = ( + TaggedRecord + | NumericTwo +) + + +def adjacent_member_from_dict(data: dict[str, object]) -> AdjacentMember: + _tag = data["kind"] + _content = data["payload"] # type: ignore[assignment] + if _tag == "record": + return TaggedRecord.from_dict(_content) # type: ignore[arg-type] + elif _tag == "numeric": + return NumericTwo.from_dict(_content) # type: ignore[arg-type] + raise ValueError(f"Unknown discriminator value for AdjacentMember: {data}") + + +def adjacent_member_to_dict(obj: AdjacentMember) -> dict[str, object]: + if isinstance(obj, TaggedRecord): + return {"kind": "record", "payload": obj.to_dict()} + elif isinstance(obj, NumericTwo): + return {"kind": "numeric", "payload": obj.to_dict()} + raise ValueError(f"Unknown variant for AdjacentMember: {type(obj)}") diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/ambiguous.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/ambiguous.py.golden new file mode 100644 index 000000000..540aa34d8 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/ambiguous.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .numeric_two import NumericTwo + +type Ambiguous = ( + NumericTwo + | NumericTwo +) + +def ambiguous_from_dict(value: object) -> Ambiguous: + return cast(Ambiguous, _union.value_3_from_dict(value)) + +def ambiguous_to_dict(value: Ambiguous) -> object: + return _union.value_3_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/ambiguous_tagged_member.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/ambiguous_tagged_member.py.golden new file mode 100644 index 000000000..bcc1f745a --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/ambiguous_tagged_member.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .adjacent_member import AdjacentMember + +type AmbiguousTaggedMember = ( + AdjacentMember + | AdjacentMember +) + +def ambiguous_tagged_member_from_dict(value: object) -> AmbiguousTaggedMember: + return cast(AmbiguousTaggedMember, _union.value_5_from_dict(value)) + +def ambiguous_tagged_member_to_dict(value: AmbiguousTaggedMember) -> object: + return _union.value_5_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/branch.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/branch.py.golden new file mode 100644 index 000000000..fae055beb --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/branch.py.golden @@ -0,0 +1,25 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union, recursive + +@dataclass +class Branch: + children: list[recursive.Recursive] + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['children'] = _union.value_8_to_dict(self.children) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Branch: + return cls( + children = cast(list[recursive.Recursive], _union.value_8_from_dict(data['children'])), + ) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/choice.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/choice.py.golden new file mode 100644 index 000000000..dbbb3f910 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/choice.py.golden @@ -0,0 +1,22 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .numeric_one import NumericOne +from .numeric_two import NumericTwo + +type Choice = ( + NumericOne + | NumericTwo +) + +def choice_from_dict(value: object) -> Choice: + return cast(Choice, _union.value_11_from_dict(value)) + +def choice_to_dict(value: Choice) -> object: + return _union.value_11_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/choice_alias.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/choice_alias.py.golden new file mode 100644 index 000000000..7063a2741 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/choice_alias.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from .choice import Choice + +type ChoiceAlias = (Choice) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/client_tool.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/client_tool.py.golden new file mode 100644 index 000000000..b65bef982 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/client_tool.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +@dataclass +class ClientTool: + input_schema: dict[str, Any] + name: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["input_schema"] = self.input_schema + result["name"] = self.name + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> ClientTool: + return cls( + input_schema=data["input_schema"], # type: ignore[assignment] + name=data["name"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/dependent_choice.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/dependent_choice.py.golden new file mode 100644 index 000000000..76ce3e523 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/dependent_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .dependent_envelope import DependentEnvelope + +type DependentChoice = ( + DependentEnvelope + | bool +) + +def dependent_choice_from_dict(value: object) -> DependentChoice: + return cast(DependentChoice, _union.value_14_from_dict(value)) + +def dependent_choice_to_dict(value: DependentChoice) -> object: + return _union.value_14_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/dependent_envelope.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/dependent_envelope.py.golden new file mode 100644 index 000000000..a3e05475d --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/dependent_envelope.py.golden @@ -0,0 +1,24 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from .acyclic_envelope import AcyclicEnvelope + +@dataclass +class DependentEnvelope: + envelope: AcyclicEnvelope + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["envelope"] = self.envelope.to_dict() + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> DependentEnvelope: + return cls( + envelope=AcyclicEnvelope.from_dict(data["envelope"]), # type: ignore[arg-type] + ) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/envelope.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/envelope.py.golden new file mode 100644 index 000000000..85977ed31 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/envelope.py.golden @@ -0,0 +1,42 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .choice import Choice +from .choice_alias import ChoiceAlias + +@dataclass +class Envelope: + choice: ChoiceAlias + choice_list: list[Choice] + choice_map: dict[str, Choice] + deep: list[dict[str, list[Choice]]] + optional_choice: Choice | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['choice'] = _union.value_13_to_dict(self.choice) + result['choice_list'] = _union.value_17_to_dict(self.choice_list) + result['choice_map'] = _union.value_18_to_dict(self.choice_map) + result['deep'] = _union.value_19_to_dict(self.deep) + if self.optional_choice is not None: + result['optional_choice'] = _union.value_11_to_dict(self.optional_choice) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Envelope: + return cls( + choice = cast(ChoiceAlias, _union.value_13_from_dict(data['choice'])), + choice_list = cast(list[Choice], _union.value_17_from_dict(data['choice_list'])), + choice_map = cast(dict[str, Choice], _union.value_18_from_dict(data['choice_map'])), + deep = cast(list[dict[str, list[Choice]]], _union.value_19_from_dict(data['deep'])), + optional_choice = cast(Choice + | None, None if 'optional_choice' not in data else _union.value_11_from_dict(data.get('optional_choice'))), + ) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/finite_envelope.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/finite_envelope.py.golden new file mode 100644 index 000000000..d1c6f6d9f --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/finite_envelope.py.golden @@ -0,0 +1,25 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union + +@dataclass +class FiniteEnvelope: + value: int | bool | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['value'] = _union.value_21_to_dict(self.value) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> FiniteEnvelope: + return cls( + value = cast(int | bool | None, _union.value_21_from_dict(data['value'])), + ) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/inner_null_choice.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/inner_null_choice.py.golden new file mode 100644 index 000000000..83addfac4 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/inner_null_choice.py.golden @@ -0,0 +1,18 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union + +type InnerNullChoice = ( + object + | str | None +) + +def inner_null_choice_from_dict(value: object) -> InnerNullChoice: + return _union.value_24_from_dict(value) + +def inner_null_choice_to_dict(value: InnerNullChoice) -> object: + return _union.value_24_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/nested_ambiguous.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/nested_ambiguous.py.golden new file mode 100644 index 000000000..199893d53 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/nested_ambiguous.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .ambiguous import Ambiguous + +type NestedAmbiguous = ( + Ambiguous + | str +) + +def nested_ambiguous_from_dict(value: object) -> NestedAmbiguous: + return cast(NestedAmbiguous, _union.value_28_from_dict(value)) + +def nested_ambiguous_to_dict(value: NestedAmbiguous) -> object: + return _union.value_28_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/nullable_choice.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/nullable_choice.py.golden new file mode 100644 index 000000000..94eb7ca64 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/nullable_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .choice import Choice + +type NullableChoice = ( + Choice + | None +) + +def nullable_choice_from_dict(value: object) -> NullableChoice: + return cast(NullableChoice, _union.value_29_from_dict(value)) + +def nullable_choice_to_dict(value: NullableChoice) -> object: + return _union.value_29_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/nullable_envelope.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/nullable_envelope.py.golden new file mode 100644 index 000000000..a039cdb7f --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/nullable_envelope.py.golden @@ -0,0 +1,32 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .nullable_choice import NullableChoice +from .wrapped_null_choice import WrappedNullChoice + +@dataclass +class NullableEnvelope: + choice: NullableChoice + ambiguous: WrappedNullChoice | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['choice'] = _union.value_29_to_dict(self.choice) + if self.ambiguous is not None: + result['ambiguous'] = _union.value_30_to_dict(self.ambiguous) + + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NullableEnvelope: + return cls( + choice = cast(NullableChoice, _union.value_29_from_dict(data['choice'])), + ambiguous = None if 'ambiguous' not in data else _union.value_30_from_dict(data.get('ambiguous')), + ) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/numeric_one.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/numeric_one.py.golden new file mode 100644 index 000000000..00917636c --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/numeric_one.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class NumericOne: + revision: int + first_value: str | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["revision"] = self.revision + if self.first_value is not None: + result["first_value"] = self.first_value + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NumericOne: + return cls( + revision=data["revision"], # type: ignore[assignment] + first_value=data.get("first_value"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/numeric_two.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/numeric_two.py.golden new file mode 100644 index 000000000..d042a0d8b --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/numeric_two.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class NumericTwo: + revision: int + second_value: str | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["revision"] = self.revision + if self.second_value is not None: + result["second_value"] = self.second_value + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NumericTwo: + return cls( + revision=data["revision"], # type: ignore[assignment] + second_value=data.get("second_value"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/open_choice.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/open_choice.py.golden new file mode 100644 index 000000000..3bcd71d81 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/open_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import Any + +from . import _union +from .numeric_two import NumericTwo + +type OpenChoice = ( + Any + | NumericTwo +) + +def open_choice_from_dict(value: object) -> OpenChoice: + return _union.value_31_from_dict(value) + +def open_choice_to_dict(value: OpenChoice) -> object: + return _union.value_31_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/recursive.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/recursive.py.golden new file mode 100644 index 000000000..79aeec558 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/recursive.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union, branch + +type Recursive = ( + branch.Branch + | str +) + +def recursive_from_dict(value: object) -> Recursive: + return cast(Recursive, _union.value_9_from_dict(value)) + +def recursive_to_dict(value: Recursive) -> object: + return _union.value_9_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/request.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/request.py.golden new file mode 100644 index 000000000..46530c8d1 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/request.py.golden @@ -0,0 +1,26 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .tool import Tool + +@dataclass +class Request: + tools: list[Tool] + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['tools'] = _union.value_33_to_dict(self.tools) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Request: + return cls( + tools = cast(list[Tool], _union.value_33_from_dict(data['tools'])), + ) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/scalar_choice.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/scalar_choice.py.golden new file mode 100644 index 000000000..a055918c9 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/scalar_choice.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union + +type ScalarChoice = ( + int + | bool +) + +def scalar_choice_from_dict(value: object) -> ScalarChoice: + return cast(ScalarChoice, _union.value_37_from_dict(value)) + +def scalar_choice_to_dict(value: ScalarChoice) -> object: + return _union.value_37_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/search_tool.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/search_tool.py.golden new file mode 100644 index 000000000..f6b31466a --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/search_tool.py.golden @@ -0,0 +1,30 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal + +@dataclass +class SearchTool: + name: Literal["web_search"] + type_: Literal["web_search_20250305"] + max_uses: int | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["name"] = self.name + result["type"] = self.type_ + if self.max_uses is not None: + result["max_uses"] = self.max_uses + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> SearchTool: + return cls( + name=data["name"], # type: ignore[assignment] + type_=data["type"], # type: ignore[assignment] + max_uses=data.get("max_uses"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/send_multipart_multipart_request_body.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/send_multipart_multipart_request_body.py.golden new file mode 100644 index 000000000..607318f62 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/send_multipart_multipart_request_body.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +from ..runtime import UploadFile +from .choice import Choice + +@dataclass +class SendMultipartMultipartRequestBody: + choice: Choice + attachment: UploadFile | None = None diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/send_multipart_request.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/send_multipart_request.py.golden new file mode 100644 index 000000000..0e9e53f29 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/send_multipart_request.py.golden @@ -0,0 +1,30 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .choice import Choice + +@dataclass +class SendMultipartRequest: + choice: Choice + attachment: bytes | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['choice'] = _union.value_11_to_dict(self.choice) + if self.attachment is not None: + result["attachment"] = self.attachment + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> SendMultipartRequest: + return cls( + choice = cast(Choice, _union.value_11_from_dict(data['choice'])), + attachment=data.get("attachment"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_ambiguous.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_ambiguous.py.golden new file mode 100644 index 000000000..d4fd8dc37 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_ambiguous.py.golden @@ -0,0 +1,23 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .ambiguous import Ambiguous + +# Discriminator: kind / content: payload (adjacent). + +type TaggedAmbiguous = ( + Ambiguous + | str +) + +def tagged_ambiguous_from_dict(value: object) -> TaggedAmbiguous: + return cast(TaggedAmbiguous, _union.value_40_from_dict(value)) + +def tagged_ambiguous_to_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], _union.value_40_to_dict(value)) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_choice.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_choice.py.golden new file mode 100644 index 000000000..4bbaf7ad5 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_choice.py.golden @@ -0,0 +1,23 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .choice import Choice + +# Discriminator: kind / content: payload (adjacent). + +type TaggedChoice = ( + Choice + | str +) + +def tagged_choice_from_dict(value: object) -> TaggedChoice: + return cast(TaggedChoice, _union.value_41_from_dict(value)) + +def tagged_choice_to_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], _union.value_41_to_dict(value)) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_container_choice.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_container_choice.py.golden new file mode 100644 index 000000000..d8a108327 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_container_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .adjacent_member import AdjacentMember + +type TaggedContainerChoice = ( + list[AdjacentMember] + | str +) + +def tagged_container_choice_from_dict(value: object) -> TaggedContainerChoice: + return cast(TaggedContainerChoice, _union.value_42_from_dict(value)) + +def tagged_container_choice_to_dict(value: TaggedContainerChoice) -> object: + return _union.value_42_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_member_alias.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_member_alias.py.golden new file mode 100644 index 000000000..65d4e105c --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_member_alias.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from .tagged_member_choice import TaggedMemberChoice + +type TaggedMemberAlias = (TaggedMemberChoice) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_member_choice.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_member_choice.py.golden new file mode 100644 index 000000000..b1a85c387 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_member_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .adjacent_member import AdjacentMember + +type TaggedMemberChoice = ( + AdjacentMember + | int +) + +def tagged_member_choice_from_dict(value: object) -> TaggedMemberChoice: + return cast(TaggedMemberChoice, _union.value_45_from_dict(value)) + +def tagged_member_choice_to_dict(value: TaggedMemberChoice) -> object: + return _union.value_45_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_member_envelope.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_member_envelope.py.golden new file mode 100644 index 000000000..6d7d76561 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_member_envelope.py.golden @@ -0,0 +1,34 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from . import _union +from .tagged_member_alias import TaggedMemberAlias +from .tagged_member_choice import TaggedMemberChoice + +@dataclass +class TaggedMemberEnvelope: + mapping: dict[str, TaggedMemberChoice] + value: TaggedMemberChoice + values: list[TaggedMemberAlias] + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result['mapping'] = _union.value_46_to_dict(self.mapping) + result['value'] = _union.value_45_to_dict(self.value) + result['values'] = _union.value_47_to_dict(self.values) + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> TaggedMemberEnvelope: + return cls( + mapping = cast(dict[str, + TaggedMemberChoice], _union.value_46_from_dict(data['mapping'])), + value = cast(TaggedMemberChoice, _union.value_45_from_dict(data['value'])), + values = cast(list[TaggedMemberAlias], _union.value_47_from_dict(data['values'])), + ) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_object_choice.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_object_choice.py.golden new file mode 100644 index 000000000..052030fcf --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_object_choice.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .tagged_member_envelope import TaggedMemberEnvelope + +type TaggedObjectChoice = ( + TaggedMemberEnvelope + | int +) + +def tagged_object_choice_from_dict(value: object) -> TaggedObjectChoice: + return cast(TaggedObjectChoice, _union.value_48_from_dict(value)) + +def tagged_object_choice_to_dict(value: TaggedObjectChoice) -> object: + return _union.value_48_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_record.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_record.py.golden new file mode 100644 index 000000000..41db830f4 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tagged_record.py.golden @@ -0,0 +1,22 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class TaggedRecord: + name: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["name"] = self.name + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> TaggedRecord: + return cls( + name=data["name"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tool.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tool.py.golden new file mode 100644 index 000000000..42944e010 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/tool.py.golden @@ -0,0 +1,22 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from typing import cast + +from . import _union +from .client_tool import ClientTool +from .search_tool import SearchTool + +type Tool = ( + ClientTool + | SearchTool +) + +def tool_from_dict(value: object) -> Tool: + return cast(Tool, _union.value_34_from_dict(value)) + +def tool_to_dict(value: Tool) -> object: + return _union.value_34_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/wrapped_null_choice.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/wrapped_null_choice.py.golden new file mode 100644 index 000000000..e47e6b7dc --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/models/wrapped_null_choice.py.golden @@ -0,0 +1,19 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from . import _union +from .inner_null_choice import InnerNullChoice + +type WrappedNullChoice = ( + InnerNullChoice + | int +) + +def wrapped_null_choice_from_dict(value: object) -> WrappedNullChoice: + return _union.value_30_from_dict(value) + +def wrapped_null_choice_to_dict(value: WrappedNullChoice) -> object: + return _union.value_30_to_dict(value) diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/py.typed.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/py.typed.golden new file mode 100644 index 000000000..e69de29bb diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/__init__.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/__init__.py.golden new file mode 100644 index 000000000..c77ee060c --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/__init__.py.golden @@ -0,0 +1,11 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from .auth import ApiKeyAuth as ApiKeyAuth +from .auth import Authenticator as Authenticator +from .auth import BearerAuth as BearerAuth +from .client import ApiResponse as ApiResponse +from .client import Client as Client +from .errors import ApiError as ApiError +from .upload_file import UploadFile as UploadFile diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/_union_support.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/_union_support.py.golden new file mode 100644 index 000000000..a4c9e6107 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/_union_support.py.golden @@ -0,0 +1,90 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""Internal structural selection support; deliberately not a schema validator.""" + +from __future__ import annotations + +from dataclasses import is_dataclass +from enum import Enum +from math import isfinite +from typing import Protocol, TypeGuard, cast + + +class _Model(Protocol): + def to_dict(self) -> dict[str, object]: ... + + +def as_dict(value: object) -> dict[str, object]: + return cast(dict[str, object], value) + + +def as_list(value: object) -> list[object]: + return cast(list[object], value) + + +def is_dict(value: object) -> TypeGuard[dict[str, object]]: + return isinstance(value, dict) and all(isinstance(key, str) for key in cast(dict[object, object], value)) + + +def is_list(value: object) -> TypeGuard[list[object]]: + return isinstance(value, list) + + +def scalar(value: object) -> object: + return value.value if isinstance(value, Enum) else value + + +def model_to_dict(value: object) -> object: + return value if is_dict(value) else cast(_Model, value).to_dict() + + +def is_object(value: object) -> bool: + return is_dict(value) or (not isinstance(value, type) and is_dataclass(value)) + + +def has(value: object, wire: str, field: str, required: bool) -> bool: + if is_dict(value): + return wire in as_dict(value) + # Optional dataclass fields use None for absence. Required fields still + # undergo their shape check; a nullable required field accepts that None. + return hasattr(value, field) and (required or getattr(value, field) is not None) + + +def get(value: object, wire: str, field: str) -> object: + return value.get(wire) if is_dict(value) else getattr(value, field, None) + + +def extras(value: object, known: list[str]) -> list[object]: + if not is_dict(value): + return [] + return [item for key, item in as_dict(value).items() if key not in known] + + +def number(value: object, integer: bool) -> bool: + value = scalar(value) + if isinstance(value, bool): + return False + if isinstance(value, int): + return True + return isinstance(value, float) and isfinite(value) and (not integer or value.is_integer()) + + +def equal(value: object, expected: object) -> bool: + value = scalar(value) + if isinstance(expected, bool): + return isinstance(value, bool) and value == expected + if isinstance(expected, (int, float)): + return number(value, False) and value == expected + if is_list(expected): + if not is_list(value): + return False + left, right = as_list(value), as_list(expected) + return len(left) == len(right) and all(equal(a, b) for a, b in zip(left, right)) + if is_dict(expected): + if not is_dict(value): + return False + left, right = as_dict(value), as_dict(expected) + return left.keys() == right.keys() and all(equal(left[key], item) for key, item in right.items()) + return type(value) is type(expected) and value == expected diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/auth.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/auth.py.golden new file mode 100644 index 000000000..eaf25448a --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/auth.py.golden @@ -0,0 +1,50 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""Authentication helpers.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Callable + + +class Authenticator(ABC): + """Base class for request authenticators.""" + + @abstractmethod + def auth_headers(self) -> dict[str, str]: + """Return headers to attach to every request.""" + ... + + +class BearerAuth(Authenticator): + """Bearer token authentication. + + Accepts a static token string or a callable that returns the + current token (evaluated on every request). + """ + + def __init__(self, token: str | Callable[[], str]) -> None: + self._token = token + + def auth_headers(self) -> dict[str, str]: + token = self._token() if callable(self._token) else self._token + return {"Authorization": f"Bearer {token}"} + + +class ApiKeyAuth(Authenticator): + """API key authentication via a custom header. + + Accepts a static key string or a callable that returns the + current key (evaluated on every request). + """ + + def __init__(self, header_name: str, api_key: str | Callable[[], str]) -> None: + self._header_name = header_name + self._api_key = api_key + + def auth_headers(self) -> dict[str, str]: + key = self._api_key() if callable(self._api_key) else self._api_key + return {self._header_name: key} diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/client.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/client.py.golden new file mode 100644 index 000000000..e11bded82 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/client.py.golden @@ -0,0 +1,116 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""HTTP client wrapping requests.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any, Generic, TypeVar, cast + +import requests + +from .auth import Authenticator + +ResponseData = TypeVar("ResponseData") + + +class Response: + """Thin wrapper over requests.Response with strict types.""" + + def __init__(self, raw: requests.Response) -> None: + self._raw = raw + + @property + def status_code(self) -> int: + return self._raw.status_code # type: ignore[return-value] + + @property + def reason(self) -> str: + return self._raw.reason or "" + + @property + def content(self) -> bytes: + return self._raw.content # type: ignore[return-value] + + @property + def headers(self) -> Mapping[str, str]: + return cast(Mapping[str, str], self._raw.headers) + + @property + def text(self) -> str: + return self._raw.text + + def json(self) -> Any: # type: ignore[explicit-override] + return self._raw.json() # type: ignore[reportUnknownMemberType] + + +@dataclass(frozen=True) +class ApiResponse(Generic[ResponseData]): + """Decoded response data together with its HTTP metadata.""" + + data: ResponseData + status_code: int + headers: Mapping[str, str] + raw: Response + + +class Client: + """Synchronous HTTP client for the generated SDK.""" + + def __init__( + self, + base_url: str, + *, + http_client: requests.Session | None = None, + authenticator: Authenticator | None = None, + ) -> None: + self._base_url = base_url.rstrip("/") + self._http_client = http_client or requests.Session() + self._authenticator = authenticator + + def request( + self, + method: str, + path: str, + *, + params: dict[str, str] | None = None, + json: Any = None, + data: Any = None, + files: Any = None, + headers: dict[str, str] | None = None, + ) -> Response: + """Send an HTTP request and return the raw response.""" + url = f"{self._base_url}{path}" + req_headers: dict[str, str] = {"Accept": "application/json"} + if json is not None: + req_headers["Content-Type"] = "application/json" + if headers: + req_headers.update(headers) + if self._authenticator is not None: + req_headers.update(self._authenticator.auth_headers()) + raw = cast( + requests.Response, + self._http_client.request( # type: ignore[reportUnknownMemberType] + method, + url, + params=params, + json=json, + data=data, + files=files, + headers=req_headers, + ), + ) + return Response(raw) + + def close(self) -> None: + """Close the underlying HTTP client.""" + self._http_client.close() + + def __enter__(self) -> Client: + return self + + def __exit__(self, *args: object) -> None: + self.close() diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/errors.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/errors.py.golden new file mode 100644 index 000000000..3530e64be --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/errors.py.golden @@ -0,0 +1,34 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +"""API error type.""" + +from __future__ import annotations + +from collections.abc import Mapping + + +class ApiError(Exception): + """Raised when the server returns a non-success status code.""" + + def __init__( + self, + status_code: int, + status: str, + body: bytes, + *, + headers: Mapping[str, str] | None = None, + response: object | None = None, + ) -> None: + self.status_code: int = status_code + self.status: str = status + self.raw_body: bytes = body + self.body: bytes = body + self.headers: Mapping[str, str] | None = headers + self.response: object | None = response + super().__init__(f"{status_code} {status}") + + @property + def text(self) -> str: + return self.raw_body.decode("utf-8", errors="replace") diff --git a/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/upload_file.py.golden b/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/upload_file.py.golden new file mode 100644 index 000000000..f218509e4 --- /dev/null +++ b/tests/golden/python/python-requests/untagged-union/python_union_conversion/runtime/upload_file.py.golden @@ -0,0 +1,20 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Python union conversion — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class UploadFile: + data: bytes + filename: str | None = None + + @classmethod + def from_bytes(cls, data: bytes, filename: str | None = None) -> UploadFile: + return cls(data=data, filename=filename) + + def filename_or_default(self, default_name: str) -> str: + return self.filename or default_name diff --git a/tests/golden_tests_python_httpx.rs b/tests/golden_tests_python_httpx.rs index 5e6243bd1..ba3a41c6b 100644 --- a/tests/golden_tests_python_httpx.rs +++ b/tests/golden_tests_python_httpx.rs @@ -18,10 +18,26 @@ fn golden_dir() -> &'static Path { const UPDATE_HINT: &str = "UPDATE_GOLDEN=1 cargo test --test golden_tests_python_httpx"; +#[test] +fn test_untagged_union_policies_golden() { + for strategy in ["strict", "raw"] { + let config = toml::toml! { untagged_union_strategy = strategy }; + let generator = PythonHttpxCodeGenerator::new(config); + run_golden_test( + &generator, + golden_dir(), + &format!("untagged-union-{strategy}"), + "valid/python-untagged-union.yaml", + UPDATE_HINT, + ); + } +} + #[rustfmt::skip] fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { test_cases_from_slice(&[ ("petstore", "valid/petstore.yaml"), + ("untagged-union", "valid/python-untagged-union.yaml"), ("comprehensive-schemas", "valid/comprehensive-schemas.yaml"), ("delete-with-response-schema", "valid/delete-with-response-schema.yaml"), @@ -111,6 +127,7 @@ macro_rules! generate_golden_tests { } generate_golden_tests! { + test_untagged_union_golden: "untagged-union", test_nested_nullable_golden: "nested-nullable", test_petstore_golden: "petstore", diff --git a/tests/golden_tests_python_requests.rs b/tests/golden_tests_python_requests.rs index c3023cd78..d93a8731d 100644 --- a/tests/golden_tests_python_requests.rs +++ b/tests/golden_tests_python_requests.rs @@ -18,10 +18,26 @@ fn golden_dir() -> &'static Path { const UPDATE_HINT: &str = "UPDATE_GOLDEN=1 cargo test --test golden_tests_python_requests"; +#[test] +fn test_untagged_union_policies_golden() { + for strategy in ["strict", "raw"] { + let config = toml::toml! { untagged_union_strategy = strategy }; + let generator = PythonRequestsCodeGenerator::new(config); + run_golden_test( + &generator, + golden_dir(), + &format!("untagged-union-{strategy}"), + "valid/python-untagged-union.yaml", + UPDATE_HINT, + ); + } +} + #[rustfmt::skip] fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { test_cases_from_slice(&[ ("petstore", "valid/petstore.yaml"), + ("untagged-union", "valid/python-untagged-union.yaml"), ("comprehensive-schemas", "valid/comprehensive-schemas.yaml"), ("delete-with-response-schema", "valid/delete-with-response-schema.yaml"), @@ -111,6 +127,7 @@ macro_rules! generate_golden_tests { } generate_golden_tests! { + test_untagged_union_golden: "untagged-union", test_nested_nullable_golden: "nested-nullable", test_petstore_golden: "petstore", diff --git a/tests/ir_scalar_enums.rs b/tests/ir_scalar_enums.rs index 07794eba2..b8c29d3c2 100644 --- a/tests/ir_scalar_enums.rs +++ b/tests/ir_scalar_enums.rs @@ -275,8 +275,6 @@ components: Box::new(GoHttpCodeGenerator::new(Default::default())), Box::new(JavaOkhttpCodeGenerator::new(Default::default())), Box::new(KotlinOkhttpCodeGenerator::new(Default::default())), - Box::new(PythonHttpxCodeGenerator::new(Default::default())), - Box::new(PythonRequestsCodeGenerator::new(Default::default())), Box::new(RustAioductCodeGenerator::new(Default::default())), Box::new(RustReqwestCodeGenerator::new(Default::default())), Box::new(RustUreqCodeGenerator::new(Default::default())), @@ -312,4 +310,45 @@ components: generator.generator_type() ); } + // Python now consumes these facts for union dispatch, while keeping ordinary + // model declarations and the other codecs on their existing representations. + // Retain exact comparison outside the new matcher, not a blanket exclusion. + let python: Vec> = vec![ + Box::new(PythonHttpxCodeGenerator::new(Default::default())), + Box::new(PythonRequestsCodeGenerator::new(Default::default())), + ]; + for generator in python { + let files = |ir| { + generator + .generate(ir) + .unwrap() + .into_iter() + .map(|file| ((file.category, file.filename), file.content)) + .collect::>() + }; + let actual = files(&decorated); + let expected = files(&legacy); + assert_eq!(actual.len(), expected.len()); + for (key, legacy_source) in expected { + let source = actual.get(&key).unwrap(); + if key.1 == "_union.py" { + assert_ne!( + source, + &legacy_source, + "{} uses retained union constraints", + generator.generator_type() + ); + assert!(source.contains("_union_support.equal(value, True)")); + assert!(source.contains("_union_support.equal(value, 1)")); + } else { + assert_eq!( + source, + &legacy_source, + "{} {}", + generator.generator_type(), + key.1 + ); + } + } + } }