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,384 @@
1
+ # Copied into each generated package, where this line says not to edit it.
2
+
3
+ from collections.abc import AsyncGenerator, Callable, Generator, Sequence
4
+ from contextlib import aclosing, closing
5
+ from typing import Any, Final, Generic, Literal, final, overload
6
+
7
+ from ._compat import TypeVar
8
+ from ._prepare import prepare, prepare_requests
9
+ from ._transport import (
10
+ AsyncSubscriptionTransport,
11
+ AsyncTransport,
12
+ SubscriptionTransport,
13
+ Transport,
14
+ )
15
+ from .injection import _Injectors
16
+ from .operation import Request
17
+
18
+ # A client's transport, whose signature types the arguments each call accepts: none, unless it says otherwise.
19
+ # Covariant, so that a client over a transport is a client over any `Protocol` the transport satisfies.
20
+ # Legacy `TypeVar`s, since a default for a PEP 695 type parameter needs Python 3.13.
21
+ _Transport_co = TypeVar(
22
+ "_Transport_co", bound=Transport[...], covariant=True, default=Transport[[]]
23
+ )
24
+ _AsyncTransport_co = TypeVar(
25
+ "_AsyncTransport_co",
26
+ bound=AsyncTransport[...],
27
+ covariant=True,
28
+ default=AsyncTransport[[]],
29
+ )
30
+ _SubscriptionTransport_co = TypeVar(
31
+ "_SubscriptionTransport_co",
32
+ bound=SubscriptionTransport[...],
33
+ covariant=True,
34
+ default=SubscriptionTransport[[]],
35
+ )
36
+ _AsyncSubscriptionTransport_co = TypeVar(
37
+ "_AsyncSubscriptionTransport_co",
38
+ bound=AsyncSubscriptionTransport[...],
39
+ covariant=True,
40
+ default=AsyncSubscriptionTransport[[]],
41
+ )
42
+
43
+
44
+ def _response_stream[Data](
45
+ bodies: Generator[bytes, None, None], parse: Callable[[bytes], Data], /
46
+ ) -> Generator[Data, None, None]:
47
+ # Closing the response stream, or an error in one of its responses, closes the transport's.
48
+ with closing(bodies):
49
+ for body in bodies:
50
+ yield parse(body)
51
+
52
+
53
+ async def _async_response_stream[Data](
54
+ bodies: AsyncGenerator[bytes, None], parse: Callable[[bytes], Data], /
55
+ ) -> AsyncGenerator[Data, None]:
56
+ async with aclosing(bodies):
57
+ async for body in bodies:
58
+ yield parse(body)
59
+
60
+
61
+ @final
62
+ class Client(Generic[_Transport_co]):
63
+ """Run query and mutation requests over *transport*, each call accepting the transport's parameters.
64
+
65
+ Several operations of one type run merged, in one call to the transport: a tuple of up to 6 returns a tuple typed position by position, and any other sequence a tuple of their common data type.
66
+ A response with errors raises a :class:`ResponseError`, and a merge an :exc:`ExceptionGroup` of a :class:`ExecutionError` per operation its errors concern, or the unsplit error when its data is null.
67
+ A request from :meth:`~Request.returning_error` gets its :class:`ExecutionError` back in place of its data instead, unless the call raises for another.
68
+
69
+ The transport's parameters are read off its signature and checked, so that the library never has to know what they are::
70
+
71
+ def transport(body: bytes, /, *, timeout: float | None = None) -> bytes: ...
72
+
73
+ client = Client(transport)
74
+ client(ListBooks({"first": 10}), timeout=5.0)
75
+
76
+ Code taking a client names its type after the transport's signature, written once as a :class:`typing.Protocol`, which the transport a client is built from is checked against::
77
+
78
+ class Transport(Protocol):
79
+ def __call__(self, body: bytes, /, *, timeout: float | None = None) -> bytes: ...
80
+
81
+ Client = runtime.Client[Transport]
82
+
83
+ def describe(isbn: ISBN, /, *, client: Client) -> str: ...
84
+
85
+ describe(isbn, client=Client(transport))
86
+
87
+ A plain alias rather than a `type` statement, since only the former can be called to build a client.
88
+ A client is generic over its transport rather than over its parameters, because a :class:`typing.ParamSpec`'s value cannot be spelled with keyword parameters, and PEP 692 only unpacks a concrete :class:`typing.TypedDict`, never a type parameter.
89
+ """
90
+
91
+ def __init__(
92
+ self,
93
+ transport: _Transport_co,
94
+ /,
95
+ *,
96
+ injectors: _Injectors | None = None,
97
+ ) -> None:
98
+ self.transport: Final = transport
99
+ self.injectors: Final = injectors
100
+
101
+ @overload
102
+ def __call__[**Params, Type: Literal["query", "mutation"], Data](
103
+ self: "Client[Transport[Params]]",
104
+ request: Request[Type, Data],
105
+ /,
106
+ *args: Params.args,
107
+ **kwargs: Params.kwargs,
108
+ ) -> Data: ...
109
+ @overload
110
+ def __call__[**Params, Type: (Literal["query"], Literal["mutation"]), D1, D2](
111
+ self: "Client[Transport[Params]]",
112
+ requests: tuple[Request[Type, D1], Request[Type, D2]],
113
+ /,
114
+ *args: Params.args,
115
+ **kwargs: Params.kwargs,
116
+ ) -> tuple[D1, D2]: ...
117
+ @overload
118
+ def __call__[**Params, Type: (Literal["query"], Literal["mutation"]), D1, D2, D3](
119
+ self: "Client[Transport[Params]]",
120
+ requests: tuple[Request[Type, D1], Request[Type, D2], Request[Type, D3]],
121
+ /,
122
+ *args: Params.args,
123
+ **kwargs: Params.kwargs,
124
+ ) -> tuple[D1, D2, D3]: ...
125
+ @overload
126
+ def __call__[
127
+ **Params,
128
+ Type: (Literal["query"], Literal["mutation"]),
129
+ D1,
130
+ D2,
131
+ D3,
132
+ D4,
133
+ ](
134
+ self: "Client[Transport[Params]]",
135
+ requests: tuple[
136
+ Request[Type, D1], Request[Type, D2], Request[Type, D3], Request[Type, D4]
137
+ ],
138
+ /,
139
+ *args: Params.args,
140
+ **kwargs: Params.kwargs,
141
+ ) -> tuple[D1, D2, D3, D4]: ...
142
+ @overload
143
+ def __call__[
144
+ **Params,
145
+ Type: (Literal["query"], Literal["mutation"]),
146
+ D1,
147
+ D2,
148
+ D3,
149
+ D4,
150
+ D5,
151
+ ](
152
+ self: "Client[Transport[Params]]",
153
+ requests: tuple[
154
+ Request[Type, D1],
155
+ Request[Type, D2],
156
+ Request[Type, D3],
157
+ Request[Type, D4],
158
+ Request[Type, D5],
159
+ ],
160
+ /,
161
+ *args: Params.args,
162
+ **kwargs: Params.kwargs,
163
+ ) -> tuple[D1, D2, D3, D4, D5]: ...
164
+ @overload
165
+ def __call__[
166
+ **Params,
167
+ Type: (Literal["query"], Literal["mutation"]),
168
+ D1,
169
+ D2,
170
+ D3,
171
+ D4,
172
+ D5,
173
+ D6,
174
+ ](
175
+ self: "Client[Transport[Params]]",
176
+ requests: tuple[
177
+ Request[Type, D1],
178
+ Request[Type, D2],
179
+ Request[Type, D3],
180
+ Request[Type, D4],
181
+ Request[Type, D5],
182
+ Request[Type, D6],
183
+ ],
184
+ /,
185
+ *args: Params.args,
186
+ **kwargs: Params.kwargs,
187
+ ) -> tuple[D1, D2, D3, D4, D5, D6]: ...
188
+ @overload
189
+ def __call__[**Params, Type: (Literal["query"], Literal["mutation"]), Data](
190
+ self: "Client[Transport[Params]]",
191
+ requests: Sequence[Request[Type, Data]],
192
+ /,
193
+ *args: Params.args,
194
+ **kwargs: Params.kwargs,
195
+ ) -> tuple[Data, ...]: ...
196
+ def __call__(self, requests: Any, /, *args: Any, **kwargs: Any) -> Any:
197
+ prepared = prepare_requests(requests, injectors=self.injectors)
198
+ return (
199
+ ()
200
+ if prepared is None
201
+ else prepared.parse(self.transport(bytes(prepared), *args, **kwargs))
202
+ )
203
+
204
+
205
+ @final
206
+ class AsyncClient(Generic[_AsyncTransport_co]):
207
+ def __init__(
208
+ self,
209
+ transport: _AsyncTransport_co,
210
+ /,
211
+ *,
212
+ injectors: _Injectors | None = None,
213
+ ) -> None:
214
+ self.transport: Final = transport
215
+ self.injectors: Final = injectors
216
+
217
+ @overload
218
+ async def __call__[**Params, Type: Literal["query", "mutation"], Data](
219
+ self: "AsyncClient[AsyncTransport[Params]]",
220
+ request: Request[Type, Data],
221
+ /,
222
+ *args: Params.args,
223
+ **kwargs: Params.kwargs,
224
+ ) -> Data: ...
225
+ @overload
226
+ async def __call__[
227
+ **Params,
228
+ Type: (Literal["query"], Literal["mutation"]),
229
+ D1,
230
+ D2,
231
+ ](
232
+ self: "AsyncClient[AsyncTransport[Params]]",
233
+ requests: tuple[Request[Type, D1], Request[Type, D2]],
234
+ /,
235
+ *args: Params.args,
236
+ **kwargs: Params.kwargs,
237
+ ) -> tuple[D1, D2]: ...
238
+ @overload
239
+ async def __call__[
240
+ **Params,
241
+ Type: (Literal["query"], Literal["mutation"]),
242
+ D1,
243
+ D2,
244
+ D3,
245
+ ](
246
+ self: "AsyncClient[AsyncTransport[Params]]",
247
+ requests: tuple[Request[Type, D1], Request[Type, D2], Request[Type, D3]],
248
+ /,
249
+ *args: Params.args,
250
+ **kwargs: Params.kwargs,
251
+ ) -> tuple[D1, D2, D3]: ...
252
+ @overload
253
+ async def __call__[
254
+ **Params,
255
+ Type: (Literal["query"], Literal["mutation"]),
256
+ D1,
257
+ D2,
258
+ D3,
259
+ D4,
260
+ ](
261
+ self: "AsyncClient[AsyncTransport[Params]]",
262
+ requests: tuple[
263
+ Request[Type, D1], Request[Type, D2], Request[Type, D3], Request[Type, D4]
264
+ ],
265
+ /,
266
+ *args: Params.args,
267
+ **kwargs: Params.kwargs,
268
+ ) -> tuple[D1, D2, D3, D4]: ...
269
+ @overload
270
+ async def __call__[
271
+ **Params,
272
+ Type: (Literal["query"], Literal["mutation"]),
273
+ D1,
274
+ D2,
275
+ D3,
276
+ D4,
277
+ D5,
278
+ ](
279
+ self: "AsyncClient[AsyncTransport[Params]]",
280
+ requests: tuple[
281
+ Request[Type, D1],
282
+ Request[Type, D2],
283
+ Request[Type, D3],
284
+ Request[Type, D4],
285
+ Request[Type, D5],
286
+ ],
287
+ /,
288
+ *args: Params.args,
289
+ **kwargs: Params.kwargs,
290
+ ) -> tuple[D1, D2, D3, D4, D5]: ...
291
+ @overload
292
+ async def __call__[
293
+ **Params,
294
+ Type: (Literal["query"], Literal["mutation"]),
295
+ D1,
296
+ D2,
297
+ D3,
298
+ D4,
299
+ D5,
300
+ D6,
301
+ ](
302
+ self: "AsyncClient[AsyncTransport[Params]]",
303
+ requests: tuple[
304
+ Request[Type, D1],
305
+ Request[Type, D2],
306
+ Request[Type, D3],
307
+ Request[Type, D4],
308
+ Request[Type, D5],
309
+ Request[Type, D6],
310
+ ],
311
+ /,
312
+ *args: Params.args,
313
+ **kwargs: Params.kwargs,
314
+ ) -> tuple[D1, D2, D3, D4, D5, D6]: ...
315
+ @overload
316
+ async def __call__[**Params, Type: (Literal["query"], Literal["mutation"]), Data](
317
+ self: "AsyncClient[AsyncTransport[Params]]",
318
+ requests: Sequence[Request[Type, Data]],
319
+ /,
320
+ *args: Params.args,
321
+ **kwargs: Params.kwargs,
322
+ ) -> tuple[Data, ...]: ...
323
+ async def __call__(self, requests: Any, /, *args: Any, **kwargs: Any) -> Any:
324
+ prepared = prepare_requests(requests, injectors=self.injectors)
325
+ return (
326
+ ()
327
+ if prepared is None
328
+ else prepared.parse(await self.transport(bytes(prepared), *args, **kwargs))
329
+ )
330
+
331
+
332
+ @final
333
+ class SubscriptionClient(Generic[_SubscriptionTransport_co]):
334
+ """Run subscription requests over *transport*, returning their response stream, which closing unsubscribes.
335
+
336
+ A response with errors raises out of the stream.
337
+ """
338
+
339
+ def __init__(
340
+ self,
341
+ transport: _SubscriptionTransport_co,
342
+ /,
343
+ *,
344
+ injectors: _Injectors | None = None,
345
+ ) -> None:
346
+ self.transport: Final = transport
347
+ self.injectors: Final = injectors
348
+
349
+ def __call__[**Params, Data](
350
+ self: "SubscriptionClient[SubscriptionTransport[Params]]",
351
+ request: Request[Literal["subscription"], Data],
352
+ /,
353
+ *args: Params.args,
354
+ **kwargs: Params.kwargs,
355
+ ) -> Generator[Data, None, None]:
356
+ prepared = prepare(request, injectors=self.injectors)
357
+ return _response_stream(
358
+ self.transport(bytes(prepared), *args, **kwargs), prepared.parse
359
+ )
360
+
361
+
362
+ @final
363
+ class AsyncSubscriptionClient(Generic[_AsyncSubscriptionTransport_co]):
364
+ def __init__(
365
+ self,
366
+ transport: _AsyncSubscriptionTransport_co,
367
+ /,
368
+ *,
369
+ injectors: _Injectors | None = None,
370
+ ) -> None:
371
+ self.transport: Final = transport
372
+ self.injectors: Final = injectors
373
+
374
+ def __call__[**Params, Data](
375
+ self: "AsyncSubscriptionClient[AsyncSubscriptionTransport[Params]]",
376
+ request: Request[Literal["subscription"], Data],
377
+ /,
378
+ *args: Params.args,
379
+ **kwargs: Params.kwargs,
380
+ ) -> AsyncGenerator[Data, None]:
381
+ prepared = prepare(request, injectors=self.injectors)
382
+ return _async_response_stream(
383
+ self.transport(bytes(prepared), *args, **kwargs), prepared.parse
384
+ )
@@ -0,0 +1,135 @@
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 copy import deepcopy
5
+ from typing import Final, Generic, NotRequired, Required, cast, final, override
6
+
7
+ from ._compat import TypedDict, TypeVar
8
+
9
+
10
+ class Location(TypedDict):
11
+ line: Required[int]
12
+ column: Required[int]
13
+
14
+
15
+ class Error(TypedDict):
16
+ """An error as a server sends it, in the spec's shape, which graphql-core's :class:`~graphql.GraphQLFormattedError` follows too.
17
+
18
+ Not graphql-core's :class:`~graphql.GraphQLError`, because four of its eight attributes (:attr:`~graphql.GraphQLError.nodes`, :attr:`~graphql.GraphQLError.source`, :attr:`~graphql.GraphQLError.positions`, and :attr:`~graphql.GraphQLError.original_error`) are server-side execution state a client cannot fill, and importing it would put graphql-core in the runtime for nothing.
19
+ """
20
+
21
+ message: Required[str]
22
+ locations: NotRequired[Sequence[Location]]
23
+ path: NotRequired[Sequence[str | int]]
24
+ extensions: NotRequired[object]
25
+
26
+
27
+ class ClientError(Exception):
28
+ """Base of every error a generated client raises.
29
+
30
+ A transport's own errors, such as a timeout or an HTTP status, pass through as they are: the client does not know what carries its requests.
31
+ Not :func:`typing.final`: it exists so a caller can catch anything this library raises with one `except`.
32
+ """
33
+
34
+
35
+ class ResponseError(ClientError):
36
+ """The response carried errors, in either of the two shapes the spec gives such a response.
37
+
38
+ A response carrying any error raises, so that the data a client returns never holds a `null` standing for an error.
39
+ Not :func:`typing.final`: it exists so a caller can catch both shapes with one `except`.
40
+ """
41
+
42
+ def __init__(
43
+ self, errors: Sequence[Error], /, *, extensions: Mapping[str, object] | None
44
+ ) -> None:
45
+ assert errors, "Cannot raise without at least one error."
46
+ self.errors: Final = tuple(errors)
47
+ self.extensions: Final = extensions
48
+ super().__init__("; ".join(error["message"] for error in self.errors))
49
+
50
+
51
+ @final
52
+ class RequestError(ResponseError):
53
+ """The response is the spec's request error result: the request failed before execution, so it has no data.
54
+
55
+ A syntax error, a validation failure, or an invalid variable value, for instance.
56
+ """
57
+
58
+
59
+ # The type of an execution error's parsed data: the operation's data type when a client returns the error, typed by its request, and a plain mapping when `except` catches it, since that names no operation.
60
+ # A legacy `TypeVar`, since a default for a PEP 695 type parameter needs Python 3.13.
61
+ _Data_co = TypeVar("_Data_co", covariant=True, default=Mapping[str, object])
62
+
63
+
64
+ @final
65
+ class ExecutionError(ResponseError, Generic[_Data_co]):
66
+ """The server raised errors executing the request, but sent the rest of the data.
67
+
68
+ A field that raised is `null`, as is its nearest nullable parent if it is non-null.
69
+ """
70
+
71
+ data: Final[Mapping[str, object] | None]
72
+
73
+ def parse_data(self) -> _Data_co | None:
74
+ """Return the data converted as in a response without errors, in a fresh copy.
75
+
76
+ A `@nonNull` field that raised moves its `null` up to its nearest nullable parent, as the server does for a non-null field.
77
+ A custom scalar's codec may raise on a value it cannot decode.
78
+ """
79
+ if self.data is None:
80
+ return None
81
+
82
+ data = deepcopy(self.data)
83
+
84
+ if self._partial_data_parser is not None:
85
+ try:
86
+ self._partial_data_parser(data)
87
+ except UnexpectedNullError:
88
+ return None
89
+
90
+ # Nothing is validated: the data is trusted to match its type, as in a response without errors.
91
+ return cast(_Data_co, data)
92
+
93
+ def __init__(
94
+ self,
95
+ errors: Sequence[Error],
96
+ /,
97
+ *,
98
+ data: Mapping[str, object] | None,
99
+ extensions: Mapping[str, object] | None,
100
+ partial_data_parser: Callable[[object], object] | None,
101
+ ) -> None:
102
+ super().__init__(errors, extensions=extensions)
103
+ self.data = data
104
+ self._partial_data_parser: Final = partial_data_parser
105
+
106
+
107
+ @final
108
+ class ProtocolError(ClientError):
109
+ """The response was not a GraphQL response.
110
+
111
+ Usually an HTML error page from a proxy.
112
+ Without this the caller would get a bare :exc:`json.JSONDecodeError` from somewhere inside the client.
113
+ """
114
+
115
+
116
+ @final
117
+ class UnexpectedNullError(ClientError):
118
+ """A field marked `@nonNull` was null.
119
+
120
+ The schema declares the field nullable, so only the client can assert otherwise.
121
+
122
+ The path names the exact value, list indices included: it is completed on the way up by each enclosing object and list, which costs nothing unless the error is raised.
123
+ """
124
+
125
+ def __init__(self, path: list[str | int], /) -> None:
126
+ self.path: Final = path
127
+ super().__init__()
128
+
129
+ @override
130
+ def __str__(self) -> str:
131
+ formatted = "".join(
132
+ f"[{segment}]" if isinstance(segment, int) else f".{segment}"
133
+ for segment in self.path
134
+ ).removeprefix(".")
135
+ return f"Expected `{formatted}` to not be null."
@@ -0,0 +1,107 @@
1
+ # Copied into each generated package, where this line says not to edit it.
2
+
3
+ import sys
4
+ from collections.abc import Callable, Mapping
5
+ from typing import Final, cast, final
6
+
7
+ from ._reflection import Convert, build_injector_serializers
8
+
9
+ if sys.version_info >= (3, 15):
10
+ OMITTED = sentinel("OMITTED") # noqa: F821
11
+ else:
12
+ from typing_extensions import Sentinel
13
+
14
+ OMITTED = Sentinel("OMITTED")
15
+ """What an injector returns to leave its value out rather than send ``null``.
16
+
17
+ The spec tells the two apart: a nullable input accepts both, a non-null one neither, and a server may treat them differently.
18
+ """
19
+
20
+
21
+ type _Injector = Callable[[], object | OMITTED]
22
+
23
+
24
+ @final
25
+ class _Injectors:
26
+ """Built by the ``injectors()`` function of a generated package's ``injection`` module, from a mapping the package's types check.
27
+
28
+ Not a mapping itself, so that a client cannot be given injectors that skipped that check: only a client calls it.
29
+ """
30
+
31
+ def __init__(
32
+ self, functions: Mapping[str, object], /, *, injector_functions_type: object
33
+ ) -> None:
34
+ self._functions: Final = {
35
+ name: cast(_Injector, function) for name, function in functions.items()
36
+ }
37
+ self._serializers: Final = build_injector_serializers(injector_functions_type)
38
+
39
+ def _inject(
40
+ self,
41
+ variables: Mapping[str, object],
42
+ /,
43
+ *,
44
+ injections: Mapping[str, frozenset[tuple[str, ...]]],
45
+ ) -> Mapping[str, object]:
46
+ """Calling the injectors is the one impure step, each once, so that every path of a request receives the same value."""
47
+ values = {
48
+ name: function()
49
+ for name, function in self._functions.items()
50
+ if name in injections
51
+ }
52
+ return _inject_values(
53
+ variables, values, serializers=self._serializers, injections=injections
54
+ )
55
+
56
+
57
+ def _inject_values(
58
+ variables: Mapping[str, object],
59
+ values: Mapping[str, object],
60
+ /,
61
+ *,
62
+ serializers: Mapping[str, Convert | None],
63
+ injections: Mapping[str, frozenset[tuple[str, ...]]],
64
+ ) -> Mapping[str, object]:
65
+ for name, value in values.items():
66
+ if value is OMITTED:
67
+ continue
68
+
69
+ serialize = serializers[name]
70
+ serialized = value if serialize is None else serialize(value)
71
+
72
+ for path in injections[name]:
73
+ injected = _inject_into(variables, path, key=name, value=serialized)
74
+
75
+ if injected is not None:
76
+ variables = injected
77
+
78
+ return variables
79
+
80
+
81
+ def _inject_into(
82
+ container: Mapping[str, object],
83
+ path: tuple[str, ...],
84
+ /,
85
+ *,
86
+ key: str,
87
+ value: object,
88
+ ) -> dict[str, object] | None:
89
+ """Copied along the path, since the caller may reuse what it passed.
90
+
91
+ ``None`` when an object on the path is absent or null, as a nullable input the caller left out is: there is nothing to inject into.
92
+ """
93
+ if not path:
94
+ return {**container, key: value}
95
+
96
+ head, *rest = path
97
+ child = container.get(head)
98
+
99
+ if child is None:
100
+ return None
101
+
102
+ # The generator only emits paths through input objects.
103
+ assert isinstance(child, Mapping), (
104
+ f"Expected `{head}` to hold an input object to inject `{key}` into."
105
+ )
106
+ injected = _inject_into(child, tuple(rest), key=key, value=value)
107
+ return None if injected is None else {**container, head: injected}