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.
Files changed (55) hide show
  1. graphql_codegen/__init__.py +7 -0
  2. graphql_codegen/__main__.py +5 -0
  3. graphql_codegen/_cli/__init__.py +41 -0
  4. graphql_codegen/_cli/_graphql_config.py +291 -0
  5. graphql_codegen/_cli/_introspection.py +87 -0
  6. graphql_codegen/_cli/_introspection_graphql.py +53 -0
  7. graphql_codegen/_cli/_parsing.py +67 -0
  8. graphql_codegen/_cli/_schema_pointer.py +108 -0
  9. graphql_codegen/_cli/_source.py +68 -0
  10. graphql_codegen/_cli/_write.py +22 -0
  11. graphql_codegen/_generator/__init__.py +0 -0
  12. graphql_codegen/_generator/_annotation.py +149 -0
  13. graphql_codegen/_generator/_ast_nodes.py +235 -0
  14. graphql_codegen/_generator/_data_type.py +532 -0
  15. graphql_codegen/_generator/_document.py +108 -0
  16. graphql_codegen/_generator/_document_module.py +54 -0
  17. graphql_codegen/_generator/_imports.py +166 -0
  18. graphql_codegen/_generator/_injector.py +267 -0
  19. graphql_codegen/_generator/_merge.py +149 -0
  20. graphql_codegen/_generator/_naming.py +80 -0
  21. graphql_codegen/_generator/_operation.py +223 -0
  22. graphql_codegen/_generator/_scalar.py +76 -0
  23. graphql_codegen/_generator/_schema.py +46 -0
  24. graphql_codegen/_generator/_schema_type.py +334 -0
  25. graphql_codegen/_generator/_selection.py +246 -0
  26. graphql_codegen/_generator/_structs.py +87 -0
  27. graphql_codegen/_generator/_typed_dict.py +154 -0
  28. graphql_codegen/_generator/dotted_name.py +48 -0
  29. graphql_codegen/_generator/package.py +695 -0
  30. graphql_codegen/_generator/spelling.py +168 -0
  31. graphql_codegen/_metadata.py +11 -0
  32. graphql_codegen/_note.py +12 -0
  33. graphql_codegen/config.py +42 -0
  34. graphql_codegen/document_sibling_module.py +58 -0
  35. graphql_codegen/generate.py +19 -0
  36. graphql_codegen/package_location.py +41 -0
  37. graphql_codegen/py.typed +0 -0
  38. graphql_codegen/runtime/__init__.py +20 -0
  39. graphql_codegen/runtime/_compat.py +27 -0
  40. graphql_codegen/runtime/_literal.py +18 -0
  41. graphql_codegen/runtime/_merge.py +153 -0
  42. graphql_codegen/runtime/_prepare.py +250 -0
  43. graphql_codegen/runtime/_reflection.py +376 -0
  44. graphql_codegen/runtime/_sigil.py +11 -0
  45. graphql_codegen/runtime/_transport.py +21 -0
  46. graphql_codegen/runtime/client.py +384 -0
  47. graphql_codegen/runtime/error.py +135 -0
  48. graphql_codegen/runtime/injection.py +107 -0
  49. graphql_codegen/runtime/operation.py +135 -0
  50. graphql_codegen/scalar.py +59 -0
  51. graphql_codegen-0.1.0.dist-info/METADATA +871 -0
  52. graphql_codegen-0.1.0.dist-info/RECORD +55 -0
  53. graphql_codegen-0.1.0.dist-info/WHEEL +4 -0
  54. graphql_codegen-0.1.0.dist-info/entry_points.txt +3 -0
  55. 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
+ ]