Skip to content

Generate the SP-API SDK with oagen (standalone client, models, resources), Amazon plugin, benchmarks - #24

Merged
dbritto-dev merged 38 commits into
mainfrom
claude/relaxed-shannon-r0j70o
Sep 12, 2026
Merged

dbritto-dev merged 38 commits into
mainfrom
claude/relaxed-shannon-r0j70o

Conversation

@dbritto-dev

@dbritto-dev dbritto-dev commented Sep 10, 2026 •

Copy link
Copy Markdown
Owner

Summary

Replaces the hand-written requests client with an SDK generated from Amazon's API models by an oagen emitter (codegen/). oagen is a framework for building SDK generators, not a generator: it parses the spec into a typed IR and the Python emitter in codegen/src/python/ (TypeScript) turns that IR into files. The emitter is built after the reference prompt for an oagen Python emitter (five modules, the verified 0.30.2 ground truth, vitest over an inline fixture and an end-to-end test of the output as the definition of done), the repository is laid out like workos/openapi-spec (committed spec, src/policy/, thin oagen.config.ts, sdk:* scripts that are plain oagen invocations), and src/amzn_selling_partner/sdk/ is the generated, standalone SDK (client, HTTP layer on httpx2 with the retry/throttling policy, errors, pydantic models, resources), committed so installing the package pulls in no generator. Everything Amazon-specific that is not in the specs (regions, LWA auth with Restricted Data Tokens and grantless scopes, document helpers, notification models) is a plugin on top (plugins.amazon_spapi). The 0.1.x entry points (client, reports, vendor, utils) keep working as wrappers.

Design notes and decisions are in docs/PLAN.md; the breaking changes are in MIGRATION.md; docs/UPDATING_SPECS.md describes a spec bump and the generator; codegen/README.md lists the generator commands and the oagen API discrepancies met on the way. The docs lead with the async client and use the SDK's enums and models in every example.

Review decisions applied: Python 3.10+; the package name stays amzn_selling_partner; pydantic v2 for schemas/validation/parsing; build-time generation; pytest-benchmark instead of a hand-written harness, no nightly job; oagen's resolved method names with operationHints for collisions; sync and async clients with the same surface (with_options(); http_client= injects any httpx2 client with its own defaults honoured; the SDK's own clients come from a factory method whose public products, DefaultHttpxClient, DefaultAsyncHttpxClient and DefaultAioHttpClient, are what a caller configures and injects, httpx2 being the default and aiohttp an explicit choice), auto-pagination helpers, header parameters and non-JSON bodies, py.typed; one models package per API version; the security checks job (bandit + safety) kept as on main; no wrapper scripts, testing lives in the test suites; ruff is the only linter and formatter and ty the only type checker; releases bump minor unless the branch commits the component to .github/release-bump; the HTTP layer delegates to httpx2 (request building, base URL, encoding, pooling, connection retries) rather than rebuilding it.

What is in the branch

  • codegen/spec/open-api-spec.yaml – the one OpenAPI 3 document every oagen command runs against, committed like the example's spec/open-api-spec.yaml. npm run spec:build writes it from Amazon's 67 Swagger 2.0 files and 23 notification schemas (swagger2openapi, #ref/dangling-reference repairs, components namespaced per API version, operations tagged with their service, sample AWS key IDs in Amazon's example URLs redacted because GitHub's push protection rejects them). spec/petstore.yaml is the same for the test fixtures. CI fails on drift.
  • codegen/src/policy/ – the resolution policy behind a barrel: operation-hints.ts (the 29 colliding derived names), mount-rules.ts (the pricing v0 split), transforms.ts (transformSpec: alias inlining, inline-object hoisting, name protection against oagen's name cleaner; schemaNameTransform; operationIdTransform). oagen.config.ts is the thin shim that spreads the emitter plugin, the policy and emitterOptions.python (the sdkBehavior overrides). test/policy.test.ts checks every hint names an operation and every mount rule a service of the committed spec.
  • codegen/src/python/ – the emitter, as the reference prescribes: types.ts (TypeRef → annotation, exhaustive with assertNever), models.ts (models + enums: every model of a package in models/<pkg>/__init__.py, required fields first, X | None = None, snake_case attributes with wire aliases; enums in models/<pkg>/enums.py with __str__ = str.__str__), resources.ts (<Service>Resource / Async<Service>Resource, one method per resolved operation; path params, body and required params positional, optional ones keyword-only; params/headers built explicitly, then self._http.request(...); iter_<method> for the 69 paginated operations; SERVICES/OPERATIONS registry), client.ts (client.py with Client/AsyncClient, one lazily created resource per service, latest-version aliases and with_options(); __init__.py; errors.py from the error policy; _http.py built on httpx2: the httpx2 client comes from a factory method (_create_client, one product per sync/async subclass) whose products are the public factories DefaultHttpxClient, DefaultAsyncHttpxClient (httpx2's transport, connection retries from the policy, transport options build the transport) and DefaultAioHttpClient (the aiohttp transport, whose httpx-targeted implementation is driven through an adapter to httpx2's transport interface); SDK-created clients carry the base_url and every request goes through build_request, so joining, URL/query/header encoding and merging are httpx2's, and a user-supplied http_client keeps its own defaults; the module adds only the status-code retries with backoff and Retry-After from the SDK behavior in the IR, per-operation token buckets, the Auth hook, decoding through cached TypeAdapters and paginate/apaginate), index.ts (assembles the Emitter; its formatCommand runs ruff over every file oagen writes, so generation has no formatting step of its own); src/plugin.ts exports { emitters, extractors, smokeRunners }. npm scripts: spec:build, sdk:parse, sdk:resolve, sdk:generate (into a clean output dir), generate, sdk:diff, test, typecheck, build.
  • Definition of done, verified – npm run generate produces both SDKs from clean output directories and a second run changes nothing; npm test (37 vitest tests: inline fixture spec written to a temp file for field ordering, nullable rendering, enum emission, header params and non-JSON bodies; the tutorial's tasks-api.yml; spec build, policy and helpers) and tsc --noEmit are clean; tests/test_sdk_end_to_end.py runs the generated Amazon SDK over httpx2.MockTransport, sync and async (a list with an enum query parameter whose value, not its name, reaches the wire, a create with a body model, a 404), tests/petstore_sdk covers every operation shape and a Hypothesis fuzz test encodes parameters through real requests; no generated file is hand-edited (CI regenerates and fails on drift).
  • Amazon plugin – SellingPartner(Client) / AsyncSellingPartner: regional servers + Marketplace, LWA auth (refresh-token and client_credentials grants, single-flight cache, pluggable store, RDTs via the Tokens API looked up by operationId, with_rdt() opt-in), document download/upload helpers, notification model registry, sandbox example helpers.
  • sandbox_tests.py – runs every operation through both clients over httpx2.MockTransport using the x-amzn-api-sandbox examples read from the raw model files.
  • benchmarks/ – pytest-benchmark suite over httpx2.MockTransport asserting the generated method stays within 1.15× of an equivalent hand-written httpx2 call (both build the request with httpx2 on a client with base_url; the margin is the request context, throttle lookup, retry bookkeeping and typed error mapping the generated path adds); runs in CI on every push.
  • CI – a codegen job (Node 24, oagen's declared engine: npm ci --ignore-scripts, npm run build, npm run typecheck, npm test, npm run generate, git diff --exit-code over the spec and the generated code), the Python matrix 3.10–3.13 (ruff, ty check over hand-written and generated code, pytest, benchmarks, wheel content check) and the security job from main (nox -s security_test: bandit over the package, generated code included, plus safety over the dependencies, Python 3.10–3.13). bandit's false positives in generated code (the pagination token_param= keyword, enum members named like credentials, the retry jitter) are marked with targeted # nosec comments by the emitter, so real findings still fail the job.
  • Release – every push to main bumps the minor version (uv version --bump minor --no-sync) unless the branch committed major, minor or patch to .github/release-bump; the release commit removes the file, so the next push is back to minor. This branch commits major, so merging it releases the next major however the merge is done (merge commit, squash or rebase).

Numbers (this container)

Tests 120 pytest, 37 vitest, ty check clean (generated code included), ruff clean, bandit clean
Generated method vs equivalent hand-written httpx2 call (best of rounds) sync 1.07–1.10×, async 1.10–1.11× (limit 1.15×)
Sandbox examples, all 55 APIs 1964 passed / 70 failed of 2034 cases; every failure is an Amazon example that violates its own schema; orders and listings_items are clean and run in CI
Generated files 233 (Amazon) + 13 (petstore fixtures), identical on a second npm run generate

Decisions to confirm (docs/PLAN.md)

  1. Package and distribution names unchanged (amzn-selling-partner / amzn_selling_partner); Python 3.10+ (so __str__ = str.__str__ rather than StrEnum).
  2. Method names are oagen's resolved names (list_orders, get_order); sdk.resources.OPERATIONS maps Amazon's operationIds to them. Collisions are settled in src/policy/operation-hints.ts.
  3. LWA-only auth; boto3 / SigV4 dropped (compat shims warn or raise).
  4. One resource and one models package per API version; enums are str enums; generated code and the merged spec committed, CI fails on drift.
  5. The RDT table (plugins/_amazon/rdt.py) was written without access to developer-docs.amazon.com; please verify against the Tokens API use-case guide.
  6. The benchmark limit moved from 1.10× to 1.15× when the generated path and the baseline were put on the same httpx2 build_request path; keeping 1.10× would mean a fast path that bypasses build_request for SDK-created clients.

Packaging note

The wheel ships Python only (plus py.typed and oagen's .oagen-manifest.json, which lets oagen generate prune stale files); the spec submodule and codegen/spec/ are generator inputs. codegen/ needs Node 24 and npm ci --ignore-scripts (oagen depends on tree-sitter grammars for its compat extractors, which the generator never imports).

🤖 Generated with Claude Code

https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn

Add spec/selling-partner-api-models as a git submodule pinned to the commit in
spec/PINNED_COMMIT, plus the survey scripts used to produce docs/PLAN.md:
API/version naming rule, Swagger 2.0 -> IR mapping, resource grouping, sandbox
example coverage, rate-limit table parse results, pagination detection results
and the spec irregularities found. Implementation follows once the plan is
approved.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
…c runtime

Add the API-agnostic core of the spapi package:
- spec/: Swagger 2.0 and OpenAPI 3.x normalisers into a frozen dataclass IR,
  relative $ref resolution, JSON Schema documents, pickle IR cache keyed by
  spec hash.
- compile/: lazy pydantic v2 model namespaces (aliases, Literal enums,
  discriminated unions, allOf merging, recursion via module forward refs),
  CompiledOp with precomputed URL/query/header serializers, body encoders,
  per-status TypeAdapters, keyword-only signatures and pagination detection,
  and the sync/async resource factory.
- runtime/: BaseClient with retries (Retry-After, backoff with jitter),
  SyncAPIClient / AsyncAPIClient over httpx2, error hierarchy, pages,
  token-bucket throttling, auth hooks, SSE/byte streams, transports with an
  httpx_aiohttp bridge.
- client.py: Client / AsyncClient with lazy per-API loading and compilation.
- tests for the loader, models, operations, serializer fuzzing (hypothesis)
  and a parametrised sync/async MockTransport suite over every fixture op.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
- plugins/amazon_spapi.py: AmazonPlugin (regional servers, rate-limit table
  parsing with logged failures, pagination overrides, RDT and grantless
  annotations, sandbox example normalisation) and SellingPartner /
  AsyncSellingPartner clients.
- plugins/_amazon/: regions and Marketplace enum, naming rule and aliases,
  LWA auth (refresh-token and client_credentials grants, token cache with
  single-flight refresh, Restricted Data Tokens via the Tokens API),
  document download/upload helpers (gzip, streaming to file), typed
  notification models from schemas/notifications.
- sandbox_tests.py: runs every operation through both clients using the
  embedded x-amzn-api-sandbox examples over httpx2.MockTransport.
- runtime: APIResponseValidationError for bodies that do not match the spec,
  per-call auth hints (RequestOptions.auth / with_rdt()).
- loader: tolerate dangling $ref and the '#ref' typo found in vendor schemas.
- tests for rate-limit parsing (300 parsed / 73 unparseable), RDT and
  grantless tables, auth refresh and single-flight (sync and async),
  pagination walks with throttling, documents, notifications, and the
  sandbox runner over all orders and listings-items operations.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
…generator

- compile/typenames.py renders IR schemas as type expressions; compiled
  signatures now use those strings so building an API never creates models.
- Decoder / BodySpec build their TypeAdapters on first use (preload warms
  them); per-API compile from the IR cache drops from 88 ms worst case to
  39 ms (all 67 versions: 273 ms).
- spapi.stubgen emits .pyi stubs (models with TypedDict raw twins, typed
  resource methods with SyncPage/AsyncPage returns and raw overloads, and
  API/version protocols); --check verifies the committed stubs.
- stubs/ generated for the pinned Amazon models (pyright clean).
- scripts/report_load_times.py measures cold vs cached per-API build time.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
…typing

- amzn_selling_partner: 0.1.x entry points (BaseClient, SellingPartnerRegion,
  vendor.orders.Client, reports.Client, query/spec models, enums, utils)
  reimplemented over spapi.SellingPartner; AWS arguments deprecated.
- benchmarks/bench.py + echo_app.py: transport-only, BaseClient raw, full
  model decoding, dynamic-op vs hand-written (sync, async, aiohttp), and
  100 KB / 1 MB / 5 MB decode timings; --compare/--threshold for CI.
- CI: pytest, pyright --strict, ruff, stub freshness and a smoke benchmark on
  3.12/3.13 with the submodule; nightly benchmark with a 15% regression gate.
- pyright --strict clean on all hand-written code (typed JSON helpers in
  spec/_jsonutil.py, PEP 695 generics/aliases), ruff clean.
- README, MIGRATION.md (breaking changes, respx/pytest-httpx note with the
  MockTransport pattern), docs/UPDATING_SPECS.md; plan updated with the
  notification-schema and dataElements irregularities.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
uv_build cannot follow directory symlinks, so scripts/sync_specs.py copies the
pinned models and schemas into spapi/plugins/_amazon (git-ignored) before
building; the release and CI workflows run it and CI asserts the wheel carries
the spec files. Development keeps reading the submodule directly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
@dbritto-dev dbritto-dev changed the title Spec-driven SP-API SDK (spapi): plan, pinned models submodule, and implementation Spec-driven SP-API SDK (spapi): runtime-loaded Amazon models, sync/async clients, plugin, stubs, benchmarks Sep 11, 2026
@dbritto-dev
dbritto-dev marked this pull request as ready for review September 11, 2026 00:46
Copilot AI lite review requested due to automatic review settings September 11, 2026 00:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
Keep the 0.2.0 pyproject; regenerate uv.lock for the new dependency set.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
- replace PEP 695 generics/aliases with TypeVar/TypeAlias, StrEnum with
  (str, Enum), datetime.UTC with timezone.utc, typing.Self with the
  typing_extensions one under TYPE_CHECKING
- overall async deadline uses asyncio.timeout on 3.11+ and asyncio.wait_for
  on 3.10 (asyncio.TimeoutError mapped to APITimeoutError)
- requires-python >=3.10, classifiers, ruff/pyright targets, CI matrix
  3.10-3.13, docs updated; uv.lock regenerated

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
Move the spec-driven implementation from the working-name package spapi into
amzn_selling_partner (generic client module is _client.py so the 0.1.x
amzn_selling_partner.client compatibility package keeps its name). Model
modules are amzn_selling_partner.models.<api>.<version>, stubs live under
stubs/amzn_selling_partner, environment variables use the
AMZN_SELLING_PARTNER_ prefix, and the stubgen entry point is
amzn-selling-partner-stubgen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
- spec/raw.py: pydantic models of Swagger 2.0, OpenAPI 3.x and JSON Schema
  documents (extra keys kept for x-* extensions); every bundled file is
  validated on load
- IR nodes are pydantic dataclasses (validated on construction, still frozen,
  slotted, keyword-only and picklable); IR cache version bumped
- normalisers and the schema converter work on the typed raw models

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
@dbritto-dev dbritto-dev changed the title Spec-driven SP-API SDK (spapi): runtime-loaded Amazon models, sync/async clients, plugin, stubs, benchmarks Spec-driven SP-API SDK: runtime-loaded Amazon models, sync/async clients, plugin, stubs, benchmarks Sep 11, 2026
The runtime-interpretation layers (spec/, compile/, stubgen, stubs/, the IR
cache and scripts/sync_specs.py) are replaced by a build-time generator in
codegen/: a TypeScript project on top of @workos/oagen with a Python emitter.
The generated pydantic v2 models, Op tables and Sync/Async resource classes
are committed under src/amzn_selling_partner/{models,resources,apis.py}
(and tests/petstore_sdk for the generic fixtures); CI regenerates and fails
on drift.

Generator (codegen/):
- convert.ts: Swagger 2.0 -> OpenAPI 3.0 (swagger2openapi) after repairing
  the "#ref" typo and dangling references in the pinned files.
- transform.ts: pre-IR fixes oagen needs (inline alias schemas, hoist inline
  objects, opaque name tokens so acronyms and casing survive cleanSchemaName).
- emitter/: models (Literal enums, union aliases, builtin-safe class names),
  resources (typed keyword-only methods, decoders, rate limits, pagination),
  apis.py registry with typed lazy accessors; amazon.ts holds the naming,
  alias, pagination-override and drop-params policy.
- Facts the oagen IR drops (body required, response media types, default
  error response, greedy path params, verbatim operationIds) are recovered
  from the converted document.

Runtime:
- Op/Param/Body/Decoder literals in runtime/_op.py, serializers keyed by
  name/kind/style, SpecModel base, APIVersionsBase/APIs containers.
- BaseClient.call(op, kwargs, raw, paginate, request_options) is the entry
  point of every generated method; empty JSON bodies decode to None.
- LWA auth reads the RDT/grantless tables by (api, version, operationId);
  notifications use the generated payload models; the sandbox runner reads
  the raw model files from the submodule.

Verified: pytest (3.12), pyright --strict 0 errors over hand-written and
generated code, ruff, codegen unit tests, benchmark ratios 0.93/0.98/0.91,
sandbox runner 1964/2034 (remaining failures are example/schema mismatches
in the Amazon files).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
@dbritto-dev dbritto-dev changed the title Spec-driven SP-API SDK: runtime-loaded Amazon models, sync/async clients, plugin, stubs, benchmarks Spec-generated SP-API SDK (oagen): pydantic v2 models, typed sync/async resources, Amazon plugin, benchmarks Sep 11, 2026
claude and others added 6 commits September 11, 2026 12:55
- benchmarks/test_benchmarks.py replaces the hand-written harness and the
  uvicorn echo server: pytest-benchmark over httpx2.MockTransport (sync,
  async, decode groups) plus a test asserting the generated method stays
  within 1.10x of a hand-written httpx2 call. CI runs it on every push;
  the nightly benchmark workflow is removed. Dev extra: uvicorn ->
  pytest-benchmark.
- codegen: the driver keeps the converted OpenAPI 3 documents under
  .build/converted so the oagen CLI (`--lang python`) can run one spec at a
  time; oagen.config.ts now harvests the document extras and applies the
  Amazon policy, so its output matches the driver's byte for byte.
- Python 3.10 remains the floor (requires-python, ruff/pyright targets, CI
  matrix); tests and benchmarks verified on 3.10 and 3.12.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
…files

- codegen/ follows the `oagen init --lang python` shape: src/python/ is the
  emitter, src/plugin.ts the plugin bundle, src/index.ts the barrel,
  oagen.config.ts the consumer config (plugin + spec policy); package scripts
  are sdk:generate / typecheck / test. The emitter is a single `python`
  target configured per spec by the driver or the config.
- Removed: codegen/README.md (folded into docs/UPDATING_SPECS.md),
  codegen/.gitignore (covered by the root one), spec/PINNED_COMMIT (git
  records the submodule commit), runtime/_json.py (unused codec), the unused
  spec_path helper.
- docs/PLAN.md now holds only the current design notes and decisions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
…ure-spec emitter tests

Follow the WorkOS oagen tutorial where it applies: per-spec settings now
travel through oagen's own option bag (generateFiles({ emitterOptions }) in
the driver, emitterOptions.python in oagen.config.ts, ctx.emitterOptions in
the emitter) instead of a module-level configure() bag, and the emitter has
tests over a fixture spec in addition to the helper unit tests. Generated
output is unchanged (driver and CLI verified identical). docs/PLAN.md
records what the tutorial prescribes and where this project deliberately
differs (operationId method names, pydantic models, hand-written runtime
policy, no build step).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
The WorkOS tutorial's workflow now runs in this repository:

- tests/fixtures/tasks-api.yml is the tutorial's spec; `npm run sdk:parse`
  and `npm run sdk:resolve` inspect its IR and resolution table (`sdk:diff`
  for spec bumps), and `npm run sdk:generate -- --spec <spec> --namespace
  <Client>` turns one spec (OpenAPI 3 or Swagger 2.0, JSON or YAML) into a
  standalone package under codegen/sdk/ with <Client>/Async<Client>.
- The emitter is assembled from types.ts, enums.ts, models.ts, resources.ts
  and client.ts (apis.py + client.py rendering, shared by the petstore and
  single-spec targets); apis.ts is renamed to client.ts.
- Tests run on vitest as in the tutorial and the scaffold: models,
  resources and client tests over tasks-api.yml plus the helper unit tests.
- package.json carries the scaffold's script names; CI and nox use
  `npm run typecheck` / `npm run sdk:generate`.

Regenerated output changes only the petstore package docstrings. Docs
describe the tutorial's steps and where this project deliberately differs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
The emitter now produces the whole SDK from the IR, the way the tutorial
describes, instead of Op tables consumed by a hand-written runtime:

- codegen/src/spec/build.ts merges Amazon's 67 Swagger 2.0 files and 23
  notification schemas into one OpenAPI 3 document (components namespaced
  per API version, operations tagged with their service).
- oagen.config.ts carries the spec policy (transformSpec, name protection,
  operationHints for the 29 colliding derived names, mountRules for the
  pricing v0 split, emitterOptions.python with the sdkBehavior overrides).
- src/python/ (types, enums, models, resources, client, http_client, errors)
  generates src/amzn_selling_partner/sdk/: client.py, http_client.py
  (retry/backoff/timeout policy from the spec's SDK behavior, token buckets,
  auth hook, paginate helpers), errors.py, pydantic models and str enums per
  API version, resource classes with explicit parameter building and
  iter_<method> pagination helpers, and the OPERATIONS registry.
- Method names are oagen's resolved names (list_orders, get_order, ...);
  sdk.resources.OPERATIONS maps Amazon's operationIds to them.
- runtime/, apis.py, _client.py and the old models/resources are gone; the
  Amazon plugin, the 0.1.x wrappers and the sandbox runner sit on top of the
  generated client.
- Tests: 116 pytest (petstore_sdk generated from the fixtures, Amazon plugin,
  compat) and 28 vitest tests over the tutorial's tasks-api.yml; pyright
  strict is clean over everything; benchmarks 0.91x sync / 0.95x async.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
@dbritto-dev dbritto-dev changed the title Spec-generated SP-API SDK (oagen): pydantic v2 models, typed sync/async resources, Amazon plugin, benchmarks Generate the SP-API SDK with oagen (standalone client, models, resources), Amazon plugin, benchmarks Sep 11, 2026
dbritto-dev and others added 4 commits September 11, 2026 14:41
…e HEAD ref file

The generated client docstring carried 'ref: refs/he' locally and the commit
sha in CI, so the drift check failed. Resolve the commit with git rev-parse.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
- The spec build writes YAML (.build/openapi.yml, .build/petstore.yml),
  the format the tutorial's spec uses; oagen reads either.
- `npm run sdk:generate -- --spec <spec.yml> --namespace <Client>` is the
  oagen CLI as in the tutorial; `npm run regenerate` is the repository's
  full run (spec build, Amazon, petstore fixtures, ruff). CI runs
  `npm run build` (tsup) before generating, like the tutorial.
- src/plugin.ts calls registerEmitter() and exports myEmittersPlugin; the
  config spreads it. The CLI bundles its own registry, so the plugin also
  lists the emitter (what `oagen init` scaffolds).
- The emitter is assembled from generateModels(models, ctx),
  generateEnums(enums, ctx), generateResources(services, ctx),
  generateClient(spec, ctx) and generateHttpClient(spec, ctx), the
  signatures the tutorial uses; the package plan is derived from the
  context.
- Docs updated; PLAN.md notes the two tutorial features the released oagen
  (0.30.2) does not have (top-level sdkBehavior, parseSpec({ content })).

Generated output is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
The example repository separates the committed spec, the resolution policy
and the emitter; mirror that layout:

- codegen/spec/open-api-spec.yaml (and petstore.yaml): the merged OpenAPI 3
  document is now committed, written by `npm run spec:build`, and every
  `oagen` command runs against it (CI fails on drift like for the code).
- codegen/src/policy/: operation-hints.ts, mount-rules.ts, transforms.ts
  (transformSpec, schemaNameTransform, operationIdTransform) behind an
  index.ts barrel; oagen.config.ts is the thin shim that spreads the plugin,
  the policy and emitterOptions.python.
- scripts/sdk-generate.sh and scripts/sdk-diff.sh wrap the CLI; package.json
  gets the example's sdk:resolve, sdk:generate, sdk:diff, sdk:check scripts
  with the spec path baked in (sdk:diff defaults to last commit -> working
  tree).
- test/policy.test.ts checks the policy against the committed spec: every
  hint names an operation, every mount rule a service.
- README, docs/UPDATING_SPECS.md and docs/PLAN.md follow the new paths.

Generated code is unchanged (zero drift after `npm run regenerate`).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
claude and others added 15 commits September 11, 2026 17:00
…itter prompt

The emitter is rebuilt as the five modules the reference prescribes, with the
ground truth of @workos/oagen 0.30.2 applied:

- src/python/types.ts (TypeRef -> annotation, exhaustive with assertNever),
  models.ts (models + enums: every model of a package in
  models/<pkg>/__init__.py, required fields first, `X | None = None`; enums in
  models/<pkg>/enums.py with `__str__ = str.__str__`), resources.ts (one
  class per service, sync and async; path params, body and required params
  positional, optional ones keyword-only; params built explicitly, then
  self._http.request(...)), client.ts (root client with with_options(),
  __init__.py, errors.py from the error policy, _http.py with the retry /
  timeout constants from the SDK behavior), index.ts assembling the Emitter.
- src/plugin.ts exports { emitters, extractors, smokeRunners } as the
  scaffold does; generation starts from a clean output directory.
- Generated layout: __init__.py, client.py, _http.py, errors.py,
  models/<pkg>/{__init__,enums}.py, resources/<pkg>.py with
  <Service>Resource / Async<Service>Resource.
- vitest: inline fixture spec (temp file, as parseSpec takes a path) covering
  field ordering, nullable rendering, enum emission, header params and
  non-JSON bodies; the tutorial's tasks-api.yml for the rest.
- scripts/smoke.py and smoke_tasks.py (npm run smoke) prove the output runs
  over httpx2.MockTransport: list with an enum query param, create with a
  body model, a 404; the query string shows status=done.
- codegen/README.md lists the commands and the oagen API discrepancies met;
  CI runs on Node 24 (oagen's engines field); docs follow the new layout.

The hand-written package, tests and benchmarks import sdk._http and the
*Resource classes; pytest (117), pyright strict, ruff, the benchmark ratio
and the sandbox runner (1964/2034) are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
The CI rewrite had dropped the `security` job and its `security_test` nox
session. Both are back as on main (bandit over src/amzn_selling_partner,
safety over the dependencies, Python 3.10-3.13), with the `security-test`
extra in pyproject.

bandit now also scans the generated code; its false positives there are
marked in the emitter with targeted `# nosec` comments (B106 on the
pagination `token_param=` keyword, B105 on enum members named like
credentials, B311 on the retry jitter) and covered by vitest, and the two
hand-written constants that look like secrets to it (a header name and the
LWA token URL) carry the same markers. Real findings still fail the job.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
README, MIGRATION.md, the package docstring and the generated sdk/__init__.py
docstring now show AsyncSellingPartner / AsyncClient first (await, async for,
async with) and introduce SellingPartner / Client in one "Sync client"
section as the synchronous twin with the same surface.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
Examples pass Marketplace.US instead of "ATVPDKIKX0DER", compare order
statuses against OrderOrderStatus, build request bodies with
CreateFeedSpecification and filter with enum members; Marketplace and Region
are exported from the package for that. Trailing comments in the code blocks
are gone, the facts they carried moved into the prose.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
…er, smoke checks become tests

- codegen/scripts/ is gone: sdk:generate, sdk:diff and generate are plain
  oagen invocations in package.json; sdk:check and the regenerate alias go.
- src/format.ts and the format step are replaced by the emitter's
  formatCommand (ruff check --fix --select I,F401, then ruff format, over
  every file oagen writes), the hook the framework provides for this.
- The smoke scripts move into the Python suite as
  tests/test_sdk_end_to_end.py (Amazon SDK over httpx2.MockTransport: a list
  with an enum query parameter, a create with a body model, a 404, sync and
  async); the petstore tests already assert the enum value on the wire.
- noxfile keeps lint, type_check, test and security_test as on main; CI and
  the docs follow.

Generated code is unchanged (zero drift).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
- ty (astral) is the type checker: `uv run ty check` over the package and the
  petstore fixture, in nox and CI; pyright and its config are gone. The two
  sync/async overrides in the generated HTTP layer that needed `type: ignore`
  are restructured (a shared bucket state, a separate async static-header
  auth) so no suppression is needed.
- ruff stays the only linter and formatter; the editor settings drop the
  pylint/flake8 leftovers.
- Release: `uv version --bump minor` on push to main, major when the head
  commit is marked breaking (`feat!:` / `BREAKING CHANGE`), and a
  workflow_dispatch input to pick the component by hand.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
httpx2 now does what it already does well: URL, query and header encoding
(httpx2.Request), connection pooling, timeouts, TLS and proxies, and
connection retries (HTTPTransport(retries=...)). The generated _http.py
keeps only what the spec's SDK behavior asks for on top: status-code
retries with backoff and Retry-After, per-operation token buckets, the auth
hook that sees the operation, decoding into the named model, pagination.

Gone with it: the hand-rolled query encoder and Joined string type, the
connection-error retry branch, the default Limits constant, the async total
deadline. Any other httpx2 transport option (limits, verify, proxy, http2)
passes straight through the client constructor. scalar() converts enums
before strings so a str enum without __str__ still encodes as its value.

Query strings now carry httpx2's form encoding (%2C for commas, + for
spaces); the tests assert that form, the fuzz test encodes through a real
request. Generated method vs hand-written call stays under the 1.10x limit
(sync 0.92x, async 0.97x).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
…the OpenAI SDK

Requests through a client passed as http_client= are built with that client's
build_request(), so its default headers, cookies and params apply (ours win
where they overlap; base URL, timeout and retries stay the SDK's). Clients the
SDK creates itself keep the direct httpx2.Request path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
… shared request path

SDK-created httpx2 clients carry base_url and every request goes through
build_request(), so joining, encoding and header merging are httpx2's for
owned and user-supplied clients alike (a user-supplied client gets the
absolute URL, its base_url being its own). set_base_url() goes.

The benchmark baseline now uses the same idiom (a client with base_url and
a relative path). The generated path adds the request context, throttle
lookup, retry bookkeeping and typed error mapping on top; after trimming
per-call allocations (no header merge or timeout dict when nothing changes,
inlined retry loops, a slotted RequestContext) that costs 10-11%, so the
limit is 1.15x, recorded in the benchmark's docstring.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
…the OpenAI SDK

The async client runs on httpx2's own transport by default. aiohttp is
chosen by passing http_client=DefaultAioHttpClient(...), an httpx2.AsyncClient
on httpx_aiohttp's transport (the aiohttp extra), exported from the SDK and
the package. The automatic transport selection (prefer_aiohttp) is gone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
…public factories

_BaseHttpClient._http() returns the injected http_client or the product of
_create_client(), the factory method the sync and async subclasses implement.
Its products are public and configurable: DefaultHttpxClient and
DefaultAsyncHttpxClient (httpx2's transport with the policy's connection
retries; transport options build the transport, the rest goes to the client)
and DefaultAioHttpClient (the aiohttp transport). Callers configure one, or
any httpx2 client, and inject it as http_client=.

The bridge that drives httpx_aiohttp's httpx-targeted transport through
httpx2's interface is named for what it is, an adapter.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
The release workflow honoured only the merge commit's message, which the
person merging writes. A branch now commits the component to
`.github/release-bump` (this one: major); the workflow reads it and the
release commit removes it, so the next push goes back to minor. Manual
runs still pick their own component and the breaking-commit markers keep
working.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
Drop the breaking-commit markers and the manual component input: the
marker file is the one way a branch picks its release, and uv rejects an
invalid component itself.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
@dbritto-dev
dbritto-dev merged commit 661cd67 into main Sep 12, 2026
13 checks passed
@dbritto-dev
dbritto-dev deleted the claude/relaxed-shannon-r0j70o branch September 12, 2026 00:18
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.

3 participants