Summary
This RFC proposes a GraphQL codegen pipeline for the Python client (robosystems-python-client) that brings it to parity with the TypeScript client. The TypeScript client (@robosystems/client) uses GraphQL Code Generator to emit typed query documents and per-query response types directly from the backend schema. The Python client has no equivalent — its GraphQL facade returns dict[str, Any], so there's no IDE narrowing, no type errors when the schema drifts, and every new backend field has to be wired in by hand.
Motivation: Problem
Today the Python client hand-maintains its GraphQL surface:
- 30+ query strings are hand-written across
graphql/queries/{ledger,investor,library}/__init__.py (~900 LOC in the ledger module alone).
- Each query has a hand-written
parse_* helper that walks the response dict and normalizes camelCase → snake_case.
- Facade methods (e.g.
LedgerClient.list_event_blocks) return list[dict[str, Any]] / dict[str, Any] — no autocomplete, no narrowing, and schema drift is silent.
In TypeScript, adding a field to a GraphQL type takes effect in callers automatically after a regenerate. In Python the same field is invisible until someone hand-edits the query string and its parser.
Consumers that feel this today:
- Demo scripts and examples
- The Python MCP server / agents that query the platform programmatically
- Internal automation and CLI tooling
- Customer-facing Python integrations (eventually)
Motivation: Why Now?
The gap widens every time the GraphQL surface grows. Recent additions (AR/AP query surface, new Information Block rule fields) each had to be brought to parity by hand — a manual query + parser edit that codegen would have eliminated. Establishing the pipeline now caps that recurring cost before the surface grows further.
Proposed Design: Overview
Adopt ariadne-codegen — the closest analog to graphql-code-generator. It reads a schema (SDL or introspection) plus a directory of .graphql operation files and emits a strongly-typed Pydantic v2 client, which aligns with the existing robosystems_client/models/ style.
Proposed Design: Detailed Design
Shape:
- Hand-authored
.graphql operation files under graphql/operations/{ledger,investor,library}/.
- Generated Pydantic response models + query constants (checked in) under
graphql/generated/.
- Facade methods return the generated typed model directly instead of
dict[str, Any].
- A checked-in
schema.graphql so codegen is hermetic, plus a just/script recipe to introspect the backend and refresh it.
# before
def list_event_blocks(self, *, graph_id: str, ...) -> list[dict[str, Any]]: ...
# after
def list_event_blocks(self, *, graph_id: str, ...) -> list[EventBlockSummary]: ...
Per-query-change workflow: author/edit the .graphql file → re-run the generate recipe → the generated module exports a typed response model + the query document → the facade method imports the model and returns it directly (no manual parsing).
Alternatives Considered
| Alternative |
Pros |
Cons |
ariadne-codegen (recommended) |
Closest 1:1 analog to graphql-code-generator; Pydantic v2 output; emits sync + async; actively maintained |
Adds a checked-in generated artifact to keep in sync |
gql + gql-codegen |
gql is a widely-used runtime client |
Codegen emits dataclasses/untyped dicts, not Pydantic; weaker generation story |
turms |
Generates Pydantic models from schema + queries |
Smaller community; reasonable fallback only |
Why this approach: ariadne-codegen matches the TS workflow conceptually, emits real Python (not runtime magic), and produces Pydantic v2 models consistent with the existing client models.
Implementation: Phases
Each phase is independently shippable; Phase 1 lands alone as a proof point.
- Pilot one domain — add
ariadne-codegen config + a generate-graphql script, extract a single ledger query end-to-end, verify the typed response model and IDE narrowing.
- Migrate the ledger domain — move all ledger queries to
.graphql operation files, refactor facade methods, delete the hand-written query module + parsers.
- Migrate investor + library domains — same pattern, both much smaller than ledger.
- CI drift detection — fail a PR if regenerated output differs from the checked-in artifacts (mirrors the TS client's gate).
- Demo + MCP migration — update examples and the Python MCP server to the typed returns; the MCP serialization layer calls
model_dump() before responding.
Implementation: Dependencies
- No backend changes required; depends only on the existing backend GraphQL schema as the source of truth.
- The MCP serialization layer needs a
model_dump() step once facade returns become Pydantic models (confirm early, in Phase 1).
Risks & Mitigations
| Risk |
Mitigation |
Breaking callers that rely on dict access (row["x"]) |
Coordinate a single major version bump (client is pre-1.0); optionally a one-cycle dual-emit wrapper preserving __getitem__/.get() |
| Generated artifacts drift from the schema |
CI gate fails a PR when regenerated output differs from checked-in artifacts (Phase 4) |
| Tooling integration friction |
turms is a viable Pydantic-emitting fallback if ariadne-codegen hits issues |
Open Questions
Out of Scope
- Not a backend change — the GraphQL schema stays as-is.
- Not a TypeScript client change — that pipeline is already in this shape.
- Not a runtime transport change — the httpx transport stays; codegen runs at build time.
- Not a REST/OpenAPI change — this covers the GraphQL portion only.
References
ariadne-codegen: https://github.com/mirumee/ariadne-codegen
- TS client GraphQL codegen:
robosystems-typescript-client
- Python client hand-written queries:
robosystems-python-client/robosystems_client/graphql/queries/
Summary
This RFC proposes a GraphQL codegen pipeline for the Python client (
robosystems-python-client) that brings it to parity with the TypeScript client. The TypeScript client (@robosystems/client) uses GraphQL Code Generator to emit typed query documents and per-query response types directly from the backend schema. The Python client has no equivalent — its GraphQL facade returnsdict[str, Any], so there's no IDE narrowing, no type errors when the schema drifts, and every new backend field has to be wired in by hand.Motivation: Problem
Today the Python client hand-maintains its GraphQL surface:
graphql/queries/{ledger,investor,library}/__init__.py(~900 LOC in the ledger module alone).parse_*helper that walks the response dict and normalizes camelCase → snake_case.LedgerClient.list_event_blocks) returnlist[dict[str, Any]]/dict[str, Any]— no autocomplete, no narrowing, and schema drift is silent.In TypeScript, adding a field to a GraphQL type takes effect in callers automatically after a regenerate. In Python the same field is invisible until someone hand-edits the query string and its parser.
Consumers that feel this today:
Motivation: Why Now?
The gap widens every time the GraphQL surface grows. Recent additions (AR/AP query surface, new Information Block rule fields) each had to be brought to parity by hand — a manual query + parser edit that codegen would have eliminated. Establishing the pipeline now caps that recurring cost before the surface grows further.
Proposed Design: Overview
Adopt
ariadne-codegen— the closest analog tographql-code-generator. It reads a schema (SDL or introspection) plus a directory of.graphqloperation files and emits a strongly-typed Pydantic v2 client, which aligns with the existingrobosystems_client/models/style.Proposed Design: Detailed Design
Shape:
.graphqloperation files undergraphql/operations/{ledger,investor,library}/.graphql/generated/.dict[str, Any].schema.graphqlso codegen is hermetic, plus ajust/script recipe to introspect the backend and refresh it.Per-query-change workflow: author/edit the
.graphqlfile → re-run the generate recipe → the generated module exports a typed response model + the query document → the facade method imports the model and returns it directly (no manual parsing).Alternatives Considered
ariadne-codegen(recommended)graphql-code-generator; Pydantic v2 output; emits sync + async; actively maintainedgql+gql-codegengqlis a widely-used runtime clientturmsWhy this approach:
ariadne-codegenmatches the TS workflow conceptually, emits real Python (not runtime magic), and produces Pydantic v2 models consistent with the existing client models.Implementation: Phases
Each phase is independently shippable; Phase 1 lands alone as a proof point.
ariadne-codegenconfig + agenerate-graphqlscript, extract a single ledger query end-to-end, verify the typed response model and IDE narrowing..graphqloperation files, refactor facade methods, delete the hand-written query module + parsers.model_dump()before responding.Implementation: Dependencies
model_dump()step once facade returns become Pydantic models (confirm early, in Phase 1).Risks & Mitigations
row["x"])__getitem__/.get()turmsis a viable Pydantic-emitting fallback ifariadne-codegenhits issuesOpen Questions
schema.graphqlfor hermetic builds vs introspect a running backend on each regen. Leaning checked-in + a refresh recipe.ariadne-codegenemits both; the existing facade is sync. Leaning sync now, async as a follow-up.Out of Scope
References
ariadne-codegen: https://github.com/mirumee/ariadne-codegenrobosystems-typescript-clientrobosystems-python-client/robosystems_client/graphql/queries/