diff --git a/EXAMPLE.md b/EXAMPLE.md index 04c3131a..15dbbc35 100644 --- a/EXAMPLE.md +++ b/EXAMPLE.md @@ -156,7 +156,7 @@ $ ariadne-codegen Structure of generated package: ``` -grapql_client/ +graphql_client/ __init__.py async_base_client.py base_model.py @@ -169,7 +169,6 @@ grapql_client/ input_types.py list_all_users.py list_users_by_country.py - scalars.py upload_file.py ``` diff --git a/LICENSE b/LICENSE index 74b19090..ecf37548 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2025, Mirumee Labs +Copyright (c) 2026, Mirumee Labs All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index cd523cf9..10883222 100644 --- a/README.md +++ b/README.md @@ -4,555 +4,96 @@ [![Build Status](https://github.com/mirumee/ariadne-codegen/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/mirumee/ariadne-codegen/actions) -Python code generator that takes graphql schema, queries, mutations and subscriptions and generates Python package with fully typed and asynchronous GraphQL client. +Python code generator that turns a GraphQL schema and your operations into a fully typed, async (or sync) Python client built on Pydantic. You write your queries, mutations and subscriptions in GraphQL, and ariadne-codegen generates a typed Python method for each one, returning Pydantic models - so you get autocompletion and static type checking instead of hand-writing query strings and parsing raw JSON. -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`. +πŸ“– **[Documentation](docs/01-introduction.md)** Β· [Step-by-step example](docs/02-guides/01-step-by-step-example.md) Β· [Configuration reference](docs/03-reference/01-configuration.md) ## Features -- Generate pydantic models from schema types, inputs and enums. -- Generate pydantic models for GraphQL results. -- Generate client package with each GraphQL operation available as async method. +- **Fully typed models** - Pydantic models for schema types, inputs, enums, fragments, and every operation's result. +- **Typed client methods** - each query, mutation, and subscription becomes a method with typed arguments and a typed return value. +- **[Async or sync](docs/02-guides/10-async-vs-sync.md)** - generate an async client (default) or a synchronous one. +- **[Subscriptions](docs/02-guides/04-subscriptions.md)** - real-time updates over WebSockets (`graphql-transport-ws`). +- **[File uploads](docs/02-guides/05-file-uploads.md)** - multipart requests via the GraphQL multipart request spec. +- **[Custom scalars](docs/02-guides/06-custom-scalars.md)** - map GraphQL scalars to your own Python types with `serialize`/`parse` hooks. +- **[Extensible output](docs/02-guides/07-extending-types.md)** - inject mixins into generated models, copy in your own files, or swap the [base client](docs/03-reference/02-generated-code-dependencies.md) (custom auth, or to drop the `httpx`/`websockets` deps). +- **[Flexible schema sources](docs/02-guides/02-schema-sources.md)** - a local file, installed Python packages, or remote introspection. +- **[Plugin system](docs/04-plugins/01-intro.md)** - customize generation through hooks, plus ready-made plugins (shorter results, extracted operation strings, forward refs, …). +- **More** - [programmatic query building](docs/02-guides/12-custom-operation-builder.md), [OpenTelemetry tracing](docs/02-guides/09-opentelemetry.md), [multiple clients per project](docs/02-guides/08-multiple-clients.md), and a [schema-copy mode](docs/02-guides/11-schema-generation.md). ## Installation -Ariadne Code Generator can be installed with pip: +Requires Python 3.10 or newer. ``` pip install ariadne-codegen ``` -To support subscriptions, default base client requires `websockets` package: +Add subscription (WebSocket) support with: ``` 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` +## Quickstart -Minimal configuration for client generation: - -```toml -[tool.ariadne-codegen] -schema_path = "schema.graphql" -queries_path = "queries.graphql" -``` +Generate a typed client from three files. -Required settings: - -- `queries_path` - path to file/directory with queries (Can be optional if `enable_custom_operations` is used) - -Exactly one of the following 3 parameters is required. They are mutually exclusive - providing more than one raises a configuration error: - -- `schema_path` - path to file/directory with graphql schema -- `schema_paths` - list of schema sources; each entry can be a local path (file or directory) or a dotted Python attribute path (resolved at codegen time). See [Loading schema from installed packages](#loading-schema-from-installed-packages). -- `remote_schema_url` - url to graphql server, where introspection query can be perfomed - -Optional settings: - -- `remote_schema_headers` - extra headers that are passed along with introspection query, eg. `{"Authorization" = "Bearer token"}`. To include an environment variable in a header value, prefix the variable with `$`, eg. `{"Authorization" = "$AUTH_TOKEN"}` -- `remote_schema_verify_ssl` (defaults to `true`) - a flag that specifies wheter to verify ssl while introspecting remote schema -- `remote_schema_timeout` (defaults to `5`) - timeout in seconds while introspecting remote schema -- `remote_schema_http_client_path` - absolute import path to the HTTP client class used to introspect remote schema. If not provided, default `httpx` client is used. -- `target_package_name` (defaults to `"graphql_client"`) - name of generated package -- `target_package_path` (defaults to cwd) - path where to generate package -- `client_name` (defaults to `"Client"`) - name of generated client class -- `client_file_name` (defaults to `"client"`) - name of file with generated client class -- `base_client_name` (defaults to `"AsyncBaseClient"`) - name of base client class -- `base_client_file_path` (defaults to `.../ariadne_codegen/client_generators/dependencies/async_base_client.py`) - path to file where `base_client_name` is defined -- `enums_module_name` (defaults to `"enums"`) - name of file with generated enums models -- `input_types_module_name` (defaults to `"input_types"`) - name of file with generated input types models -- `fragments_module_name` (defaults to `"fragments"`) - name of file with generated fragments models -- `include_comments` (defaults to `"stable"`) - option which sets content of comments included at the top of every generated file. Valid choices are: `"none"` (no comments), `"timestamp"` (comment with generation timestamp), `"stable"` (comment contains a message that this is a generated file) -- `convert_to_snake_case` (defaults to `true`) - a flag that specifies whether to convert fields and arguments names to snake case -- `include_all_inputs` (defaults to `true`) - a flag specifying whether to include all inputs defined in the schema, or only those used in supplied operations -- `include_all_enums` (defaults to `true`) - a flag specifying whether to include all enums defined in the schema, or only those used in supplied operations -- `async_client` (defaults to `true`) - default generated client is `async`, change this to option `false` to generate synchronous client instead -- `opentelemetry_client` (defaults to `false`) - default base clients don't support any performance tracing. Change this option to `true` to use the base client with Open Telemetry support. -- `multipart_uploads` (defaults to `true`) - when set to `false`, a lighter base client variant is generated that omits multipart file upload support. -- `files_to_include` (defaults to `[]`) - list of files which will be copied into generated package -- `plugins` (defaults to `[]`) - list of plugins to use during generation -- `enable_custom_operations` (defaults to `false`) - enables building custom operations. Generates additional files that contains all the classes and methods for generation. - -These options control which fields are included in the GraphQL introspection query when using `remote_schema_url`. - -- `introspection_descriptions` (defaults to `false`) – include descriptions in the introspection result -- `introspection_input_value_deprecation` (defaults to `false`) – include deprecation information for input values -- `introspection_specified_by_url` (defaults to `false`) – include `specifiedByUrl` for custom scalars -- `introspection_schema_description` (defaults to `false`) – include schema description -- `introspection_directive_is_repeatable` (defaults to `false`) – include `isRepeatable` information for directives -- `introspection_input_object_one_of` (defaults to `false`) – include `oneOf` information for input objects - -## Loading schema from installed packages - -`schema_paths` lets you pull type definitions from installed Python packages alongside your local schema files, so codegen can resolve types that live in a shared library without copying them manually. - -Each entry in `schema_paths` must be one of the following: - -- **an absolute import path to a callable** that returns a `list[str]` of file paths, eg. `some_package.get_schema_files` -- **an absolute import path to a variable** holding the path to a single schema file, eg. `some_package.SCHEMA_FILE` -- **an absolute import path to a variable** holding the path to a directory β€” all `.graphql`, `.graphqls` and `.gql` files from it are included, eg. `some_package.SCHEMA_DIR` -- **a path to a directory** β€” all `.graphql`, `.graphqls` and `.gql` files from it are included, eg. `./my_schemas/` -- **a path to a specific file** to be used, eg. `./shared/types.graphql` +**1. Describe your schema** in `schema.graphql`: -```toml -[tool.ariadne-codegen] -schema_paths = [ - "some_gql_commontypes.get_schema_files", # callable β†’ returns list of paths - "other_pkg.SCHEMA_DIR", # variable β†’ directory - "./my_other_packages/", # local directory - "./foo/bar.graphql", # local file -] -queries_path = "queries.graphql" +```graphql +type Query { + hello(name: String!): String! +} ``` -`schema_path`, `schema_paths` and `remote_schema_url` are mutually exclusive - only one schema source may be used at a time. - -## Custom operation builder +**2. Write the operations you need** in `queries.graphql`: -The custom operation builder allows you to create complex GraphQL queries in a structured and intuitive way. - -### Example Code - -```python -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()) +```graphql +query Greet($name: String!) { + hello(name: $name) +} ``` -### 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` +**3. Point `ariadne-codegen` at them** in `pyproject.toml`: ```toml [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](https://github.com/mirumee/ariadne-codegen/blob/main/PLUGINS.md) file. - -### Standard plugins - -Ariadne Codegen ships with optional plugins importable from the `ariadne_codegen.contrib` package: - -- [`ariadne_codegen.contrib.shorter_results.ShorterResultsPlugin`](ariadne_codegen/contrib/shorter_results.py) - This plugin processes generated client methods for operations where only single top field is requested, so they return this field's value directly instead of operation's result type. For example get_user method generated for query `GetUser() { user(...) { ... }}` will return value of user field directly instead of `GetUserResult`. - -- [`ariadne_codegen.contrib.extract_operations.ExtractOperationsPlugin`](ariadne_codegen/contrib/extract_operations.py) - This extracts query strings from generated client's methods into separate `operations.py` module. It also modifies the generated client to import these definitions. Generated module name can be customized by adding `operations_module_name="custom_name"` to the `[tool.ariadne-codegen.operations]` section in config. Eg.: - - ```toml - [tool.ariadne-codegen] - ... - plugins = ["ariadne_codegen.contrib.extract_operations.ExtractOperationsPlugin"] - - [tool.ariadne-codegen.extract_operations] - operations_module_name = "custom_operations_module_name" - ``` - -- [`ariadne_codegen.contrib.client_forward_refs.ClientForwardRefsPlugin`](ariadne_codegen/contrib/client_forward_refs.py) - This plugin changes generated client module moving all Pydantic models imports under the `TYPE_CHECKING` condition, making them forward references. This greatly improves the import performance of the `client` module. - -- [`ariadne_codegen.contrib.no_reimports.NoReimportsPlugin`](ariadne_codegen/contrib/no_reimports.py) - This plugin removes content of generated `__init__.py`. This is useful in scenarios where generated plugins contain so many Pydantic models that client's eager initialization of entire package on first import is very slow. - -## Using generated client - -Generated client can be imported from package: - -```py -from {target_package_name}.{client_file_name} import {client_name} -``` - -Example with default settings: - -```py -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. - -```py -client = Client("https://example.com/graphql", {"Authorization": "Bearer token"}) -``` - -### Using custom http client - -The default base class http client can be replaced with another client: - -```py -client = Client(http_client=CustomComplexHttpClient()) -``` - -`CustomComplexHttpClient` needs to fulfill the following protocol for async client: - -```py -class Response(Protocol): - status_code: int - - def json(self, **kwargs: Any) -> Any: ... - - -class HttpClient(Protocol): - async def post( - self, - url: Any | str, - json: Any | None = None, - data: Any | None = None, - files: Any | None = None, - headers: Any | None = None, - **kwargs: Any, - ) -> Response: ... - - async def aclose(self) -> None: ... -``` - -Protocol for sync client: - -```py -class Response(Protocol): - status_code: int - - def json(self, **kwargs: Any) -> Any: ... - - -class HttpClient(Protocol): - def post( - self, - url: Any | str, - json: Any | None = None, - data: Any | None = None, - files: Any | None = None, - headers: Any | None = None, - **kwargs: Any, - ) -> Response: ... - - def close(self) -> None: ... -``` - -The protocol for sync client is also fulfilled by some commonly known classes, like `requests.Session`. - -### Websockets - -To handle subscriptions, default `AsyncBaseClient` uses [websockets](https://github.com/python-websockets/websockets) and implements [graphql-transport-ws](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) 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](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md#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](https://github.com/jaydenseric/graphql-multipart-request-spec). - -Class `Upload` is included in generated client and can be imported from it: - -```py -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: - -```toml -[tool.ariadne-codegen.scalars.OTHERSCALAR] -type = "Upload" -``` - -If your schema does not use file uploads, you can set `multipart_uploads = false` in your config to generate a lighter client that omits multipart handling entirely: - -```toml -[tool.ariadne-codegen] -multipart_uploads = false -``` - -### 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`: - -- `tracer`: `Optional[Union[str, Tracer]] = None` - tracer object or name which will be passed to the `get_tracer` method -- `root_context`: `Optional[Context] = None` - optional context added to root span -- `root_span_name`: `str = "GraphQL Operation"` - name of root span - -`AsyncBaseClientOpenTelemetry` supports all arguments which `BaseClientOpenTelemetry` does, but also exposes additional arguments regarding websockets: - -- `ws_root_context`: `Optional[Context] = None` - optional context added to root span for websocket connection -- `ws_root_span_name`: `str = "GraphQL Subscription"` - name of root span for websocket connection - -## 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`: - -```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. - -```toml -[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. - -```toml -[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. - -```toml -[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" -``` - -```py -# inputs.py - -class TestInput(BaseModel): - value_b: Annotated[TypeB, PlainSerializer(serialize_b)] -``` - -```py -# get_b.py - -class GetB(BaseModel): - query_b: Annotated[TypeB, BeforeValidator(parse_b)] -``` - -```py -# 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: - -- `from` - name of a module to import from -- `import` - name of a parent class - -Generated class will use `import` as extra base class, and import will be added to the file. - -```py -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: - -```gql -query listUsers { - users @mixin(from: ".mixins", import: "UsersMixin") { - id - } -} -``` - -Part of `pyproject.toml` with `files_to_include` (`mixins.py` contains `UsersMixin` implementation) - -```toml -files_to_include = [".../mixins.py"] -``` - -Part of generated `list_users.py` file: - -```py -... -from .mixins import UsersMixin -... -class ListUsersUsers(BaseModel, UsersMixin): - ... +queries_path = "queries.graphql" ``` -## 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. +Then generate the client: ``` -ariadne-codegen --config clientA.toml -ariadne-codegen --config clientB.toml +ariadne-codegen ``` -## Generated code dependencies - -Generated code requires: +This creates a `graphql_client` package. Use it: -- [pydantic](https://github.com/pydantic/pydantic) -- [httpx](https://github.com/encode/httpx) -- [websockets](https://github.com/python-websockets/websockets) (only for default async base client) - -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 +```python +import asyncio +from graphql_client import Client -Example with simple schema and few queries and mutations is available [here](https://github.com/mirumee/ariadne-codegen/blob/main/EXAMPLE.md). -## Generating a copy of GraphSQL schema +async def main(): + async with Client(url="https://example.com/graphql") as client: + result = await client.greet(name="World") + print(result.hello) -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: +asyncio.run(main()) ``` -ariadne-codegen graphqlschema -``` - -`graphqlschema` mode reads configuration from the same place as [`client`](#configuration) but uses only `schema_path`, `schema_paths`, `remote_schema_url`, `remote_schema_headers`, `remote_schema_verify_ssl`, `remote_schema_timeout`, `remote_schema_http_client_path` 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: - -- Necessary imports -- Type map declaration `{type_map_variable_name}: TypeMap = {...}` -- Schema declaration `{schema_variable_name}: GraphQLSchema = GraphQLSchema(...)` - -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. +`greet` is a typed method generated from your `Greet` operation, and `result` is a +validated Pydantic model. See the [step-by-step example](docs/02-guides/01-step-by-step-example.md) +for a walk-through of everything that gets generated. -### `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. - ---- - -## Versioning policy ## - -`ariadne-codegen` follows a custom versioning scheme where the minor version increases for breaking changes, while the patch version increments for bug fixes, enhancements, and other non-breaking updates. - -Since `ariadne-codegen` has not yet reached a stable API, this approach is in place until version 1.0.0. Once the API stabilizes, the project will adopt [Semantic Versioning](https://semver.org/).. - -## Development - -Formatting and linting use [ruff](https://github.com/astral-sh/ruff). We use [Hatch](https://github.com/pypa/hatch) for local tasks: - -- `hatch run lint` – lint (formatting check + typecheck) -- `hatch fmt` – auto-format code -- `hatch test` – tests with coverage (default Python 3.10) -- `hatch test -a -p` – tests across all supported Python versions -- `hatch run check` – full check (format, typecheck, tests) ## Contributing -We welcome all contributions to Ariadne! If you've found a bug or issue, feel free to use [GitHub issues](https://github.com/mirumee/ariadne-codegen/issues). If you have any questions or feedback, don't hesitate to catch us on [GitHub discussions](https://github.com/mirumee/ariadne/discussions/). - -For guidance and instructions, please see [CONTRIBUTING.md](CONTRIBUTING.md). +Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for how to report bugs, work on issues, and open pull requests. Also make sure you follow [@AriadneGraphQL](https://twitter.com/AriadneGraphQL) on Twitter for latest updates, news and random musings! -## **Crafted with ❀️ by [Mirumee Software](http://mirumee.com)** +# Crafted with ❀️ by [Mirumee Labs](http://mirumee.com) diff --git a/docs/01-intro.md b/docs/01-intro.md deleted file mode 100644 index 30418384..00000000 --- a/docs/01-intro.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: Introduction ---- - -# Ariadne Client based on 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 - -- Generate pydantic models from schema types, inputs and enums. -- Generate pydantic models for GraphQL results. -- Generate client package with each GraphQL operation available as async method. - -## 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] -``` diff --git a/docs/01-introduction.md b/docs/01-introduction.md new file mode 100644 index 00000000..b6ba0bc1 --- /dev/null +++ b/docs/01-introduction.md @@ -0,0 +1,119 @@ +--- +title: Introduction +slug: / +--- + +# Ariadne Code Generator + +[![Ariadne](https://ariadnegraphql.org/img/logo-horizontal-sm.png)](https://ariadnegraphql.org) + +[![Build Status](https://github.com/mirumee/ariadne-codegen/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/mirumee/ariadne-codegen/actions) + +Python code generator that turns a GraphQL schema and your operations into a fully typed, async (or sync) Python client built on Pydantic. You write your queries, mutations and subscriptions in GraphQL, and ariadne-codegen generates a typed Python method for each one, returning Pydantic models - so you get autocompletion and static type checking instead of hand-writing query strings and parsing raw JSON. + +πŸ“– **[Documentation](./01-introduction.md)** Β· [Step-by-step example](./02-guides/01-step-by-step-example.md) Β· [Configuration reference](./03-reference/01-configuration.md) + +## Features + +- **Fully typed models** - Pydantic models for schema types, inputs, enums, fragments, and every operation's result. +- **Typed client methods** - each query, mutation, and subscription becomes a method with typed arguments and a typed return value. +- **[Async or sync](./02-guides/10-async-vs-sync.md)** - generate an async client (default) or a synchronous one. +- **[Subscriptions](./02-guides/04-subscriptions.md)** - real-time updates over WebSockets (`graphql-transport-ws`). +- **[File uploads](./02-guides/05-file-uploads.md)** - multipart requests via the GraphQL multipart request spec. +- **[Custom scalars](./02-guides/06-custom-scalars.md)** - map GraphQL scalars to your own Python types with `serialize`/`parse` hooks. +- **[Extensible output](./02-guides/07-extending-types.md)** - inject mixins into generated models, copy in your own files, or swap the [base client](./03-reference/02-generated-code-dependencies.md) (custom auth, or to drop the `httpx`/`websockets` deps). +- **[Flexible schema sources](./02-guides/02-schema-sources.md)** - a local file, installed Python packages, or remote introspection. +- **[Plugin system](./04-plugins/01-intro.md)** - customize generation through hooks, plus ready-made plugins (shorter results, extracted operation strings, forward refs, …). +- **More** - [programmatic query building](./02-guides/12-custom-operation-builder.md), [OpenTelemetry tracing](./02-guides/09-opentelemetry.md), [multiple clients per project](./02-guides/08-multiple-clients.md), and a [schema-copy mode](./02-guides/11-schema-generation.md). + +## Installation + +``` +pip install ariadne-codegen +``` + +Add subscription (WebSocket) support with: + +``` +pip install ariadne-codegen[subscriptions] +``` + +## Quickstart + +Generate a typed client from three files. + +**1. Describe your schema** in `schema.graphql`: + +```graphql +type Query { + hello(name: String!): String! +} +``` + +**2. Write the operations you need** in `queries.graphql`: + +```graphql +query Greet($name: String!) { + hello(name: $name) +} +``` + +**3. Point `ariadne-codegen` at them** in `pyproject.toml`: + +```toml +[tool.ariadne-codegen] +schema_path = "schema.graphql" +queries_path = "queries.graphql" +``` + +Then generate the client: + +``` +ariadne-codegen +``` + +This creates a Python package (named `graphql_client` by default) next to your +config, with the typed client and Pydantic models generated from your schema and +operations: + +``` +graphql_client/ + __init__.py + async_base_client.py # networking base client + base_model.py + client.py # the typed Client, with a greet() method + enums.py + exceptions.py + greet.py # result model for the Greet operation + input_types.py +``` + +You get one result module per operation plus shared modules for enums, input types, +fragments and scalars. Import the client and use it right away: + +```python +import asyncio +from graphql_client import Client + + +async def main(): + async with Client(url="https://example.com/graphql") as client: + result = await client.greet(name="World") + print(result.hello) + + +asyncio.run(main()) +``` + +`greet` is a typed method generated from your `Greet` operation, and `result` is a +validated Pydantic model. See the [step-by-step example](./02-guides/01-step-by-step-example.md) +for a walk-through of everything that gets generated. + + +## Contributing + +Contributions are welcome! See [Contributing](./05-community/01-contributing.md) for how to report bugs, work on issues, and open pull requests. + +Also make sure you follow [@AriadneGraphQL](https://twitter.com/AriadneGraphQL) on Twitter for latest updates, news and random musings! + +# Crafted with ❀️ by [Mirumee Labs](http://mirumee.com) diff --git a/docs/04-step-by-step-example.md b/docs/02-guides/01-step-by-step-example.md similarity index 94% rename from docs/04-step-by-step-example.md rename to docs/02-guides/01-step-by-step-example.md index b0264fe1..ea890f73 100644 --- a/docs/04-step-by-step-example.md +++ b/docs/02-guides/01-step-by-step-example.md @@ -6,12 +6,15 @@ title: Step-by-step example This example shows how **ariadne-codegen** can take a GraphQL schema and a set of queries, generate a fully typed Python client, and produce ready-to-use models for queries, mutations, and subscriptions. +The full source for this example (schema, queries, and generated client) is available in [EXAMPLE.md](https://github.com/mirumee/ariadne-codegen/blob/main/EXAMPLE.md). + ## Schema file We start with a GraphQL schema that defines queries, mutations, and subscriptions. This schema includes `User` types, input objects for creating users and setting preferences, and a `Color` enum. It also shows how to use default values in input objects. ```graphql +# /schema.graphql schema { query: Query mutation: Mutation @@ -97,6 +100,7 @@ Next we define the operations we want to use: a mutation to create a user, queri Notice that we also define fragments (`BasicUser` and `UserPersonalData`) to reuse fields across queries. ```graphql +# /queries.graphql mutation CreateUser($userData: UserCreateInput!) { userCreate(userData: $userData) { id @@ -180,7 +184,6 @@ graphql_client/ input_types.py list_all_users.py list_users_by_country.py - scalars.py upload_file.py ``` @@ -200,7 +203,7 @@ class Client(AsyncBaseClient): async def upload_file(...): ... ``` -Each method executes the corresponding GraphQL operation and returns a typed Pydantic model. +Each method executes the corresponding GraphQL operation and returns a typed Pydantic model, except `get_users_counter`, which is a subscription method - it returns an `AsyncIterator` and yields a new model for every message. See [Subscriptions](04-subscriptions.md) for details. ### Base client diff --git a/docs/02-guides/02-schema-sources.md b/docs/02-guides/02-schema-sources.md new file mode 100644 index 00000000..a32584fa --- /dev/null +++ b/docs/02-guides/02-schema-sources.md @@ -0,0 +1,104 @@ +--- +title: Schema sources +--- + +# Schema sources + +`ariadne-codegen` needs a GraphQL schema to generate a client. You can provide it +from a local file/directory, from installed Python packages, or by introspecting a +remote GraphQL server. + +Exactly one of the following settings is required - `schema_path`, `schema_paths` +and `remote_schema_url` are **mutually exclusive**, so only one schema source may be +used at a time: + +- `schema_path` - path to file/directory with GraphQL schema +- `schema_paths` - list of local paths and/or installed-package sources to build the schema from +- `remote_schema_url` - URL to GraphQL server, where introspection query can be performed + +## Local schema file + +Point `schema_path` at a single schema file or at a directory: + +```toml +[tool.ariadne-codegen] +schema_path = "schema.graphql" +queries_path = "queries.graphql" +``` + +When `schema_path` points to a directory, `ariadne-codegen` reads every file with a +`.graphql`, `.graphqls` or `.gql` extension in it (searched recursively) and combines +them into a single schema. + +## Combining multiple schema files + +`schema_paths` lets you build a single schema from several sources in different +locations - multiple files and/or directories - instead of a single `schema_path`. +Each source is resolved and all the collected files are combined into one schema. + +It additionally supports importing schema files from installed Python packages, so +codegen can resolve types that live in a shared library without copying them +manually. + +Each entry in `schema_paths` is first tried as a local filesystem path, and if it +is neither an existing file nor directory it is treated as a dotted Python import +path. An entry must be one of the following: + +- a path to a directory - all `.graphql`, `.graphqls` and `.gql` files from it are included (searched recursively), eg. `./my_schemas/` +- a path to a specific file to be used, eg. `./shared/types.graphql` +- an absolute import path to a callable that returns a `list[str]` of file paths, eg. `some_package.get_schema_files` +- an absolute import path to a variable holding the path to a single schema file, eg. `some_package.SCHEMA_FILE` +- an absolute import path to a variable holding the path to a directory - all `.graphql`, `.graphqls` and `.gql` files from it are included, eg. `some_package.SCHEMA_DIR` + +```toml +[tool.ariadne-codegen] +schema_paths = [ + "some_gql_commontypes.get_schema_files", # callable -> returns list of paths + "other_pkg.SCHEMA_DIR", # variable -> directory + "./my_other_packages/", # local directory + "./foo/bar.graphql", # local file +] +queries_path = "queries.graphql" +``` + +## Remote schema introspection + +Set `remote_schema_url` to a GraphQL endpoint and `ariadne-codegen` will run an +introspection query to retrieve the schema: + +```toml +[tool.ariadne-codegen] +remote_schema_url = "https://example.com/graphql/" +queries_path = "queries.graphql" +``` + +The following options control how the introspection request is performed: + +- `remote_schema_headers` - extra headers that are passed along with introspection query, eg. `{"Authorization" = "Bearer token"}`. To include an environment variable in a header value, prefix the variable with `$`, eg. `{"Authorization" = "$AUTH_TOKEN"}` +- `remote_schema_verify_ssl` (defaults to `true`) - a flag that specifies whether to verify ssl while introspecting remote schema +- `remote_schema_timeout` (defaults to `5`) - timeout in seconds while introspecting remote schema +- `remote_schema_http_client_path` (defaults to `None`) - dotted import path to a custom HTTP client used only for the introspection request, instead of the default `httpx` client + +### Introspection query options + +These options only apply when the schema is fetched via `remote_schema_url`. They +map directly to the arguments of `graphql-core`'s +[`get_introspection_query()`](https://graphql-core-3.readthedocs.io/en/latest/modules/utilities.html#graphql.utilities.get_introspection_query) +(the config key `introspection_` becomes the `` argument), and they +control which optional fields the generated introspection query asks the server +for. They have no effect when using a local `schema_path` / `schema_paths`, because +a local SDL already contains everything. + +```toml +[tool.ariadne-codegen] +remote_schema_url = "https://example.com/graphql/" +queries_path = "queries.graphql" +introspection_descriptions = true +``` + +- `introspection_descriptions` (defaults to `false`) – include descriptions in the introspection result +- `introspection_input_value_deprecation` (defaults to `false`) – include deprecation information for input values +- `introspection_specified_by_url` (defaults to `false`) – include `specifiedByUrl` for custom scalars +- `introspection_schema_description` (defaults to `false`) – include schema description +- `introspection_directive_is_repeatable` (defaults to `false`) – include `isRepeatable` information for directives +- `introspection_input_object_one_of` (defaults to `false`) – include `oneOf` information for input objects diff --git a/docs/02-guides/03-using-generated-client.md b/docs/02-guides/03-using-generated-client.md new file mode 100644 index 00000000..88d31dc9 --- /dev/null +++ b/docs/02-guides/03-using-generated-client.md @@ -0,0 +1,174 @@ +--- +title: Using generated client +--- + +# Using generated client + +Generated client can be imported from package: + +```py +from {target_package_name}.{client_file_name} import {client_name} +``` + +Example with default settings: + +```py +from graphql_client.client import Client +``` + +Initialize the imported client with the server URL: + +```py +client = Client("https://example.com/graphql") +``` + +The client (with the default base client) takes passed headers and attaches them to every sent request. + +```py +client = Client("https://example.com/graphql", {"Authorization": "Bearer token"}) +``` + +For more complex scenarios, you can pass your own HTTP client - see [Advanced HTTP configuration with httpx clients](#advanced-http-configuration-with-httpx-clients) or [with a custom client](#advanced-http-configuration-with-a-custom-client). + +## Using the client as a context manager + +Both base clients support the context manager protocol, which closes the underlying +HTTP client on exit. Use `async with` for the async client: + +```py +async with Client(url="https://example.com/graphql") as client: + result = await client.list_all_users() +``` + +and `with` for the synchronous client: + +```py +with Client(url="https://example.com/graphql") as client: + result = client.list_all_users() +``` + +## Client configuration + +The generated client inherits its constructor from the base client. With the default +async base client (`AsyncBaseClient`), all constructor arguments are optional: + +```py +client = Client( + url="https://example.com/graphql", + headers={"Authorization": "Bearer token"}, + http_client=None, # supply your own HTTP client if needed + ws_url="wss://example.com/graphql", + ws_headers={"Authorization": "Bearer token"}, + ws_origin="https://example.com", + ws_connection_init_payload={"Authorization": "Bearer token"}, +) +``` + +- `url` (defaults to `""`) - HTTP endpoint the client sends operations to. +- `headers` (defaults to `None`) - headers attached to every HTTP request. When no `http_client` is supplied, they are also used to build the default `httpx` client. +- `http_client` (defaults to `None`) - your own HTTP client instance; when none is provided a default `httpx` client is used. See [Advanced HTTP configuration with httpx clients](#advanced-http-configuration-with-httpx-clients) and [with a custom client](#advanced-http-configuration-with-a-custom-client) for details. +- `ws_url` (defaults to `""`) - WebSocket endpoint used for subscriptions. +- `ws_headers` (defaults to `None`) - headers added to the WebSocket handshake request. +- `ws_origin` (defaults to `None`) - value of the `Origin` header for the WebSocket handshake. +- `ws_connection_init_payload` (defaults to `None`) - payload sent in the `ConnectionInit` message. + +The `ws_*` arguments only exist on the async base client (subscriptions are not +supported by the synchronous client). See [Subscriptions](./04-subscriptions.md) for +details. The synchronous base client (`async_client = false`) accepts only `url`, +`headers` and `http_client` - see [Async vs sync client](./10-async-vs-sync.md). + +If you enable the OpenTelemetry base client (`opentelemetry_client = true`), the +constructor also accepts tracing arguments - see [Open Telemetry](./09-opentelemetry.md). + +## Advanced HTTP configuration with httpx clients + +When you don't supply an `http_client`, the base client builds a default +[httpx](https://www.python-httpx.org/) client (`httpx.AsyncClient`/`httpx.Client`), +passing only `headers` to it; `url` is stored separately and used as the target of each +request. Any HTTP behaviour beyond that (authentication, timeouts, proxies, HTTP/2, SSL +verification, cookies, connection limits, custom transports/retries, event hooks, +etc.) is configured on the httpx client itself. Build a configured httpx client and +pass it as `http_client`: + +```py +import httpx +from graphql_client.client import Client + +http_client = httpx.AsyncClient( + base_url="https://example.com/graphql", + auth=("user", "pass"), + timeout=httpx.Timeout(10.0), + http2=True, + verify=False, + proxy="http://localhost:8080", + limits=httpx.Limits(max_connections=100), + headers={"Authorization": "Bearer token"}, +) + +client = Client(http_client=http_client) +``` + +When you pass your own `http_client`, the client sends every request through it as-is. +The `headers` you would otherwise pass to `Client` are *not* merged into requests (they +only build the default client), so set them directly on the httpx client. Configure the +endpoint either by passing `url` to `Client` or by setting `base_url` on the httpx client +(as above). Refer to the +[httpx `Client` / `AsyncClient` API](https://www.python-httpx.org/api/#client) +for the full list of options. + +## Advanced HTTP configuration with a custom client + +The default base class HTTP client can be replaced with any client - it doesn't have to +be httpx, it only needs to fulfill the expected protocol: + +```py +client = Client(http_client=CustomComplexHttpClient()) +``` + +`CustomComplexHttpClient` needs to fulfill the following protocol for the async client: + +```py +class Response(Protocol): + status_code: int + + def json(self, **kwargs: Any) -> Any: ... + + +class HttpClient(Protocol): + async def post( + self, + url: Any | str, + json: Any | None = None, + data: Any | None = None, + files: Any | None = None, + headers: Any | None = None, + **kwargs: Any, + ) -> Response: ... + + async def aclose(self) -> None: ... +``` + +Protocol for the sync client: + +```py +class Response(Protocol): + status_code: int + + def json(self, **kwargs: Any) -> Any: ... + + +class HttpClient(Protocol): + def post( + self, + url: Any | str, + json: Any | None = None, + data: Any | None = None, + files: Any | None = None, + headers: Any | None = None, + **kwargs: Any, + ) -> Response: ... + + def close(self) -> None: ... +``` + +The protocol for the sync client is also fulfilled by some commonly known classes, like `requests.Session`. \ No newline at end of file diff --git a/docs/02-guides/04-subscriptions.md b/docs/02-guides/04-subscriptions.md new file mode 100644 index 00000000..16957f95 --- /dev/null +++ b/docs/02-guides/04-subscriptions.md @@ -0,0 +1,101 @@ +--- +title: Subscriptions +--- + +# Subscriptions + +To handle subscriptions, the default `AsyncBaseClient` uses +[websockets](https://github.com/python-websockets/websockets) and implements the +[graphql-transport-ws](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) +subprotocol. + +## Installation + +Subscription support requires the `websockets` package, which is installed with the +`subscriptions` extra: + +``` +pip install ariadne-codegen[subscriptions] +``` + +Subscriptions are only available on the async client. The synchronous base client +(`async_client = false`) has no subscription support - see +[Async vs sync client](./10-async-vs-sync.md). + +## Connecting + +The WebSocket endpoint is separate from the HTTP `url` and is configured with +`ws_url`. Set it when constructing the client (the HTTP `url` is still used for +queries and mutations): + +```py +from graphql_client.client import Client + +client = Client( + url="https://example.com/graphql", + ws_url="wss://example.com/graphql", +) +``` + +## Consuming a subscription + +Generated subscription methods return an `AsyncIterator` of the result model and +yield a new value for every message the server pushes, so iterate over them with +`async for`: + +```py +async for result in client.get_users_counter(): + print(result.users_counter) +``` + +Here `get_users_counter` is the method generated from your `subscription` operation +(`subscription GetUsersCounter { usersCounter }`) and `result` is the typed response +model for that operation. + +## Full example + +Given a schema with a subscription field: + +```graphql +type Subscription { + usersCounter: Int! +} +``` + +and a subscription operation in your queries file: + +```graphql +subscription GetUsersCounter { + usersCounter +} +``` + +`ariadne-codegen` generates a `get_users_counter` method. Connect over WebSockets and +consume the stream with `async for`: + +```py +import asyncio +from graphql_client.client import Client + + +async def main(): + client = Client( + url="https://example.com/graphql", + ws_url="wss://example.com/graphql", + ) + async for result in client.get_users_counter(): + print("users online:", result.users_counter) + + +asyncio.run(main()) +``` + +Each server push yields a new `GetUsersCounter` instance; the loop runs until the +server closes the subscription (or you `break` out of it). + +## Connection arguments + +Arguments `ws_origin` and `ws_headers` are added as headers to the handshake +request, and `ws_connection_init_payload` is used as the payload of the +[ConnectionInit](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md#connectioninit) +message. diff --git a/docs/02-guides/05-file-uploads.md b/docs/02-guides/05-file-uploads.md new file mode 100644 index 00000000..5e539b4b --- /dev/null +++ b/docs/02-guides/05-file-uploads.md @@ -0,0 +1,89 @@ +--- +title: File uploads +--- + +# File uploads + +The default base client (`AsyncBaseClient` or `BaseClient`) checks if any part of +the `variables` dictionary is an instance of `Upload`. If at least one instance is +found then the client sends a multipart request according to the +[GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec). + +The `Upload` class is included in the generated client and can be imported from it: + +```py +from {target_package_name} import Upload +``` + +By default this class is used to represent the GraphQL scalar `Upload`. For a schema +with a different name for this scalar, you can still use `Upload` and the default +client for file uploads: + +```toml +[tool.ariadne-codegen.scalars.OTHERSCALAR] +type = "Upload" +``` + +## Constructing an `Upload` + +`Upload` takes three required arguments: + +- `filename` (`str`) - name reported to the server for the file. +- `content` (a file-like object / `IOBase`, e.g. the result of `open(...)` or an `io.BytesIO`) - the file contents. It is streamed as-is, so open binary files in binary mode (`"rb"`). +- `content_type` (`str`) - MIME type of the file, e.g. `"image/png"`. + +```py +from graphql_client import Upload + +upload = Upload( + filename="avatar.png", + content=open("avatar.png", "rb"), + content_type="image/png", +) +``` + +## Full example + +Given a mutation that accepts the `Upload` scalar: + +```graphql +mutation uploadFile($file: Upload!) { + fileUpload(file: $file) +} +``` + +`ariadne-codegen` generates an `upload_file` method that takes a typed `Upload` +argument. Pass an `Upload` instance and the client automatically sends a multipart +request: + +```py +import asyncio +from graphql_client.client import Client +from graphql_client import Upload + + +async def main(): + client = Client(url="https://example.com/graphql") + with open("avatar.png", "rb") as f: + result = await client.upload_file( + file=Upload(filename="avatar.png", content=f, content_type="image/png") + ) + print(result) + + +asyncio.run(main()) +``` + +Opening the file inside a `with` block ensures it is closed after the request +completes. Any `Upload` found anywhere in the operation's variables triggers the +multipart request, so this also works for lists of files or nested input objects. + +## Disabling multipart uploads + +If your schema does not use file uploads, you can set `multipart_uploads = false` +in your config to generate a lighter client that omits multipart handling entirely: + +```toml +[tool.ariadne-codegen] +multipart_uploads = false +``` diff --git a/docs/02-guides/06-custom-scalars.md b/docs/02-guides/06-custom-scalars.md new file mode 100644 index 00000000..682dc9dc --- /dev/null +++ b/docs/02-guides/06-custom-scalars.md @@ -0,0 +1,93 @@ +--- +title: Custom scalars +--- + +# Custom scalars + +By default, non built-in scalars are represented as `typing.Any` in the generated +client. You can provide information about a specific scalar by adding a section to +`pyproject.toml`: + +```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 the client will use the 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 the +annotation will use `PlainSerializer`, eg. `Annotated[type, PlainSerializer(serialize)]`. + +If `type`/`serialize`/`parse` contains at least one `.` then the string will be +split by its last occurrence. The first part will be used as the module to import +from, and the second part as the 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 the scalar is mapped to the built-in `str` which doesn't require custom +`serialize` and `parse` methods. + +```toml +[tool.ariadne-codegen.scalars.SCALARA] +type = "str" +``` + +## Example with type supported by pydantic + +In this scenario the scalar is represented as `datetime`, so it needs to be imported. +Pydantic handles serialization and deserialization so custom `parse` and `serialize` +is not necessary. + +```toml +[tool.ariadne-codegen.scalars.DATETIME] +type = "datetime.datetime" +``` + +## Example with fully custom type + +In this example the 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 the `type_b.py` file. + +```toml +[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" +``` + +```py +# inputs.py + +class TestInput(BaseModel): + value_b: Annotated[TypeB, PlainSerializer(serialize_b)] +``` + +```py +# get_b.py + +class GetB(BaseModel): + query_b: Annotated[TypeB, BeforeValidator(parse_b)] +``` + +```py +# client.py + +class Client(AsyncBaseClient): + async def test_mutation(self, value: TypeB) -> TestMutation: + ... + variables: dict[str, object] = { + "value": serialize_b(value), + } + ... +``` diff --git a/docs/02-guides/07-extending-types.md b/docs/02-guides/07-extending-types.md new file mode 100644 index 00000000..235189cd --- /dev/null +++ b/docs/02-guides/07-extending-types.md @@ -0,0 +1,103 @@ +--- +title: Extending generated types +--- + +# Extending generated types + +## Extending models with custom mixins + +The `mixin` directive allows you to extend a generated class with custom logic. +`mixin` takes two required arguments: + +- `from` - name of a module to import from +- `import` - name of a parent class + +The generated class will use `import` as an extra base class, and the import will be +added to the file. + +```py +from {from} import {import} +... +class OperationNameField(BaseModel, {import}): + ... +``` + +You don't need to declare the directive in your schema - `ariadne-codegen` registers +it automatically before processing your operations, so you can use `@mixin` right +away. + +Both arguments must be string literals; if either `from` or `import` is missing (or +a non-string value is passed), generation fails with a `ParsingError`. + +The directive only emits the `from {from} import {import}` line - it does not copy any +code - so the imported module must be reachable from the generated package. This +depends on what `from` points to: + +- a **relative import** (e.g. `from: ".mixins"`) refers to a module inside the generated + package, so you must copy that module in with the [`files_to_include`](../03-reference/01-configuration.md) option. +- an **absolute import** (e.g. `from: "my_package.mixins"`) just has to be importable in + the environment where the generated code runs; `files_to_include` is not needed. + +### Where `@mixin` can be used + +The directive is valid in two locations: + +- on a **field** - extends the model generated for that field's selection set. +- on a **fragment definition** - extends the model generated for that fragment. + +```gql +fragment UserData on User @mixin(from: ".mixins", import: "UsersMixin") { + id +} +``` + +### Applying multiple mixins + +`@mixin` is repeatable, so you can apply it several times to the same field or +fragment to add multiple base classes (they are added in the order they appear): + +```gql +query listUsers { + users + @mixin(from: ".mixins", import: "UsersMixin") + @mixin(from: ".mixins", import: "AuditMixin") { + id + } +} +``` + +```py +from .mixins import AuditMixin, UsersMixin +... +class ListUsersUsers(BaseModel, UsersMixin, AuditMixin): + ... +``` + +### Example of usage of `mixin` and `files_to_include` + +Query with `mixin` directive: + +```gql +query listUsers { + users @mixin(from: ".mixins", import: "UsersMixin") { + id + } +} +``` + +Part of `pyproject.toml` with `files_to_include` (`mixins.py` contains the +`UsersMixin` implementation): + +```toml +files_to_include = [".../mixins.py"] +``` + +Part of generated `list_users.py` file: + +```py +... +from .mixins import UsersMixin +... +class ListUsersUsers(BaseModel, UsersMixin): + ... +``` diff --git a/docs/02-guides/08-multiple-clients.md b/docs/02-guides/08-multiple-clients.md new file mode 100644 index 00000000..b1686d8c --- /dev/null +++ b/docs/02-guides/08-multiple-clients.md @@ -0,0 +1,13 @@ +--- +title: Multiple clients +--- + +# Multiple clients + +To generate multiple different clients you can store the config for each in a +different file, then provide the path to the config file with the `--config` option, eg. + +``` +ariadne-codegen --config clientA.toml +ariadne-codegen --config clientB.toml +``` diff --git a/docs/02-guides/09-opentelemetry.md b/docs/02-guides/09-opentelemetry.md new file mode 100644 index 00000000..1371e779 --- /dev/null +++ b/docs/02-guides/09-opentelemetry.md @@ -0,0 +1,64 @@ +--- +title: Open Telemetry +--- + +# Open Telemetry + +When the config option `opentelemetry_client` is set to `true` then the default, +included base client is replaced with one that implements opt-in Open Telemetry +support. By default this support does nothing, but when the +[`opentelemetry-api`](https://pypi.org/project/opentelemetry-api/) package is installed +and the `tracer` argument is provided then the client will create spans with data about +performed requests. + +## Enabling + +Turn it on in your config: + +```toml +[tool.ariadne-codegen] +opentelemetry_client = true +``` + +The exact base client that gets generated depends on this option together with +`async_client` and `multipart_uploads` - you get an async or sync variant, with or +without multipart upload support. Regardless of the variant, the tracing arguments +below work the same way. + +The `opentelemetry` import is optional: if the package is not installed the tracing +code is a no-op, so the generated client still works - it just doesn't produce spans. +To actually export spans, install and configure OpenTelemetry (`opentelemetry-api` +plus an SDK/exporter). + +## Usage + +Pass a `tracer` (an OpenTelemetry `Tracer`, or a string name that is resolved via +`get_tracer`) when constructing the client: + +```py +from opentelemetry import trace +from graphql_client.client import Client + +# A configured TracerProvider/exporter is assumed to be set up elsewhere. +tracer = trace.get_tracer("my-app") + +client = Client( + url="https://example.com/graphql", + tracer=tracer, # or tracer="my-app" + root_span_name="MyGraphQLCall", +) +``` + +## Tracing arguments + +Tracing arguments handled by `BaseClientOpenTelemetry`: + +- `tracer`: `Optional[Union[str, Tracer]] = None` - tracer object or name which will be passed to the `get_tracer` method +- `root_context`: `Optional[Context] = None` - optional context added to root span +- `root_span_name` - name of root span; defaults to `"GraphQL Operation"` + +`AsyncBaseClientOpenTelemetry` supports all arguments which `BaseClientOpenTelemetry` +does, but also exposes additional arguments regarding websockets: + +- `ws_root_context`: `Optional[Context] = None` - optional context added to root span for websocket connection +- `ws_root_span_name`: `str = "GraphQL Subscription"` - name of root span for websocket connection diff --git a/docs/02-guides/10-async-vs-sync.md b/docs/02-guides/10-async-vs-sync.md new file mode 100644 index 00000000..37b4b206 --- /dev/null +++ b/docs/02-guides/10-async-vs-sync.md @@ -0,0 +1,57 @@ +--- +title: Async vs sync client +--- + +# Async vs sync client + +By default the generated client is asynchronous - every operation is available as an +`async` method and the client inherits from `AsyncBaseClient`. + +To generate a synchronous client instead, set `async_client` to `false`: + +```toml +[tool.ariadne-codegen] +async_client = false +``` + +## What changes + +| | Async (default) | Sync (`async_client = false`) | +| ---------------------- | ----------------------------------- | --------------------------------- | +| Base class | `AsyncBaseClient` | `BaseClient` | +| Operation methods | `async def` (must be `await`ed) | plain `def` | +| Context manager | `async with` | `with` | +| Custom `http_client` | async client (default `httpx.AsyncClient`) | sync client (default `httpx.Client`) | +| Subscriptions | supported | **not supported** | + +The generated method bodies mirror this - the async client does +`response = await self.execute(...)`, the sync client does `response = self.execute(...)`: + +```py +# async +async def list_all_users(self, **kwargs: Any) -> ListAllUsers: + ... + response = await self.execute(query=query, ...) + data = self.get_data(response) + return ListAllUsers.model_validate(data) +``` + +```py +# sync +def list_all_users(self, **kwargs: Any) -> ListAllUsers: + ... + response = self.execute(query=query, ...) + data = self.get_data(response) + return ListAllUsers.model_validate(data) +``` + +## Subscriptions require the async client + +Subscriptions are only available on the async client. If your operations include a +`subscription` while `async_client = false`, generation fails with: + +``` +Subscriptions are only available when using async client. +``` + +See [Subscriptions](./04-subscriptions.md). diff --git a/docs/02-guides/11-schema-generation.md b/docs/02-guides/11-schema-generation.md new file mode 100644 index 00000000..4e3a18ee --- /dev/null +++ b/docs/02-guides/11-schema-generation.md @@ -0,0 +1,84 @@ +--- +title: Schema generation +--- + +# Generating a copy of the GraphQL 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 the `graphqlschema` argument: + +``` +ariadne-codegen graphqlschema +``` + +`graphqlschema` mode reads configuration from the same place as the client, but uses +only a subset of the options: + +- to retrieve the schema (see [Schema sources](./02-schema-sources.md)): + - `schema_path` + - `schema_paths` + - `remote_schema_url` + - `remote_schema_headers` + - `remote_schema_verify_ssl` + - `remote_schema_timeout` + - `remote_schema_http_client_path` + - `introspection_*` +- `plugins` - to load plugins + +Before writing the file, the loaded schema is passed through each plugin's +`process_schema` hook and then validated, so this mode can also be used to apply +plugin transformations to a schema or to fail early on an invalid one. See +[Plugins](../04-plugins/01-intro.md) (and the [`process_schema` hook](../04-plugins/03-hooks.md#process_schema)) +for details. + +In addition to the above, `graphqlschema` mode also accepts additional settings +specific to it: + +## `target_file_path` + +A string with the destination path for the generated file. Must be either a Python +(`.py`), or GraphQL (`.graphql` or `.gql`) file. + +Defaults to `schema.py`. + +A generated Python file will contain: + +- Necessary imports +- Type map declaration `{type_map_variable_name}: TypeMap = {...}` +- Schema declaration `{schema_variable_name}: GraphQLSchema = GraphQLSchema(...)` + +A 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 the schema variable, must be a valid python identifier. + +Defaults to `"schema"`. Used only if the target is a Python file. + +## `type_map_variable_name` + +A string with a name for the type map variable, must be a valid python identifier. + +Defaults to `"type_map"`. Used only if the target is a Python file. + +## Example + +The output format is chosen from the `target_file_path` extension. To emit a GraphQL +SDL file: + +```toml +[tool.ariadne-codegen] +remote_schema_url = "https://example.com/graphql/" +target_file_path = "schema.graphql" +``` + +These settings live in the same `[tool.ariadne-codegen]` section as the rest of the +configuration - there is no separate subsection for `graphqlschema` mode. + +``` +ariadne-codegen graphqlschema +``` + +Point `target_file_path` at a `.py` file instead to emit an importable Python module +with the `schema` / `type_map` variables described above. diff --git a/docs/05-custom-operation-builder.md b/docs/02-guides/12-custom-operation-builder.md similarity index 60% rename from docs/05-custom-operation-builder.md rename to docs/02-guides/12-custom-operation-builder.md index 163ac2f0..60b4bcdc 100644 --- a/docs/05-custom-operation-builder.md +++ b/docs/02-guides/12-custom-operation-builder.md @@ -4,9 +4,25 @@ title: Custom operation builder # Custom operation builder -The custom operation builder allows you to create complex GraphQL queries in a structured and intuitive way. +The custom operation builder allows you to create complex GraphQL queries in a structured and intuitive way. This feature is disabled by default. To enable it, set `enable_custom_operations = true` in your config. It generates additional helper modules instead of (or alongside) the per-operation client methods, so `queries_path` becomes optional: -## Example Code +```toml +[tool.ariadne-codegen] +schema_path = "schema.graphql" +enable_custom_operations = true +``` + +With it enabled, the following modules are generated in your package: + +- `custom_fields.py` - a `…Fields` class per object type, used to select fields. +- `custom_typing_fields.py` - supporting types for field selection. +- `custom_queries.py` - a `Query` class with a builder method per root query field (generated only if the schema has a `Query` type). +- `custom_mutations.py` - a `Mutation` class with a builder method per root mutation field (generated only if the schema has a `Mutation` type). + +The client also gains `query(...)`, `mutation(...)` and `execute_custom_operation(...)` +methods for executing the built operations. + +## Example: custom queries ```python import asyncio @@ -73,6 +89,32 @@ asyncio.run(get_products()) 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. +Unlike the per-operation client methods, `client.query(...)` and +`client.mutation(...)` return the raw response as a `dict[str, Any]` - the result is +not parsed into a generated Pydantic model. + +## Example: custom mutations + +Mutations are built the same way, using the generated `Mutation` class from +`custom_mutations` and executed with `client.mutation(...)`: + +```python +from graphql_client import Client +from graphql_client.custom_fields import ProductFields +from graphql_client.custom_mutations import Mutation + + +async def update_product(): + client = Client(url="https://saleor.cloud/graphql/") + + build = Mutation.product_update(id="...").fields( + ProductFields.id, + ProductFields.name, + ) + response = await client.mutation(build, operation_name="update_product") + print(response) +``` + ## Example pyproject.toml configuration. `Note: queries_path is optional when enable_custom_operations is set to true` diff --git a/docs/02-guides/_category_.yml b/docs/02-guides/_category_.yml new file mode 100644 index 00000000..f1646d8f --- /dev/null +++ b/docs/02-guides/_category_.yml @@ -0,0 +1,2 @@ +label: 'Guides' +collapsible: false diff --git a/docs/02-configuration.md b/docs/03-reference/01-configuration.md similarity index 61% rename from docs/02-configuration.md rename to docs/03-reference/01-configuration.md index a3ada0df..69666c8b 100644 --- a/docs/02-configuration.md +++ b/docs/03-reference/01-configuration.md @@ -18,37 +18,16 @@ queries_path = "queries.graphql" - `queries_path` - path to file/directory with queries (Can be optional if `enable_custom_operations` is used) -Exactly one of the following 3 parameters is required. They are mutually exclusive - providing more than one raises a configuration error: +Exactly one of the following parameters is required - they are mutually exclusive, so only one schema source may be used at a time (see [Schema sources](../02-guides/02-schema-sources.md)): - `schema_path` - path to file/directory with graphql schema -- `schema_paths` - list of schema sources resolved at codegen time; each entry may be a local path (file or directory) or a dotted Python attribute path (`pkg.ATTR` or `pkg.callable`). See details below. -- `remote_schema_url` - url to graphql server, where introspection query can be perfomed - -### `schema_paths` entries - -Each entry in `schema_paths` must be one of the following: - -- **an absolute import path to a callable** that returns a `list[str]` of file paths, eg. `some_pkg.get_schema_files` -- **an absolute import path to a variable** holding the path to a single schema file, eg. `some_pkg.SCHEMA_FILE` -- **an absolute import path to a variable** holding the path to a directory - all `.graphql`, `.graphqls` and `.gql` files from it are included, eg. `some_pkg.SCHEMA_DIR` -- **a path to a directory** - all `.graphql`, `.graphqls` and `.gql` files from it are included, eg. `./schemas/` -- **a path to a specific file** to be used, eg. `./foo/bar.graphql` - -```toml -[tool.ariadne-codegen] -schema_paths = [ - "some_gql_commontypes.get_schema_files", - "other_pkg.SCHEMA_DIR", - "./my_other_packages/", - "./foo/bar.graphql", -] -queries_path = "queries.graphql" -``` +- `schema_paths` - list of local paths and/or installed-package sources to build the schema from +- `remote_schema_url` - url to graphql server, where introspection query can be performed ## Optional settings: -- `remote_schema_headers` - extra headers that are passed along with introspection query, eg. `{"Authorization" = "Bearer: token"}`. To include an environment variable in a header value, prefix the variable with `$`, eg. `{"Authorization" = "$AUTH_TOKEN"}` -- `remote_schema_verify_ssl` (defaults to `true`) - a flag that specifies wheter to verify ssl while introspecting remote schema +- `remote_schema_headers` - extra headers that are passed along with introspection query, eg. `{"Authorization" = "Bearer token"}`. To include an environment variable in a header value, prefix the variable with `$`, eg. `{"Authorization" = "$AUTH_TOKEN"}` +- `remote_schema_verify_ssl` (defaults to `true`) - a flag that specifies whether to verify ssl while introspecting remote schema - `remote_schema_timeout` (defaults to `5`) - timeout in seconds while introspecting remote schema - `remote_schema_http_client_path` - absolute import path to the HTTP client class used to introspect remote schema. If not provided, default `httpx` client is used. - `target_package_name` (defaults to `"graphql_client"`) - name of generated package @@ -57,6 +36,7 @@ queries_path = "queries.graphql" - `client_file_name` (defaults to `"client"`) - name of file with generated client class - `base_client_name` (defaults to `"AsyncBaseClient"`) - name of base client class - `base_client_file_path` (defaults to `.../ariadne_codegen/client_generators/dependencies/async_base_client.py`) - path to file where `base_client_name` is defined +- `base_client_module_name` (defaults to the file name of `base_client_file_path`) - name of the module the base client is copied to and imported from in the generated package - `enums_module_name` (defaults to `"enums"`) - name of file with generated enums models - `input_types_module_name` (defaults to `"input_types"`) - name of file with generated input types models - `fragments_module_name` (defaults to `"fragments"`) - name of file with generated fragments models @@ -70,8 +50,25 @@ queries_path = "queries.graphql" - `files_to_include` (defaults to `[]`) - list of files which will be copied into generated package - `plugins` (defaults to `[]`) - list of plugins to use during generation - `enable_custom_operations` (defaults to `false`) - enables building custom operations. Generates additional files that contains all the classes and methods for generation. +- `include_typename` (defaults to `true`) - a flag that specifies whether to include the `__typename` field in generated models +- `ignore_extra_fields` (defaults to `true`) - when `true`, generated models ignore extra fields returned by the server; set to `false` to add `extra="forbid"` to the base model so unexpected fields raise a validation error +- `default_optional_fields_to_none` (defaults to `false`) - when `true`, optional fields in generated models default to `None` instead of being required keyword arguments +- `skip_validation_rules` (defaults to `["NoUnusedFragments"]`) - list of [graphql-core validation rule](https://github.com/graphql-python/graphql-core) names to skip when validating operations against the schema + +### Scalars -These options control which fields are included in the GraphQL introspection query when using `remote_schema_url`. +Custom scalar mappings are configured in per-scalar subsections rather than a single key: + +```toml +[tool.ariadne-codegen.scalars.{graphql scalar name}] +type = "..." +``` + +See [Custom scalars](../02-guides/06-custom-scalars.md) for the full syntax. + +## Introspection query settings: + +These options control which fields are included in the GraphQL introspection query when using `remote_schema_url`. See [Schema sources](../02-guides/02-schema-sources.md) for more details. - `introspection_descriptions` (defaults to `false`) – include descriptions in the introspection result - `introspection_input_value_deprecation` (defaults to `false`) – include deprecation information for input values @@ -80,34 +77,15 @@ These options control which fields are included in the GraphQL introspection que - `introspection_directive_is_repeatable` (defaults to `false`) – include `isRepeatable` information for directives - `introspection_input_object_one_of` (defaults to `false`) – include `oneOf` information for input objects -## Base Client customization - -The `base_client_file_path` and `base_client_name` can be used to provide a custom base client implementation. It should implement the following protocol for async client: +## Related guides -```py -class AsyncBaseClient(Protocol): - async def execute( - self, - query: str, - operation_name: Optional[str] = None, - variables: Optional[dict[str, Any]] = None, - **kwargs: Any, - ) -> Response: ... +Several settings have dedicated guides that explain them in context: - def get_data(self, response: Response) -> dict[str, Any]: ... -``` - -Protocol for sync client: - -```py -class BaseClient(Protocol): - def execute( - self, - query: str, - operation_name: Optional[str] = None, - variables: Optional[dict[str, Any]] = None, - **kwargs: Any, - ) -> Response: ... - - def get_data(self, response: Response) -> dict[str, Any]: ... -``` +- [Schema sources](../02-guides/02-schema-sources.md) - `schema_path`, `remote_schema_url`, `remote_schema_*`, and `introspection_*` +- [Schema generation](../02-guides/11-schema-generation.md) - `graphqlschema` mode and its settings +- [File uploads](../02-guides/05-file-uploads.md) - `multipart_uploads` +- [Custom scalars](../02-guides/06-custom-scalars.md) - scalar sections and `files_to_include` +- [Async vs sync client](../02-guides/10-async-vs-sync.md) - `async_client` +- [Open Telemetry](../02-guides/09-opentelemetry.md) - `opentelemetry_client` +- [Plugins](../04-plugins/01-intro.md) - `plugins` +- [Custom operation builder](../02-guides/12-custom-operation-builder.md) - `enable_custom_operations` diff --git a/docs/03-reference/02-generated-code-dependencies.md b/docs/03-reference/02-generated-code-dependencies.md new file mode 100644 index 00000000..09913578 --- /dev/null +++ b/docs/03-reference/02-generated-code-dependencies.md @@ -0,0 +1,44 @@ +--- +title: Generated code dependencies +--- + +# Generated code dependencies + +Generated code requires: + +- [pydantic](https://github.com/pydantic/pydantic) +- [httpx](https://github.com/encode/httpx) +- [websockets](https://github.com/python-websockets/websockets) (only for the default async base client) +- [graphql-core](https://github.com/graphql-python/graphql-core) (only when `enable_custom_operations` is used) +- [opentelemetry-api](https://github.com/open-telemetry/opentelemetry-python) (only when `opentelemetry_client` is enabled) + +## When each dependency is required + +- **pydantic** is always required - all generated models (inputs, enums, results, + fragments) inherit from a pydantic `BaseModel`. +- **httpx** is required by the default base clients (`AsyncBaseClient` and + `BaseClient`), which use it to perform HTTP requests. +- **websockets** is required only by the default async base client + (`AsyncBaseClient`), and only when handling subscriptions over WebSockets. See + [Subscriptions](../02-guides/04-subscriptions.md). +- **graphql-core** is required only when the custom operation builder is enabled + (`enable_custom_operations = true`); the generated `client.py` and `base_operation.py` + then import GraphQL AST nodes from it. Regular generated clients do not import it. + See [Custom operation builder](../02-guides/12-custom-operation-builder.md). +- **opentelemetry-api** is required only when `opentelemetry_client = true`, which + swaps in an OpenTelemetry-instrumented base client that imports `opentelemetry.trace` + and `opentelemetry.context`. See [OpenTelemetry](../02-guides/09-opentelemetry.md). + +## Avoiding httpx and websockets + +Both the `httpx` and `websockets` dependencies can be avoided by providing another +base client class with the `base_client_file_path` and `base_client_name` options. +When you supply your own base client, `ariadne-codegen` copies it into the generated +package instead of the default one, so the generated code only depends on whatever +your base client imports. + +```toml +[tool.ariadne-codegen] +base_client_file_path = "path/to/custom_base_client.py" +base_client_name = "CustomBaseClient" +``` diff --git a/docs/03-reference/_category_.yml b/docs/03-reference/_category_.yml new file mode 100644 index 00000000..360f10f1 --- /dev/null +++ b/docs/03-reference/_category_.yml @@ -0,0 +1,2 @@ +label: 'Reference' +collapsible: false diff --git a/docs/03-using-generated-client.md b/docs/03-using-generated-client.md deleted file mode 100644 index fd7ffbeb..00000000 --- a/docs/03-using-generated-client.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: Using generated client ---- - -# Using generated client - -Generated client can be imported from package: - -```py -from {target_package_name}.{client_file_name} import {client_name} -``` - -Example with default settings: - -```py -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. - -```py -client = Client("https://example.com/graphql", {"Authorization": "Bearer token"}) -``` - -### Using custom http client - -The default base class http client can be replaced with another client: - -```py -client = Client(http_client=CustomComplexHttpClient()) -``` - -`CustomComplexHttpClient` needs to fulfill the following protocol for async client: - -```py -class Response(Protocol): - status_code: int - - def json(self, **kwargs: Any) -> Any: ... - - -class HttpClient(Protocol): - async def post( - self, - url: Any | str, - json: Any | None = None, - data: Any | None = None, - files: Any | None = None, - headers: Any | None = None, - **kwargs: Any, - ) -> Response: ... - - async def aclose(self) -> None: ... -``` - -Protocol for sync client: - -```py -class Response(Protocol): - status_code: int - - def json(self, **kwargs: Any) -> Any: ... - - -class HttpClient(Protocol): - def post( - self, - url: Any | str, - json: Any | None = None, - data: Any | None = None, - files: Any | None = None, - headers: Any | None = None, - **kwargs: Any, - ) -> Response: ... - - def close(self) -> None: ... -``` - -The protocol for sync client is also fulfilled by some commonly known classes, like `requests.Session`. diff --git a/docs/06-plugins/01-intro.md b/docs/04-plugins/01-intro.md similarity index 100% rename from docs/06-plugins/01-intro.md rename to docs/04-plugins/01-intro.md diff --git a/docs/06-plugins/02-custom-plugins.md b/docs/04-plugins/02-custom-plugins.md similarity index 100% rename from docs/06-plugins/02-custom-plugins.md rename to docs/04-plugins/02-custom-plugins.md diff --git a/docs/06-plugins/03-hooks.md b/docs/04-plugins/03-hooks.md similarity index 100% rename from docs/06-plugins/03-hooks.md rename to docs/04-plugins/03-hooks.md diff --git a/docs/06-plugins/04-standard-plugins.md b/docs/04-plugins/04-standard-plugins.md similarity index 94% rename from docs/06-plugins/04-standard-plugins.md rename to docs/04-plugins/04-standard-plugins.md index 62e93f4d..a19aeb1a 100644 --- a/docs/06-plugins/04-standard-plugins.md +++ b/docs/04-plugins/04-standard-plugins.md @@ -6,9 +6,9 @@ title: Standard plugins Ariadne Codegen ships with optional plugins importable from the `ariadne_codegen.contrib` package: -- [`ariadne_codegen.contrib.shorter_results.ShorterResultsPlugin`](https://github.com/mirumee/ariadne-codegen/blob/main/ariadne_codegen/contrib/shorter_results.py#L61) - This plugin processes generated client methods for operations where only single top field is requested, so they return this field's value directly instead of operation's result type. For example get_user method generated for query `GetUser() { user(...) { ... }}` will return value of user field directly instead of `GetUserResult`. +- [`ariadne_codegen.contrib.shorter_results.ShorterResultsPlugin`](https://github.com/mirumee/ariadne-codegen/blob/main/ariadne_codegen/contrib/shorter_results.py#L62) - This plugin processes generated client methods for operations where only single top field is requested, so they return this field's value directly instead of operation's result type. For example get_user method generated for query `GetUser() { user(...) { ... }}` will return value of user field directly instead of `GetUserResult`. -- [`ariadne_codegen.contrib.extract_operations.ExtractOperationsPlugin`](https://github.com/mirumee/ariadne-codegen/blob/main/ariadne_codegen/contrib/extract_operations.py#L29) - This extracts query strings from generated client's methods into separate `operations.py` module. It also modifies the generated client to import these definitions. Generated module name can be customized by adding `operations_module_name="custom_name"` to the `[tool.ariadne-codegen.extract-operations]` section in config. Eg.: +- [`ariadne_codegen.contrib.extract_operations.ExtractOperationsPlugin`](https://github.com/mirumee/ariadne-codegen/blob/main/ariadne_codegen/contrib/extract_operations.py#L31) - This extracts query strings from generated client's methods into separate `operations.py` module. It also modifies the generated client to import these definitions. Generated module name can be customized by adding `operations_module_name="custom_name"` to the `[tool.ariadne-codegen.extract-operations]` section in config. Eg.: ```toml [tool.ariadne-codegen] diff --git a/docs/06-plugins/_category_.yml b/docs/04-plugins/_category_.yml similarity index 100% rename from docs/06-plugins/_category_.yml rename to docs/04-plugins/_category_.yml diff --git a/docs/05-community/01-contributing.md b/docs/05-community/01-contributing.md new file mode 100644 index 00000000..2c38c249 --- /dev/null +++ b/docs/05-community/01-contributing.md @@ -0,0 +1,20 @@ +--- +title: Contributing +--- + +# Contributing + +Ariadne Codegen is developed on GitHub, and the canonical contributor documents live +in the repository. This page just points to them so they stay in a single place: + +- [CONTRIBUTING.md](https://github.com/mirumee/ariadne-codegen/blob/main/CONTRIBUTING.md) - how to report bugs, work on issues, and open pull requests (also shown by GitHub on issues/PRs) +- [EXAMPLE.md](https://github.com/mirumee/ariadne-codegen/blob/main/EXAMPLE.md) - full example project referenced by the [step-by-step guide](../02-guides/01-step-by-step-example.md) +- [LICENSE](https://github.com/mirumee/ariadne-codegen/blob/main/LICENSE) - project license + +Local development tasks (formatting, linting, type checking, tests) are covered in +the [Development setup](https://github.com/mirumee/ariadne-codegen/blob/main/CONTRIBUTING.md#development-setup) +section of `CONTRIBUTING.md`. + +To report a bug, ask a question, or share feedback, use +[GitHub issues](https://github.com/mirumee/ariadne-codegen/issues) or +[GitHub discussions](https://github.com/mirumee/ariadne/discussions/). diff --git a/docs/05-community/02-versioning-policy.md b/docs/05-community/02-versioning-policy.md new file mode 100644 index 00000000..cc9e2551 --- /dev/null +++ b/docs/05-community/02-versioning-policy.md @@ -0,0 +1,20 @@ +--- +title: Versioning policy +--- + +# Versioning policy + +`ariadne-codegen` follows a custom versioning scheme where the minor version +increases for breaking changes, while the patch version increments for bug fixes, +enhancements, and other non-breaking updates. + +Since `ariadne-codegen` has not yet reached a stable API, this approach is in place +until version 1.0.0. Once the API stabilizes, the project will adopt +[Semantic Versioning](https://semver.org/). + +## Release history + +Per-version changes are published on the +[Releases](https://github.com/mirumee/ariadne-codegen/releases) page, and +unreleased changes (including any breaking changes) are tracked in +[CHANGELOG.md](https://github.com/mirumee/ariadne-codegen/blob/main/CHANGELOG.md). diff --git a/docs/05-community/_category_.yml b/docs/05-community/_category_.yml new file mode 100644 index 00000000..d2810dc0 --- /dev/null +++ b/docs/05-community/_category_.yml @@ -0,0 +1,2 @@ +label: 'Community' +collapsible: false