Skip to content

feat(python): emit response_type so generated calls deserialize into Pydantic models - #28

Merged
calvin-archastro merged 1 commit into
mainfrom
feat/6266-python-response-type
Jun 12, 2026
Merged

feat(python): emit response_type so generated calls deserialize into Pydantic models#28
calvin-archastro merged 1 commit into
mainfrom
feat/6266-python-response-type

Conversation

@rob-archastro

Copy link
Copy Markdown
Contributor

What changed

The Python backend now emits response_type=Model / list[Model] / hoisted inline BaseModel on every typed operation, in both sync and async resource classes, so generated return annotations are true at runtime (fixes firstlanding#6266; requires ArchAstro/archastro-python#20). One shared classifier (response-type.ts) drives the resource emitter and the contract-tests emitter — assertions and behavior cannot drift, the inline-model naming rule lives in one place, and generation fails loudly if a schema ref appears in a response shape the classifier doesn't deserialize. Contract tests assert concrete classes via type(result).__name__. Also fixes a latent bug the new validation exposed: fields named object shadow the builtin during pydantic's deferred annotation resolution (dict[str, object] silently became dict[str, None]); free-form types now emit typing.Any, with a repro-backed regression test.

sequenceDiagram
    participant S as OpenAPI spec
    participant RT as response-type classifier
    participant RE as resource emitter
    participant CE as contract-tests emitter
    S->>RE: OperationDef
    RE->>RT: pythonResponseTypeExpr(op)
    alt model or list shape
        RT-->>RE: expression like list of RoutinePreset
        RE->>RE: emit request call with response_type
    else ref buried in union or optional
        RT-->>RE: throws, generation fails
    end
    S->>CE: same OperationDef
    CE->>RT: pythonResponseShape(op)
    RT-->>CE: shape verdict
    CE->>CE: emit isinstance and class-name assertions
Loading
classDiagram
    class ResponseTypeModule {
        pythonResponseShape(op) Shape
        pythonResponseTypeExpr(op, inlineName) string
        pythonInlineResponseName(className, opName) string
    }
    class ResourceEmitter {
        emitOperation()
        emitSyncOperation()
    }
    class ContractTestsEmitter {
        emitResultAssertions()
    }
    ResourceEmitter ..> ResponseTypeModule : uses
    ContractTestsEmitter ..> ResponseTypeModule : uses
Loading

Scope: tooling-only (SDK generator); no service code.

Risk: low-medium — output changes are mechanical and were verified by full local regeneration (byte-identical to the committed archastro-python output) plus 1,720 passing contract tests. Merge order matters once: ArchAstro/archastro-python#20 must merge before this version is published to npm, since the regenerate workflow installs @latest.

User impact: none until published + regenerated; then Python SDK responses become typed models.

Testing: 254 generator unit tests (9 new emission tests, 5 contract-assertion tests, shadowing regression, fail-loud throw test).

Follow-ups: version bump + npm publish after merge (release-process owner's call). After publish, running Regenerate SDK on archastro-python should produce a zero diff — a free pipeline verification.

🤖 Generated with Claude Code

…Pydantic models (firstlanding#6266)

Every typed operation now passes response_type=Model (or list[Model],
or the hoisted inline-response BaseModel) to the runtime's request(),
making the generated return annotations true at runtime. A shared
response-shape classifier (response-type.ts) drives both the resource
emitter and the contract-tests emitter, so emitted behavior and test
assertions cannot drift; generation fails loudly if a schema ref ever
appears in a shape the classifier does not deserialize.

Contract tests now assert concrete model classes (isinstance BaseModel
plus type(result).__name__) instead of dict access.

Also fixes a latent typegen bug surfaced by the new validation: fields
named `object` shadowed the builtin during pydantic's deferred
annotation resolution, turning dict[str, object] into dict[str, None].
Free-form types now emit typing.Any.

Requires archastro-python runtime support for response_type (companion
PR) — merge that first; publish this generator version after both land.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@archastro

archastro Bot commented Jun 12, 2026

Copy link
Copy Markdown

✅ Clean PR, @rob-archastro! No blocking findings on 93eb411 — a few nice things I clocked:

  • Nice single-source-of-truth move on response-type.ts — one pythonResponseShape drives both the resource emitter's response_type= and the contract-tests emitResultAssertions, so deserialization behavior and assertions can't drift. 🎯
  • Smart fail-loud guard: the classifier throws on a ref buried in an undeserialized union/optional shape (response-type.ts containsRef branch) so spec evolution can't silently reintroduce a return annotation the runtime never honors. ✨
  • Solid red-green discipline on the object-shadowing fix — the repro test at python.test.ts:208 locks in dict[str, Any] over the dict[str, None] regression before the one-line emitter change. 🙌

Reply @archastro <verb>: review · do <pattern> · don't <pattern> · forget <slug> · list

@calvin-archastro
calvin-archastro merged commit f47ac4f into main Jun 12, 2026
2 checks passed
calvin-archastro added a commit that referenced this pull request Jun 30, 2026
…ckend (TS + Python)

Previously emitStreamContractTestFile was only wired into the channel-harness
sample — so SDK regens (contract-tests-ts/py) shipped the generated stream()
methods with no contract test. Wire SSE coverage into generateContractTests
itself, for both backends.

- method-chain-builder: add buildStreamCalls (the streaming inverse of
  buildMethodCalls), sharing the resource walk so streaming ops get the same
  accessor chains (client.v1.ai.chat.completions.stream).
- typescript-emitter: emit __tests__/contract/streams/<resource>.contract.test.ts
  — construct PlatformClient pointed at the harness, registerStreamScenario with
  autoEmit (harness synthesizes contract-valid event payloads, resolving $refs),
  iterate stream(), assert event order; plus a non-2xx -> ApiError case.
- python-emitter: emit tests/contract/streams/test_<resource>.py — same shape
  over AsyncPlatformClient.stream().
- Both gated by the existing opt-in env (ARCHASTRO_RUN_CHANNEL_CONTRACT_TESTS):
  excluded from the default test run + the harness subprocess boots when the
  spec has channels OR streams.

These run green once their deps land: runtime streamSSE/stream_sse
(archastro-js #34 / archastro-python #28) and registerStreamScenario on the
harness client (TS: shipped in this PR's channel-harness; Python: phx_channel
harness.py). Generator suite 299; channel-harness 63.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
calvin-archastro added a commit that referenced this pull request Jun 30, 2026
…ckend (TS + Python)

Previously emitStreamContractTestFile was only wired into the channel-harness
sample — so SDK regens (contract-tests-ts/py) shipped the generated stream()
methods with no contract test. Wire SSE coverage into generateContractTests
itself, for both backends.

- method-chain-builder: add buildStreamCalls (the streaming inverse of
  buildMethodCalls), sharing the resource walk so streaming ops get the same
  accessor chains (client.v1.ai.chat.completions.stream).
- typescript-emitter: emit __tests__/contract/streams/<resource>.contract.test.ts
  — construct PlatformClient pointed at the harness, registerStreamScenario with
  autoEmit (harness synthesizes contract-valid event payloads, resolving $refs),
  iterate stream(), assert event order; plus a non-2xx -> ApiError case.
- python-emitter: emit tests/contract/streams/test_<resource>.py — same shape
  over AsyncPlatformClient.stream().
- Both gated by the existing opt-in env (ARCHASTRO_RUN_CHANNEL_CONTRACT_TESTS):
  excluded from the default test run + the harness subprocess boots when the
  spec has channels OR streams.

These run green once their deps land: runtime streamSSE/stream_sse
(archastro-js #34 / archastro-python #28) and registerStreamScenario on the
harness client (TS: shipped in this PR's channel-harness; Python: phx_channel
harness.py). Generator suite 299; channel-harness 63.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants