No description
Find a file
2026-04-09 09:38:42 -07:00
.github/workflows Add GitHub actions. 2026-03-25 13:59:28 -07:00
graphql_client_generator Add support for pretty indenting generated GQL queries. 2026-04-09 09:38:42 -07:00
tests Add support for pretty indenting generated GQL queries. 2026-04-09 09:38:42 -07:00
.gitignore Add initial version. 2026-03-25 13:52:50 -07:00
LICENSE Add initial version. 2026-03-25 13:52:50 -07:00
pyproject.toml Dynamically generate version. 2026-03-27 11:51:10 -07:00
README.md Add support for building queries from flat lists of fields rather than 2026-04-08 23:45:30 -07:00

GraphQL Client Generator

Generate typed, tab-completable Python clients from GraphQL schema files.

Warning: This project is in early beta. APIs may change without notice, edge cases abound, and there are dragons. Use in production at your own risk.

Installation

pip install graphql-client-generator

Requires Python >= 3.10.

Quick Start

Given a schema like bookstore.graphqls:

type Query {
  book(id: ID!): Book
  books(limit: Int = 10): [Book!]!
  genres: [Genre!]!
}

type Mutation {
  addBook(input: AddBookInput!): Book!
}

type Book {
  id: ID!
  title: String!
  isbn: String
  pageCount: Int
  author: Author!
  reviews: [Review!]!
}

type Author {
  id: ID!
  name: String!
  books: [Book!]!
}

type Review {
  id: ID!
  rating: Int!
  text: String
}

type Genre {
  id: ID!
  name: String!
  description: String
}

input AddBookInput {
  title: String!
  authorId: ID!
  isbn: String
}

Generate a client from a local schema file

python -m graphql_client_generator bookstore.graphqls

This produces a standalone Python package (bookstore/) with typed models, a client class, and a query builder.

To embed the generated client inside an existing package instead, pass --module to skip the pyproject.toml:

python -m graphql_client_generator bookstore.graphqls --module

Generate a client from a live endpoint

Pass an http:// or https:// URL instead of a file path and the schema is fetched automatically via GraphQL introspection:

python -m graphql_client_generator https://api.example.com/graphql -n bookstore

Use -H (repeatable) to add HTTP headers - useful for authenticated endpoints:

python -m graphql_client_generator https://api.example.com/graphql \
    -H "Authorization: Bearer $TOKEN" \
    -n bookstore

Generate from a notebook or script

generate_from_endpoint lets you pass a requests.Session directly, so any auth, cookies, or TLS settings you have already configured are reused:

import requests
import graphql_client_generator as gcg

session = requests.Session()
session.headers["Authorization"] = "Bearer <token>"

gcg.generate_from_endpoint(
    "https://api.example.com/graphql",
    name="bookstore",
    session=session,
    # as_package=False  # omit pyproject.toml when embedding in an existing package
)

CLI options

Flag Description Default
-n, --name Package name Schema filename stem (file) or client (URL)
-o, --output Output directory Current directory
--module Emit Python files only, no pyproject.toml Off
-H, --header HTTP header for introspection (Name: Value), repeatable

Use the generated client

from bookstore import BookstoreClient, BookstoreQuery, BookstoreMutation, Variable
from bookstore.outputs import Book, Author

client = BookstoreClient("https://api.example.com/graphql")

Query Builder

The generated BookstoreQuery object provides a typed, tab-completable interface for building GraphQL queries. Every field from the schema is available as an attribute with full IDE support.

Syntax

Syntax Meaning
Query.field Select a field
Query.field(arg=val) Pass arguments to a field
selector[field1, field2] Select sub-fields (tuple, list, or any iterable)
selector[lambda s: (s.a, s.b)] Lambda shorthand for sub-field selection
selector.as_("alias") Alias a field
selector.ALL Recursively select all fields without required args
selector.ALL_SCALAR Select only scalar fields (default when no sub-fields given)
selector.ALL_SHALLOW Select all fields one level deep (composites get scalar children)
Variable.name Reference a query variable
Query[...] Build a complete query

Examples

Basic query:

result = client.query(
    BookstoreQuery[
        BookstoreQuery.book(id=42)[
            Book.title,
            Book.isbn,
        ],
    ],
)
print(result.book.title)

Book.title and BookstoreQuery.book.title are equivalent -- use whichever reads better in context.

Multiple root fields:

result = client.query(
    BookstoreQuery[
        BookstoreQuery.book(id=42)[Book.title, Book.isbn],
        BookstoreQuery.genres,  # auto-selects all scalar sub-fields
    ],
)

Aliases:

result = client.query(
    BookstoreQuery[
        BookstoreQuery.book(id=42)[
            Book.title,
            Book.reviews.as_("recent_reviews")[
                Review.rating,
                Review.text,
            ],
        ],
    ],
)
print(result.book.recent_reviews)

Query variables:

result = client.query(
    BookstoreQuery[
        BookstoreQuery.book(id=Variable.book_id)[
            Book.title,
            Book.author[Author.name],
        ],
    ],
    variables={"book_id": 42},
)

Variable types are automatically inferred from the schema and included in the generated query string (e.g. query($book_id: ID!) { ... }).

Raw GraphQL strings are still supported:

result = client.query(
    '{ book(id: 42) { title author { name } } }',
)

Tab Completion

In Jupyter notebooks and IPython, pressing <Tab> after BookstoreQuery. or BookstoreQuery.book. lists all available fields. dir() on any FieldSelector returns its child fields.

Argument Validation

Fields that accept arguments have those arguments defined in the schema. If you pass an unknown argument, a TypeError is raised immediately:

BookstoreQuery.book(bad_arg=1)
# TypeError: Unknown argument 'bad_arg' for field 'book'. Valid arguments: id

Fields that take no arguments raise TypeError if called:

Book.title(x=1)
# TypeError: Field 'title' takes no arguments

Flat Path Syntax

Deep dotted paths in sub-field selections are automatically nested. Instead of manually wrapping each level with []:

BookstoreQuery[
    BookstoreQuery.book(id=42)[
        Book.title,
        Book.author[Author.name, Author.books[Book.title]],
    ],
]

You can write the paths flat:

Q = BookstoreQuery
BookstoreQuery[
    Q.book(id=42)[
        Q.book.title,
        Q.book.author.name,
        Q.book.author.books.title,
    ],
]

Paths through the same intermediate field are merged automatically (author.name and author.books.title become author { name books { title } }).

Selections are validated: using a path that doesn't match the receiver raises TypeError.

Lambda Shorthand

A callable passed to [] receives the field selector itself, avoiding the need to repeat the prefix:

BookstoreQuery[
    BookstoreQuery.book(id=42)[lambda b: (
        b.title,
        b.author.name,
        b.author.books.title,
    )],
]

The lambda (or any callable) can return a single selector, a tuple, a list, or any iterable.

Expansion Modes

When a composite-type field is used without explicit sub-field selections, its scalar fields are automatically included (ALL_SCALAR mode):

BookstoreQuery.genres
# Equivalent to:
BookstoreQuery.genres[Genre.id, Genre.name, Genre.description]

Three explicit expansion modes are available on any composite field:

Mode Behavior
.ALL_SCALAR Scalar fields only (default)
.ALL Recursively expand all fields that don't require arguments
.ALL_SHALLOW All fields one level deep; composite children get scalar-only expansion
# Fetch everything recursively (skips fields that require arguments)
BookstoreQuery[BookstoreQuery.book(id=42).ALL]

# Fetch scalars only
BookstoreQuery[BookstoreQuery.book(id=42).ALL_SCALAR]

# Fetch one level: scalars + immediate composites (with their scalars)
BookstoreQuery[BookstoreQuery.book(id=42).ALL_SHALLOW]

Response Objects

