graphql-api 1.6.7__tar.gz → 1.6.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {graphql_api-1.6.7 → graphql_api-1.6.8}/.github/workflows/pages.yml +1 -1
- {graphql_api-1.6.7 → graphql_api-1.6.8}/AGENTS.md +2 -1
- {graphql_api-1.6.7 → graphql_api-1.6.8}/CLAUDE.md +2 -1
- {graphql_api-1.6.7 → graphql_api-1.6.8}/PKG-INFO +36 -26
- {graphql_api-1.6.7 → graphql_api-1.6.8}/README.md +35 -25
- graphql_api-1.6.8/VERSION +1 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/fundamentals/async-and-subscriptions.md +23 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/fundamentals/defining-schemas.md +33 -1
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/fundamentals/getting-started.md +40 -5
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/fundamentals/mutations.md +28 -1
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/reference/api-reference.md +49 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/api.py +208 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/executor.py +69 -11
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/utils.py +6 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api.egg-info/PKG-INFO +36 -26
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api.egg-info/SOURCES.txt +1 -0
- graphql_api-1.6.8/tests/test_plain_functions.py +461 -0
- graphql_api-1.6.7/VERSION +0 -1
- {graphql_api-1.6.7 → graphql_api-1.6.8}/.github/workflows/python-package.yml +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/.gitignore +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/.vscode/settings.json +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/LICENSE +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/README.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/_index.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/_index.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/advanced-features/_index.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/advanced-features/directives.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/advanced-features/performance.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/advanced-features/schema-filtering.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/building-applications/_index.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/building-applications/documentation.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/building-applications/error-handling.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/distributed-systems/_index.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/distributed-systems/federation.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/distributed-systems/pagination-relay.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/distributed-systems/remote-graphql.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/fundamentals/_index.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/fundamentals/enums-interfaces.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/fundamentals/field-types.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/fundamentals/object-types.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/fundamentals/pydantic-and-dataclasses.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/production/_index.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/production/context-metadata.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/production/middleware.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/reference/_index.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/reference/contributing.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/reference/examples.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/hugo.toml +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/index.md +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/__init__.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/context.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/dataclass_mapping.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/decorators.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/directives.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/error.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/exception.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/federation/__init__.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/federation/directives.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/federation/federation.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/federation/types.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/mapper.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/middleware.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/mutation_schema.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/pydantic.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/pydantic_utils.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/reduce.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/relay.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/remote.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/schema.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/schema_filter.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/type_utils.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/types.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api.egg-info/dependency_links.txt +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api.egg-info/requires.txt +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api.egg-info/top_level.txt +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/pyproject.toml +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/setup.cfg +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/__init__.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_async.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_calculator.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_custom_suffixes.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_custom_types.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_dataclass.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_dataclass_relationships.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_decorator_patterns.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_directives.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_docs_advanced.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_docs_async_subscriptions.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_docs_defining_schemas.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_docs_field_types.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_docs_getting_started.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_docs_relay.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_docstrings.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_error.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_federation.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_federation_example.graphql +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_federation_example.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_filtering.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_graphql.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_middleware.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_performance.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_pydantic.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_relay.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_remote.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_schema.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_subscriptions.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_unified_vs_explicit_schema.py +0 -0
- {graphql_api-1.6.7 → graphql_api-1.6.8}/uv.lock +0 -0
|
@@ -30,8 +30,9 @@ uv run flake8 graphql_api tests
|
|
|
30
30
|
|
|
31
31
|
- **Automatic snake_case to camelCase**: All Python field names, arguments, and dataclass/Pydantic fields are auto-converted to camelCase in GraphQL. Input arguments are converted back to snake_case. Conversion lives in `graphql_api/utils.py` (`to_camel_case()`, `to_snake_case()`)
|
|
32
32
|
- `@field` marks a method as a query; `@field(mutable=True)` makes it a mutation; `AsyncGenerator` return type or `subscription=True` makes it a subscription
|
|
33
|
+
- Plain functions: `@api.query` / `@api.mutation` / `@api.subscription` register a free function as a root-level field — no class required. Composes with class-based roots (registered fns are folded onto the user-provided `query_type` / `mutation_type` / `subscription_type` / `root_type`)
|
|
33
34
|
- Python types auto-map: `str`→String, `int`→Int, `bool`→Boolean, `float`→Float, `UUID`→UUID scalar, `datetime`→DateTime, `Optional[T]`→nullable, `List[T]`→list, dataclasses/Pydantic models→object types, enums→enum types
|
|
34
|
-
- Schema setup: `GraphQLAPI(query_type=Q, mutation_type=M, subscription_type=S)` or legacy `root_type=Root`
|
|
35
|
+
- Schema setup: `GraphQLAPI(query_type=Q, mutation_type=M, subscription_type=S)` or legacy `root_type=Root` (or neither — just `@api.query`/`@api.mutation`/`@api.subscription`)
|
|
35
36
|
|
|
36
37
|
## Releasing
|
|
37
38
|
|
|
@@ -30,8 +30,9 @@ uv run flake8 graphql_api tests
|
|
|
30
30
|
|
|
31
31
|
- **Automatic snake_case to camelCase**: All Python field names, arguments, and dataclass/Pydantic fields are auto-converted to camelCase in GraphQL. Input arguments are converted back to snake_case. Conversion lives in `graphql_api/utils.py` (`to_camel_case()`, `to_snake_case()`)
|
|
32
32
|
- `@field` marks a method as a query; `@field(mutable=True)` makes it a mutation; `AsyncGenerator` return type or `subscription=True` makes it a subscription
|
|
33
|
+
- Plain functions: `@api.query` / `@api.mutation` / `@api.subscription` register a free function as a root-level field — no class required. Composes with class-based roots (registered fns are folded onto the user-provided `query_type` / `mutation_type` / `subscription_type` / `root_type`)
|
|
33
34
|
- Python types auto-map: `str`→String, `int`→Int, `bool`→Boolean, `float`→Float, `UUID`→UUID scalar, `datetime`→DateTime, `Optional[T]`→nullable, `List[T]`→list, dataclasses/Pydantic models→object types, enums→enum types
|
|
34
|
-
- Schema setup: `GraphQLAPI(query_type=Q, mutation_type=M, subscription_type=S)` or legacy `root_type=Root`
|
|
35
|
+
- Schema setup: `GraphQLAPI(query_type=Q, mutation_type=M, subscription_type=S)` or legacy `root_type=Root` (or neither — just `@api.query`/`@api.mutation`/`@api.subscription`)
|
|
35
36
|
|
|
36
37
|
## Releasing
|
|
37
38
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphql-api
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.8
|
|
4
4
|
Summary: A framework for building Python GraphQL APIs.
|
|
5
5
|
Author-email: Robert Parker <rob@parob.com>
|
|
6
6
|
License: MIT
|
|
@@ -57,46 +57,56 @@ pip install graphql-api
|
|
|
57
57
|
|
|
58
58
|
## Quick Start
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
For simple APIs, register plain functions directly — no class required:
|
|
61
61
|
|
|
62
62
|
```python
|
|
63
63
|
# example.py
|
|
64
64
|
from graphql_api.api import GraphQLAPI
|
|
65
65
|
|
|
66
|
-
# 1. Initialize the API
|
|
67
66
|
api = GraphQLAPI()
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
""
|
|
73
|
-
The root query for our amazing API.
|
|
74
|
-
"""
|
|
75
|
-
@api.field
|
|
76
|
-
def hello(self, name: str = "World") -> str:
|
|
77
|
-
"""
|
|
78
|
-
A classic greeting.
|
|
79
|
-
"""
|
|
80
|
-
return f"Hello, {name}!"
|
|
68
|
+
@api.query
|
|
69
|
+
def hello(name: str = "World") -> str:
|
|
70
|
+
"""A classic greeting."""
|
|
71
|
+
return f"Hello, {name}!"
|
|
81
72
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
hello(name: "Developer")
|
|
86
|
-
}
|
|
87
|
-
"""
|
|
73
|
+
@api.mutation
|
|
74
|
+
def set_name(name: str) -> str:
|
|
75
|
+
return f"Name set to {name}"
|
|
88
76
|
|
|
89
|
-
# 4. Execute the query
|
|
90
77
|
if __name__ == "__main__":
|
|
91
|
-
result = api.execute(
|
|
78
|
+
result = api.execute('query { hello(name: "Developer") }')
|
|
92
79
|
print(result.data)
|
|
93
80
|
```
|
|
94
81
|
|
|
95
|
-
Running this script will produce:
|
|
96
|
-
|
|
97
82
|
```bash
|
|
98
83
|
$ python example.py
|
|
99
|
-
{'hello': 'Hello, Developer'}
|
|
84
|
+
{'hello': 'Hello, Developer!'}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Use `@api.query`, `@api.mutation`, and `@api.subscription` for root-level
|
|
88
|
+
fields. For nested object types, reach for the class-based form below.
|
|
89
|
+
|
|
90
|
+
### Class-based form
|
|
91
|
+
|
|
92
|
+
When you need object types with their own fields (relationships, computed
|
|
93
|
+
properties, nested resolvers), define a root class:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from graphql_api.api import GraphQLAPI
|
|
97
|
+
|
|
98
|
+
api = GraphQLAPI()
|
|
99
|
+
|
|
100
|
+
@api.type(is_root_type=True)
|
|
101
|
+
class Query:
|
|
102
|
+
"""The root query for our amazing API."""
|
|
103
|
+
@api.field
|
|
104
|
+
def hello(self, name: str = "World") -> str:
|
|
105
|
+
return f"Hello, {name}!"
|
|
106
|
+
|
|
107
|
+
result = api.execute('query { hello(name: "Developer") }')
|
|
108
|
+
print(result.data)
|
|
109
|
+
# {'hello': 'Hello, Developer!'}
|
|
100
110
|
```
|
|
101
111
|
|
|
102
112
|
## Examples
|
|
@@ -32,46 +32,56 @@ pip install graphql-api
|
|
|
32
32
|
|
|
33
33
|
## Quick Start
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
For simple APIs, register plain functions directly — no class required:
|
|
36
36
|
|
|
37
37
|
```python
|
|
38
38
|
# example.py
|
|
39
39
|
from graphql_api.api import GraphQLAPI
|
|
40
40
|
|
|
41
|
-
# 1. Initialize the API
|
|
42
41
|
api = GraphQLAPI()
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
""
|
|
48
|
-
The root query for our amazing API.
|
|
49
|
-
"""
|
|
50
|
-
@api.field
|
|
51
|
-
def hello(self, name: str = "World") -> str:
|
|
52
|
-
"""
|
|
53
|
-
A classic greeting.
|
|
54
|
-
"""
|
|
55
|
-
return f"Hello, {name}!"
|
|
43
|
+
@api.query
|
|
44
|
+
def hello(name: str = "World") -> str:
|
|
45
|
+
"""A classic greeting."""
|
|
46
|
+
return f"Hello, {name}!"
|
|
56
47
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
hello(name: "Developer")
|
|
61
|
-
}
|
|
62
|
-
"""
|
|
48
|
+
@api.mutation
|
|
49
|
+
def set_name(name: str) -> str:
|
|
50
|
+
return f"Name set to {name}"
|
|
63
51
|
|
|
64
|
-
# 4. Execute the query
|
|
65
52
|
if __name__ == "__main__":
|
|
66
|
-
result = api.execute(
|
|
53
|
+
result = api.execute('query { hello(name: "Developer") }')
|
|
67
54
|
print(result.data)
|
|
68
55
|
```
|
|
69
56
|
|
|
70
|
-
Running this script will produce:
|
|
71
|
-
|
|
72
57
|
```bash
|
|
73
58
|
$ python example.py
|
|
74
|
-
{'hello': 'Hello, Developer'}
|
|
59
|
+
{'hello': 'Hello, Developer!'}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Use `@api.query`, `@api.mutation`, and `@api.subscription` for root-level
|
|
63
|
+
fields. For nested object types, reach for the class-based form below.
|
|
64
|
+
|
|
65
|
+
### Class-based form
|
|
66
|
+
|
|
67
|
+
When you need object types with their own fields (relationships, computed
|
|
68
|
+
properties, nested resolvers), define a root class:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from graphql_api.api import GraphQLAPI
|
|
72
|
+
|
|
73
|
+
api = GraphQLAPI()
|
|
74
|
+
|
|
75
|
+
@api.type(is_root_type=True)
|
|
76
|
+
class Query:
|
|
77
|
+
"""The root query for our amazing API."""
|
|
78
|
+
@api.field
|
|
79
|
+
def hello(self, name: str = "World") -> str:
|
|
80
|
+
return f"Hello, {name}!"
|
|
81
|
+
|
|
82
|
+
result = api.execute('query { hello(name: "Developer") }')
|
|
83
|
+
print(result.data)
|
|
84
|
+
# {'hello': 'Hello, Developer!'}
|
|
75
85
|
```
|
|
76
86
|
|
|
77
87
|
## Examples
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.6.8
|
{graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/fundamentals/async-and-subscriptions.md
RENAMED
|
@@ -101,6 +101,29 @@ Subscriptions can be defined using either of the two schema definition modes des
|
|
|
101
101
|
|
|
102
102
|
### Subscription Examples
|
|
103
103
|
|
|
104
|
+
**Using Plain Functions (Simplest):**
|
|
105
|
+
|
|
106
|
+
Register an async generator directly with `@api.subscription`:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
import asyncio
|
|
110
|
+
from typing import AsyncGenerator
|
|
111
|
+
from graphql_api.api import GraphQLAPI
|
|
112
|
+
|
|
113
|
+
api = GraphQLAPI()
|
|
114
|
+
|
|
115
|
+
@api.query
|
|
116
|
+
def placeholder() -> str:
|
|
117
|
+
return "ok"
|
|
118
|
+
|
|
119
|
+
@api.subscription
|
|
120
|
+
async def count(to: int = 5) -> AsyncGenerator[int, None]:
|
|
121
|
+
"""Counts up to a given number, yielding each number."""
|
|
122
|
+
for i in range(1, to + 1):
|
|
123
|
+
await asyncio.sleep(1)
|
|
124
|
+
yield i
|
|
125
|
+
```
|
|
126
|
+
|
|
104
127
|
**Using Mode 1 (Single Root Type):**
|
|
105
128
|
|
|
106
129
|
```python
|
|
@@ -82,7 +82,39 @@ Global decorators avoid this issue since they don't require importing an API ins
|
|
|
82
82
|
|
|
83
83
|
## Schema Definition Modes
|
|
84
84
|
|
|
85
|
-
`graphql-api` offers
|
|
85
|
+
`graphql-api` offers three approaches for organizing your GraphQL operations (queries, mutations, subscriptions). They can be mixed in the same API.
|
|
86
|
+
|
|
87
|
+
### Mode 0: Plain Functions (Simplest)
|
|
88
|
+
|
|
89
|
+
Register free functions directly on the API — no class required. Good for small APIs, scripts, and cases where you don't need nested object types at the root.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from graphql_api.api import GraphQLAPI
|
|
93
|
+
|
|
94
|
+
api = GraphQLAPI()
|
|
95
|
+
|
|
96
|
+
@api.query
|
|
97
|
+
def hello(name: str = "World") -> str:
|
|
98
|
+
return f"Hello, {name}!"
|
|
99
|
+
|
|
100
|
+
@api.mutation
|
|
101
|
+
def set_counter(value: int) -> int:
|
|
102
|
+
return value
|
|
103
|
+
|
|
104
|
+
@api.subscription
|
|
105
|
+
async def ticks(to: int = 3): # AsyncGenerator[int, None]
|
|
106
|
+
for i in range(1, to + 1):
|
|
107
|
+
yield i
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Registered functions compose with any of the class-based modes below — they're folded onto whatever root/query/mutation/subscription type you supply, so you can gradually migrate from plain functions to classes (or vice versa) without breaking changes.
|
|
111
|
+
|
|
112
|
+
**Use when:**
|
|
113
|
+
- Building a small API or script where class boilerplate isn't earning its keep
|
|
114
|
+
- You want the lowest-friction onboarding for teammates new to GraphQL
|
|
115
|
+
- Mixing a handful of root fields onto an existing class-based schema
|
|
116
|
+
|
|
117
|
+
**Limitations:** plain functions have no `self` — so they can't share instance state. For request-scoped state, declare `context: GraphQLContext` as a parameter (same mechanism used by class-based fields). For nested object types, define them as classes and return instances from your registered function.
|
|
86
118
|
|
|
87
119
|
### Mode 1: Single Root Type (Strongly Recommended)
|
|
88
120
|
|
|
@@ -19,9 +19,44 @@ pip install graphql-api
|
|
|
19
19
|
|
|
20
20
|
Let's create a simple GraphQL API that returns a classic "Hello, World!" greeting.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
There are two ways to define root fields — pick whichever fits the shape of your API:
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
- **Plain functions** — register a free function as a root query/mutation/subscription. Great for small APIs, scripts, and anything that doesn't need nested object types.
|
|
25
|
+
- **Class-based** — define a root class and decorate its methods. Use this when you have nested object types, shared state, or interrelated resolvers.
|
|
26
|
+
|
|
27
|
+
You can mix both in the same API. We'll show the plain-function version first, then the class-based version.
|
|
28
|
+
|
|
29
|
+
### Option A: Plain functions (simplest)
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
# main.py
|
|
33
|
+
from graphql_api.api import GraphQLAPI
|
|
34
|
+
|
|
35
|
+
api = GraphQLAPI()
|
|
36
|
+
|
|
37
|
+
@api.query
|
|
38
|
+
def hello(name: str = "World") -> str:
|
|
39
|
+
"""A classic greeting. This docstring becomes the field description."""
|
|
40
|
+
return f"Hello, {name}!"
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
result = api.execute('query { hello(name: "Developer") }')
|
|
44
|
+
print(result.data) # {'hello': 'Hello, Developer!'}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Three decorators are available on the API instance:
|
|
48
|
+
|
|
49
|
+
- `@api.query` — register a query field
|
|
50
|
+
- `@api.mutation` — register a mutation field
|
|
51
|
+
- `@api.subscription` — register a subscription field (function must return `AsyncGenerator[T, None]`)
|
|
52
|
+
|
|
53
|
+
They accept the same `meta={...}` and `directives=[...]` parameters as `@api.field`. Type hints, defaults, `Optional[T]`, dataclass/Pydantic inputs, and `GraphQLContext` injection all work identically to class-based fields.
|
|
54
|
+
|
|
55
|
+
### Option B: Class-based
|
|
56
|
+
|
|
57
|
+
Use a class when you need nested object types, shared state, or multiple interrelated resolvers.
|
|
58
|
+
|
|
59
|
+
#### 1. Initialize the API
|
|
25
60
|
|
|
26
61
|
```python
|
|
27
62
|
# main.py
|
|
@@ -30,9 +65,9 @@ from graphql_api.api import GraphQLAPI
|
|
|
30
65
|
api = GraphQLAPI()
|
|
31
66
|
```
|
|
32
67
|
|
|
33
|
-
|
|
68
|
+
#### 2. Define the Root Query
|
|
34
69
|
|
|
35
|
-
|
|
70
|
+
Define a class that will serve as your root query object. Use the `@api.type` decorator to mark it as the root type for your schema and the `@api.field` decorator to expose a method as a GraphQL field.
|
|
36
71
|
|
|
37
72
|
```python
|
|
38
73
|
# main.py
|
|
@@ -74,7 +109,7 @@ This generates:
|
|
|
74
109
|
example(requiredArg: String!, optionalArg: String): String!
|
|
75
110
|
```
|
|
76
111
|
|
|
77
|
-
|
|
112
|
+
#### 3. Execute a Query
|
|
78
113
|
|
|
79
114
|
Now you're ready to execute a query against your API.
|
|
80
115
|
|
|
@@ -9,7 +9,34 @@ description: >
|
|
|
9
9
|
|
|
10
10
|
GraphQL mutations allow clients to modify data on the server. This guide covers how to create mutations and define input types for complex data operations.
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## Plain Functions (Simplest)
|
|
13
|
+
|
|
14
|
+
The lowest-friction way to define a mutation is a free function decorated with `@api.mutation` — no class required:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from graphql_api.api import GraphQLAPI
|
|
18
|
+
|
|
19
|
+
api = GraphQLAPI()
|
|
20
|
+
|
|
21
|
+
@api.query
|
|
22
|
+
def get_user(user_id: str):
|
|
23
|
+
return find_user_by_id(user_id)
|
|
24
|
+
|
|
25
|
+
@api.mutation
|
|
26
|
+
def create_user(name: str, email: str) -> User:
|
|
27
|
+
"""Create a new user account."""
|
|
28
|
+
user = User(id=generate_id(), name=name, email=email)
|
|
29
|
+
save_user(user)
|
|
30
|
+
return user
|
|
31
|
+
|
|
32
|
+
@api.mutation
|
|
33
|
+
def delete_user(user_id: str) -> bool:
|
|
34
|
+
return delete_user_by_id(user_id)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Input types, `Optional[T]`, defaults, dataclass/Pydantic inputs, and `GraphQLContext` injection all work exactly as they do for class-based mutations (covered below).
|
|
38
|
+
|
|
39
|
+
## Basic Mutations (Class-based)
|
|
13
40
|
|
|
14
41
|
In Mode 1 (Single Root Type), mark mutation fields with `mutable=True`:
|
|
15
42
|
|
|
@@ -191,6 +191,55 @@ async def user_updates(self) -> AsyncGenerator[User, None]:
|
|
|
191
191
|
```
|
|
192
192
|
|
|
193
193
|
|
|
194
|
+
### `@api.query`, `@api.mutation`, `@api.subscription`
|
|
195
|
+
|
|
196
|
+
Register a plain (free) function as a root-level GraphQL field. Useful when you don't want to wrap root fields in a class.
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
@api.query(
|
|
200
|
+
meta: Optional[Dict] = None,
|
|
201
|
+
directives: Optional[List] = None,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
@api.mutation(
|
|
205
|
+
meta: Optional[Dict] = None,
|
|
206
|
+
directives: Optional[List] = None,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
@api.subscription(
|
|
210
|
+
meta: Optional[Dict] = None,
|
|
211
|
+
directives: Optional[List] = None,
|
|
212
|
+
)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Parameters:**
|
|
216
|
+
- `meta`: Optional metadata dictionary, retrievable in middleware (same semantics as `@api.field(meta=...)`).
|
|
217
|
+
- `directives`: Optional list of applied schema directives.
|
|
218
|
+
|
|
219
|
+
**Behavior:**
|
|
220
|
+
- `@api.query` adds the function to the root `Query` type.
|
|
221
|
+
- `@api.mutation` adds the function to the root `Mutation` type.
|
|
222
|
+
- `@api.subscription` adds the function to the root `Subscription` type. The function must return `AsyncGenerator[T, None]`.
|
|
223
|
+
- Registered functions compose with `query_type=` / `mutation_type=` / `subscription_type=` / `root_type=` — they are folded onto the user-supplied class so you can mix class-based and function-based root fields freely.
|
|
224
|
+
- Type hints, default values, `Optional[T]`, dataclass/Pydantic inputs, enum args, and `context: GraphQLContext` injection all work identically to `@api.field`.
|
|
225
|
+
- Plain functions have no `self`; use a `GraphQLContext` parameter for request-scoped state, and module-level or captured state otherwise.
|
|
226
|
+
|
|
227
|
+
**Bare and parameterized usage:**
|
|
228
|
+
```python
|
|
229
|
+
@api.query
|
|
230
|
+
def hello(name: str = "World") -> str:
|
|
231
|
+
return f"Hello, {name}!"
|
|
232
|
+
|
|
233
|
+
@api.mutation(meta={"audit": True})
|
|
234
|
+
def set_counter(value: int) -> int:
|
|
235
|
+
return value
|
|
236
|
+
|
|
237
|
+
@api.subscription
|
|
238
|
+
async def ticks(to: int = 3) -> AsyncGenerator[int, None]:
|
|
239
|
+
for i in range(1, to + 1):
|
|
240
|
+
yield i
|
|
241
|
+
```
|
|
242
|
+
|
|
194
243
|
### `@api.enum`
|
|
195
244
|
|
|
196
245
|
Mark an enum class as a GraphQL enum.
|
|
@@ -266,6 +266,9 @@ class GraphQLAPI(GraphQLBaseExecutor):
|
|
|
266
266
|
self.enum_suffix = enum_suffix
|
|
267
267
|
self.interface_suffix = interface_suffix
|
|
268
268
|
self.input_suffix = input_suffix
|
|
269
|
+
self._registered_queries: List[Callable] = []
|
|
270
|
+
self._registered_mutations: List[Callable] = []
|
|
271
|
+
self._registered_subscriptions: List[Callable] = []
|
|
269
272
|
|
|
270
273
|
# --------------------------------------------------------------------------
|
|
271
274
|
# DECORATORS
|
|
@@ -330,9 +333,207 @@ class GraphQLAPI(GraphQLBaseExecutor):
|
|
|
330
333
|
self.root_type = root_type
|
|
331
334
|
return root_type
|
|
332
335
|
|
|
336
|
+
def _register_plain(
|
|
337
|
+
self,
|
|
338
|
+
arg,
|
|
339
|
+
graphql_type: str,
|
|
340
|
+
meta: Optional[Dict],
|
|
341
|
+
directives: Optional[List],
|
|
342
|
+
target_list: List[Callable],
|
|
343
|
+
):
|
|
344
|
+
"""
|
|
345
|
+
Shared implementation for api.query / api.mutation / api.subscription.
|
|
346
|
+
|
|
347
|
+
Supports both bare `@api.query` and parameterized `@api.query(meta={...})`
|
|
348
|
+
usage. In the bare form, `arg` is the function being decorated. In the
|
|
349
|
+
parameterized form, `arg` is None (only keyword args were given) and we
|
|
350
|
+
return a decorator.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
def tag_and_register(func):
|
|
354
|
+
tagged = tag_value(
|
|
355
|
+
value=func,
|
|
356
|
+
graphql_type=graphql_type,
|
|
357
|
+
schema=self,
|
|
358
|
+
meta=meta,
|
|
359
|
+
directives=directives,
|
|
360
|
+
)
|
|
361
|
+
if tagged not in target_list:
|
|
362
|
+
target_list.append(tagged)
|
|
363
|
+
self._cached_schema = None
|
|
364
|
+
return tagged
|
|
365
|
+
|
|
366
|
+
if arg is not None and callable(arg):
|
|
367
|
+
return tag_and_register(arg)
|
|
368
|
+
return tag_and_register
|
|
369
|
+
|
|
370
|
+
def query(
|
|
371
|
+
self=None,
|
|
372
|
+
func: Optional[Callable] = None,
|
|
373
|
+
meta: Optional[Dict] = None,
|
|
374
|
+
directives: Optional[List] = None,
|
|
375
|
+
):
|
|
376
|
+
"""
|
|
377
|
+
Register a plain function as a root-level GraphQL query field.
|
|
378
|
+
|
|
379
|
+
Usage:
|
|
380
|
+
@api.query
|
|
381
|
+
def hello(name: str) -> str: ...
|
|
382
|
+
|
|
383
|
+
@api.query(meta={"note": "..."})
|
|
384
|
+
def goodbye(name: str) -> str: ...
|
|
385
|
+
"""
|
|
386
|
+
return self._register_plain(
|
|
387
|
+
arg=func,
|
|
388
|
+
graphql_type="field",
|
|
389
|
+
meta=meta,
|
|
390
|
+
directives=directives,
|
|
391
|
+
target_list=self._registered_queries,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
def mutation(
|
|
395
|
+
self=None,
|
|
396
|
+
func: Optional[Callable] = None,
|
|
397
|
+
meta: Optional[Dict] = None,
|
|
398
|
+
directives: Optional[List] = None,
|
|
399
|
+
):
|
|
400
|
+
"""
|
|
401
|
+
Register a plain function as a root-level GraphQL mutation field.
|
|
402
|
+
"""
|
|
403
|
+
return self._register_plain(
|
|
404
|
+
arg=func,
|
|
405
|
+
graphql_type="mutable_field",
|
|
406
|
+
meta=meta,
|
|
407
|
+
directives=directives,
|
|
408
|
+
target_list=self._registered_mutations,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
def subscription(
|
|
412
|
+
self=None,
|
|
413
|
+
func: Optional[Callable] = None,
|
|
414
|
+
meta: Optional[Dict] = None,
|
|
415
|
+
directives: Optional[List] = None,
|
|
416
|
+
):
|
|
417
|
+
"""
|
|
418
|
+
Register a plain function as a root-level GraphQL subscription field.
|
|
419
|
+
The function should return `AsyncGenerator[T, None]`.
|
|
420
|
+
"""
|
|
421
|
+
return self._register_plain(
|
|
422
|
+
arg=func,
|
|
423
|
+
graphql_type="subscription_field",
|
|
424
|
+
meta=meta,
|
|
425
|
+
directives=directives,
|
|
426
|
+
target_list=self._registered_subscriptions,
|
|
427
|
+
)
|
|
428
|
+
|
|
333
429
|
# --------------------------------------------------------------------------
|
|
334
430
|
# SCHEMA BUILDING & EXECUTION
|
|
335
431
|
# --------------------------------------------------------------------------
|
|
432
|
+
def _fold_registered_functions(self):
|
|
433
|
+
"""
|
|
434
|
+
Synthesize root classes from functions registered via
|
|
435
|
+
@api.query / @api.mutation / @api.subscription, or subclass existing
|
|
436
|
+
user-supplied root types to include them.
|
|
437
|
+
|
|
438
|
+
Each registered function is attached to the synthesized class as a
|
|
439
|
+
method wrapper that accepts (and discards) the leading `self` arg,
|
|
440
|
+
forwarding keyword args to the real function. This matches the
|
|
441
|
+
resolver's call convention at graphql_api/mapper.py:485-517 — which
|
|
442
|
+
ends up calling `function_type(_self, **_args)` for root-level fields
|
|
443
|
+
where `_self` is typically None — so the wrapper absorbs `_self` and
|
|
444
|
+
the free function sees only its declared keyword args.
|
|
445
|
+
|
|
446
|
+
The wrapper preserves `__annotations__`, `__wrapped__`, and the
|
|
447
|
+
`_graphql` / `_schemas` tags so `typing.get_type_hints` and
|
|
448
|
+
`inspect.signature` (which follows `__wrapped__`) return the original
|
|
449
|
+
function's hints and defaults.
|
|
450
|
+
"""
|
|
451
|
+
if not (
|
|
452
|
+
self._registered_queries
|
|
453
|
+
or self._registered_mutations
|
|
454
|
+
or self._registered_subscriptions
|
|
455
|
+
):
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
def make_method(fn: Callable) -> Callable:
|
|
459
|
+
def _method(_self, **kwargs):
|
|
460
|
+
return fn(**kwargs)
|
|
461
|
+
_method.__name__ = fn.__name__
|
|
462
|
+
_method.__qualname__ = getattr(fn, "__qualname__", fn.__name__)
|
|
463
|
+
_method.__doc__ = fn.__doc__
|
|
464
|
+
_method.__module__ = getattr(fn, "__module__", None)
|
|
465
|
+
_method.__annotations__ = dict(getattr(fn, "__annotations__", {}))
|
|
466
|
+
_method.__wrapped__ = fn # inspect.signature follows this
|
|
467
|
+
# Preserve the @field-style tags attached by tag_value()
|
|
468
|
+
for attr in ("_graphql", "_schemas", "_defined_on",
|
|
469
|
+
"_applied_directives"):
|
|
470
|
+
if hasattr(fn, attr):
|
|
471
|
+
setattr(_method, attr, getattr(fn, attr))
|
|
472
|
+
return _method
|
|
473
|
+
|
|
474
|
+
def unwrap(base: Optional[Type]) -> Optional[Type]:
|
|
475
|
+
# If we previously synthesized on top of this slot, peel that
|
|
476
|
+
# layer off so re-builds don't stack subclasses.
|
|
477
|
+
while base is not None and getattr(
|
|
478
|
+
base, "__graphql_api_synthesized__", False
|
|
479
|
+
):
|
|
480
|
+
parents = [b for b in base.__bases__ if b is not object]
|
|
481
|
+
base = parents[0] if parents else None
|
|
482
|
+
return base
|
|
483
|
+
|
|
484
|
+
def synthesize(name: str, base: Optional[Type], fns: List[Callable]):
|
|
485
|
+
if not fns:
|
|
486
|
+
return base
|
|
487
|
+
attrs: Dict[str, Any] = {fn.__name__: make_method(fn) for fn in fns}
|
|
488
|
+
attrs["__graphql_api_synthesized__"] = True
|
|
489
|
+
bases: Tuple[type, ...] = (base,) if base else ()
|
|
490
|
+
return type(name, bases, attrs)
|
|
491
|
+
|
|
492
|
+
# root_type path: legacy single-class mode. Fold every registered
|
|
493
|
+
# function (queries + mutations) onto a subclass of root_type.
|
|
494
|
+
if self.root_type is not None:
|
|
495
|
+
if self._registered_subscriptions:
|
|
496
|
+
raise GraphQLError(
|
|
497
|
+
"api.subscription is not supported together with "
|
|
498
|
+
"root_type=. Use query_type/mutation_type/subscription_type "
|
|
499
|
+
"instead."
|
|
500
|
+
)
|
|
501
|
+
combined = [
|
|
502
|
+
*self._registered_queries,
|
|
503
|
+
*self._registered_mutations,
|
|
504
|
+
]
|
|
505
|
+
base_root = unwrap(self.root_type)
|
|
506
|
+
if base_root is None:
|
|
507
|
+
return
|
|
508
|
+
self.root_type = synthesize(
|
|
509
|
+
base_root.__name__, base_root, combined
|
|
510
|
+
)
|
|
511
|
+
return
|
|
512
|
+
|
|
513
|
+
# Explicit mode: fold each list onto its matching *_type, always
|
|
514
|
+
# starting from the unwrapped original so re-builds are idempotent.
|
|
515
|
+
base_query = unwrap(self.query_type)
|
|
516
|
+
base_mutation = unwrap(self.mutation_type)
|
|
517
|
+
base_subscription = unwrap(self.subscription_type)
|
|
518
|
+
|
|
519
|
+
self.query_type = synthesize(
|
|
520
|
+
"Query" if base_query is None else base_query.__name__,
|
|
521
|
+
base_query,
|
|
522
|
+
self._registered_queries,
|
|
523
|
+
)
|
|
524
|
+
self.mutation_type = synthesize(
|
|
525
|
+
"Mutation" if base_mutation is None else base_mutation.__name__,
|
|
526
|
+
base_mutation,
|
|
527
|
+
self._registered_mutations,
|
|
528
|
+
)
|
|
529
|
+
self.subscription_type = synthesize(
|
|
530
|
+
"Subscription"
|
|
531
|
+
if base_subscription is None
|
|
532
|
+
else base_subscription.__name__,
|
|
533
|
+
base_subscription,
|
|
534
|
+
self._registered_subscriptions,
|
|
535
|
+
)
|
|
536
|
+
|
|
336
537
|
def schema(self, ignore_cache: bool = False) -> GraphQLSchema:
|
|
337
538
|
return self.build(ignore_cache)[0]
|
|
338
539
|
|
|
@@ -345,6 +546,13 @@ class GraphQLAPI(GraphQLBaseExecutor):
|
|
|
345
546
|
if not ignore_cache and self._cached_schema:
|
|
346
547
|
return self._cached_schema
|
|
347
548
|
|
|
549
|
+
# Fold any plain functions registered via @api.query / @api.mutation /
|
|
550
|
+
# @api.subscription into query_type / mutation_type / subscription_type
|
|
551
|
+
# (or subclass them onto root_type). The synthesized classes attach each
|
|
552
|
+
# registered function as a staticmethod so the resolver in
|
|
553
|
+
# graphql_api/mapper.py treats them as free functions (no self injected).
|
|
554
|
+
self._fold_registered_functions()
|
|
555
|
+
|
|
348
556
|
# Federation support
|
|
349
557
|
if self.federation:
|
|
350
558
|
from graphql_api.federation.federation import add_federation_types
|