graphql-api 1.6.10__tar.gz → 2.0.0__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 (110) hide show
  1. {graphql_api-1.6.10 → graphql_api-2.0.0}/.github/workflows/python-package.yml +3 -1
  2. {graphql_api-1.6.10 → graphql_api-2.0.0}/PKG-INFO +8 -3
  3. graphql_api-2.0.0/VERSION +1 -0
  4. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/fundamentals/field-types.md +13 -10
  5. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/executor.py +29 -28
  6. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/mapper.py +2 -25
  7. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/remote.py +17 -3
  8. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/types.py +49 -39
  9. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api.egg-info/PKG-INFO +8 -3
  10. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api.egg-info/requires.txt +1 -1
  11. {graphql_api-1.6.10 → graphql_api-2.0.0}/pyproject.toml +7 -2
  12. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_custom_types.py +7 -6
  13. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_docs_field_types.py +8 -7
  14. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_executor_signature.py +36 -17
  15. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_remote.py +18 -2
  16. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_subscriptions.py +9 -4
  17. graphql_api-2.0.0/uv.lock +1506 -0
  18. graphql_api-1.6.10/VERSION +0 -1
  19. graphql_api-1.6.10/uv.lock +0 -952
  20. {graphql_api-1.6.10 → graphql_api-2.0.0}/.github/workflows/pages.yml +0 -0
  21. {graphql_api-1.6.10 → graphql_api-2.0.0}/.gitignore +0 -0
  22. {graphql_api-1.6.10 → graphql_api-2.0.0}/.vscode/settings.json +0 -0
  23. {graphql_api-1.6.10 → graphql_api-2.0.0}/AGENTS.md +0 -0
  24. {graphql_api-1.6.10 → graphql_api-2.0.0}/CLAUDE.md +0 -0
  25. {graphql_api-1.6.10 → graphql_api-2.0.0}/LICENSE +0 -0
  26. {graphql_api-1.6.10 → graphql_api-2.0.0}/README.md +0 -0
  27. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/README.md +0 -0
  28. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/_index.md +0 -0
  29. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/_index.md +0 -0
  30. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/advanced-features/_index.md +0 -0
  31. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/advanced-features/directives.md +0 -0
  32. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/advanced-features/performance.md +0 -0
  33. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/advanced-features/schema-filtering.md +0 -0
  34. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/building-applications/_index.md +0 -0
  35. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/building-applications/documentation.md +0 -0
  36. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/building-applications/error-handling.md +0 -0
  37. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/distributed-systems/_index.md +0 -0
  38. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/distributed-systems/federation.md +0 -0
  39. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/distributed-systems/pagination-relay.md +0 -0
  40. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/distributed-systems/remote-graphql.md +0 -0
  41. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/fundamentals/_index.md +0 -0
  42. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/fundamentals/async-and-subscriptions.md +0 -0
  43. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/fundamentals/defining-schemas.md +0 -0
  44. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/fundamentals/enums-interfaces.md +0 -0
  45. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/fundamentals/getting-started.md +0 -0
  46. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/fundamentals/mutations.md +0 -0
  47. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/fundamentals/object-types.md +0 -0
  48. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/fundamentals/pydantic-and-dataclasses.md +0 -0
  49. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/production/_index.md +0 -0
  50. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/production/context-metadata.md +0 -0
  51. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/production/middleware.md +0 -0
  52. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/reference/_index.md +0 -0
  53. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/reference/api-reference.md +0 -0
  54. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/reference/contributing.md +0 -0
  55. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/reference/examples.md +0 -0
  56. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/hugo.toml +0 -0
  57. {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/index.md +0 -0
  58. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/__init__.py +0 -0
  59. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/api.py +0 -0
  60. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/context.py +0 -0
  61. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/dataclass_mapping.py +0 -0
  62. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/decorators.py +0 -0
  63. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/directives.py +0 -0
  64. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/error.py +0 -0
  65. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/exception.py +0 -0
  66. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/federation/__init__.py +0 -0
  67. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/federation/directives.py +0 -0
  68. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/federation/federation.py +0 -0
  69. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/federation/types.py +0 -0
  70. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/middleware.py +0 -0
  71. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/mutation_schema.py +0 -0
  72. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/pydantic.py +0 -0
  73. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/pydantic_utils.py +0 -0
  74. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/reduce.py +0 -0
  75. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/relay.py +0 -0
  76. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/schema.py +0 -0
  77. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/schema_filter.py +0 -0
  78. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/type_utils.py +0 -0
  79. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/utils.py +0 -0
  80. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api.egg-info/SOURCES.txt +0 -0
  81. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api.egg-info/dependency_links.txt +0 -0
  82. {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api.egg-info/top_level.txt +0 -0
  83. {graphql_api-1.6.10 → graphql_api-2.0.0}/setup.cfg +0 -0
  84. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/__init__.py +0 -0
  85. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_async.py +0 -0
  86. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_calculator.py +0 -0
  87. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_custom_suffixes.py +0 -0
  88. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_dataclass.py +0 -0
  89. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_dataclass_relationships.py +0 -0
  90. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_decorator_patterns.py +0 -0
  91. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_directives.py +0 -0
  92. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_docs_advanced.py +0 -0
  93. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_docs_async_subscriptions.py +0 -0
  94. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_docs_defining_schemas.py +0 -0
  95. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_docs_getting_started.py +0 -0
  96. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_docs_relay.py +0 -0
  97. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_docstrings.py +0 -0
  98. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_error.py +0 -0
  99. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_federation.py +0 -0
  100. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_federation_example.graphql +0 -0
  101. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_federation_example.py +0 -0
  102. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_filtering.py +0 -0
  103. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_graphql.py +0 -0
  104. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_middleware.py +0 -0
  105. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_performance.py +0 -0
  106. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_plain_functions.py +0 -0
  107. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_pydantic.py +0 -0
  108. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_relay.py +0 -0
  109. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_schema.py +0 -0
  110. {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_unified_vs_explicit_schema.py +0 -0
@@ -17,7 +17,7 @@ jobs:
17
17
  strategy:
18
18
  fail-fast: false
19
19
  matrix:
20
- python-version: ["3.11", "3.12"] # graphql-api requires Python 3.11+
20
+ python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"] # graphql-api requires Python 3.11+
21
21
 
22
22
  steps:
23
23
  - name: Checkout repository
@@ -30,6 +30,8 @@ jobs:
30
30
 
31
31
  - name: Install UV
32
32
  uses: astral-sh/setup-uv@v3
33
+ with:
34
+ enable-cache: true
33
35
 
34
36
  - name: Install dependencies
35
37
  run: uv sync --dev
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphql-api
3
- Version: 1.6.10
3
+ Version: 2.0.0
4
4
  Summary: A framework for building Python GraphQL APIs.
5
5
  Author-email: Robert Parker <rob@parob.com>
6
6
  License: MIT
@@ -10,12 +10,17 @@ Keywords: GraphQL,GraphQL-API,GraphQLAPI,Server
10
10
  Classifier: Development Status :: 3 - Alpha
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Python :: 3.15
13
18
  Classifier: License :: OSI Approved :: MIT License
14
19
  Classifier: Operating System :: OS Independent
15
- Requires-Python: <3.13,>=3.11
20
+ Requires-Python: >=3.11
16
21
  Description-Content-Type: text/markdown
17
22
  License-File: LICENSE
18
- Requires-Dist: graphql-core
23
+ Requires-Dist: graphql-core<3.4,>=3.3.1
19
24
  Requires-Dist: requests
20
25
  Requires-Dist: typing-inspect
21
26
  Requires-Dist: aiohttp
@@ -0,0 +1 @@
1
+ 2.0.0
@@ -180,22 +180,23 @@ You can define custom GraphQL scalar types for specialized data handling:
180
180
  from graphql import GraphQLScalarType, StringValueNode
181
181
 
182
182
  # Define a custom scalar
183
- def parse_value(value):
183
+ def coerce_input_value(value):
184
184
  return str(value) + "_parsed"
185
185
 
186
- def parse_literal(node):
186
+ def coerce_input_literal(node):
187
187
  if isinstance(node, StringValueNode):
188
- return parse_value(node.value)
188
+ return coerce_input_value(node.value)
189
+ raise TypeError("A Key literal must be a string")
189
190
 
190
- def serialize(value):
191
+ def coerce_output_value(value):
191
192
  return str(value) + "_serialized"
192
193
 
193
194
  GraphQLKey = GraphQLScalarType(
194
195
  name="Key",
195
196
  description="The `Key` scalar type represents a unique key.",
196
- serialize=serialize,
197
- parse_value=parse_value,
198
- parse_literal=parse_literal,
197
+ coerce_output_value=coerce_output_value,
198
+ coerce_input_value=coerce_input_value,
199
+ coerce_input_literal=coerce_input_literal,
199
200
  )
200
201
 
201
202
  @api.type(is_root_type=True)
@@ -206,9 +207,11 @@ class Root:
206
207
  ```
207
208
 
208
209
  **Key functions:**
209
- - `serialize`: Converts Python value to GraphQL response format
210
- - `parse_value`: Parses variable values from GraphQL requests
211
- - `parse_literal`: Parses literal values from GraphQL queries
210
+ - `coerce_output_value`: Converts a Python value to the GraphQL response format
211
+ - `coerce_input_value`: Parses variable values from GraphQL requests
212
+ - `coerce_input_literal`: Parses literal values written in GraphQL queries
213
+
214
+ These are the graphql-core 3.3 names. The older `serialize`, `parse_value` and `parse_literal` still work until graphql-core 3.4, but `parse_literal` is now always called with two arguments, `(node, variables)`.
212
215
 
213
216
  ## GraphQL ID Type
214
217
 
@@ -3,7 +3,6 @@ from inspect import isawaitable
3
3
  from typing import Any, Dict, Optional, List, Callable, AsyncIterator, Tuple
4
4
 
5
5
  from graphql import (
6
- ExecutionContext,
7
6
  GraphQLError,
8
7
  GraphQLOutputType,
9
8
  execute,
@@ -14,7 +13,9 @@ from graphql import (
14
13
  validate_schema,
15
14
  )
16
15
  from graphql.execution import ExecutionResult
16
+ from graphql.execution.executor import Executor
17
17
  from graphql.execution.middleware import MiddlewareManager
18
+ from graphql.pyutils import Path
18
19
  from graphql.language import DocumentNode
19
20
  from graphql.type.schema import GraphQLSchema
20
21
 
@@ -54,10 +55,10 @@ class GraphQLBaseExecutor:
54
55
  raise NotImplementedError
55
56
 
56
57
 
57
- class ErrorProtectionExecutionContext(ExecutionContext):
58
+ class ErrorProtectionExecutionContext(Executor):
58
59
  """
59
- A custom GraphQL ExecutionContext that can selectively suppress or raise errors
60
- based on an 'error_protection' attribute in the GraphQLError or its original error.
60
+ A custom graphql-core Executor that can selectively suppress or raise errors
61
+ based on an 'error_protection' attribute in the error or its original error.
61
62
  """
62
63
 
63
64
  # Default to True, meaning errors are protected by default (not raised directly).
@@ -66,40 +67,38 @@ class ErrorProtectionExecutionContext(ExecutionContext):
66
67
 
67
68
  def handle_field_error(
68
69
  self,
69
- error: GraphQLError,
70
+ raw_error: Exception,
70
71
  return_type: GraphQLOutputType,
71
- *args: Any,
72
- **kwargs: Any,
72
+ field_details_list: Any,
73
+ path: Path,
73
74
  ) -> None:
74
75
  """
75
76
  Intercept field errors to decide whether to raise or suppress them based on the
76
77
  'error_protection' attribute, either on the error or its original error.
77
78
 
78
- Anything after ``return_type`` is forwarded to graphql-core untouched.
79
- graphql-core added a ``path`` argument to this method in 3.2.8, and an
80
- override with a fixed arity turns every field error into a TypeError
81
- raised from inside the executor — so the resolver's real error never
82
- reaches the caller and a validation failure reads as a crash. Since
83
- this class exists precisely to sit in the error path, it must not be
84
- the thing that breaks it: accepting whatever the installed version
85
- passes keeps it working across both signatures.
79
+ graphql-core passes the error as raised. That is usually the resolver's own
80
+ exception, but it can be a GraphQLError that wraps one (e.g. from a nested
81
+ executor), so the flag is looked up on both.
86
82
  """
87
83
  error_protection = self.default_error_protection
88
- original_error = error.original_error
84
+ original_error = (
85
+ raw_error.original_error if isinstance(raw_error, GraphQLError) else None
86
+ )
89
87
 
90
88
  # Check if this error or its original error has a custom error_protection flag
91
- if hasattr(error, self.error_protection):
92
- error_protection = getattr(error, self.error_protection)
89
+ if hasattr(raw_error, self.error_protection):
90
+ error_protection = getattr(raw_error, self.error_protection)
93
91
  elif hasattr(original_error, self.error_protection):
94
92
  error_protection = getattr(original_error, self.error_protection)
95
93
 
96
94
  # If error protection is disabled, re-raise the original error
97
95
  if not error_protection:
98
- raise error.original_error if error.original_error is not None else error
96
+ raise original_error if original_error is not None else raw_error
99
97
 
100
- # Otherwise, call the default error handler. Positionally, because the
101
- # trailing arguments differ by version and only the caller knows them.
102
- return super().handle_field_error(error, return_type, *args, **kwargs)
98
+ # Otherwise, let graphql-core locate and collect the error.
99
+ return super().handle_field_error(
100
+ raw_error, return_type, field_details_list, path
101
+ )
103
102
 
104
103
 
105
104
  class NoErrorProtectionExecutionContext(ErrorProtectionExecutionContext):
@@ -153,8 +152,8 @@ class GraphQLExecutor(GraphQLBaseExecutor):
153
152
  # can serve every execution.
154
153
  self._middleware_manager = MiddlewareManager(*self.middleware)
155
154
 
156
- # Set the custom ExecutionContext class to handle error protection
157
- self.execution_context_class = (
155
+ # Set the custom Executor class to handle error protection
156
+ self.executor_class = (
158
157
  ErrorProtectionExecutionContext
159
158
  if error_protection
160
159
  else NoErrorProtectionExecutionContext
@@ -221,7 +220,7 @@ class GraphQLExecutor(GraphQLBaseExecutor):
221
220
  operation_name=operation_name,
222
221
  middleware=self._middleware_manager,
223
222
  root_value=root_value,
224
- execution_context_class=self.execution_context_class,
223
+ executor_class=self.executor_class,
225
224
  )
226
225
  return result
227
226
 
@@ -248,7 +247,7 @@ class GraphQLExecutor(GraphQLBaseExecutor):
248
247
  operation_name=operation_name,
249
248
  middleware=self._middleware_manager,
250
249
  root_value=root_value,
251
- execution_context_class=self.execution_context_class,
250
+ executor_class=self.executor_class,
252
251
  )
253
252
  if isawaitable(result):
254
253
  result = await result
@@ -267,13 +266,15 @@ class GraphQLExecutor(GraphQLBaseExecutor):
267
266
  # graphql-core subscribe expects a parsed document
268
267
  document = parse(query)
269
268
  # Use the same middleware and execution context where applicable
270
- async_iter = await subscribe(
269
+ async_iter = subscribe(
271
270
  schema=self.schema,
272
271
  document=document,
273
272
  variable_values=variables,
274
273
  operation_name=operation_name,
275
274
  context_value=context,
276
275
  root_value=root_value,
277
- # execution_context_class is not a parameter for subscribe in graphql-core v3
278
276
  )
277
+ # graphql-core >= 3.3 returns the result directly when it can.
278
+ if isawaitable(async_iter):
279
+ async_iter = await async_iter
279
280
  return async_iter # type: ignore
@@ -1,7 +1,5 @@
1
- import collections.abc
2
1
  import enum
3
2
  import inspect
4
- import types
5
3
  import typing
6
4
  from datetime import date, datetime
7
5
  from typing import Any, Annotated, Callable, List, Optional, Set, Tuple, Type, Union, cast, get_origin, get_args
@@ -21,7 +19,7 @@ from graphql import (
21
19
  is_union_type,
22
20
  GraphQLWrappingType,
23
21
  )
24
- from graphql.pyutils import Undefined, UndefinedType
22
+ from graphql.pyutils import Undefined
25
23
  from graphql.type.definition import (
26
24
  GraphQLArgument,
27
25
  GraphQLEnumType,
@@ -659,27 +657,6 @@ class GraphQLTypeMapper:
659
657
 
660
658
  enum_type.enum_type = type_
661
659
 
662
- def serialize(_self, value) -> Union[str, None, UndefinedType]:
663
- if value and isinstance(value, collections.abc.Hashable):
664
- # For enums, always return the enum name (which matches the GraphQL enum value)
665
- if isinstance(value, enum.Enum):
666
- return value.name
667
- else:
668
- # For non-enum values, try to look them up in the value lookup
669
- # This handles cases where the value might be a string representation
670
- lookup_key = value
671
- # noinspection PyProtectedMember
672
- lookup_value = _self._value_lookup.get(lookup_key)
673
- if lookup_value:
674
- return lookup_value
675
- else:
676
- # If lookup fails, the value might be invalid
677
- return Undefined
678
-
679
- return None
680
-
681
- enum_type.serialize = types.MethodType(serialize, enum_type)
682
-
683
660
  self.add_applied_directives(enum_type, name, type_)
684
661
 
685
662
  return enum_type
@@ -831,7 +808,7 @@ class GraphQLTypeMapper:
831
808
 
832
809
  # Use the raw Python default for input coercion.
833
810
  # Do not convert to GraphQL query literals here.
834
- default_value = local_default_args.get(key, None)
811
+ default_value = local_default_args.get(key, Undefined)
835
812
 
836
813
  arguments[to_camel_case(key)] = GraphQLInputField(
837
814
  type_=input_arg_type, default_value=default_value # type: ignore
@@ -157,6 +157,14 @@ class GraphQLRemoteExecutor(GraphQLBaseExecutor, GraphQLObjectType):
157
157
  A GraphQL executor that forwards all operations to a remote GraphQL service.
158
158
  """
159
159
 
160
+ def __new__(cls, url, name="Remote", *args, **kwargs):
161
+ # graphql-core >= 3.2.7 gives GraphQLNamedType a __new__(cls, name, ...)
162
+ # that checks for reserved type names. Our first argument is the URL, so
163
+ # pass the type name explicitly (older versions have no such __new__).
164
+ if GraphQLNamedType.__new__ is object.__new__:
165
+ return super().__new__(cls)
166
+ return super().__new__(cls, name)
167
+
160
168
  def __init__(
161
169
  self,
162
170
  url,
@@ -594,9 +602,15 @@ class GraphQLRemoteObject:
594
602
  ast_val = to_ast_value(val, unwrapped_field_type)
595
603
  if isinstance(
596
604
  unwrapped_field_type, (GraphQLScalarType, GraphQLEnumType)
597
- ) and hasattr(unwrapped_field_type, "parse_literal"):
598
- parsed_val = unwrapped_field_type.parse_literal(
599
- ast_val, None) # type: ignore
605
+ ):
606
+ # Scalars defined with the pre-3.3 hooks have no
607
+ # coerce_input_literal; they still provide parse_literal.
608
+ coerce_literal = unwrapped_field_type.coerce_input_literal
609
+ if coerce_literal is not None:
610
+ parsed_val = coerce_literal(ast_val) # type: ignore
611
+ else:
612
+ parsed_val = unwrapped_field_type.parse_literal(
613
+ ast_val, None) # type: ignore
600
614
  # If the field is an enum, convert to the Python enum if available
601
615
  if isinstance(unwrapped_field_type, GraphQLEnumType) and hasattr(
602
616
  unwrapped_field_type, "enum_type"
@@ -1,10 +1,11 @@
1
1
  import binascii
2
+ import collections.abc
2
3
  import datetime
3
4
  import json
4
5
  import uuid
5
6
  import enum
6
7
 
7
- from typing import Any, Dict, List, Optional, Type, Union, cast
8
+ from typing import Any, Dict, List, Optional, Type, Union
8
9
 
9
10
  from graphql import (
10
11
  GraphQLEnumType,
@@ -13,45 +14,54 @@ from graphql import (
13
14
  Undefined,
14
15
  ValueNode,
15
16
  )
17
+ from graphql.pyutils import UndefinedType
16
18
  from graphql.language import ast
17
19
 
18
20
 
19
21
  class GraphQLMappedEnumType(GraphQLEnumType):
22
+ """A GraphQL enum backed by a Python Enum.
23
+
24
+ Inputs (inline literals and variables) are coerced to the Python Enum member,
25
+ and outputs accept either the member or its value.
26
+ """
20
27
 
21
28
  def __init__(self, *args, **kwargs):
22
29
  super().__init__(*args, **kwargs)
23
30
  self.enum_type: Optional[Type[enum.Enum]] = None
24
31
 
25
- def parse_literal(self, *args, **kwargs):
26
- result = super().parse_literal(*args, **kwargs)
27
- if result is not Undefined and result is not None and getattr(self, "enum_type", None):
32
+ def _to_enum_member(self, result):
33
+ if result is not Undefined and result is not None and self.enum_type is not None:
28
34
  try:
29
- enum_type: Optional[Type[enum.Enum]] = self.enum_type
30
- if enum_type is None:
31
- return result
32
- return cast(Type[enum.Enum], enum_type)(result)
35
+ return self.enum_type(result)
33
36
  except (ValueError, KeyError, TypeError):
34
37
  # Invalid enum value or type casting error
35
38
  return Undefined
36
39
  return result
37
40
 
38
- def parse_value(self, value):
39
- """Coerce incoming variable values to the mapped Python Enum instance.
41
+ def coerce_input_literal(self, value_node, hide_suggestions=False):
42
+ return self._to_enum_member(
43
+ super().coerce_input_literal(value_node, hide_suggestions)
44
+ )
45
+
46
+ def coerce_input_value(self, input_value, hide_suggestions=False):
47
+ return self._to_enum_member(
48
+ super().coerce_input_value(input_value, hide_suggestions)
49
+ )
50
+
51
+ def coerce_output_value(self, output_value) -> Union[str, None, UndefinedType]:
52
+ if output_value and isinstance(output_value, collections.abc.Hashable):
53
+ # For enums, always return the enum name (which matches the GraphQL enum value)
54
+ if isinstance(output_value, enum.Enum):
55
+ return output_value.name
56
+ # For non-enum values, try to look them up in the value lookup.
57
+ # This handles cases where the value might be a string representation.
58
+ lookup_value = self._value_lookup.get(output_value)
59
+ if lookup_value:
60
+ return lookup_value
61
+ # If lookup fails, the value is invalid
62
+ return Undefined
40
63
 
41
- GraphQL variables use `parse_value`, while inline literals use
42
- `parse_literal`. Ensure both paths produce the Python Enum.
43
- """
44
- result = super().parse_value(value)
45
- if result is not Undefined and result is not None and getattr(self, "enum_type", None):
46
- try:
47
- enum_type: Optional[Type[enum.Enum]] = self.enum_type
48
- if enum_type is None:
49
- return result
50
- return cast(Type[enum.Enum], enum_type)(result)
51
- except (ValueError, KeyError, TypeError):
52
- # Invalid enum value or type casting error
53
- return Undefined
54
- return result
64
+ return None
55
65
 
56
66
 
57
67
  def parse_uuid_literal(
@@ -85,9 +95,9 @@ def parse_uuid_value(value: Any) -> uuid.UUID:
85
95
  GraphQLUUID = GraphQLScalarType(
86
96
  name="UUID",
87
97
  description="The `UUID` scalar type represents a unique identifer.",
88
- serialize=str,
89
- parse_value=parse_uuid_value,
90
- parse_literal=parse_uuid_literal,
98
+ coerce_output_value=str,
99
+ coerce_input_value=parse_uuid_value,
100
+ coerce_input_literal=parse_uuid_literal,
91
101
  )
92
102
 
93
103
 
@@ -134,9 +144,9 @@ GraphQLDateTime = GraphQLScalarType(
134
144
  name="DateTime",
135
145
  description="The `DateTime` scalar type represents a datetime, "
136
146
  "the datetime should be in the format `2018-01-22 17:46:32`",
137
- serialize=serialize_datetime,
138
- parse_value=parse_datetime_value,
139
- parse_literal=parse_datetime_literal,
147
+ coerce_output_value=serialize_datetime,
148
+ coerce_input_value=parse_datetime_value,
149
+ coerce_input_literal=parse_datetime_literal,
140
150
  )
141
151
 
142
152
 
@@ -166,9 +176,9 @@ GraphQLDate = GraphQLScalarType(
166
176
  name="Date",
167
177
  description="The `Date` scalar type represents a datetime, "
168
178
  "the datetime should be in the format `2018-01-22`",
169
- serialize=serialize_date,
170
- parse_value=parse_date_value,
171
- parse_literal=parse_date_literal,
179
+ coerce_output_value=serialize_date,
180
+ coerce_input_value=parse_date_value,
181
+ coerce_input_literal=parse_date_literal,
172
182
  )
173
183
 
174
184
 
@@ -204,9 +214,9 @@ GraphQLJSON = GraphQLScalarType(
204
214
  description="The `JSON` scalar type represents JSON values as specified by"
205
215
  " [ECMA-404](http://www.ecma-international.org/"
206
216
  "publications/files/ECMA-ST/ECMA-404.pdf).",
207
- serialize=serialize_json,
208
- parse_value=parse_json_value,
209
- parse_literal=parse_json_literal,
217
+ coerce_output_value=serialize_json,
218
+ coerce_input_value=parse_json_value,
219
+ coerce_input_literal=parse_json_literal,
210
220
  )
211
221
 
212
222
 
@@ -234,7 +244,7 @@ GraphQLBytes = GraphQLScalarType(
234
244
  description="The `Bytes` scalar type expects and returns a "
235
245
  "Byte array in UTF-8 string format that represents the Bytes. "
236
246
  "If the data is not UTF-encodable the errors will be ignored",
237
- serialize=serialize_bytes,
238
- parse_value=parse_bytes_value,
239
- parse_literal=parse_bytes_literal,
247
+ coerce_output_value=serialize_bytes,
248
+ coerce_input_value=parse_bytes_value,
249
+ coerce_input_literal=parse_bytes_literal,
240
250
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphql-api
3
- Version: 1.6.10
3
+ Version: 2.0.0
4
4
  Summary: A framework for building Python GraphQL APIs.
5
5
  Author-email: Robert Parker <rob@parob.com>
6
6
  License: MIT
@@ -10,12 +10,17 @@ Keywords: GraphQL,GraphQL-API,GraphQLAPI,Server
10
10
  Classifier: Development Status :: 3 - Alpha
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Python :: 3.15
13
18
  Classifier: License :: OSI Approved :: MIT License
14
19
  Classifier: Operating System :: OS Independent
15
- Requires-Python: <3.13,>=3.11
20
+ Requires-Python: >=3.11
16
21
  Description-Content-Type: text/markdown
17
22
  License-File: LICENSE
18
- Requires-Dist: graphql-core
23
+ Requires-Dist: graphql-core<3.4,>=3.3.1
19
24
  Requires-Dist: requests
20
25
  Requires-Dist: typing-inspect
21
26
  Requires-Dist: aiohttp
@@ -1,4 +1,4 @@
1
- graphql-core
1
+ graphql-core<3.4,>=3.3.1
2
2
  requests
3
3
  typing-inspect
4
4
  aiohttp
@@ -4,7 +4,7 @@ dynamic = ["version"]
4
4
  description = "A framework for building Python GraphQL APIs."
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
7
- requires-python = ">=3.11,<3.13"
7
+ requires-python = ">=3.11"
8
8
  authors = [
9
9
  {name = "Robert Parker", email = "rob@parob.com"}
10
10
  ]
@@ -13,11 +13,16 @@ classifiers = [
13
13
  "Development Status :: 3 - Alpha",
14
14
  "Intended Audience :: Developers",
15
15
  "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3.14",
20
+ "Programming Language :: Python :: 3.15",
16
21
  "License :: OSI Approved :: MIT License",
17
22
  "Operating System :: OS Independent",
18
23
  ]
19
24
  dependencies = [
20
- "graphql-core",
25
+ "graphql-core>=3.3.1,<3.4",
21
26
  "requests",
22
27
  "typing-inspect",
23
28
  "aiohttp",
@@ -371,19 +371,20 @@ class TestCustomTypes:
371
371
  def test_custom_scalar_type(self) -> None:
372
372
  api = GraphQLAPI()
373
373
 
374
- def parse_value(value):
374
+ def coerce_input_value(value):
375
375
  return str(value) + "_parsed_value"
376
376
 
377
- def parse_literal(node):
377
+ def coerce_input_literal(node):
378
378
  if isinstance(node, StringValueNode):
379
- return parse_value(node.value)
379
+ return coerce_input_value(node.value)
380
+ raise TypeError("A Key literal must be a string")
380
381
 
381
382
  GraphQLKey = GraphQLScalarType(
382
383
  name="Key",
383
384
  description="The `Key` scalar type represents a key.",
384
- serialize=lambda value: str(value) + "_serialized",
385
- parse_value=parse_value,
386
- parse_literal=parse_literal, # type: ignore[reportIncompatibleMethodOverride]
385
+ coerce_output_value=lambda value: str(value) + "_serialized",
386
+ coerce_input_value=coerce_input_value,
387
+ coerce_input_literal=coerce_input_literal,
387
388
  )
388
389
 
389
390
  @api.type(is_root_type=True)
@@ -189,22 +189,23 @@ class TestFieldTypesExamples:
189
189
  api = GraphQLAPI()
190
190
 
191
191
  # Define a custom scalar
192
- def parse_value(value):
192
+ def coerce_input_value(value):
193
193
  return str(value) + "_parsed"
194
194
 
195
- def parse_literal(node):
195
+ def coerce_input_literal(node):
196
196
  if isinstance(node, StringValueNode):
197
- return parse_value(node.value)
197
+ return coerce_input_value(node.value)
198
+ raise TypeError("A Key literal must be a string")
198
199
 
199
- def serialize(value):
200
+ def coerce_output_value(value):
200
201
  return str(value) + "_serialized"
201
202
 
202
203
  GraphQLKey = GraphQLScalarType(
203
204
  name="Key",
204
205
  description="The `Key` scalar type represents a unique key.",
205
- serialize=serialize,
206
- parse_value=parse_value,
207
- parse_literal=parse_literal,
206
+ coerce_output_value=coerce_output_value,
207
+ coerce_input_value=coerce_input_value,
208
+ coerce_input_literal=coerce_input_literal,
208
209
  )
209
210
 
210
211
  @api.type(is_root_type=True)