Skip to content

Typed Python SDK: GraphQL codegen parity with the TypeScript client #753

Description

@jfrench9

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.

  1. 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.
  2. Migrate the ledger domain — move all ledger queries to .graphql operation files, refactor facade methods, delete the hand-written query module + parsers.
  3. Migrate investor + library domains — same pattern, both much smaller than ledger.
  4. CI drift detection — fail a PR if regenerated output differs from the checked-in artifacts (mirrors the TS client's gate).
  5. 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

  • Schema source: check in schema.graphql for hermetic builds vs introspect a running backend on each regen. Leaning checked-in + a refresh recipe.
  • Sync vs async: ariadne-codegen emits both; the existing facade is sync. Leaning sync now, async as a follow-up.
  • Backward compatibility: hard cut to typed returns vs a dual-emit transition. The client is pre-1.0 and consumers are mostly internal, so a hard cut behind a coordinated version bump is likely simplest.
  • Typed inputs: variables also become Pydantic input models, which tightens facade signatures to match the GraphQL variable declarations.

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/

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No fields configured for RFC.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions