mirumee / ariadne-codegen

Generate fully typed Python client for any GraphQL API from schema, queries and mutations
BSD 3-Clause "New" or "Revised" License
275 stars 35 forks source link
ariadne client codegen codegenerator graphql python

Ariadne

Build Status


Ariadne Code Generator

Python code generator that takes graphql schema, queries, mutations and subscriptions and generates Python package with fully typed and asynchronous GraphQL client.

It's available as ariadne-codegen command and reads configuration from the pyproject.toml file:

$ ariadne-codegen

It can also be run as python -m ariadne_codegen.

Features

Installation

Ariadne Code Generator can be installed with pip:

$ pip install ariadne-codegen

To support subscriptions, default base client requires websockets package:

$ pip install ariadne-codegen[subscriptions]

Configuration

ariadne-codegen reads configuration from [tool.ariadne-codegen] section in your pyproject.toml. You can use other configuration file with --config option, eg. ariadne-codegen --config custom_file.toml

Minimal configuration for client generation:

[tool.ariadne-codegen]
schema_path = "schema.graphql"
queries_path = "queries.graphql"

Required settings:

One of the following 2 parameters is required, in case of providing both of them schema_path is prioritized:

Optional settings:

Custom operation builder

The custom operation builder allows you to create complex GraphQL queries in a structured and intuitive way.

Example Code

import asyncio
from graphql_client import Client
from graphql_client.custom_fields import (
    ProductFields,
    ProductTranslatableContentFields,
    ProductTranslationFields,
    TranslatableItemConnectionFields,
    TranslatableItemEdgeFields,
)
from graphql_client.custom_queries import Query
from graphql_client.enums import LanguageCodeEnum, TranslatableKinds

async def get_products():
    # Create a client instance with the specified URL and headers
    client = Client(
        url="https://saleor.cloud/graphql/",
        headers={"authorization": "bearer ..."},
    )

    # Build the queries
    product_query = Query.product(id="...", channel="channel-uk").fields(
        ProductFields.id,
        ProductFields.name,
    )

    translation_query = Query.translations(kind=TranslatableKinds.PRODUCT, first=10).fields(
        TranslatableItemConnectionFields.edges().alias("aliased_edges").fields(
            TranslatableItemEdgeFields.node.on(
                "ProductTranslatableContent",
                ProductTranslatableContentFields.id,
                ProductTranslatableContentFields.product_id,
                ProductTranslatableContentFields.name,
            )
        )
    )

    # Execute the queries with an operation name
    response = await client.query(
        product_query,
        translation_query,
        operation_name="get_products",
    )

    print(response)

# Run the async function
asyncio.run(get_products())

Explanation

  1. Building the Product Query:
    1. The Query.product(id="...", channel="channel-uk") initiates a query for a product with the specified ID and channel.
    2. .fields(ProductFields.id, ProductFields.name) specifies the fields to retrieve for the product: id and name.
  2. Building the Translation Query:
    1. The Query.translations(kind=TranslatableKinds.PRODUCT, first=10) initiates a query for product translations.
    2. .fields(...) specifies the fields to retrieve for the translations.
    3. .alias("aliased_edges") renames the edges field to aliased_edges.
    4. .on("ProductTranslatableContent", ...) specifies the fields to retrieve if the node is of type ProductTranslatableContent: id, product_id, and name.
  3. Executing the Queries:
    1. The client.query(...) method is called with the built queries and an operation name "get_products".
    2. This method sends the queries to the server and retrieves the response.

Example pyproject.toml configuration.

Note: queries_path is optional when enable_custom_operations is set to true

[tool.ariadne-codegen]
schema_path = "schema.graphql"
include_comments = "none"
target_package_name = "example_client"
enable_custom_operations = true

Plugins

Ariadne Codegen implements a plugin system that enables further customization and fine-tuning of generated Python code. It’s documentation is available separately in the PLUGINS.md file.

Standard plugins

Ariadne Codegen ships with optional plugins importable from the ariadne_codegen.contrib package:

Using generated client

Generated client can be imported from package:

from {target_package_name}.{client_file_name} import {client_name}

Example with default settings:

from graphql_client.client import Client

Passing headers to client

Client (with default base client), takes passed headers and attaches them to every sent request.

client = Client("https://example.com/graphql", {"Authorization": "Bearer token"})

For more complex scenarios, you can pass your own http client:

client = Client(http_client=CustomComplexHttpClient())

CustomComplexHttpClient needs to be an instance of httpx.AsyncClient for async client, or httpx.Client for sync.

Websockets

To handle subscriptions, default AsyncBaseClient uses websockets and implements graphql-transport-ws subprotocol. Arguments ws_origin and ws_headers are added as headers to the handshake request and ws_connection_init_payload is used as payload of ConnectionInit message.

File upload

Default base client (AsyncBaseClient or BaseClient) checks if any part of variables dictionary is an instance of Upload. If at least one instance is found then client sends multipart request according to GraphQL multipart request specification.

Class Upload is included in generated client and can be imported from it:

from {target_package_name} import Upload

By default we use this class to represent graphql scalar Upload. For schema with different name for this scalar, you can still use Upload and default client for file uploads:

[tool.ariadne-codegen.scalars.OTHERSCALAR]
type = "Upload"

Open Telemetry

When config option opentelemetry_client is set to true then default, included base client is replaced with one that implements the opt-in Open Telemetry support. By default this support does nothing but when the opentelemetry-api package is installed and the tracer argument is provided then the client will create spans with data about performed requests.

Tracing arguments handled by BaseClientOpenTelemetry:

AsyncBaseClientOpenTelemetry supports all arguments which BaseClientOpenTelemetry does, but also exposes additional arguments regarding websockets:

Custom scalars

By default, not built-in scalars are represented as typing.Any in generated client. You can provide information about specific scalar by adding section to pyproject.toml:

[tool.ariadne-codegen.scalars.{graphql scalar name}]
type = "(required) python type name"
serialize = "function used to serialize scalar"
parse = "function used to create scalar instance from serialized form"

For each custom scalar client will use given type in all occurrences of {graphql scalar name}. If provided, serialize and parse will be used for serialization and deserialization. In result models type will be annotated with BeforeValidator, eg. Annotated[type, BeforeValidator(parse)]. In inputs annotation will use PlainSerializer, eg. Annotated[type, PlainSerializer(serialize)]. If type/serialize/parse contains at least one . then string will be split by it's last occurrence. First part will be used as module to import from, and second part as type/method name. For example, type = "custom_scalars.a.ScalarA" will produce from custom_scalars.a import ScalarA.

Example with scalar mapped to built-in type

In this case scalar is mapped to built-in str which doesn't require custom serialize and parse methods.

[tool.ariadne-codegen.scalars.SCALARA]
type = "str"

Example with type supported by pydantic

In this scenario scalar is represented as datetime, so it needs to be imported. Pydantic handles serialization and deserialization so custom parse and serialize is not necessary.

[tool.ariadne-codegen.scalars.DATETIME]
type = "datetime.datetime"

Example with fully custom type

In this example scalar is represented as class TypeB. Pydantic can't handle serialization and deserialization so custom parse and serialize is necessary. To provide type, parse and serialize implementation we can use files_to_include to copy type_b.py file.

[tool.ariadne-codegen]
...
files_to_include = [".../type_b.py"]

[tool.ariadne-codegen.scalars.SCALARB]
type = ".type_b.TypeB"
parse = ".type_b.parse_b"
serialize = ".type_b.serialize_b"
# inputs.py

class TestInput(BaseModel):
    value_b: Annotated[TypeB, PlainSerializer(serialize_b)]
# get_b.py

class GetB(BaseModel):
    query_b: Annotated[TypeB, BeforeValidator(parse_b)]
# client.py

class Client(AsyncBaseClient):
    async def test_mutation(self, value: TypeB) -> TestMutation:
        ...
        variables: Dict[str, object] = {
            "value": serialize_b(value),
        }
        ...

Extending generated types

Extending models with custom mixins

mixin directive allows to extend class generated for query/mutation field with custom logic. mixin takes two required arguments:

Generated class will use import as extra base class, and import will be added to the file.

from {from} import {import}
...
class OperationNameField(BaseModel, {import}):
    ...

This directive can be used along with files_to_include option to extend functionality of generated classes.

Example of usage of mixin and files_to_include:

Query with mixin directive:

query listUsers {
    users @mixin(from: ".mixins", import: "UsersMixin") {
        id
    }
}

Part of pyproject.toml with files_to_include (mixins.py contains UsersMixin implementation)

files_to_include = [".../mixins.py"]

Part of generated list_users.py file:

...
from .mixins import UsersMixin
...
class ListUsersUsers(BaseModel, UsersMixin):
    ...

Multiple clients

To generate multiple different clients you can store config for each in different file, then provide path to config file by --config option, eg.

ariadne-codegen --config clientA.toml
ariadne-codegen --config clientB.toml

Generated code dependencies

Generated code requires:

Both httpx and websockets dependencies can be avoided by providing another base client class with base_client_file_path and base_client_name options.

Example

Example with simple schema and few queries and mutations is available here.

Generating a copy of GraphSQL schema

Instead of generating a client, you can generate a file with a copy of a GraphQL schema. To do this call ariadne-codegen with graphqlschema argument:

ariadne-codegen graphqlschema

graphqlschema mode reads configuration from the same place as client but uses only schema_path, remote_schema_url, remote_schema_headers, remote_schema_verify_ssl options to retrieve the schema and plugins option to load plugins.

In addition to the above, graphqlschema mode also accepts additional settings specific to it:

target_file_path

A string with destination path for generated file. Must be either a Python (.py), or GraphQL (.graphql or .gql) file.

Defaults to schema.py.

Generated Python file will contain:

Generated GraphQL file will contain a formatted output of the print_schema function from the graphql-core package.

schema_variable_name

A string with a name for schema variable, must be valid python identifier.

Defaults to "schema". Used only if target is a Python file.

type_map_variable_name

A string with a name for type map variable, must be valid python identifier.

Defaults to "type_map". Used only if target is a Python file.

Contributing

We welcome all contributions to Ariadne! If you've found a bug or issue, feel free to use GitHub issues. If you have any questions or feedback, don't hesitate to catch us on GitHub discussions.

For guidance and instructions, please see CONTRIBUTING.md.

Also make sure you follow @AriadneGraphQL on Twitter for latest updates, news and random musings!

Crafted with ❤️ by Mirumee Software hello@mirumee.com