Skip to main content

graphql-codegen powered by pydantic

Project description

turms

codecov PyPI version Maintenance Maintainer PyPI pyversions PyPI status PyPI download month

turms is a graphql-codegen-inspired code generator for Python. It reads your GraphQL schema and documents and generates fully typed, serializable Python code — Pydantic models for clients, Strawberry types for servers. Write your queries in plain GraphQL, and get autocomplete, type checking, and validation in your IDE for free.

query get_countries($filter: CountryFilterInput) {
  countries(filter: $filter) {
    code
    name
  }
}

⬇️ turms gen ⬇️

class GetCountriesCountries(BaseModel):
    typename: Optional[Literal["Country"]] = Field(alias="__typename")
    code: str
    name: str

class GetCountries(BaseModel):
    countries: List[GetCountriesCountries]

    class Arguments(BaseModel):
        filter: Optional[CountryFilterInput] = None

    class Meta:
        document = "query get_countries($filter: CountryFilterInput) { ... }"

turms is a development-time tool only: the generated code depends on Pydantic (or Strawberry), never on turms itself. Nothing of turms ships with your application.

Features

  • Fully typed, fully documented code generation — GraphQL descriptions become docstrings, deprecations become warnings
  • Client-side generation from documents: enums, inputs, fragments and operations as Pydantic models (v2 and v1)
  • Server-side generation from SDL schemas: typed Strawberry scaffolds with resolver stubs
  • Operation functions — call get_capsules() instead of assembling query strings (sync and async, via the funcs plugin)
  • Transport agnostic — works with rath, gql, or any HTTP client you like
  • Extensible pipeline — plugins, parsers, stylers, and processors are all swappable and configurable
  • Custom scalars, custom bases, frozen (hashable) models, interface catch-alls, and more
  • Code migration — the merge processor preserves your hand-written resolver bodies across regenerations
  • graphql-config compliant — one config file, multiple projects
  • Watch mode — regenerate on every save

Installation

Because turms is a development-time tool that never becomes a runtime dependency, the easiest way to use it is to not install it at all and run it with uvx:

uvx turms init   # scaffold a config
uvx turms gen    # generate code

uvx downloads turms into a cached, isolated environment and runs it — your project stays clean. turms init adapts to your environment: it only scaffolds formatter processors (black, isort) into the config when those tools are actually installed, so the generated config always works out of the box.

For reproducible builds, pin the version: uvx "turms==0.11.0" gen.

As a project dev dependency

If you prefer turms tracked in your project (so the whole team uses the same version), add it as a development dependency:

uv add --dev "turms[black,isort]"     # uv
pip install "turms[black,isort]"      # pip / requirements-dev.txt

and run it with uv run turms gen (or plain turms gen in an activated environment). Optional extras pull in the tools used by specific components:

Extra Enables
black BlackProcessor
isort IsortProcessor
ruff RuffProcessor
merge MergeProcessor (libcst)

Python 3.10 or higher is required.

Quickstart

1. Scaffold a config in your project root:

uvx turms init

This creates a graphql.config.yaml:

projects:
  default:
    schema: https://countries.trevorblades.com/
    documents: graphql/**.graphql
    extensions:
      turms:
        out_dir: examples/api
        plugins:
          - type: turms.plugins.enums.EnumsPlugin
          - type: turms.plugins.inputs.InputsPlugin
          - type: turms.plugins.fragments.FragmentsPlugin
          - type: turms.plugins.operations.OperationsPlugin
          - type: turms.plugins.funcs.FuncsPlugin
        scalar_definitions:
          uuid: str

If formatters are installed in your environment, init also wires them up — e.g. with black and isort available, a processors section with BlackProcessor and IsortProcessor is added automatically.

2. Write a query in graphql/countries.graphql:

fragment Continent on Continent {
  code
  name
}

query get_countries($filter: CountryFilterInput) {
  countries(filter: $filter) {
    code
    name
    continent {
      ...Continent
    }
  }
}

3. Generate:

uvx turms gen

4. Use the typed models with the client of your choice — every field is validated, aliased, and autocompleted:

from examples.api.schema import GetCountries, CountryFilterInput, StringQueryOperatorInput

variables = GetCountries.Arguments(
    filter=CountryFilterInput(code=StringQueryOperatorInput(eq="DE"))
)
response = my_client.execute(GetCountries.Meta.document, variables.model_dump(by_alias=True, exclude_unset=True))
countries = GetCountries(**response["data"])

for country in countries.countries:
    print(country.continent.name)  # typed all the way down

How it works

turms runs your schema and documents through a four-stage pipeline, each stage pluggable through the config:

GraphQL schema + documents
        │
   ┌────▼────┐   generate Python AST nodes
   │ Plugins │   (enums, inputs, fragments, operations, funcs, strawberry…)
   └────┬────┘
   ┌────▼────┐   transform the AST
   │ Parsers │   (e.g. polyfill typing imports for older Python)
   └────┬────┘
   ┌────▼────┐   style class/field names as they are generated
   │ Stylers │   (snake_case, capitalize, suffix appending…)
   └────┬────┘
   ┌────▼────┐   post-process the rendered source code
   │Processors│  (black, isort, ruff, merge, custom commands…)
   └────┬────┘
        ▼
   out_dir/schema.py

Plugins

Plugins generate the actual code. Enable them per project; order matters (enums and inputs should come before fragments and operations).

Plugin Generates
turms.plugins.enums.EnumsPlugin Enum classes from GraphQL enums
turms.plugins.inputs.InputsPlugin Pydantic models for input types
turms.plugins.objects.ObjectsPlugin Pydantic models for schema object types
turms.plugins.fragments.FragmentsPlugin Pydantic models for GraphQL fragments
turms.plugins.operations.OperationsPlugin One Pydantic model per query/mutation/subscription, with nested Arguments and Meta (the exact document)
turms.plugins.funcs.FuncsPlugin Typed, documented call functions per operation (sync + async) that delegate to your client through configurable proxies
turms.plugins.strawberry.StrawberryPlugin A Strawberry server schema with typed resolver stubs

The funcs plugin turns operations into plain function calls. With an executor proxy configured (see examples/rath-usage):

async def aget_capsules(rath: Rath = None) -> List[GetCapsulesCountries]:
    """get_capsules

    Arguments:
        rath (rath.Rath, optional): The client we want to use (defaults to the currently active client)

    Returns:
        List[GetCapsulesCountries]"""
    return (await aexecute(GetCapsules, {}, rath=rath)).countries

Stylers

Stylers normalize GraphQL naming to Python conventions — renamed fields automatically receive a Pydantic alias, so serialization stays wire-compatible.

Styler Effect
turms.stylers.default.DefaultStyler Capitalized class names + snake_case fields (recommended)
turms.stylers.capitalize.CapitalizeStyler Capitalizes the first letter of class names
turms.stylers.snake_case.SnakeCaseStyler Converts camelCase fields/arguments to snake_case
turms.stylers.appender.AppenderStyler Appends configurable suffixes (Fragment, Query, Mutation, …) to class names

Processors

Processors run over the rendered source before it is written to disk.

Processor Purpose
turms.processors.black.BlackProcessor Format with black
turms.processors.isort.IsortProcessor Sort imports with isort
turms.processors.ruff.RuffProcessor Format (format: true) and/or autofix lints (fix: true) with ruff
turms.processors.command.CommandProcessor Pipe the code through any command via stdin/stdout, e.g. command: "uvx ruff format -"
turms.processors.merge.MergeProcessor Merge regenerated code into the existing file, keeping your hand-written function bodies
turms.processors.disclaimer.DisclaimerProcessor Prepend a "this file is generated" disclaimer

Parsers

Parser Purpose
turms.parsers.polyfill.PolyfillPlugin Rewrites typing constructs for an older target python_version

Configuration

turms complies with graphql-config and discovers graphql.config.yaml|yml|json|toml (or .graphqlrc.*) in the working directory. Everything turms-specific lives under extensions.turms of a project. The most important options:

Option Default Meaning
out_dir "api" Output directory
generated_name "schema.py" Output file name
documents Glob of documents (overrides the project-level documents)
scalar_definitions {} Map of custom GraphQL scalars to Python types (DateTime: datetime.datetime) — required for every non-standard scalar
object_bases ["pydantic.BaseModel"] Base class(es) for generated models
interface_bases Separate base classes for interfaces
additional_bases {} Extra bases per GraphQL type name (great for mixin "traits")
freeze disabled Generate frozen (immutable, hashable) models; configurable per kind, with include/exclude lists
options disabled Set Pydantic model options (extra, use_enum_values, validate_assignment, …) per kind
exclude_typenames false Skip __typename literal fields
always_resolve_interfaces true Resolve interfaces to concrete implementation unions
create_catchall true Add a catch-all model for unknown interface implementations
skip_forwards false Skip generating forward-reference updates
pydantic_version "v2" Target Pydantic major version ("v1" or "v2")
omited_document_rules [] GraphQL validation rules to skip for documents
dump_schema / dump_configuration false Also write the resolved schema / project config next to the output

Every option can also be supplied through environment variables with the TURMS_ prefix (e.g. TURMS_OUT_DIR=generated), courtesy of Pydantic settings.

A schema can be an introspection URL (optionally with headers), a local SDL file, or a list of either:

schema:
  - https://api.example.org/graphql:
      headers:
        Authorization: Bearer ${TOKEN}

See the documentation website for the full configuration reference, including all per-plugin options.

Custom scalars

GraphQL only ships five builtin scalars (String, Int, Float, Boolean, ID) — everything else (DateTime, JSON, UUID, …) is schema-specific, and turms requires you to decide what each one becomes in Python via scalar_definitions:

scalar_definitions:
  DateTime: datetime.datetime # ISO strings are parsed into real datetime objects
  UUID: uuid.UUID             # validated UUIDs instead of raw strings
  JSON: typing.Any            # pass through untouched
  Slug: myapp.scalars.Slug    # your own type with custom validation

The mapping is more than a type annotation: because the generated models are Pydantic models, the type you choose drives parsing and validation. Map DateTime to str and you get raw strings; map it to datetime.datetime and every response is parsed into timezone-aware datetime objects on arrival — and serialized back correctly when you send inputs. Any dotted path works, so you can point at your own types (anything Pydantic can validate, e.g. a str subclass with __get_pydantic_core_schema__, or an annotated type with constraints) to centralize invariants like "a Slug is always lowercase" right in the deserialization layer.

Traits: extending generated models

Generated code shouldn't be edited — but it can be extended. additional_bases injects your own mixin classes ("traits") as base classes of every generated model for a given GraphQL type:

additional_bases:
  Country:
    - myapp.traits.CountryTrait
# myapp/traits.py
from pydantic import BaseModel, field_validator

class CountryTrait(BaseModel):
    @field_validator("code", check_fields=False)
    @classmethod
    def code_must_be_upper(cls, v: str) -> str:
        if not v.isupper():
            raise ValueError("country codes are uppercase")
        return v

    @property
    def flag_url(self) -> str:
        return f"https://flagcdn.com/{self.code.lower()}.svg"

Now every generated model that selects the Country type — top-level objects, fragments, and even the deeply nested classes inside operation results — inherits the trait:

class GetCountriesCountries(CountryTrait, BaseModel):
    code: str
    name: str

This gives you, everywhere that type appears:

  • Validators — enforce domain invariants (field_validator, model_validator) on data the moment it arrives from the API
  • Computed properties and methodscountry.flag_url, conversion helpers (.to_numpy(), .as_tuple()), business logic that travels with the data
  • isinstance checksisinstance(obj, CountryTrait) works across all generated variants of the type, however deeply nested the selection was

Traits are prepended to the base list, so their method resolution order beats the generated defaults.

Server-side generation (Strawberry)

Point turms at an SDL file and use the Strawberry plugin to scaffold a typed server:

projects:
  default:
    schema: beasts.graphql
    extensions:
      turms:
        out_dir: server
        skip_forwards: true
        stylers:
          - type: turms.stylers.default.DefaultStyler
        plugins:
          - type: turms.plugins.strawberry.StrawberryPlugin
        processors:
          - type: turms.processors.disclaimer.DisclaimerProcessor
          - type: turms.processors.black.BlackProcessor
          - type: turms.processors.merge.MergeProcessor
@strawberry.type
class Query:
    @strawberry.field(description="get all the beasts on the server")
    def beasts(self) -> Optional[List[Optional[Beast]]]:
        """get all the beasts on the server"""
        return None  # fill in your resolver — MergeProcessor keeps it on regeneration

With the MergeProcessor enabled you can evolve the schema and regenerate freely: your resolver implementations survive.

CLI

Command Description
turms init Create a starter graphql.config.yaml in the current directory. --template picks a scaffold (see below), --config the file name
turms gen [PROJECT] Generate all (or one named) project. --config selects a config file explicitly
turms watch [PROJECT] Watch the documents glob and regenerate on save
turms download Download a project's schema as SDL (--out suffix, --dir target directory)

Init templates

turms init --template <name> scaffolds sensible defaults for common setups:

Template Scaffolds
documents (default) Pydantic models (enums, inputs, fragments, operations) from your documents
rath documents plus typed call functions for the rath client (async + sync)
gql documents plus typed call functions for the gql client
strawberry A server-side Strawberry schema from a local SDL file, with disclaimer and merge-on-regenerate

Every template adapts to your environment: formatter processors (black, isort — and merge for strawberry) are only included when the tools are actually installed.

Examples

The examples/ directory contains complete, runnable setups:

Example Shows
pydantic-basic Plain document-based generation of Pydantic models
rath-usage Funcs plugin with async/sync proxies for the rath client
gql-usage Funcs plugin with the gql client passed as argument
beasts-strawberry Server-side Strawberry schema generation with merge-on-regenerate
countries-code Minimal generated client for the countries API

Transport layer

turms deliberately ships no transport layer — it generates models and (optionally) functions that delegate to whichever client you configure. If you want an Apollo-like GraphQL client for Python, have a look at rath, which pairs particularly well with turms.

Development

turms uses uv for project management:

uv sync --all-extras --dev   # install everything
uv run pytest                # run the test suite
uv run pytest --snapshot-update   # update generation snapshots after intentional changes

The architecture is plugin-first: to add your own plugin, subclass turms.plugins.base.Plugin and implement generate_ast(client_schema, config, registry); to add a processor, subclass turms.processors.base.Processor and implement run(gen_file, config). Any importable class can be referenced from the config by its dotted path — no registration step needed.

Contributions are welcome! Please open an issue or PR on GitHub.

Why "turms"?

In Etruscan religion, Turms (usually written as 𐌕𐌖𐌓𐌌𐌑 Turmś in the Etruscan alphabet) was the equivalent of Roman Mercury and Greek Hermes — the messenger god between people and gods. turms is the messenger between your GraphQL schema and your Python code.

License

MIT

Project details


Release history Release notifications | RSS feed

This version

1.0.1

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

turms-1.0.1.tar.gz (71.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

turms-1.0.1-py3-none-any.whl (88.5 kB view details)

Uploaded Python 3

File details

Details for the file turms-1.0.1.tar.gz.

File metadata

  • Download URL: turms-1.0.1.tar.gz
  • Upload date:
  • Size: 71.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for turms-1.0.1.tar.gz
Algorithm Hash digest
SHA256 ec9dfc657411426b8105c0bc505e7ce0dca1ec21b474c1a57835dfc8aeb1012a
MD5 1d51ae9cb375f0e3b58e1a66339e9ac7
BLAKE2b-256 45f7265f0c796b64390396405db2f9c1ceb21cfbbefbf8c4cb3f090b7288df2f

See more details on using hashes here.

File details

Details for the file turms-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: turms-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 88.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for turms-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4d061267e68389d7c9491c3b69d14a0243f621aa998bb4e861bd93187b08a0de
MD5 e42916ca03e52f330e3765d6e2aeb47b
BLAKE2b-256 03e8433d4347b18dccf40b2f4c530e58262014f10e4a2f3e8ccc1691bba79c65

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page