Repository navigation
Generate the SP-API SDK with oagen (standalone client, models, resources), Amazon plugin, benchmarks - #24
Merged
Conversation
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
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
dbritto-dev
marked this pull request as ready for review
September 11, 2026 00:46
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
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
- 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
…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
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RDH3P3vmiBCWwaDYnvpWHn
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the hand-written
requestsclient 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 incodegen/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/, thinoagen.config.ts,sdk:*scripts that are plainoageninvocations), andsrc/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 inMIGRATION.md;docs/UPDATING_SPECS.mddescribes a spec bump and the generator;codegen/README.mdlists 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 withoperationHintsfor 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,DefaultAsyncHttpxClientandDefaultAioHttpClient, 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 onmain; 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 everyoagencommand runs against, committed like the example'sspec/open-api-spec.yaml.npm run spec:buildwrites 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.yamlis 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.tsis the thin shim that spreads the emitter plugin, the policy andemitterOptions.python(thesdkBehavioroverrides).test/policy.test.tschecks 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 withassertNever),models.ts(models + enums: every model of a package inmodels/<pkg>/__init__.py, required fields first,X | None = None, snake_case attributes with wire aliases; enums inmodels/<pkg>/enums.pywith__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/headersbuilt explicitly, thenself._http.request(...);iter_<method>for the 69 paginated operations;SERVICES/OPERATIONSregistry),client.ts(client.pywithClient/AsyncClient, one lazily created resource per service, latest-version aliases andwith_options();__init__.py;errors.pyfrom the error policy;_http.pybuilt on httpx2: the httpx2 client comes from a factory method (_create_client, one product per sync/async subclass) whose products are the public factoriesDefaultHttpxClient,DefaultAsyncHttpxClient(httpx2's transport, connection retries from the policy, transport options build the transport) andDefaultAioHttpClient(the aiohttp transport, whose httpx-targeted implementation is driven through an adapter to httpx2's transport interface); SDK-created clients carry thebase_urland every request goes throughbuild_request, so joining, URL/query/header encoding and merging are httpx2's, and a user-suppliedhttp_clientkeeps its own defaults; the module adds only the status-code retries with backoff andRetry-Afterfrom the SDK behavior in the IR, per-operation token buckets, theAuthhook, decoding through cachedTypeAdapters andpaginate/apaginate),index.ts(assembles theEmitter; itsformatCommandruns ruff over every file oagen writes, so generation has no formatting step of its own);src/plugin.tsexports{ emitters, extractors, smokeRunners }. npm scripts:spec:build,sdk:parse,sdk:resolve,sdk:generate(into a clean output dir),generate,sdk:diff,test,typecheck,build.npm run generateproduces 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'stasks-api.yml; spec build, policy and helpers) andtsc --noEmitare clean;tests/test_sdk_end_to_end.pyruns the generated Amazon SDK overhttpx2.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_sdkcovers 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).SellingPartner(Client)/AsyncSellingPartner: regional servers +Marketplace, LWA auth (refresh-token andclient_credentialsgrants, 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 overhttpx2.MockTransportusing thex-amzn-api-sandboxexamples read from the raw model files.benchmarks/– pytest-benchmark suite overhttpx2.MockTransportasserting the generated method stays within 1.15× of an equivalent hand-writtenhttpx2call (both build the request with httpx2 on a client withbase_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.codegenjob (Node 24, oagen's declared engine:npm ci --ignore-scripts,npm run build,npm run typecheck,npm test,npm run generate,git diff --exit-codeover the spec and the generated code), the Python matrix 3.10–3.13 (ruff,ty checkover hand-written and generated code, pytest, benchmarks, wheel content check) and thesecurityjob frommain(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 paginationtoken_param=keyword, enum members named like credentials, the retry jitter) are marked with targeted# noseccomments by the emitter, so real findings still fail the job.mainbumps the minor version (uv version --bump minor --no-sync) unless the branch committedmajor,minororpatchto.github/release-bump; the release commit removes the file, so the next push is back to minor. This branch commitsmajor, so merging it releases the next major however the merge is done (merge commit, squash or rebase).Numbers (this container)
ty checkclean (generated code included), ruff clean, bandit cleanordersandlistings_itemsare clean and run in CInpm run generateDecisions to confirm (docs/PLAN.md)
amzn-selling-partner/amzn_selling_partner); Python 3.10+ (so__str__ = str.__str__rather thanStrEnum).list_orders,get_order);sdk.resources.OPERATIONSmaps Amazon's operationIds to them. Collisions are settled insrc/policy/operation-hints.ts.strenums; generated code and the merged spec committed, CI fails on drift.plugins/_amazon/rdt.py) was written without access to developer-docs.amazon.com; please verify against the Tokens API use-case guide.build_requestpath; keeping 1.10× would mean a fast path that bypassesbuild_requestfor SDK-created clients.Packaging note
The wheel ships Python only (plus
py.typedand oagen's.oagen-manifest.json, which letsoagen generateprune stale files); the spec submodule andcodegen/spec/are generator inputs.codegen/needs Node 24 andnpm 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