Skip to content

[Feature] Modernization: pydantic types generated from the OpenAPI spec and GraphQL schema - #99

Merged
fostimus merged 4 commits into
masterfrom
df/modernize-generated-types
Oct 28, 2026
Merged

fostimus merged 4 commits into
masterfrom
df/modernize-generated-types

Conversation

@fostimus

@fostimus fostimus commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Layer 3 of 4, on master now that #97 and #98 have merged. python_anvil.types is the Python counterpart of @anvilco/types: pydantic models generated from the API's OpenAPI spec and GraphQL schema, so the request types stop drifting from the API by hand.

  • types/rest.py: the REST request bodies, from schema/openapi.json.
  • types/graphql.py: every type in the GraphQL schema, from schema/anvil_schema.graphql.
  • types/operations.py: the arguments of every query and mutation, like CreateEtchPacketArgs, generated from the same schema with graphql-core.
  • dev/fetch_schema.py refreshes both specs (the schema from the published @anvilco/types package, so the two clients stay in step, and the OpenAPI spec from production; neither needs a key). dev/generate_types.py regenerates offline, and CI fails if the generated files are out of date with schema/.
  • The hand-written payload models extend the generated ones: FillPDFPayload, EtchSigner, CreateEtchPacketPayload and ForgeSubmitPayload now accept every option the API has and reject misspelled enum values, with the same dumps as before.
  • Also here, since it was found after [Feature] Modernization: one requests transport with base_url, OAuth, adaptive rate limits and typed errors #98 merged: create_etch_packet and forge_submit dict payloads accept camelCase keys, as their docstrings always said; the builders raised on them. The camel_to_snake helper now keeps acronyms whole (webhookURL -> webhook_url, mergePDFs -> merge_pdfs).
  • The Sync API Types workflow is anvil-js's: after a production deploy, and weekly as a backstop, it refreshes the specs, regenerates, and opens a PR when anything changed.

The workflow needs a CI_GITHUB_TOKEN secret, and python-anvil added to Foundry's --webhook-repos list in anvil-infra for the deploy dispatch; until then only the weekly and manual runs happen. Both are in CONTRIBUTING.md.

An audit of this layer against #98 found one regression, fixed in the last commit: a signer's token_valid_until had become a pydantic datetime, which the JSON encoder can't send and which rejected a date with no timezone. It is a plain string again. The same commit hardens the generator (the import rewrite handles a wrapped import, files are UTF-8, fetches and the sync job time out, the generator is pinned to a minor since CI byte-compares its output) and keeps font_size and routing_order as lenient as 5.x.

A final compatibility audit ran every call pattern from 5.1.2's docs, examples and tests against the stack: every request body was identical. The gaps it found are closed in the last commit, and the friction it found is removed without changing any existing call: Anvil() reads ANVIL_API_KEY; every method takes timeout; query/mutate take timeout and JSON-string variables; create_etch_packet and forge_submit take a JSON string and dicts holding any kind of file or model instances; an in-person signer works; response_query may omit its braces; an uploaded open file keeps its filename; GeneratePDFPayload is typed like the other payloads; and the 5.x extension points (BaseQuery.mutation, CREATE_ETCH_PACKET, FORGE_SUBMIT, client.api_key) still exist.

How to verify

  • With ANVIL_API_KEY set, Anvil() works; without it, the error says to set it
  • anvil.fill_pdf(..., timeout=600) and anvil.query(q, '{"eid": "x"}', timeout=10) work
  • anvil.create_etch_packet({"name": ..., "files": [{"id": "tpl", "castEid": ...}, EtchCastRef(...)], "signers": [EtchSigner(...)]}) sends all of them; anvil.create_etch_packet('{"name": ...}') works
  • create_etch_packet(packet, response_query="eid status") sends { eid status }
  • DocumentUpload(file=open("nda.pdf", "rb")) is sent as nda.pdf, not document.pdf
  • A 5.x BaseQuery subclass with mutation = "... {query} ..." and mutation_res_query still sends through mutate
  • EtchSigner(..., tokenValidUntil="2030-01-01T00:00:00Z") dumps the string unchanged and json.dumps accepts it; a date with no timezone is accepted too
  • FillPDFPayload(data=..., font_size=36) and EtchSigner(..., routing_order=0) are accepted, as in 5.x
  • create_etch_packet({"name": ..., "webhookURL": ..., "mergePDFs": True}) and forge_submit({"forgeEid": ..., "payload": {}}) accept the camelCase keys
  • make generate is a no-op on this branch (make check-generated passes)
  • FillPDFPayload(data=..., use_interactive_fields=True) dumps useInteractiveFields and does not send outputFormat unless set
  • EtchSigner(..., signature_mode="wave") is a ValidationError; "draw" is accepted
  • CreateEtchPacketPayload is a CreateEtchPacketArgs and dumps allowUpdates and mergePDFs under those names
  • Every variable in the client's createEtchPacket, forgeSubmit, generateEtchSignURL, removeWeldData and etchPacket operations is a field of the matching *Args model
  • The existing create_etch_packet and forge_submit tests still produce the same variables as before this layer

Breaking Changes

The payload models validate options the way the API documents them: an enum value the API doesn't accept, or a number outside its documented range (other than font_size and routing_order), is a ValidationError before anything is sent, where 5.x passed it through for the API to reject. Listed in the changelog. (ForgeSubmitPayload.webhook_url becoming a str moved down to #98.)

@fostimus
fostimus added this pull request to stack #101 October 21, 2026 21:04
@fostimus fostimus changed the title Generate pydantic types from the API's OpenAPI spec and GraphQL schema [Feature] Modernization: pydantic types generated from the OpenAPI spec and GraphQL schema Oct 21, 2026
@fostimus
fostimus force-pushed the df/modernize-generated-types branch from a95dd54 to a21feed Compare October 21, 2026 22:18
@fostimus
fostimus force-pushed the df/modernize-generated-types branch 7 times, most recently from 6e392d6 to 94b5c87 Compare October 28, 2026 17:52
Base automatically changed from df/modernize-transport to master October 28, 2026 17:52
@fostimus
fostimus force-pushed the df/modernize-generated-types branch 2 times, most recently from 59612b7 to 8cd0be9 Compare October 28, 2026 20:13
fostimus and others added 3 commits October 28, 2026 20:14
…documented

The docstrings said a dict payload's keys may be snake_case or camelCase,
but the builders passed the keys straight to their constructors, so a
camelCase key raised. The keys are converted now, with a camel_to_snake
that keeps acronyms whole: webhookURL -> webhook_url, mergePDFs ->
merge_pdfs. The old one gave webhook_u_r_l.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
python_anvil.types is the Python counterpart of @anvilco/types:

- types/rest.py: the REST request bodies, from schema/openapi.json
- types/graphql.py: every type in the GraphQL schema, from
  schema/anvil_schema.graphql
- types/operations.py: the arguments of every query and mutation, like
  CreateEtchPacketArgs, generated from the same schema with graphql-core

dev/fetch_schema.py refreshes both specs (the schema from the published
@anvilco/types package, so the clients stay in step, and the OpenAPI spec
from production; neither needs a key), and dev/generate_types.py regenerates
the types offline. CI fails if the types are out of date with the specs.

The hand-written payload models now extend the generated ones, so
FillPDFPayload, EtchSigner, CreateEtchPacketPayload and ForgeSubmitPayload
accept every option the API has and check enum values, instead of drifting
from the API by hand.

The Sync API Types workflow is anvil-js's: it runs after a production
deploy (and weekly as a backstop), refreshes the specs, regenerates, and
opens a PR when anything changed. It needs the CI_GITHUB_TOKEN secret, and
python-anvil added to Foundry's --webhook-repos list for the dispatch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An audit of the generated types against the transport layer found one
regression: `EtchSigner.token_valid_until` had become a pydantic
AwareDatetime, which turned the string a caller passed into a datetime
the JSON encoder can't send, and rejected a date with no timezone. It is
a plain string, like emails already were.

- The import rewrite that makes a pydantic type a plain `str` handles a
  wrapped import too. It only matched a one-line import, so the first
  sync that added a few pydantic types would have produced a module that
  tried to import `str` from pydantic.
- `font_size` and `routing_order` keep accepting any value, as in 5.x;
  the other documented ranges and enums the generated models now check
  are in the changelog as a breaking change.
- datamodel-code-generator is pinned to a minor, since its output is
  byte-compared in CI. Files are read and written as UTF-8, the spec
  fetches time out, the sync job has a timeout, and its changelog note
  goes under Unreleased once per release rather than once ever.
- A generated field starting with `model_` doesn't warn.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@fostimus
fostimus force-pushed the df/modernize-generated-types branch from 8cd0be9 to a669e6a Compare October 28, 2026 20:16
…asier

A compatibility audit against 5.1.2's docs, examples and tests found every
request body identical, and these gaps, all closed:

- `query` and `mutate` take `timeout`, like `request_graphql`, and
  `variables` as a JSON string, which the 5.x CLI sent.
- An empty dict payload raises TypeError as it did; the builder's message
  says `name` is required rather than naming an optional field.
- A `BaseQuery` subclass with its own `mutation` template, the 5.x
  extension point, still works; CREATE_ETCH_PACKET and FORGE_SUBMIT are
  still exported; the client keeps `api_key` and `access_token`.

And what the audit found tedious, with every existing call unchanged:

- `Anvil()` reads ANVIL_API_KEY, the way every example and the CLI do.
- Every method takes `timeout`.
- `create_etch_packet` and `forge_submit` take a JSON string, as the PDF
  methods do; their dicts may hold any kind of file (template reference,
  Markdown, HTML, upload) or model instances, where only an upload dict
  worked; an in-person signer is accepted; `response_query` may be given
  without braces; an uploaded open file is sent under its own name.
- `GeneratePDFPayload` extends the generated request like the other
  payload models, so page, logo and output options are typed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@fostimus
fostimus marked this pull request as ready for review October 28, 2026 22:31
@fostimus
fostimus merged commit 99d3b25 into master Oct 28, 2026
7 checks passed
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