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.
Files changed (108) hide show
  1. {graphql_api-1.6.7 → graphql_api-1.6.8}/.github/workflows/pages.yml +1 -1
  2. {graphql_api-1.6.7 → graphql_api-1.6.8}/AGENTS.md +2 -1
  3. {graphql_api-1.6.7 → graphql_api-1.6.8}/CLAUDE.md +2 -1
  4. {graphql_api-1.6.7 → graphql_api-1.6.8}/PKG-INFO +36 -26
  5. {graphql_api-1.6.7 → graphql_api-1.6.8}/README.md +35 -25
  6. graphql_api-1.6.8/VERSION +1 -0
  7. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/fundamentals/async-and-subscriptions.md +23 -0
  8. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/fundamentals/defining-schemas.md +33 -1
  9. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/fundamentals/getting-started.md +40 -5
  10. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/fundamentals/mutations.md +28 -1
  11. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/reference/api-reference.md +49 -0
  12. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/api.py +208 -0
  13. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/executor.py +69 -11
  14. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/utils.py +6 -0
  15. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api.egg-info/PKG-INFO +36 -26
  16. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api.egg-info/SOURCES.txt +1 -0
  17. graphql_api-1.6.8/tests/test_plain_functions.py +461 -0
  18. graphql_api-1.6.7/VERSION +0 -1
  19. {graphql_api-1.6.7 → graphql_api-1.6.8}/.github/workflows/python-package.yml +0 -0
  20. {graphql_api-1.6.7 → graphql_api-1.6.8}/.gitignore +0 -0
  21. {graphql_api-1.6.7 → graphql_api-1.6.8}/.vscode/settings.json +0 -0
  22. {graphql_api-1.6.7 → graphql_api-1.6.8}/LICENSE +0 -0
  23. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/README.md +0 -0
  24. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/_index.md +0 -0
  25. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/_index.md +0 -0
  26. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/advanced-features/_index.md +0 -0
  27. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/advanced-features/directives.md +0 -0
  28. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/advanced-features/performance.md +0 -0
  29. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/advanced-features/schema-filtering.md +0 -0
  30. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/building-applications/_index.md +0 -0
  31. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/building-applications/documentation.md +0 -0
  32. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/building-applications/error-handling.md +0 -0
  33. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/distributed-systems/_index.md +0 -0
  34. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/distributed-systems/federation.md +0 -0
  35. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/distributed-systems/pagination-relay.md +0 -0
  36. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/distributed-systems/remote-graphql.md +0 -0
  37. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/fundamentals/_index.md +0 -0
  38. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/fundamentals/enums-interfaces.md +0 -0
  39. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/fundamentals/field-types.md +0 -0
  40. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/fundamentals/object-types.md +0 -0
  41. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/fundamentals/pydantic-and-dataclasses.md +0 -0
  42. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/production/_index.md +0 -0
  43. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/production/context-metadata.md +0 -0
  44. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/production/middleware.md +0 -0
  45. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/reference/_index.md +0 -0
  46. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/reference/contributing.md +0 -0
  47. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/content/docs/reference/examples.md +0 -0
  48. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/hugo.toml +0 -0
  49. {graphql_api-1.6.7 → graphql_api-1.6.8}/docs/index.md +0 -0
  50. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/__init__.py +0 -0
  51. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/context.py +0 -0
  52. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/dataclass_mapping.py +0 -0
  53. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/decorators.py +0 -0
  54. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/directives.py +0 -0
  55. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/error.py +0 -0
  56. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/exception.py +0 -0
  57. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/federation/__init__.py +0 -0
  58. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/federation/directives.py +0 -0
  59. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/federation/federation.py +0 -0
  60. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/federation/types.py +0 -0
  61. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/mapper.py +0 -0
  62. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/middleware.py +0 -0
  63. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/mutation_schema.py +0 -0
  64. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/pydantic.py +0 -0
  65. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/pydantic_utils.py +0 -0
  66. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/reduce.py +0 -0
  67. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/relay.py +0 -0
  68. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/remote.py +0 -0
  69. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/schema.py +0 -0
  70. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/schema_filter.py +0 -0
  71. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/type_utils.py +0 -0
  72. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api/types.py +0 -0
  73. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api.egg-info/dependency_links.txt +0 -0
  74. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api.egg-info/requires.txt +0 -0
  75. {graphql_api-1.6.7 → graphql_api-1.6.8}/graphql_api.egg-info/top_level.txt +0 -0
  76. {graphql_api-1.6.7 → graphql_api-1.6.8}/pyproject.toml +0 -0
  77. {graphql_api-1.6.7 → graphql_api-1.6.8}/setup.cfg +0 -0
  78. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/__init__.py +0 -0
  79. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_async.py +0 -0
  80. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_calculator.py +0 -0
  81. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_custom_suffixes.py +0 -0
  82. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_custom_types.py +0 -0
  83. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_dataclass.py +0 -0
  84. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_dataclass_relationships.py +0 -0
  85. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_decorator_patterns.py +0 -0
  86. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_directives.py +0 -0
  87. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_docs_advanced.py +0 -0
  88. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_docs_async_subscriptions.py +0 -0
  89. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_docs_defining_schemas.py +0 -0
  90. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_docs_field_types.py +0 -0
  91. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_docs_getting_started.py +0 -0
  92. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_docs_relay.py +0 -0
  93. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_docstrings.py +0 -0
  94. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_error.py +0 -0
  95. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_federation.py +0 -0
  96. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_federation_example.graphql +0 -0
  97. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_federation_example.py +0 -0
  98. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_filtering.py +0 -0
  99. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_graphql.py +0 -0
  100. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_middleware.py +0 -0
  101. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_performance.py +0 -0
  102. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_pydantic.py +0 -0
  103. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_relay.py +0 -0
  104. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_remote.py +0 -0
  105. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_schema.py +0 -0
  106. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_subscriptions.py +0 -0
  107. {graphql_api-1.6.7 → graphql_api-1.6.8}/tests/test_unified_vs_explicit_schema.py +0 -0
  108. {graphql_api-1.6.7 → graphql_api-1.6.8}/uv.lock +0 -0
@@ -22,7 +22,7 @@ jobs:
22
22
  build:
23
23
  runs-on: ubuntu-latest
24
24
  env:
25
- HUGO_VERSION: 0.146.0
25
+ HUGO_VERSION: 0.160.1
26
26
  steps:
27
27
  - name: Checkout
28
28
  uses: actions/checkout@v4
@@ -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.7
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
- Create a simple GraphQL API in just a few lines of code.
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
- # 2. Define your root type with decorators
70
- @api.type(is_root_type=True)
71
- class Query:
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
- # 3. Define a query
83
- graphql_query = """
84
- query Greetings {
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(graphql_query)
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
- Create a simple GraphQL API in just a few lines of code.
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
- # 2. Define your root type with decorators
45
- @api.type(is_root_type=True)
46
- class Query:
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
- # 3. Define a query
58
- graphql_query = """
59
- query Greetings {
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(graphql_query)
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
@@ -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 two distinct approaches for organizing your GraphQL operations (queries, mutations, subscriptions):
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
- ### 1. Initialize the API
22
+ There are two ways to define root fields — pick whichever fits the shape of your API:
23
23
 
24
- First, create a new Python file (e.g., `main.py`) and initialize `GraphQLAPI`:
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
- ### 2. Define the Root Query
68
+ #### 2. Define the Root Query
34
69
 
35
- Next, 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.
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
- ### 3. Execute a Query
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
- ## Basic Mutations
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