feat: generate response types from the API spec - #28
Merged
Conversation
Every route method was annotated `-> Dict` or `-> List[Dict]`, which tells a type checker nothing and an IDE less. The API's OpenAPI document now describes 209 of its 240 operations precisely, so the shapes are generated from it rather than hand-written - the arrangement datamaker-js already uses, and the one that stops an SDK drifting behind its API. spec/openapi.json -> src/datamaker/generated/schema.py -> src/datamaker/types.py 93 methods across 15 route modules now return a named type. TypedDicts, not pydantic: methods still return the plain dicts `response.json()` produces, so `project["name"]` works exactly as before and no existing code breaks. The types are checker-only. WHY THERE IS AN ALIAS MODULE. datamodel-code-generator lets an inline schema claim a bare name and renames the real component: the class called `Template` is an unrelated nested object from DatabaseTemplatesProposal, while the actual Template entity is `Template2`. Six of 123 schemas were affected, including Plan, Scenario and Template - the three this SDK uses most. Importing those blind would have shipped exactly the failure this work exists to prevent: a wrong type that type-checks. So the generator now derives `types.py` by matching KEY SETS against the spec on every run, and fails if any schema stops resolving uniquely. Nothing hand-written ever names a numbered class. Three schemas still cannot be generated - SetDetail, PackInstallListItem and SchemaGraphNavigation are composed with allOf, which the generator merges away instead of emitting. Those methods keep `Dict`. The list is explicit in the script and the audit fails if it grows. mypy found two mis-annotations while this was being written, both fixed rather than silenced: `read_file_by_path` returns raw bytes, not the metadata row its endpoint answers with, and `generate_from_template_id` was writing a `quantity` key onto a fetched Template. The latter now copies instead of mutating - identical behaviour, minus writing a field back onto a response object that the API never sends. Net mypy: 8 errors -> 6. Nothing introduced; two pre-existing bugs in connections.py fixed (a bool assigned into a dict inferred str-valued). CI gains the drift check and, for the first time, actually runs the tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merged
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.
Every route method in this package was annotated
-> Dictor-> List[Dict]. That tells a type checker nothing and an IDE less. The API's OpenAPI document now describes 209 of its 240 operations precisely, so the shapes are generated from it rather than hand-written — the arrangementdatamaker-jsalready uses, and the one that stops an SDK drifting behind its API.93 methods across 15 route modules now return a named type.
Non-breaking by construction
TypedDict, not pydantic. Methods still return the plain dictsresponse.json()produces, soproject["name"]works exactly as before and no existing code changes behaviour. The types are checker-only. 69 tests pass unchanged.Why there is an alias module
This is the part worth reviewing.
datamodel-code-generatorlets an inline schema claim a bare name and silently renames the real component. The generated class calledTemplateis an unrelated nested object fromDatabaseTemplatesProposal; the actualTemplateentity isTemplate2. Six of 123 schemas were affected — includingPlan,ScenarioandTemplate, the three this SDK uses most.My first pass imported those blind and shipped exactly the failure this work exists to prevent: a wrong type that type-checks. mypy caught it (
TypedDict "Template" has no key "id"), and an audit of all 123 schemas against their generated classes showed the full extent.So
scripts/generate_schema.pynow derivessrc/datamaker/types.pyby matching key sets against the spec on every run, and fails if any schema stops resolving uniquely. Nothing hand-written ever names a numbered class, and the unstable numbering cannot leak into the package.What is deliberately still
DictThree schemas cannot be generated:
SetDetail,PackInstallListItemandSchemaGraphNavigationare composed withallOf(zod's.extend()), which the generator merges away instead of emitting as a named class. Methods returning those keepDict. The list is explicit in the script and the audit fails if it grows, so this cannot quietly become four.The other untyped methods are the ones the API deliberately leaves bodyless upstream — connections,
/endpoints/auth-resolve,/export/rest— which return credentials or third-party bodies.Bugs found on the way, fixed rather than silenced
read_file_by_pathreturns raw bytes, not the metadata row its endpoint answers with. My endpoint-matcher got this wrong and mypy caught it.generate_from_template_idwas writing aquantitykey onto a fetchedTemplate. It now copies ({**template, "quantity": quantity}) — identical behaviour, minus writing a field back onto a response object the API never sends.connections.py: aboolassigned into a dict mypy infers as str-valued.Net mypy: 8 errors → 6. Nothing introduced. The remaining 6 are pre-existing (implicit-Optional defaults and two
Literalarg-types inmain.py) and left alone.Verification
pytest -m "not integration"— 69 passed, 24 skippedpython scripts/generate_schema.py --check— clean; verified it fails on a hand-editmypy src/datamaker— 6, all pre-existing, diffed line-number-insensitively againstmainuv build— sdist + wheelCI gains the drift check and, for the first time, actually runs the tests —
build.yamlonly ranuv buildbefore.Follow-up worth considering
The three
allOfschemas would generate cleanly if the API declared them as standalone objects rather than.extend(). That is a small upstream change inapps/datamaker-api/src/schemas/entities.tsand would benefitdatamaker-jstoo.Companion to automators-com/datamaker-js#21.
🤖 Generated with Claude Code