graphql-codegen 0.1.0__py3-none-any.whl
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_codegen/__init__.py +7 -0
- graphql_codegen/__main__.py +5 -0
- graphql_codegen/_cli/__init__.py +41 -0
- graphql_codegen/_cli/_graphql_config.py +291 -0
- graphql_codegen/_cli/_introspection.py +87 -0
- graphql_codegen/_cli/_introspection_graphql.py +53 -0
- graphql_codegen/_cli/_parsing.py +67 -0
- graphql_codegen/_cli/_schema_pointer.py +108 -0
- graphql_codegen/_cli/_source.py +68 -0
- graphql_codegen/_cli/_write.py +22 -0
- graphql_codegen/_generator/__init__.py +0 -0
- graphql_codegen/_generator/_annotation.py +149 -0
- graphql_codegen/_generator/_ast_nodes.py +235 -0
- graphql_codegen/_generator/_data_type.py +532 -0
- graphql_codegen/_generator/_document.py +108 -0
- graphql_codegen/_generator/_document_module.py +54 -0
- graphql_codegen/_generator/_imports.py +166 -0
- graphql_codegen/_generator/_injector.py +267 -0
- graphql_codegen/_generator/_merge.py +149 -0
- graphql_codegen/_generator/_naming.py +80 -0
- graphql_codegen/_generator/_operation.py +223 -0
- graphql_codegen/_generator/_scalar.py +76 -0
- graphql_codegen/_generator/_schema.py +46 -0
- graphql_codegen/_generator/_schema_type.py +334 -0
- graphql_codegen/_generator/_selection.py +246 -0
- graphql_codegen/_generator/_structs.py +87 -0
- graphql_codegen/_generator/_typed_dict.py +154 -0
- graphql_codegen/_generator/dotted_name.py +48 -0
- graphql_codegen/_generator/package.py +695 -0
- graphql_codegen/_generator/spelling.py +168 -0
- graphql_codegen/_metadata.py +11 -0
- graphql_codegen/_note.py +12 -0
- graphql_codegen/config.py +42 -0
- graphql_codegen/document_sibling_module.py +58 -0
- graphql_codegen/generate.py +19 -0
- graphql_codegen/package_location.py +41 -0
- graphql_codegen/py.typed +0 -0
- graphql_codegen/runtime/__init__.py +20 -0
- graphql_codegen/runtime/_compat.py +27 -0
- graphql_codegen/runtime/_literal.py +18 -0
- graphql_codegen/runtime/_merge.py +153 -0
- graphql_codegen/runtime/_prepare.py +250 -0
- graphql_codegen/runtime/_reflection.py +376 -0
- graphql_codegen/runtime/_sigil.py +11 -0
- graphql_codegen/runtime/_transport.py +21 -0
- graphql_codegen/runtime/client.py +384 -0
- graphql_codegen/runtime/error.py +135 -0
- graphql_codegen/runtime/injection.py +107 -0
- graphql_codegen/runtime/operation.py +135 -0
- graphql_codegen/scalar.py +59 -0
- graphql_codegen-0.1.0.dist-info/METADATA +871 -0
- graphql_codegen-0.1.0.dist-info/RECORD +55 -0
- graphql_codegen-0.1.0.dist-info/WHEEL +4 -0
- graphql_codegen-0.1.0.dist-info/entry_points.txt +3 -0
- graphql_codegen-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# Copied into each generated package, where this line says not to edit it.
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
4
|
+
from json import JSONDecodeError, loads
|
|
5
|
+
from typing import Any, Final, assert_never, cast, final
|
|
6
|
+
|
|
7
|
+
from ._literal import OperationType
|
|
8
|
+
from ._merge import encode_body, split_merged_data, split_merged_errors
|
|
9
|
+
from ._reflection import Convert
|
|
10
|
+
from .error import (
|
|
11
|
+
ClientError,
|
|
12
|
+
Error,
|
|
13
|
+
ExecutionError,
|
|
14
|
+
ProtocolError,
|
|
15
|
+
RequestError,
|
|
16
|
+
UnexpectedNullError,
|
|
17
|
+
)
|
|
18
|
+
from .injection import _Injectors
|
|
19
|
+
from .operation import Operation, Request
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@final
|
|
23
|
+
class PreparedRequest[Data]:
|
|
24
|
+
"""A call without its IO and awaiting, which are the clients' part."""
|
|
25
|
+
|
|
26
|
+
__slots__ = ("_body", "parse")
|
|
27
|
+
|
|
28
|
+
def __init__(self, body: bytes, /, *, parse: Callable[[bytes], Data]) -> None:
|
|
29
|
+
self._body: Final = body
|
|
30
|
+
self.parse: Final = parse
|
|
31
|
+
|
|
32
|
+
def __bytes__(self) -> bytes:
|
|
33
|
+
return self._body
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve_variables(
|
|
37
|
+
operation: Operation[OperationType, Any, object],
|
|
38
|
+
variables: Mapping[str, object],
|
|
39
|
+
/,
|
|
40
|
+
*,
|
|
41
|
+
injectors: _Injectors | None = None,
|
|
42
|
+
) -> Mapping[str, object]:
|
|
43
|
+
"""The caller's are never mutated: serializing copies them, and injecting copies what it writes into."""
|
|
44
|
+
serialize = operation._variables_serializer
|
|
45
|
+
values = (
|
|
46
|
+
variables
|
|
47
|
+
if serialize is None
|
|
48
|
+
else cast(Mapping[str, object], serialize(variables))
|
|
49
|
+
)
|
|
50
|
+
return (
|
|
51
|
+
values
|
|
52
|
+
if injectors is None
|
|
53
|
+
else injectors._inject(values, injections=operation._injections)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def prepare[Data](
|
|
58
|
+
request: Request[OperationType, Data], /, *, injectors: _Injectors | None = None
|
|
59
|
+
) -> PreparedRequest[Data]:
|
|
60
|
+
operation = request._operation
|
|
61
|
+
variables = resolve_variables(operation, request._variables, injectors=injectors)
|
|
62
|
+
|
|
63
|
+
def parse(raw: bytes, /) -> Data:
|
|
64
|
+
try:
|
|
65
|
+
return parse_data(operation, raw)
|
|
66
|
+
except ClientError as error:
|
|
67
|
+
error.add_note(_origin_note(operation._name, variables))
|
|
68
|
+
|
|
69
|
+
if isinstance(error, ExecutionError):
|
|
70
|
+
match request._on_execution_error:
|
|
71
|
+
case "raise":
|
|
72
|
+
pass
|
|
73
|
+
case "return":
|
|
74
|
+
# `Data` is the request's data type or its error: see `Request.returning_error()`.
|
|
75
|
+
return cast(Data, error)
|
|
76
|
+
case _ as never:
|
|
77
|
+
assert_never(never)
|
|
78
|
+
|
|
79
|
+
raise
|
|
80
|
+
|
|
81
|
+
return PreparedRequest(operation._request_body(variables), parse=parse)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def parse_data[Data](
|
|
85
|
+
operation: Operation[OperationType, Any, Data], raw: bytes, /
|
|
86
|
+
) -> Data:
|
|
87
|
+
"""Nothing is validated: the data is trusted to match its static type, from this cast on."""
|
|
88
|
+
data = parse_response(raw, partial_data_parser=operation._partial_data_parser)
|
|
89
|
+
|
|
90
|
+
if operation._data_parser is not None:
|
|
91
|
+
operation._data_parser(data)
|
|
92
|
+
|
|
93
|
+
return cast(Data, data)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _execution_error[Data](
|
|
97
|
+
operation: Operation[OperationType, Any, Data],
|
|
98
|
+
errors: Sequence[Error],
|
|
99
|
+
/,
|
|
100
|
+
*,
|
|
101
|
+
data: Mapping[str, object] | None,
|
|
102
|
+
extensions: Mapping[str, object] | None,
|
|
103
|
+
) -> ExecutionError[Data]:
|
|
104
|
+
return ExecutionError[Data](
|
|
105
|
+
errors,
|
|
106
|
+
data=data,
|
|
107
|
+
extensions=extensions,
|
|
108
|
+
partial_data_parser=operation._partial_data_parser,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def parse_response(
|
|
113
|
+
raw: bytes, /, *, partial_data_parser: Convert | None
|
|
114
|
+
) -> Mapping[str, object]:
|
|
115
|
+
"""A response with errors but no `data` entry is the spec's request error result."""
|
|
116
|
+
try:
|
|
117
|
+
payload = loads(raw)
|
|
118
|
+
except JSONDecodeError as error:
|
|
119
|
+
raise ProtocolError(
|
|
120
|
+
f"Expected a JSON response but got: {raw[:200]!r}."
|
|
121
|
+
) from error
|
|
122
|
+
|
|
123
|
+
if not isinstance(payload, dict):
|
|
124
|
+
raise ProtocolError(f"Expected a JSON object response but got: {raw[:200]!r}.")
|
|
125
|
+
|
|
126
|
+
errors = payload.get("errors")
|
|
127
|
+
|
|
128
|
+
if errors:
|
|
129
|
+
# Trusted to be the spec's errors, as the data is trusted to match its type.
|
|
130
|
+
formatted = cast(list[Error], errors)
|
|
131
|
+
extensions = payload.get("extensions")
|
|
132
|
+
|
|
133
|
+
if "data" not in payload:
|
|
134
|
+
raise RequestError(formatted, extensions=extensions)
|
|
135
|
+
|
|
136
|
+
raise ExecutionError(
|
|
137
|
+
formatted,
|
|
138
|
+
data=payload["data"],
|
|
139
|
+
extensions=extensions,
|
|
140
|
+
partial_data_parser=partial_data_parser,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
data = payload.get("data")
|
|
144
|
+
|
|
145
|
+
if not isinstance(data, dict):
|
|
146
|
+
raise ProtocolError(
|
|
147
|
+
f"Expected the response to carry `data` but got: {raw[:200]!r}."
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# The keys of a JSON object are strings.
|
|
151
|
+
return cast(Mapping[str, object], data)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _origin_note(operation_name: str, variables: Mapping[str, object], /) -> str:
|
|
155
|
+
"""The variables are shown by :func:`repr`, so that a :class:`str` subclass redacting its own keeps a secret out of logs."""
|
|
156
|
+
return f"Raised by `{operation_name}` with variables {variables!r}."
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _raises_execution_error(request: Request[OperationType, object], /) -> bool:
|
|
160
|
+
match request._on_execution_error:
|
|
161
|
+
case "raise":
|
|
162
|
+
return True
|
|
163
|
+
case "return":
|
|
164
|
+
return False
|
|
165
|
+
case _ as never:
|
|
166
|
+
assert_never(never)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _prepare_merged(
|
|
170
|
+
requests: Sequence[Request[OperationType, object]],
|
|
171
|
+
/,
|
|
172
|
+
*,
|
|
173
|
+
injectors: _Injectors | None,
|
|
174
|
+
) -> PreparedRequest[tuple[object, ...]]:
|
|
175
|
+
operations = [request._operation for request in requests]
|
|
176
|
+
# Resolved now rather than when the response comes back, like a lone request's.
|
|
177
|
+
variables = [
|
|
178
|
+
resolve_variables(request._operation, request._variables, injectors=injectors)
|
|
179
|
+
for request in requests
|
|
180
|
+
]
|
|
181
|
+
body = encode_body(list(zip(operations, variables, strict=True)))
|
|
182
|
+
count = len(requests)
|
|
183
|
+
|
|
184
|
+
def parse(raw: bytes, /) -> tuple[object, ...]:
|
|
185
|
+
failures: dict[int, ExecutionError[object]] = {}
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
data = parse_response(raw, partial_data_parser=None)
|
|
189
|
+
except ExecutionError as error:
|
|
190
|
+
# With no data, the merge failed as a whole, which is no one request's error to return.
|
|
191
|
+
if error.data is None:
|
|
192
|
+
raise
|
|
193
|
+
|
|
194
|
+
split = split_merged_data(error.data, count=count)
|
|
195
|
+
|
|
196
|
+
for index, (operation, operation_variables, operation_errors) in enumerate(
|
|
197
|
+
zip(
|
|
198
|
+
operations,
|
|
199
|
+
variables,
|
|
200
|
+
split_merged_errors(error.errors, count=count),
|
|
201
|
+
strict=True,
|
|
202
|
+
)
|
|
203
|
+
):
|
|
204
|
+
if operation_errors:
|
|
205
|
+
failure = _execution_error(
|
|
206
|
+
operation,
|
|
207
|
+
operation_errors,
|
|
208
|
+
data=split[index],
|
|
209
|
+
extensions=error.extensions,
|
|
210
|
+
)
|
|
211
|
+
failure.add_note(_origin_note(operation._name, operation_variables))
|
|
212
|
+
failures[index] = failure
|
|
213
|
+
|
|
214
|
+
# A call returns or raises: when one failing request raises its error, the group holds every failure, those of requests returning theirs included.
|
|
215
|
+
if any(_raises_execution_error(requests[index]) for index in failures):
|
|
216
|
+
raise ExceptionGroup(
|
|
217
|
+
f"{len(failures)} of the {count} merged operations raised.",
|
|
218
|
+
list(failures.values()),
|
|
219
|
+
) from None
|
|
220
|
+
else:
|
|
221
|
+
split = split_merged_data(data, count=count)
|
|
222
|
+
|
|
223
|
+
for index, (operation, operation_variables, operation_data) in enumerate(
|
|
224
|
+
zip(operations, variables, split, strict=True)
|
|
225
|
+
):
|
|
226
|
+
if index not in failures and operation._data_parser is not None:
|
|
227
|
+
try:
|
|
228
|
+
operation._data_parser(operation_data)
|
|
229
|
+
except UnexpectedNullError as null_error:
|
|
230
|
+
null_error.add_note(
|
|
231
|
+
_origin_note(operation._name, operation_variables)
|
|
232
|
+
)
|
|
233
|
+
raise
|
|
234
|
+
|
|
235
|
+
return tuple(
|
|
236
|
+
failures.get(index, operation_data)
|
|
237
|
+
for index, operation_data in enumerate(split)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return PreparedRequest(body, parse=parse)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def prepare_requests(
|
|
244
|
+
requests: object, /, *, injectors: _Injectors | None
|
|
245
|
+
) -> PreparedRequest[Any] | None:
|
|
246
|
+
if isinstance(requests, Request):
|
|
247
|
+
return prepare(requests, injectors=injectors)
|
|
248
|
+
|
|
249
|
+
assert isinstance(requests, Sequence)
|
|
250
|
+
return _prepare_merged(requests, injectors=injectors) if requests else None
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# Copied into each generated package, where this line says not to edit it.
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from threading import Lock
|
|
6
|
+
from types import NoneType, UnionType
|
|
7
|
+
from typing import (
|
|
8
|
+
Annotated,
|
|
9
|
+
Final,
|
|
10
|
+
Literal,
|
|
11
|
+
NotRequired,
|
|
12
|
+
Required,
|
|
13
|
+
TypeAliasType,
|
|
14
|
+
Union,
|
|
15
|
+
assert_never,
|
|
16
|
+
cast,
|
|
17
|
+
final,
|
|
18
|
+
get_args,
|
|
19
|
+
get_origin,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from ._compat import (
|
|
23
|
+
ReadOnly,
|
|
24
|
+
SentinelType,
|
|
25
|
+
get_type_hints,
|
|
26
|
+
is_typeddict,
|
|
27
|
+
)
|
|
28
|
+
from .error import UnexpectedNullError
|
|
29
|
+
|
|
30
|
+
type Mode = Literal["parse", "partial parse", "serialize"]
|
|
31
|
+
"""What a builder's converters do.
|
|
32
|
+
|
|
33
|
+
- ``"parse"`` converts the data of a response in place, and raises on the null of a `@nonNull` field.
|
|
34
|
+
- ``"partial parse"`` converts the data of a response with errors in place, propagating the null of a `@nonNull` field to its nearest nullable parent, as the server propagates that of a non-null field which raised.
|
|
35
|
+
- ``"serialize"`` copies variables, converting what they hold to what goes on the wire.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
type Convert = Callable[[object], object]
|
|
39
|
+
"""A parser may convert a mapping in place, a serializer never does."""
|
|
40
|
+
|
|
41
|
+
_QUALIFIERS: Final = frozenset({NotRequired, ReadOnly, Required})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
NON_NULL: Final = SentinelType("NON_NULL")
|
|
45
|
+
"""Marks a `@nonNull` field, whose null is legal for the schema but not for the query: null there raises :class:`UnexpectedNullError` instead of passing."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@final
|
|
49
|
+
@dataclass(frozen=True, kw_only=True)
|
|
50
|
+
class Codec[Value, Wire]:
|
|
51
|
+
decode: Callable[[Wire], Value]
|
|
52
|
+
encode: Callable[[Value], Wire]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _unwrap(type_: object, /) -> tuple[object, tuple[object, ...]]:
|
|
56
|
+
"""Split *type_* from its :data:`typing.Annotated` metadata, which says what a plain type cannot, such as :data:`NON_NULL` or a :class:`Codec`, and which type checkers ignore."""
|
|
57
|
+
metadata: list[object] = []
|
|
58
|
+
|
|
59
|
+
while True:
|
|
60
|
+
if isinstance(type_, TypeAliasType):
|
|
61
|
+
type_ = type_.__value__
|
|
62
|
+
elif get_origin(type_) in _QUALIFIERS:
|
|
63
|
+
(type_,) = get_args(type_)
|
|
64
|
+
elif get_origin(type_) is Annotated:
|
|
65
|
+
type_, *extra = get_args(type_)
|
|
66
|
+
metadata.extend(extra)
|
|
67
|
+
else:
|
|
68
|
+
return type_, tuple(metadata)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _union_members(type_: object, /) -> tuple[object, ...] | None:
|
|
72
|
+
return get_args(type_) if get_origin(type_) in (Union, UnionType) else None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _typename_values(type_: object, /) -> tuple[object, ...] | None:
|
|
76
|
+
hints = get_type_hints(type_, include_extras=True)
|
|
77
|
+
typename = hints.get("__typename")
|
|
78
|
+
|
|
79
|
+
if typename is None:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
literal, _ = _unwrap(typename)
|
|
83
|
+
assert get_origin(literal) is Literal, (
|
|
84
|
+
f"Expected `{type_}.__typename` to be a `Literal`."
|
|
85
|
+
)
|
|
86
|
+
return get_args(literal)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _children(unwrapped_type: object, /) -> tuple[object, ...]:
|
|
90
|
+
if (members := _union_members(unwrapped_type)) is not None:
|
|
91
|
+
return members
|
|
92
|
+
|
|
93
|
+
if get_origin(unwrapped_type) in (list, Sequence):
|
|
94
|
+
return get_args(unwrapped_type)
|
|
95
|
+
|
|
96
|
+
if is_typeddict(unwrapped_type):
|
|
97
|
+
return tuple(get_type_hints(unwrapped_type, include_extras=True).values())
|
|
98
|
+
|
|
99
|
+
return ()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@final
|
|
103
|
+
class _Builder:
|
|
104
|
+
"""Builds converters for one mode, caching them, so that the work is paid once per type rather than once per call, and tying the knot on recursive types.
|
|
105
|
+
|
|
106
|
+
The build recurses over the grammar of types (:class:`typing.TypedDict`, lists, `X | None`, type aliases, and unions told apart by `__typename` or, for `@oneOf` inputs, by their single key) rather than over the shapes of any particular document.
|
|
107
|
+
Nesting, lists of lists, a `@nonNull` under a list, and recursive inputs thus need no special case.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(self, mode: Mode, /) -> None:
|
|
111
|
+
match mode:
|
|
112
|
+
case "parse":
|
|
113
|
+
parses, partial = True, False
|
|
114
|
+
case "partial parse":
|
|
115
|
+
parses, partial = True, True
|
|
116
|
+
case "serialize":
|
|
117
|
+
parses, partial = False, False
|
|
118
|
+
case _ as never:
|
|
119
|
+
assert_never(never)
|
|
120
|
+
|
|
121
|
+
self._parses: Final = parses
|
|
122
|
+
self._partial: Final = partial
|
|
123
|
+
self._needs: Final[dict[object, bool]] = {}
|
|
124
|
+
self._converters: Final[dict[object, Convert]] = {}
|
|
125
|
+
# Taken by `build()` alone: a recursive type caches a forwarding converter before its real one exists, which another thread must not get, and call, until it resolves.
|
|
126
|
+
self._lock: Final = Lock()
|
|
127
|
+
|
|
128
|
+
def _needs_work(self, type_: object, /) -> bool:
|
|
129
|
+
if type_ not in self._needs:
|
|
130
|
+
self._solve_needs(type_)
|
|
131
|
+
|
|
132
|
+
return self._needs[type_]
|
|
133
|
+
|
|
134
|
+
def _solve_needs(self, root: object, /) -> None:
|
|
135
|
+
"""Settle whether each type reachable from *root* needs work, as a least fixed point.
|
|
136
|
+
|
|
137
|
+
Types can reach each other in cycles, such as a filter combining filters, so a type cannot be settled from its children alone: it is settled with everything it reaches.
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
children: dict[object, tuple[object, ...]] = {}
|
|
141
|
+
needs: dict[object, bool] = {}
|
|
142
|
+
pending = [root]
|
|
143
|
+
|
|
144
|
+
while pending:
|
|
145
|
+
type_ = pending.pop()
|
|
146
|
+
|
|
147
|
+
if type_ in children or type_ in self._needs:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
inner, metadata = _unwrap(type_)
|
|
151
|
+
needs[type_] = any(isinstance(item, Codec) for item in metadata) or (
|
|
152
|
+
NON_NULL in metadata and self._parses
|
|
153
|
+
)
|
|
154
|
+
children[type_] = _children(inner)
|
|
155
|
+
pending.extend(children[type_])
|
|
156
|
+
|
|
157
|
+
changed = True
|
|
158
|
+
|
|
159
|
+
while changed:
|
|
160
|
+
changed = False
|
|
161
|
+
|
|
162
|
+
for type_, type_children in children.items():
|
|
163
|
+
if not needs[type_] and any(
|
|
164
|
+
needs[child] if child in needs else self._needs[child]
|
|
165
|
+
for child in type_children
|
|
166
|
+
):
|
|
167
|
+
needs[type_] = True
|
|
168
|
+
changed = True
|
|
169
|
+
|
|
170
|
+
self._needs.update(needs)
|
|
171
|
+
|
|
172
|
+
def build(self, type_: object, /) -> Convert | None:
|
|
173
|
+
"""The converter for *type_*, or ``None`` if its values need no work, so that a response needing nothing goes from the transport to the caller untouched."""
|
|
174
|
+
with self._lock:
|
|
175
|
+
return self._build(type_)
|
|
176
|
+
|
|
177
|
+
def _build(self, type_: object, /) -> Convert | None:
|
|
178
|
+
return self._converter(type_) if self._needs_work(type_) else None
|
|
179
|
+
|
|
180
|
+
def _converter(self, type_: object, /) -> Convert:
|
|
181
|
+
if type_ in self._converters:
|
|
182
|
+
return self._converters[type_]
|
|
183
|
+
|
|
184
|
+
# Recursive types reach themselves while being built: they get a forwarding converter, which reads the real one once it exists.
|
|
185
|
+
cell: list[Convert] = []
|
|
186
|
+
self._converters[type_] = lambda value: cell[0](value) # noqa: PLW0108
|
|
187
|
+
converter = self._compute_build(type_)
|
|
188
|
+
cell.append(converter)
|
|
189
|
+
self._converters[type_] = converter
|
|
190
|
+
return converter
|
|
191
|
+
|
|
192
|
+
def _compute_build(self, type_: object, /) -> Convert:
|
|
193
|
+
inner, metadata = _unwrap(type_)
|
|
194
|
+
codecs = [item for item in metadata if isinstance(item, Codec)]
|
|
195
|
+
convert: Convert | None = None
|
|
196
|
+
|
|
197
|
+
if codecs:
|
|
198
|
+
(codec,) = codecs
|
|
199
|
+
# It annotates this very type, so it converts its values.
|
|
200
|
+
convert = cast(Convert, codec.decode if self._parses else codec.encode)
|
|
201
|
+
elif self._needs_work(inner):
|
|
202
|
+
convert = self._build_structure(inner)
|
|
203
|
+
|
|
204
|
+
if NON_NULL in metadata and self._parses:
|
|
205
|
+
return _non_null_converter(convert)
|
|
206
|
+
|
|
207
|
+
assert convert is not None, f"Expected `{type_}` to need work."
|
|
208
|
+
return convert
|
|
209
|
+
|
|
210
|
+
def _build_structure(self, type_: object, /) -> Convert:
|
|
211
|
+
if (members := _union_members(type_)) is not None:
|
|
212
|
+
return self._build_union(type_, members)
|
|
213
|
+
|
|
214
|
+
if get_origin(type_) in (list, Sequence):
|
|
215
|
+
(item,) = get_args(type_)
|
|
216
|
+
return _list_converter(self._converter(item))
|
|
217
|
+
|
|
218
|
+
assert is_typeddict(type_), f"Expected `{type_}` to need no work."
|
|
219
|
+
return self._build_typed_dict(type_)
|
|
220
|
+
|
|
221
|
+
def _build_typed_dict(self, type_: object, /) -> Convert:
|
|
222
|
+
steps = tuple(
|
|
223
|
+
(key, converter)
|
|
224
|
+
for key, hint in get_type_hints(type_, include_extras=True).items()
|
|
225
|
+
if (converter := self._build(hint)) is not None
|
|
226
|
+
)
|
|
227
|
+
in_place = self._parses
|
|
228
|
+
|
|
229
|
+
def convert(value: object, /) -> object:
|
|
230
|
+
assert isinstance(value, Mapping)
|
|
231
|
+
# A response is the client's own, so it is converted in place; variables are the caller's, who may reuse them, so they are copied.
|
|
232
|
+
result = cast(dict[str, object], value) if in_place else dict(value)
|
|
233
|
+
|
|
234
|
+
for key, convert_value in steps:
|
|
235
|
+
# A key may be absent: an optional input, or a field the server skipped.
|
|
236
|
+
if key in result:
|
|
237
|
+
try:
|
|
238
|
+
result[key] = convert_value(result[key])
|
|
239
|
+
except UnexpectedNullError as error:
|
|
240
|
+
error.path.insert(0, key)
|
|
241
|
+
raise
|
|
242
|
+
|
|
243
|
+
return result
|
|
244
|
+
|
|
245
|
+
return convert
|
|
246
|
+
|
|
247
|
+
def _build_union(self, type_: object, members: tuple[object, ...], /) -> Convert:
|
|
248
|
+
# A sentinel, such as `OMITTED`, is never converted, as `None` is not.
|
|
249
|
+
non_null = tuple(
|
|
250
|
+
member
|
|
251
|
+
for member in members
|
|
252
|
+
if member is not NoneType and not isinstance(member, SentinelType)
|
|
253
|
+
)
|
|
254
|
+
nullable = NoneType in members
|
|
255
|
+
|
|
256
|
+
if len(non_null) == 1:
|
|
257
|
+
convert = self._converter(non_null[0])
|
|
258
|
+
else:
|
|
259
|
+
convert = self._build_dispatch(type_, non_null)
|
|
260
|
+
|
|
261
|
+
if not nullable:
|
|
262
|
+
return convert
|
|
263
|
+
|
|
264
|
+
partial = self._partial
|
|
265
|
+
|
|
266
|
+
def convert_nullable(value: object, /) -> object:
|
|
267
|
+
try:
|
|
268
|
+
return None if value is None else convert(value)
|
|
269
|
+
except UnexpectedNullError:
|
|
270
|
+
if partial:
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
raise
|
|
274
|
+
|
|
275
|
+
return convert_nullable
|
|
276
|
+
|
|
277
|
+
def _build_dispatch(self, type_: object, members: tuple[object, ...], /) -> Convert:
|
|
278
|
+
"""Tell the members of a union apart, by `__typename` or else by their single key."""
|
|
279
|
+
typed_dicts = [_unwrap(member)[0] for member in members]
|
|
280
|
+
|
|
281
|
+
if not all(is_typeddict(member) for member in typed_dicts):
|
|
282
|
+
raise TypeError(
|
|
283
|
+
f"Expected the members of `{type_}` needing work to be TypedDicts."
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
converters = [self._build(member) for member in members]
|
|
287
|
+
typenames = [_typename_values(member) for member in typed_dicts]
|
|
288
|
+
|
|
289
|
+
if all(values is not None for values in typenames):
|
|
290
|
+
by_typename = {
|
|
291
|
+
value: converter
|
|
292
|
+
for values, converter in zip(typenames, converters, strict=True)
|
|
293
|
+
for value in values or ()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
def convert_by_typename(value: object, /) -> object:
|
|
297
|
+
assert isinstance(value, Mapping)
|
|
298
|
+
# A member added after generation is not converted: nothing is validated.
|
|
299
|
+
converter = by_typename.get(value["__typename"])
|
|
300
|
+
return value if converter is None else converter(value)
|
|
301
|
+
|
|
302
|
+
return convert_by_typename
|
|
303
|
+
|
|
304
|
+
keys = [tuple(get_type_hints(member)) for member in typed_dicts]
|
|
305
|
+
|
|
306
|
+
if not all(len(member_keys) == 1 for member_keys in keys) or len(
|
|
307
|
+
set(keys)
|
|
308
|
+
) != len(keys):
|
|
309
|
+
raise TypeError(f"Cannot tell the members of `{type_}` apart.")
|
|
310
|
+
|
|
311
|
+
by_key = {
|
|
312
|
+
member_key: converter
|
|
313
|
+
for (member_key,), converter in zip(keys, converters, strict=True)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
def convert_by_key(value: object, /) -> object:
|
|
317
|
+
assert isinstance(value, Mapping)
|
|
318
|
+
(key,) = value
|
|
319
|
+
# A struct's payload may hold a member added after generation, which is not converted either.
|
|
320
|
+
converter = by_key.get(key)
|
|
321
|
+
return value if converter is None else converter(value)
|
|
322
|
+
|
|
323
|
+
return convert_by_key
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _list_converter(convert_item: Convert, /) -> Convert:
|
|
327
|
+
def convert(value: object, /) -> object:
|
|
328
|
+
assert isinstance(value, Sequence)
|
|
329
|
+
result: list[object] = []
|
|
330
|
+
|
|
331
|
+
for index, item in enumerate(value):
|
|
332
|
+
try:
|
|
333
|
+
result.append(convert_item(item))
|
|
334
|
+
except UnexpectedNullError as error:
|
|
335
|
+
error.path.insert(0, index)
|
|
336
|
+
raise
|
|
337
|
+
|
|
338
|
+
return result
|
|
339
|
+
|
|
340
|
+
return convert
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _non_null_converter(convert: Convert | None, /) -> Convert:
|
|
344
|
+
def check(value: object, /) -> object:
|
|
345
|
+
if value is None:
|
|
346
|
+
raise UnexpectedNullError([])
|
|
347
|
+
|
|
348
|
+
return value if convert is None else convert(value)
|
|
349
|
+
|
|
350
|
+
return check
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
_PARSE: Final = _Builder("parse")
|
|
354
|
+
_PARTIAL_PARSE: Final = _Builder("partial parse")
|
|
355
|
+
_SERIALIZE: Final = _Builder("serialize")
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def build_parser(type_: object, /, *, partial: bool) -> Convert | None:
|
|
359
|
+
"""A *partial* parser is for the data of a response with errors, where a `@nonNull` field is null when it raised: the null propagates to the field's nearest nullable parent, and a :class:`UnexpectedNullError` only escapes when there is none."""
|
|
360
|
+
return (_PARTIAL_PARSE if partial else _PARSE).build(type_)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def build_serializer(type_: object, /) -> Convert | None:
|
|
364
|
+
return _SERIALIZE.build(type_)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def build_injector_serializers(
|
|
368
|
+
injector_functions_type: object, /
|
|
369
|
+
) -> dict[str, Convert | None]:
|
|
370
|
+
return {
|
|
371
|
+
# Each hint is `Callable[[], T]`, whose `T` tells the serializer.
|
|
372
|
+
name: build_serializer(get_args(_unwrap(hint)[0])[-1])
|
|
373
|
+
for name, hint in get_type_hints(
|
|
374
|
+
injector_functions_type, include_extras=True
|
|
375
|
+
).items()
|
|
376
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Copied into each generated package, where this line says not to edit it.
|
|
2
|
+
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
SIGIL: Final = "§"
|
|
6
|
+
"""Where an operation's index in a merge goes in its document: after each root field's response name, as its alias, and after each variable's name.
|
|
7
|
+
|
|
8
|
+
An operation run alone drops it, leaving each root field aliased as itself, which GraphQL executes as if it were not aliased.
|
|
9
|
+
A document without it cannot be merged: a subscription's, or that of an operation with directives of its own.
|
|
10
|
+
The generator escapes it in strings, so that every one left is a sigil.
|
|
11
|
+
"""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Copied into each generated package, where this line says not to edit it.
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncGenerator, Awaitable, Callable, Generator
|
|
4
|
+
from typing import Concatenate
|
|
5
|
+
|
|
6
|
+
type Transport[**Params] = Callable[Concatenate[bytes, Params], bytes]
|
|
7
|
+
"""The spec leaves the transport mechanism to the implementation, and so does this library.
|
|
8
|
+
Bytes in, bytes out keeps it sans-IO: it owns neither the transport nor the connection, so it works under any IO model.
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
type AsyncTransport[**Params] = Callable[Concatenate[bytes, Params], Awaitable[bytes]]
|
|
13
|
+
|
|
14
|
+
type SubscriptionTransport[**Params] = Callable[
|
|
15
|
+
Concatenate[bytes, Params], Generator[bytes, None, None]
|
|
16
|
+
]
|
|
17
|
+
"""A generator rather than any iterator, because closing the response stream closes it: its cleanup is where it unsubscribes."""
|
|
18
|
+
|
|
19
|
+
type AsyncSubscriptionTransport[**Params] = Callable[
|
|
20
|
+
Concatenate[bytes, Params], AsyncGenerator[bytes, None]
|
|
21
|
+
]
|