Query results are wrapped in GraphQLResponse objects that provide typed attribute access. Field names are automatically converted to snake_case.

result = client.query(
    BookstoreQuery[
        BookstoreQuery.book(id=42)[
            Book.title,
            Book.page_count,
            Book.author[Author.id, Author.name],
        ],
    ],
)

result.book.title        # str
result.book.page_count   # int
result.book.author       # GraphQLResponse (typed as Author)
result.book.author.name  # str

Repr

Response objects display with their schema type name and loaded fields:

>>> result.book
Book(title='The Great Gatsby', page_count=180, author=Author(id=7, name='F. Scott Fitzgerald'))

Long reprs automatically wrap with indentation at 80 characters.

Serialization

result.book.to_dict()   # -> {"title": "The Great Gatsby", "pageCount": 180, ...}
result.book.to_json()   # -> '{"title": "The Great Gatsby", "pageCount": 180, ...}'

Lazy Loading

If auto_fetch is enabled (the default), accessing a field that was not included in the original query triggers an automatic re-query:

result = client.query(
    BookstoreQuery[
        BookstoreQuery.book(id=42)[Book.title],
    ],
)

# 'author' was not in the query, but accessing it triggers a lazy fetch:
result.book.author  # automatically re-queries the server

The lazy loader:

  1. Finds the field's type from the schema metadata
  2. Determines which scalar sub-fields to request
  3. Modifies the original query to include the missing field
  4. Re-executes the query and extracts the new data
  5. Caches the result for subsequent access

Disable with auto_fetch=False:

client = BookstoreClient("...", auto_fetch=False)

Accessing an unloaded field then raises FieldNotLoadedError.

Mutations

The generated BookstoreMutation object works the same way as BookstoreQuery. When a mutation field takes a single Input type, its fields are flattened into keyword arguments -- no need to construct the Input object manually:

result = client.mutate(
    BookstoreMutation[
        BookstoreMutation.add_book(title="New Book", author_id="123")[
            Book.id,
            Book.title,
        ],
    ],
)
print(result.add_book.title)

The keyword arguments are forwarded to the AddBookInput constructor for validation, then serialized as the input argument in the GraphQL request.

Raw GraphQL strings are also accepted:

result = client.mutate(
    'mutation { addBook(input: { title: "New Book", authorId: "123" }) { id title } }',
)

Generated Package Structure

bookstore/                   # Project root (dist name, hyphens)
  bookstore/                 # Python module (import name, underscores)
    __init__.py              # Exports client, query builder, enums, inputs, outputs
    client.py                # BookstoreClient class
    schema.py                # BookstoreQuery and BookstoreMutation query builders
    outputs.py               # Schema types (Book, Author, ...) with SchemaField descriptors
    enums.py                 # Python Enum classes
    inputs.py                # @dataclass input types with to_dict()
    _runtime/                # Standalone runtime library
      builder.py             # Query builder (Variable, FieldSelector, SchemaField)
      model.py               # GraphQLResponse, lazy loading
      client.py              # GraphQLClientBase, HTTP transport
      query.py               # __typename insertion, query modification
      serialization.py       # Case conversion, input serialization
  pyproject.toml             # Package metadata

With --module the pyproject.toml is omitted and the source files are written directly into bookstore/ (no nesting), ready to drop into an existing package.

Schema Support

GraphQL Feature Python Representation
Object types GraphQLModel subclass with SchemaField descriptors
Interfaces Base class; implementing types inherit from it
Unions Type alias (A | B | C)
Enums enum.Enum subclass
Input types @dataclass with to_dict()
Custom scalars Mapped to Python types (DateTime -> str, JSON -> Any)
@oneOf inputs Validated in __post_init__ (exactly one field set)
__typename Automatically inserted into all queries

Type Mapping

GraphQL Python
String str
Int int
Float float
Boolean bool
ID str
DateTime str
JSON Any
[Type] list[Type]
Type! Non-null
Type Type | None