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.
- {graphql_api-1.6.10 → graphql_api-2.0.0}/.github/workflows/python-package.yml +3 -1
- {graphql_api-1.6.10 → graphql_api-2.0.0}/PKG-INFO +8 -3
- graphql_api-2.0.0/VERSION +1 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/fundamentals/field-types.md +13 -10
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/executor.py +29 -28
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/mapper.py +2 -25
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/remote.py +17 -3
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/types.py +49 -39
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api.egg-info/PKG-INFO +8 -3
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api.egg-info/requires.txt +1 -1
- {graphql_api-1.6.10 → graphql_api-2.0.0}/pyproject.toml +7 -2
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_custom_types.py +7 -6
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_docs_field_types.py +8 -7
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_executor_signature.py +36 -17
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_remote.py +18 -2
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_subscriptions.py +9 -4
- graphql_api-2.0.0/uv.lock +1506 -0
- graphql_api-1.6.10/VERSION +0 -1
- graphql_api-1.6.10/uv.lock +0 -952
- {graphql_api-1.6.10 → graphql_api-2.0.0}/.github/workflows/pages.yml +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/.gitignore +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/.vscode/settings.json +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/AGENTS.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/CLAUDE.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/LICENSE +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/README.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/README.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/_index.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/_index.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/advanced-features/_index.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/advanced-features/directives.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/advanced-features/performance.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/advanced-features/schema-filtering.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/building-applications/_index.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/building-applications/documentation.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/building-applications/error-handling.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/distributed-systems/_index.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/distributed-systems/federation.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/distributed-systems/pagination-relay.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/distributed-systems/remote-graphql.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/fundamentals/_index.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/fundamentals/async-and-subscriptions.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/fundamentals/defining-schemas.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/fundamentals/enums-interfaces.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/fundamentals/getting-started.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/fundamentals/mutations.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/fundamentals/object-types.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/fundamentals/pydantic-and-dataclasses.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/production/_index.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/production/context-metadata.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/production/middleware.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/reference/_index.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/reference/api-reference.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/reference/contributing.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/content/docs/reference/examples.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/hugo.toml +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/docs/index.md +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/__init__.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/api.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/context.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/dataclass_mapping.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/decorators.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/directives.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/error.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/exception.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/federation/__init__.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/federation/directives.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/federation/federation.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/federation/types.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/middleware.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/mutation_schema.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/pydantic.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/pydantic_utils.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/reduce.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/relay.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/schema.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/schema_filter.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/type_utils.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api/utils.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api.egg-info/SOURCES.txt +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api.egg-info/dependency_links.txt +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/graphql_api.egg-info/top_level.txt +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/setup.cfg +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/__init__.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_async.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_calculator.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_custom_suffixes.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_dataclass.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_dataclass_relationships.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_decorator_patterns.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_directives.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_docs_advanced.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_docs_async_subscriptions.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_docs_defining_schemas.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_docs_getting_started.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_docs_relay.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_docstrings.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_error.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_federation.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_federation_example.graphql +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_federation_example.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_filtering.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_graphql.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_middleware.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_performance.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_plain_functions.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_pydantic.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_relay.py +0 -0
- {graphql_api-1.6.10 → graphql_api-2.0.0}/tests/test_schema.py +0 -0
- {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:
|
|
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:
|
|
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
|
|
183
|
+
def coerce_input_value(value):
|
|
184
184
|
return str(value) + "_parsed"
|
|
185
185
|
|
|
186
|
-
def
|
|
186
|
+
def coerce_input_literal(node):
|
|
187
187
|
if isinstance(node, StringValueNode):
|
|
188
|
-
return
|
|
188
|
+
return coerce_input_value(node.value)
|
|
189
|
+
raise TypeError("A Key literal must be a string")
|
|
189
190
|
|
|
190
|
-
def
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
- `
|
|
210
|
-
- `
|
|
211
|
-
- `
|
|
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(
|
|
58
|
+
class ErrorProtectionExecutionContext(Executor):
|
|
58
59
|
"""
|
|
59
|
-
A custom
|
|
60
|
-
based on an 'error_protection' attribute in the
|
|
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
|
-
|
|
70
|
+
raw_error: Exception,
|
|
70
71
|
return_type: GraphQLOutputType,
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 =
|
|
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(
|
|
92
|
-
error_protection = getattr(
|
|
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
|
|
96
|
+
raise original_error if original_error is not None else raw_error
|
|
99
97
|
|
|
100
|
-
# Otherwise,
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
157
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
-
)
|
|
598
|
-
|
|
599
|
-
|
|
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
|
|
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
|
|
26
|
-
result
|
|
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
|
-
|
|
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
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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:
|
|
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:
|
|
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
|
|
@@ -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
|
|
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
|
|
374
|
+
def coerce_input_value(value):
|
|
375
375
|
return str(value) + "_parsed_value"
|
|
376
376
|
|
|
377
|
-
def
|
|
377
|
+
def coerce_input_literal(node):
|
|
378
378
|
if isinstance(node, StringValueNode):
|
|
379
|
-
return
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
|
192
|
+
def coerce_input_value(value):
|
|
193
193
|
return str(value) + "_parsed"
|
|
194
194
|
|
|
195
|
-
def
|
|
195
|
+
def coerce_input_literal(node):
|
|
196
196
|
if isinstance(node, StringValueNode):
|
|
197
|
-
return
|
|
197
|
+
return coerce_input_value(node.value)
|
|
198
|
+
raise TypeError("A Key literal must be a string")
|
|
198
199
|
|
|
199
|
-
def
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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)
|