Skip to content

codegen: support body/manual operation kinds and nested namespaces#54

Closed
jadenfix wants to merge 1 commit into
mainfrom
sdk-contract-codegen
Closed

codegen: support body/manual operation kinds and nested namespaces#54
jadenfix wants to merge 1 commit into
mainfrom
sdk-contract-codegen

Conversation

@jadenfix

Copy link
Copy Markdown
Contributor

Phase B1 of the SDK facade centralization (roe-main #3456 is the contract-side groundwork). Teaches scripts/generate_wrappers.py to consume the upcoming wrapper shapes:

  • body kind — any single HTTP call (path/query/body params, optional request model, optional empty response), with or_empty, return_shape: list, refetch_with_retrieve, coerce: uuid, and inject_organization_id conventions matching the existing hand-written facades.
  • manual kind — generator skips it but asserts the method still exists in the hand-written facade source (drift check).
  • namespaces — nested accessors (client.agents.versions.*).
  • Split-file emission — APIs with manual ops emit _{api}_generated.py base classes; the hand-written facade subclasses them. All-manual APIs are parity-checked only.

scripts/fixtures/candidate_wrappers.yml is the parity-verified rendering of the unified contract for this repo — roe-main's full-cycle test (test_b2_draft_contract_renders_the_language_repo_fixtures) asserts its renderer reproduces it exactly.

No-op today: openapi/wrappers.yml still only carries simple/table_upload ops, so generated output is unchanged until roe-main flips the contract (B2). Rebased on v1.1.0 (kept the release-banner sync in main()).

Test Plan

  • uv run pytest — 43/43 (includes new tests/unit/test_generate_wrappers.py codegen suite against the candidate fixture)
  • Generator runs clean against the current openapi/wrappers.yml (existing goldens unchanged)
  • AST-verified: generated body methods are signature-identical to the 22 hand-written facade methods they will replace

🤖 Generated with Claude Code

Groundwork for migrating the hand-written facades (agents.py, policies.py,
users.py) to contract-generated code:

- kind `body`: any single HTTP call — path/query/body parameter locations,
  UUID coercion, pass_unset_when_none, or_empty list/dict defaults,
  inject_organization_id body insertion, return_shape list with typed
  error label, refetch_with_retrieve (create-then-retrieve), no-content
  returns. Conventions mirror the hand-written facades exactly
  (request_raw/from_dict for GETs, request_json/.parsed for mutations).
- kind `manual`: never generated; codegen asserts the method exists in the
  hand-written facade and fails loudly listing all missing methods.
- namespaces: generated nested classes proxy _raw/_org_id to the parent,
  wired via ctor + property, mirroring AgentVersionsAPI.
- split mode: APIs with manual ops emit base classes to
  src/roe/api/_{api}_generated.py (public class stays hand-written);
  all-manual APIs (users) are parity-checked only; whole-file APIs
  (discovery, tables) are byte-identical to before.

scripts/fixtures/candidate_wrappers.yml is the migration parity fixture:
every signature derived from the hand-written facades; AST comparison of
generated vs hand-written shows 22/22 body methods signature-identical.
Production no-op: regenerating from the committed openapi/wrappers.yml
produces zero diff.

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

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown

Greptile Summary

This PR extends scripts/generate_wrappers.py with body/manual operation kinds, nested namespace class emission, and split-file (base-class) mode so the codegen can incrementally absorb the hand-written facades. The output is intentionally a no-op against the current openapi/wrappers.yml; the candidate fixture and new tests exist solely to gate the upcoming B2 contract swap.

  • body kind codegen_body_kind_method handles path/query/body params, UUID coercion, or_empty, pass_unset_when_none, inject_organization_id, return_shape: list, and refetch_with_retrieve conventions, verified AST-equivalent to 22 hand-written facade methods.
  • manual kind + parity check — generator skips manual ops but _check_manual_parity asserts every manual method exists in the hand-written facade file, raising ManualWrapperParityError with a full problem list on failure.
  • Nested namespaces + split-file emission — APIs with at least one manual op emit a *Generated base class; all-manual APIs are parity-checked only and produce no file.

Confidence Score: 3/5

Safe to merge today (no production code changes), but the candidate fixture carries a return-type mismatch that will silently change the public API surface when B2 activates it.

The generator itself is well-structured and currently a no-op against openapi/wrappers.yml. The concern is the candidate fixture: jobs.retrieve_status declares return_type: AgentJobStatus while the hand-written facade returns AgentJobSingleStatus. The fixture's own header promises "reproduce the existing public surface exactly," so this is a broken guarantee that would become a real regression the moment B2 swaps the contract in. The test suite catches parity at the method-name level but not at the return-type level, so the mismatch went undetected.

scripts/fixtures/candidate_wrappers.yml — verify jobs.retrieve_status return type (AgentJobStatus vs AgentJobSingleStatus). scripts/generate_wrappers.py — the hardcoded version_id local in the refetch path and the missing list_error_label guard are worth addressing before the B2 migration lands.

Important Files Changed

Filename Overview
scripts/generate_wrappers.py Adds body/manual operation codegen, nested namespace class emission, and split-file (base-class) mode; core logic is well-structured but the hardcoded version_id local in the refetch path and the missing list_error_label guard are latent defects.
scripts/fixtures/candidate_wrappers.yml New parity fixture for the B2 migration; jobs.retrieve_status return type (AgentJobStatus) contradicts the hand-written facade (AgentJobSingleStatus), breaking the fixture's own "identical surface" guarantee.
tests/unit/test_generate_wrappers.py New test suite covering manual parity checks, split-module rendering, and all-manual no-emit behavior; does not assert per-method return types in generated output, which allowed the retrieve_status type mismatch to go undetected.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[main] --> B[_generate_modules]
    B --> C[_check_manual_parity]
    C -->|facade file missing or method absent| E[ManualWrapperParityError]
    C -->|all present| D{_spec_has_manual?}
    D -->|yes + has generated ops| F[_render_module split=True\n→ _api_generated.py]
    D -->|yes + all manual| G[no file emitted\nparity-checked only]
    D -->|no| H[_render_module split=False\n→ api.py]
    F --> I{operation kind}
    H --> I
    I -->|body| J[_scan_body_operation\n+ _body_kind_method]
    I -->|manual| K[skip generation]
    I -->|simple / table_upload| L[existing handlers]
    J --> M{refetch_with_retrieve?}
    M -->|yes| N[request_raw + hardcoded version_id\n→ self.retrieve]
    M -->|no + body_type| O[request_json → resp.parsed]
    M -->|no + return_shape=list| P[request_raw → list comprehension]
    M -->|no + return_type| Q[request_raw → .from_dict]
    M -->|no + none| R[request_raw void]
    B --> S[_render_registry\nwhole-file APIs only]
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "codegen: support body/manual operation k..." | Re-trigger Greptile

Comment on lines +363 to +388
return_import: roe._generated.models.agent_job_status.AgentJobStatus
parameters:
- name: job_id
location: path
annotation: str
coerce: uuid
- kind: body
method_name: retrieve_result
docstring: ''
method: GET
path: /v1/agents/jobs/{agent_job_id}/result/
endpoint_module: roe._generated.api.agents.agents_jobs_result_retrieve
return_type: AgentJobResultResponse
return_import: roe._generated.models.agent_job_result_response.AgentJobResultResponse
parameters:
- name: job_id
location: path
annotation: str
coerce: uuid
- kind: body
method_name: cancel
docstring: ''
method: POST
path: /v1/agents/jobs/{job_id}/cancel/
endpoint_module: roe._generated.api.agents.agents_jobs_cancel_create
parameters:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 retrieve_status return type contradicts the hand-written facade

The fixture declares return_type: AgentJobStatus, but AgentJobsAPI.retrieve_status in agents.py (line 234-241) returns AgentJobSingleStatus and calls AgentJobSingleStatus.from_dict(response.json()). The fixture's own header states "swapping this file into openapi/wrappers.yml and running scripts/generate_wrappers.py must reproduce the existing public surface exactly." Activating this in B2 will silently change the method's return type, breaking any caller that type-checks against AgentJobSingleStatus.

Fix in Claude Code

Comment on lines +521 to +532
refetch_args = ", ".join(param["name"] for param in path_params)
lines.append(_request_call("response = ", "request_raw"))
lines.append(
" data = response.json()\n"
' version_id = data.get("id") if isinstance(data, dict) else None\n'
" if version_id is None:\n"
" raise RoeAPIException(\n"
' f"Unexpected response from server: status={response.status_code}"\n'
" )\n"
" # POST returns a partial create payload; re-fetch to get the full version.\n"
f" return self.retrieve({refetch_args}, str(version_id))\n"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Hardcoded version_id local variable can shadow a path parameter

The refetch path always introduces a local variable named version_id = data.get("id"). Any future refetch_with_retrieve operation that includes a path parameter also named version_id will have that parameter silently shadowed: the generated method will receive version_id as an argument, then immediately overwrite it with the server response value before the self.retrieve(...) call. The current fixture avoids this only because agents.versions.create has agent_id as its sole path parameter, but the generator itself provides no guard against the collision.

Fix in Claude Code

Comment on lines +550 to +551
elif return_shape == "list":
label = operation["list_error_label"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing guard on list_error_label produces a cryptic KeyError

When return_shape: list is specified but list_error_label is absent from the operation spec, operation["list_error_label"] raises a bare KeyError with no useful diagnostic. The generator already validates other constraints (e.g. return_type required, body_type forbidden) before reaching this line; adding an explicit check here keeps error messages consistent.

Suggested change
elif return_shape == "list":
label = operation["list_error_label"]
elif return_shape == "list":
if "list_error_label" not in operation:
raise ValueError(
f"return_shape: list requires list_error_label "
f"(operation {method_name!r})"
)
label = operation["list_error_label"]

Fix in Claude Code

@jadenfix

Copy link
Copy Markdown
Contributor Author

Closing as superseded by the focused SDK fanout stack: #56 for baseline contract fanout and #57 for complete public SDK coverage. This older codegen branch should not be rebased into the current release stack.

@jadenfix jadenfix closed this Jun 18, 2026
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.

1 participant