cliV1 0.0.6__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 (92) hide show
  1. cliv1-0.0.6.dist-info/METADATA +212 -0
  2. cliv1-0.0.6.dist-info/RECORD +92 -0
  3. cliv1-0.0.6.dist-info/WHEEL +4 -0
  4. cliv1-0.0.6.dist-info/licenses/LICENSE +21 -0
  5. swagger_petstore_open_api_3_1_0/__init__.py +15 -0
  6. swagger_petstore_open_api_3_1_0/apis/__init__.py +5 -0
  7. swagger_petstore_open_api_3_1_0/apis/pet_api.py +922 -0
  8. swagger_petstore_open_api_3_1_0/apis/store.py +432 -0
  9. swagger_petstore_open_api_3_1_0/apis/user_api.py +829 -0
  10. swagger_petstore_open_api_3_1_0/async_client.py +87 -0
  11. swagger_petstore_open_api_3_1_0/auth.py +17 -0
  12. swagger_petstore_open_api_3_1_0/base_client.py +27 -0
  13. swagger_petstore_open_api_3_1_0/client.py +85 -0
  14. swagger_petstore_open_api_3_1_0/core/__init__.py +395 -0
  15. swagger_petstore_open_api_3_1_0/core/_internal/__init__.py +9 -0
  16. swagger_petstore_open_api_3_1_0/core/_internal/content_disposition.py +154 -0
  17. swagger_petstore_open_api_3_1_0/core/_internal/flattening.py +104 -0
  18. swagger_petstore_open_api_3_1_0/core/_internal/headers.py +243 -0
  19. swagger_petstore_open_api_3_1_0/core/_internal/redaction.py +208 -0
  20. swagger_petstore_open_api_3_1_0/core/_internal/sse_parser.py +124 -0
  21. swagger_petstore_open_api_3_1_0/core/_internal/subscripts.py +50 -0
  22. swagger_petstore_open_api_3_1_0/core/_internal/urls.py +172 -0
  23. swagger_petstore_open_api_3_1_0/core/_internal/wire.py +201 -0
  24. swagger_petstore_open_api_3_1_0/core/adapters.py +177 -0
  25. swagger_petstore_open_api_3_1_0/core/auth/__init__.py +70 -0
  26. swagger_petstore_open_api_3_1_0/core/auth/composition.py +175 -0
  27. swagger_petstore_open_api_3_1_0/core/auth/models.py +216 -0
  28. swagger_petstore_open_api_3_1_0/core/auth/protocols.py +103 -0
  29. swagger_petstore_open_api_3_1_0/core/auth/schemes/__init__.py +94 -0
  30. swagger_petstore_open_api_3_1_0/core/auth/schemes/api_key_auth.py +53 -0
  31. swagger_petstore_open_api_3_1_0/core/auth/schemes/basic_auth.py +73 -0
  32. swagger_petstore_open_api_3_1_0/core/auth/schemes/bearer_auth.py +21 -0
  33. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_authorization_code.py +395 -0
  34. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_client_credentials.py +130 -0
  35. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_password.py +145 -0
  36. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_schemes.py +300 -0
  37. swagger_petstore_open_api_3_1_0/core/base_raw_response.py +49 -0
  38. swagger_petstore_open_api_3_1_0/core/bodies.py +590 -0
  39. swagger_petstore_open_api_3_1_0/core/converters/__init__.py +68 -0
  40. swagger_petstore_open_api_3_1_0/core/converters/date_time.py +237 -0
  41. swagger_petstore_open_api_3_1_0/core/converters/encoded_bytes.py +273 -0
  42. swagger_petstore_open_api_3_1_0/core/converters/open_enum.py +35 -0
  43. swagger_petstore_open_api_3_1_0/core/decoding.py +498 -0
  44. swagger_petstore_open_api_3_1_0/core/discriminators.py +129 -0
  45. swagger_petstore_open_api_3_1_0/core/event_streams.py +225 -0
  46. swagger_petstore_open_api_3_1_0/core/exceptions.py +78 -0
  47. swagger_petstore_open_api_3_1_0/core/file_responses.py +337 -0
  48. swagger_petstore_open_api_3_1_0/core/files.py +174 -0
  49. swagger_petstore_open_api_3_1_0/core/httpx2_transport.py +748 -0
  50. swagger_petstore_open_api_3_1_0/core/logging.py +472 -0
  51. swagger_petstore_open_api_3_1_0/core/models.py +143 -0
  52. swagger_petstore_open_api_3_1_0/core/optionality.py +144 -0
  53. swagger_petstore_open_api_3_1_0/core/pagination.py +452 -0
  54. swagger_petstore_open_api_3_1_0/core/paginator.py +55 -0
  55. swagger_petstore_open_api_3_1_0/core/params.py +157 -0
  56. swagger_petstore_open_api_3_1_0/core/raw_client.py +480 -0
  57. swagger_petstore_open_api_3_1_0/core/request_options.py +172 -0
  58. swagger_petstore_open_api_3_1_0/core/results.py +178 -0
  59. swagger_petstore_open_api_3_1_0/core/retries.py +237 -0
  60. swagger_petstore_open_api_3_1_0/core/runtime_env.py +43 -0
  61. swagger_petstore_open_api_3_1_0/core/servers.py +33 -0
  62. swagger_petstore_open_api_3_1_0/core/transport.py +218 -0
  63. swagger_petstore_open_api_3_1_0/errors/__init__.py +48 -0
  64. swagger_petstore_open_api_3_1_0/errors/add_pet_error.py +21 -0
  65. swagger_petstore_open_api_3_1_0/errors/delete_order_error.py +21 -0
  66. swagger_petstore_open_api_3_1_0/errors/delete_pet_error.py +21 -0
  67. swagger_petstore_open_api_3_1_0/errors/delete_user_error.py +21 -0
  68. swagger_petstore_open_api_3_1_0/errors/find_pets_by_status_error.py +21 -0
  69. swagger_petstore_open_api_3_1_0/errors/find_pets_by_tags_error.py +21 -0
  70. swagger_petstore_open_api_3_1_0/errors/get_order_by_id_error.py +21 -0
  71. swagger_petstore_open_api_3_1_0/errors/get_pet_by_id_error.py +21 -0
  72. swagger_petstore_open_api_3_1_0/errors/get_user_by_name_error.py +21 -0
  73. swagger_petstore_open_api_3_1_0/errors/login_user_error.py +21 -0
  74. swagger_petstore_open_api_3_1_0/errors/place_order_error.py +21 -0
  75. swagger_petstore_open_api_3_1_0/errors/update_pet_error.py +21 -0
  76. swagger_petstore_open_api_3_1_0/errors/update_pet_with_form_error.py +21 -0
  77. swagger_petstore_open_api_3_1_0/errors/update_user_error.py +21 -0
  78. swagger_petstore_open_api_3_1_0/errors/upload_file_error.py +21 -0
  79. swagger_petstore_open_api_3_1_0/models/__init__.py +23 -0
  80. swagger_petstore_open_api_3_1_0/models/api_response.py +18 -0
  81. swagger_petstore_open_api_3_1_0/models/category.py +15 -0
  82. swagger_petstore_open_api_3_1_0/models/enums/__init__.py +4 -0
  83. swagger_petstore_open_api_3_1_0/models/enums/order_status.py +17 -0
  84. swagger_petstore_open_api_3_1_0/models/enums/pet_status.py +17 -0
  85. swagger_petstore_open_api_3_1_0/models/order.py +27 -0
  86. swagger_petstore_open_api_3_1_0/models/pet.py +28 -0
  87. swagger_petstore_open_api_3_1_0/models/tag_model.py +15 -0
  88. swagger_petstore_open_api_3_1_0/models/user.py +29 -0
  89. swagger_petstore_open_api_3_1_0/py.typed +0 -0
  90. swagger_petstore_open_api_3_1_0/server/__init__.py +21 -0
  91. swagger_petstore_open_api_3_1_0/server/server.py +17 -0
  92. swagger_petstore_open_api_3_1_0/server/server_config.py +52 -0
@@ -0,0 +1,590 @@
1
+ """Request bodies: the wire shapes a transport receives, and the factories that build them.
2
+
3
+ A buffered body **is** its wire bytes by the time it exists -- UTF-8 for JSON (RFC 8259) and for a
4
+ percent-encoded form (pure ASCII, so the same bytes under any ASCII-compatible charset), and the
5
+ declared charset for a text body. A multipart body's text parts carry their UTF-8 bytes the same
6
+ way; only the framing around them is the encoder's. File content is deliberately
7
+ the opposite: a file is *described* (a ``Path``, a handle, an iterable of chunks) and first read
8
+ inside the transport when the request is sent, so building a body never opens a resource. Each
9
+ factory validates any typed value against the type the endpoint declared and dumps it through that
10
+ type's adapter, so a transport, including a caller-supplied one, never serializes anything, and
11
+ :class:`HttpRequest` never carries a pydantic adapter. A body also carries its own label:
12
+ :func:`content_headers` is the one place a body's ``Content-Type`` (and a raw body's
13
+ ``Content-Disposition``) is spelled, folded onto the request beneath every parameter layer.
14
+
15
+ ``RequestBody`` is a closed union rather than a Protocol: all five shapes live here and are
16
+ generator-emitted, so the set cannot grow behind the runtime's back."""
17
+
18
+ from __future__ import annotations
19
+
20
+ import codecs
21
+ from collections.abc import Sequence
22
+ from dataclasses import dataclass
23
+ from pathlib import Path
24
+ from typing import Any, Final, Generic, TypeAlias, TypeVar, overload
25
+
26
+ from pydantic import TypeAdapter
27
+ from typing_extensions import TypeForm, assert_never
28
+
29
+ from ._internal.content_disposition import render_content_disposition
30
+ from ._internal.flattening import flatten
31
+ from ._internal.subscripts import SubscriptOnly
32
+ from ._internal.urls import urlencode_params
33
+ from ._internal.wire import compact_json, to_text
34
+ from .adapters import adapter_for, validation_target
35
+ from .files import AsyncBinaryContent, AsyncFileInput, NamedFile
36
+ from .optionality import strip_unset
37
+ from .params import Param
38
+
39
+ T = TypeVar("T")
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class JsonBody(Generic[T]):
44
+ """A JSON body of declared type ``T``: its wire bytes, under the operation's declared media type.
45
+
46
+ ``T`` records the type the value was validated and dumped *as*; ``content`` is that dump --
47
+ compact, UTF-8, produced once by ``json_body[...]`` -- so the transport sends it unchanged and a
48
+ log line describing the body shows the same bytes. ``media_type`` is the operation's content key:
49
+ ``application/json`` almost always, ``application/merge-patch+json`` and the ``+json`` vendor
50
+ types being the same wire shape under a different label. Construct it through ``json_body[...]``,
51
+ whose subscript binds ``T``."""
52
+
53
+ content: bytes
54
+ media_type: str
55
+
56
+
57
+ @dataclass(frozen=True, slots=True)
58
+ class FormBody:
59
+ """A url-encoded form body: its wire bytes.
60
+
61
+ Rendered by the encoder the query string uses (``urlencode_params``), so a form field and a
62
+ query parameter of one value are the same bytes, in the same order. Percent-encoding leaves
63
+ ASCII, so the UTF-8 bytes of that text are the same bytes under any ASCII-compatible charset."""
64
+
65
+ content: bytes
66
+
67
+
68
+ @dataclass(frozen=True, slots=True)
69
+ class MultipartText:
70
+ """One non-file part: a named text value, already encoded as UTF-8, optionally under a declared media type.
71
+
72
+ A JSON part is not a separate shape: it is a text part under the media type the operation
73
+ declares for it (``application/json``, unless its encoding object says otherwise) -- the
74
+ identical ``(None, content, media_type)`` wire spelling either way. Classes split only where the
75
+ transport must act differently, and here it does not. The bytes are the factory's, as every
76
+ in-memory body's are: the encoder frames a part and encodes nothing."""
77
+
78
+ name: str
79
+ content: bytes
80
+ media_type: str | None = None
81
+
82
+
83
+ @dataclass(frozen=True, slots=True)
84
+ class MultipartFile:
85
+ """One file part, its filename and media type already resolved.
86
+
87
+ Kept distinct from :class:`NamedFile` deliberately: there ``filename=None`` means *derive it*;
88
+ here it means *resolved to absent*. Merging the two would conflate an unresolved state with a
89
+ resolved one. ``media_type`` has no absent state at all -- :func:`file_part` always resolves
90
+ one, so a transport is never handed the question and the underlying library's own guess is
91
+ unreachable."""
92
+
93
+ name: str
94
+
95
+ content: AsyncBinaryContent
96
+ """The bytes, however the caller held them.
97
+
98
+ The *async* union, for the reason :attr:`BinaryBody.content` is one: ``HttpRequest`` is a single
99
+ shape for both flavours, and which arms are reachable is fixed by the emitted endpoint's own
100
+ parameter type. A transport is handed them unresolved -- the sync one rejects the async arms
101
+ with a guided ``TypeError``, and an async one must read them before encoding, because a
102
+ multipart encoder pulls from a synchronous generator and cannot await."""
103
+
104
+ filename: str | None
105
+ media_type: str
106
+
107
+
108
+ MultipartPart: TypeAlias = MultipartText | MultipartFile
109
+ """One part of a multipart body -- text under a name, or a file. The transport's ``match`` over
110
+ this union is exhaustiveness-checked, so a new part kind cannot be half-wired."""
111
+
112
+
113
+ @dataclass(frozen=True, slots=True)
114
+ class MultipartFiles:
115
+ """One file field carrying several files -- the resolved part for each, under one name.
116
+
117
+ What ``file_part`` returns for a list, and what ``multipart_body`` unfolds into its
118
+ :class:`MultipartFile` members, exactly as it flattens a :class:`Param` into
119
+ :class:`MultipartText` parts. Deliberately **not** a member of :data:`MultipartPart`: a
120
+ transport sees one part per file and never this wrapper, so the wire vocabulary stays two
121
+ classes and no ``match`` over it gains an arm."""
122
+
123
+ parts: Sequence[MultipartFile]
124
+
125
+
126
+ @dataclass(frozen=True, slots=True)
127
+ class MultipartBody:
128
+ """A multipart body: its parts, in the order they go on the wire.
129
+
130
+ One ordered sequence rather than a fields/files mapping pair, because part order is observable
131
+ on the wire and a mapping cannot interleave text parts with file parts."""
132
+
133
+ parts: Sequence[MultipartPart]
134
+
135
+
136
+ @dataclass(frozen=True, slots=True)
137
+ class BinaryBody:
138
+ """A raw request body -- its declared media type, the caller's name for it, and the bytes.
139
+
140
+ ``content`` is typed as the *async* union because ``HttpRequest`` is one shape for both
141
+ flavours; which arms are reachable is fixed by the endpoint's own parameter type, exactly as
142
+ ``custom_http_client`` vs ``custom_async_http_client`` fixes the transport -- and the sync
143
+ transport rejects the two async arms with a guided ``TypeError``.
144
+
145
+ ``filename`` is resolved, in :class:`MultipartFile`'s exact sense: ``None`` means *resolved to
146
+ absent*, never *derive one*. :func:`binary_body` has already applied the same fallback a part
147
+ gets, so the two classes' ``filename`` fields mean the same thing. :func:`content_headers`
148
+ renders it onto the request as ``Content-Disposition``."""
149
+
150
+ content: AsyncBinaryContent
151
+ filename: str | None
152
+ media_type: str
153
+
154
+
155
+ @dataclass(frozen=True, slots=True)
156
+ class TextBody:
157
+ """A body that is one text value, already encoded, under a declared media type and charset.
158
+
159
+ ``charset`` is the spec's own label (``utf-8``, ``iso-8859-1``, ``windows-1252``) and does two
160
+ jobs from one field: the factory encoded the text with it, and :func:`content_headers` writes
161
+ the label ``media_type; charset=<charset>`` onto the request from that same field -- so the bytes
162
+ and the label cannot disagree, and a transport encodes nothing. Text the charset cannot represent
163
+ is refused at the factory, before any request is composed. The factory defaults ``charset`` to
164
+ ``utf-8`` and ``media_type`` to ``text/plain``, and an endpoint writes either only where the spec
165
+ differs -- so the body a transport sees is always explicit."""
166
+
167
+ content: bytes
168
+ media_type: str
169
+ charset: str
170
+
171
+
172
+ RequestBody: TypeAlias = JsonBody[Any] | FormBody | MultipartBody | BinaryBody | TextBody
173
+ """The body an endpoint hands to ``execute`` -- exactly one of the five shapes above.
174
+
175
+ A union rather than one class with five optional fields: of the combinations such a class would
176
+ admit, only five are legal, and the illegal rest had to be rejected at runtime. Here they cannot
177
+ be written down. A transport ``match``es on this to pick its body arguments."""
178
+
179
+
180
+ def content_headers(body: RequestBody | None) -> dict[str, str]:
181
+ """The headers a body carries for itself: its media type, and a raw body's name.
182
+
183
+ Folded **under** the resolved parameter layers by ``BaseRawClient._build_request``, so a caller's
184
+ ``extra_headers`` still wins, and a transport sends ``request.headers`` as given. A multipart body
185
+ contributes nothing: its ``Content-Type`` carries the boundary the encoder mints, so only the
186
+ transport can write it (ADR-0051 keeps httpx's encoder).
187
+
188
+ Args:
189
+ body: The body an endpoint built, or ``None`` for a request that carries none.
190
+
191
+ Returns:
192
+ Lowercased header names to values; empty for no body and for a multipart one."""
193
+ match body:
194
+ case None | MultipartBody():
195
+ return {}
196
+ case JsonBody(_, media_type):
197
+ return {"content-type": media_type}
198
+ case FormBody():
199
+ return {"content-type": "application/x-www-form-urlencoded"}
200
+ case BinaryBody(_, str() as filename, media_type):
201
+ return {"content-type": media_type, "content-disposition": render_content_disposition(filename)}
202
+ case BinaryBody(_, _, media_type):
203
+ return {"content-type": media_type}
204
+ case TextBody(_, media_type, charset):
205
+ return {"content-type": f"{media_type}; charset={charset}"}
206
+
207
+
208
+ def _json_value(adapter: TypeAdapter[T], value: T) -> object:
209
+ # The one validate -> dump -> strip_unset pipeline json_body and json_part share.
210
+ validated = adapter.validate_python(value)
211
+ return strip_unset(validated, adapter.dump_python(validated, mode="json"))
212
+
213
+
214
+ def _derive_filename(content: AsyncBinaryContent) -> str | None:
215
+ # Both destinations' fallback. A Path names its file; a handle may carry a filesystem-shaped
216
+ # ``name`` -- used only when it is a str, since ``open(fd)`` yields an int one. The basename
217
+ # only, so a caller's directories never reach the wire. Otherwise no filename at all: the
218
+ # underlying library's fallback is the literal string "upload", which is worse than omitting an
219
+ # optional parameter (RFC 7578's at a part, RFC 6266's at a raw body).
220
+ if isinstance(content, Path):
221
+ return content.name
222
+ name = getattr(content, "name", None)
223
+ if not isinstance(name, str):
224
+ return None
225
+ return Path(name).name or None
226
+
227
+
228
+ @dataclass(frozen=True, slots=True)
229
+ class _DeclaredJsonBody(Generic[T]):
230
+ """A JSON body's declared type, bound to its adapter; call it with the value and its media type.
231
+
232
+ Built by ``json_body[T]``, the ``_DeclaredParam`` shape: the keyword-only ``media_type`` is
233
+ checked at the call, and a ``Callable`` could spell neither it nor the check. It defaults to
234
+ ``application/json``, so an endpoint writes it only where the operation's key differs -- the
235
+ rule ``param``'s ``serialization_format`` set."""
236
+
237
+ adapter: TypeAdapter[T]
238
+
239
+ def __call__(self, value: T, /, *, media_type: str = "application/json") -> JsonBody[T]:
240
+ return JsonBody(compact_json(_json_value(self.adapter, value), ensure_ascii=False).encode("utf-8"), media_type)
241
+
242
+
243
+ class _JsonBodyFactory(SubscriptOnly):
244
+ """``json_body[T](value)`` -- name the body's declared type in the subscript.
245
+
246
+ **A subscript and then a call, and the split is the whole point.** ``T`` is solved from the
247
+ subscript alone, *before* the value is compared against it, which is what makes a generator
248
+ emitting the wrong type a build failure: ``json_body[int](employee)`` is rejected with
249
+ ``Argument 1 has incompatible type "Employee | EmployeeDict"; expected "int"``. Written as one
250
+ call it could not be: ``json_body(employee, int)`` makes mypy solve ``T`` from *both*
251
+ arguments and join them to ``object``, so every mismatch type-checks -- measured, along with
252
+ the other shapes that fail the same way, in ADR-0013 (whose curried spelling this subscript
253
+ replaced; the re-measurement is in docs/plans/subscripted-typed-factories.md).
254
+
255
+ The factory declares no ``__call__``, so *omitting* the declared type is a build failure too:
256
+ ``json_body(employee)`` is rejected statically (``[operator]``) -- the guarantee the curried
257
+ signature used to carry. The runtime ``__call__`` inherited from :class:`SubscriptOnly` is
258
+ invisible to type checkers and
259
+ exists only to turn that same mistake into a guided ``TypeError``.
260
+
261
+ The subscript is a ``TypeForm``, exactly as in ``json_decoder[...]``, so a runtime union alias
262
+ (``Person``, which is ``Employee | Boss``) binds ``T`` as precisely as a concrete class does.
263
+
264
+ The subscript is the endpoint's declared parameter type **in full**, dict-shaped companion
265
+ included: ``json_body[Employee | EmployeeDict]``. The accepted value is then exactly ``T``, so
266
+ there is no permissive mapping half for a stray ``dict[str, Any]`` to slip through -- and only
267
+ the model arm reaches :func:`adapter_for`, via :func:`validation_target`, because a companion
268
+ is an input shape and never a wire shape.
269
+
270
+ Handles scalars, dicts, lists and nested structures, and honours ``Annotated`` serializers
271
+ (``PlainSerializer`` and friends) because serialization goes through the type's own adapter.
272
+ Validation runs before the dump, so dict-shaped input is coerced into the model through its own
273
+ validation; a model instance passes through unchanged (``revalidate_instances`` defaults to
274
+ ``"never"``), making this byte-identical for non-dict values. ``strip_unset`` then distinguishes
275
+ a field the caller never touched (``OptionalNullable[...] = UNSET``, omitted) from one set
276
+ explicitly to ``None`` (kept as null) -- see docs/designs/optional-nullable-fields.md.
277
+
278
+ ``media_type`` is the operation's content key -- ``application/json`` by default, and written at
279
+ the emission site only where the key differs: a ``+json`` vendor type is the same wire shape under
280
+ its own label, only the endpoint knows which, and the transport carries whichever the body holds
281
+ (ADR-0048)."""
282
+
283
+ factory_name = "json_body"
284
+ spelling = "json_body[T](value), e.g. json_body[Employee | EmployeeDict](model)"
285
+
286
+ def __getitem__(self, declared: TypeForm[T]) -> _DeclaredJsonBody[T]:
287
+ return _DeclaredJsonBody(adapter_for(validation_target(declared)))
288
+
289
+
290
+ json_body: Final = _JsonBodyFactory()
291
+
292
+
293
+ def form_body(*params: Param[Any]) -> FormBody:
294
+ """Render ``params`` into a url-encoded form body.
295
+
296
+ Args:
297
+ params: The form parameters, in wire order, each already carrying its declared type's adapter.
298
+
299
+ Returns:
300
+ A :class:`FormBody` holding the wire bytes.
301
+
302
+ Raises:
303
+ ValueError: If a parameter's value fails validation against its declared type."""
304
+ return FormBody(urlencode_params(params).encode("utf-8"))
305
+
306
+
307
+ def multipart_body(*parts: Param[Any] | MultipartPart | MultipartFiles | None) -> MultipartBody:
308
+ """Build a multipart body from parameters and parts, in the order written.
309
+
310
+ Variadic, like ``AllSchemes``: the parts *are* the argument list, so an emission site nests no
311
+ list literal. A field that is an array of files is still one argument --
312
+ ``file_part("files", files)`` -- and is unfolded here, one part per file, so no emission site
313
+ loops or splats.
314
+
315
+ A :class:`Param` is flattened exactly as a form field is -- so an array explodes and a map
316
+ becomes bracketed keys -- and each resulting pair becomes one text part. A
317
+ :class:`MultipartFiles` contributes each of its files as its own part. A part built by
318
+ :func:`file_part` for one file, or by ``json_part[...]``, passes through untouched. ``None`` is
319
+ an optional part the caller omitted and contributes nothing -- the part counterpart of a
320
+ ``param`` whose value is ``None``, spelled ``file_part(...) if x is not None else None`` at the
321
+ emission site, for one file and for a list alike.
322
+
323
+ Args:
324
+ parts: Parameters and prebuilt parts, in wire order.
325
+
326
+ Returns:
327
+ A :class:`MultipartBody` holding one part per flattened pair, per file, or per prebuilt part.
328
+
329
+ Raises:
330
+ ValueError: If a parameter's value fails validation against its declared type."""
331
+ resolved: list[MultipartPart] = []
332
+ for part in parts:
333
+ match part:
334
+ case None:
335
+ pass
336
+ case Param():
337
+ resolved.extend(MultipartText(key, text.encode("utf-8")) for key, text in flatten([part]))
338
+ case MultipartFiles(files):
339
+ resolved.extend(files)
340
+ case MultipartText() | MultipartFile():
341
+ resolved.append(part)
342
+ case _:
343
+ assert_never(part)
344
+ return MultipartBody(resolved)
345
+
346
+
347
+ def _resolve_file(name: str, value: AsyncFileInput, media_type: str) -> MultipartFile:
348
+ """One file's part, its filename and media type resolved through the two tiers.
349
+
350
+ The override first, then the operation's declared value -- the rule :func:`binary_body` applies
351
+ to the one of the two fields a raw body can carry.
352
+
353
+ Lifted out of :func:`file_part` rather than reached by recursion through its own overloads: the
354
+ list arm needs a :class:`MultipartFile` per item, and saying so beats resolving an overload
355
+ against the function being defined.
356
+
357
+ Args:
358
+ name: The form field name the part is sent under.
359
+ value: One file's content, or a ``NamedFile`` overriding its filename or media type.
360
+ media_type: The media type the operation declares for this field.
361
+
362
+ Returns:
363
+ A :class:`MultipartFile` with no unresolved field left."""
364
+ match value:
365
+ case NamedFile(content, filename, override):
366
+ return MultipartFile(
367
+ name=name,
368
+ content=content,
369
+ filename=filename if filename is not None else _derive_filename(content),
370
+ media_type=override if override is not None else media_type,
371
+ )
372
+ case _:
373
+ return MultipartFile(name=name, content=value, filename=_derive_filename(value), media_type=media_type)
374
+
375
+
376
+ @overload
377
+ def file_part(
378
+ name: str, value: Sequence[AsyncFileInput], *, media_type: str = "application/octet-stream"
379
+ ) -> MultipartFiles: ...
380
+
381
+
382
+ @overload
383
+ def file_part(name: str, value: AsyncFileInput, *, media_type: str = "application/octet-stream") -> MultipartFile: ...
384
+
385
+
386
+ def file_part(
387
+ name: str,
388
+ value: AsyncFileInput | Sequence[AsyncFileInput],
389
+ *,
390
+ media_type: str = "application/octet-stream",
391
+ ) -> MultipartFile | MultipartFiles:
392
+ """One file part -- or one part per file of a list -- under ``name``; the annotation decides which.
393
+
394
+ A binary field is emitted the same way whether its schema is one file or an array of them:
395
+ ``file_part(name, value, media_type=...)`` with the parameter as declared, ``FileInput`` or
396
+ ``list[FileInput]``. A list yields a :class:`MultipartFiles` that ``multipart_body`` unfolds
397
+ into one :class:`MultipartFile` per item under the same name (RFC 7578's repeated field), each
398
+ resolved exactly as a single file is -- so an item may be a ``NamedFile`` overriding its own
399
+ filename or media type.
400
+
401
+ ``media_type`` is the operation's declared one, defaulting to ``application/octet-stream`` -- RFC
402
+ 9110 8.3's answer for a payload of unknown type, and OpenAPI's own default for a binary part that
403
+ declares none. An endpoint writes it only where the spec differs (ADR-0048), and a ``NamedFile``
404
+ overrides whatever it is, per item for a list.
405
+
406
+ **The filename is never read as a media type.** An extension is a claim the caller typed, not
407
+ evidence about the bytes -- ``report.pdf`` may hold a PNG -- and checking would mean opening the
408
+ content here, which nothing in this module does. ``application/octet-stream`` is what the SDK
409
+ actually knows; a caller who knows better says so through ``NamedFile(content, media_type=...)``.
410
+
411
+ **A list is always many files, and nothing has to arbitrate that.** One file's content streams
412
+ as an ``Iterator[bytes]``, never a ``Sequence`` of them, so a ``list[bytes]`` satisfies only the
413
+ first overload and the two arms do not overlap. A chunk *list* that is one file's content says
414
+ so by handing over an iterator -- ``NamedFile(iter([b"a", b"b"]))`` -- and that is the whole of
415
+ the rule (ADR-0049).
416
+
417
+ The runtime agrees by a different route: PEP 634 defines a sequence pattern to exclude ``str``,
418
+ ``bytes`` and ``bytearray``, the one place the language already knows a byte string is content
419
+ rather than a collection, and an iterator is no ``Sequence`` at either level.
420
+ ``isinstance(value, Sequence)`` would be true of ``bytes`` and send each byte as its own part.
421
+
422
+ The overloads earn their keep beyond the return type: they give a heterogeneous list literal --
423
+ ``[b"one", NamedFile(b"two", filename="2.bin")]`` -- an element type to check against. Collapsed
424
+ into one signature over a union, that literal joins to ``list[object]`` and is rejected.
425
+
426
+ Nothing is opened or read here -- the content descriptor is carried as given, and the first read
427
+ happens in the transport when the request is sent.
428
+
429
+ A part with no filename reads as a form *field* rather than a file to many servers (measured:
430
+ ASP.NET's ``IFormFile`` binding requires one). Bare ``bytes`` and a nameless reader derive no
431
+ filename -- deliberately, since the underlying library's fallback is the literal string
432
+ ``"upload"`` -- so name them through ``NamedFile(content, filename=...)`` when the operation
433
+ expects a file.
434
+
435
+ Args:
436
+ name: The form field name this part -- or every part of the list -- is sent under.
437
+ value: One file's content, a ``NamedFile`` overriding its filename or media type, or a
438
+ list of either.
439
+ media_type: The media type the operation declares for this field;
440
+ ``application/octet-stream`` when omitted.
441
+
442
+ Returns:
443
+ A part carrying the content descriptor with its filename and media type resolved -- or, for
444
+ a list, the resolved part for each item."""
445
+ match value:
446
+ case [*items]:
447
+ return MultipartFiles(tuple(_resolve_file(name, item, media_type) for item in items))
448
+ case _:
449
+ return _resolve_file(name, value, media_type)
450
+
451
+
452
+ @dataclass(frozen=True, slots=True)
453
+ class _DeclaredJsonPart(Generic[T]):
454
+ """A JSON part's declared type, bound to its adapter; call it with the name, value and media type.
455
+
456
+ Built by ``json_part[T]``, the ``_DeclaredParam`` shape, for the reason ``_DeclaredJsonBody``
457
+ gives; ``media_type`` defaults to ``application/json`` for the same reason."""
458
+
459
+ adapter: TypeAdapter[T]
460
+
461
+ def __call__(self, name: str, value: T, /, *, media_type: str = "application/json") -> MultipartText:
462
+ return MultipartText(name, compact_json(_json_value(self.adapter, value)).encode("utf-8"), media_type)
463
+
464
+
465
+ class _JsonPartFactory(SubscriptOnly):
466
+ """``json_part[T](name, value)`` -- a multipart part whose body is JSON, not a form field.
467
+
468
+ Subscripted for the reason ``json_body`` documents: ``T`` is solved from the subscript alone,
469
+ so a mismatched emission is a build failure. The declared type is the endpoint's in full, dict
470
+ companion included, and only the model arm reaches the adapter.
471
+
472
+ This is the part spelling ``(None, json, media_type)`` that the field hand-rolls at the call
473
+ site, under the media type the operation's encoding object declares for the part --
474
+ ``application/json`` by default, written at the emission site only where the encoding object
475
+ says otherwise (ADR-0048). Declaring it is what makes a map-as-JSON-part and a
476
+ map-as-form-fields two different factories rather than one factory and a flag."""
477
+
478
+ factory_name = "json_part"
479
+ spelling = 'json_part[T](name, value), e.g. json_part[FileMetadata | FileMetadataDict]("metadata", metadata)'
480
+
481
+ def __getitem__(self, declared: TypeForm[T]) -> _DeclaredJsonPart[T]:
482
+ return _DeclaredJsonPart(adapter_for(validation_target(declared)))
483
+
484
+
485
+ json_part: Final = _JsonPartFactory()
486
+
487
+
488
+ def binary_body(content: AsyncFileInput, *, media_type: str = "application/octet-stream") -> BinaryBody:
489
+ """A raw request body under the operation's declared media type.
490
+
491
+ ``media_type`` is the operation's ``requestBody`` content key, defaulting to RFC 9110 8.3's own
492
+ answer for a payload of unknown type -- so an endpoint writes it only where the key differs
493
+ (``image/png``), the rule every other body factory follows (ADR-0048).
494
+
495
+ A ``NamedFile`` here contributes both of its overrides, and both resolve through the two tiers
496
+ :func:`file_part` applies -- the override, then the SDK's own answer. ``media_type`` falls back
497
+ to the operation's declared value; ``filename`` falls back to :func:`_derive_filename`, so a
498
+ ``Path`` or a named handle is sent under its name and a nameless source under none at all
499
+ (ADR-0053, amended). :func:`content_headers` renders it onto the request as a
500
+ ``Content-Disposition``.
501
+
502
+ Only the basename is sent, so a caller's directories never reach the wire.
503
+
504
+ Nothing is opened or read here -- the content descriptor is carried as given, and the first
505
+ read happens in the transport when the request is sent.
506
+
507
+ Args:
508
+ content: The body content descriptor, or a ``NamedFile`` overriding its media type or
509
+ naming it.
510
+ media_type: The media type the operation declares for the body;
511
+ ``application/octet-stream`` when omitted.
512
+
513
+ Returns:
514
+ A :class:`BinaryBody` carrying the content, its resolved name and its resolved media
515
+ type."""
516
+ match content:
517
+ case NamedFile(inner, filename, override):
518
+ return BinaryBody(
519
+ inner,
520
+ filename if filename is not None else _derive_filename(inner),
521
+ override if override is not None else media_type,
522
+ )
523
+ case _:
524
+ return BinaryBody(content, _derive_filename(content), media_type)
525
+
526
+
527
+ @dataclass(frozen=True, slots=True)
528
+ class _DeclaredTextBody(Generic[T]):
529
+ """A text body's declared type, bound to its adapter; call it with the value and its wire facts.
530
+
531
+ Built by ``text_body[T]`` exactly as ``_DeclaredParam`` is by ``param[T]``: a frozen dataclass
532
+ with a typed ``__call__``, so the keyword-only arguments are checked at the call without a
533
+ callback Protocol -- which would be the one Protocol in the package not standing for an open
534
+ seam (ADR-0041)."""
535
+
536
+ adapter: TypeAdapter[T]
537
+
538
+ def __call__(self, value: T, /, *, media_type: str = "text/plain", charset: str = "utf-8") -> TextBody:
539
+ """Validate and dump ``value`` through the declared type, under the operation's wire facts.
540
+
541
+ Args:
542
+ value: The body value, of the declared type.
543
+ media_type: The operation's content key, less its ``charset`` parameter; ``text/plain``
544
+ when omitted.
545
+ charset: That ``charset`` parameter -- the spec's label, which the codec registry
546
+ resolves as written; ``utf-8`` when omitted.
547
+
548
+ Returns:
549
+ A :class:`TextBody` carrying the dumped text encoded with ``charset``, and both facts unchanged.
550
+
551
+ Raises:
552
+ ValueError: If ``charset`` names no codec, ``value`` fails validation, or the text holds a
553
+ character ``charset`` cannot represent (``UnicodeEncodeError``, a ``ValueError``)."""
554
+ try:
555
+ codecs.lookup(charset)
556
+ except LookupError as error:
557
+ # ``str.encode`` would report this as LookupError, outside the ValueError contract every
558
+ # other rejection here honours.
559
+ raise ValueError(f"Unknown charset for a text body: {charset!r}") from error
560
+ validated = self.adapter.validate_python(value)
561
+ dumped = self.adapter.dump_python(validated, mode="json")
562
+ text = dumped if isinstance(dumped, str) else to_text(dumped)
563
+ return TextBody(text.encode(charset), media_type, charset)
564
+
565
+
566
+ class _TextBodyFactory(SubscriptOnly):
567
+ """``text_body[T](value)`` -- a text body whose *type* picks the wire text and whose *charset*
568
+ picks the bytes.
569
+
570
+ ``text_body[Base64AsciiEncodedBytes](data)`` is what carries an operation whose whole body is a
571
+ base64 string: validation passes the raw bytes through, the dump encodes -- the encoding is the
572
+ declared type's own dump, not a call at the emission site. It is fully resident by construction,
573
+ and the peak is about **3.7x** the payload: the caller's ``bytes``, the base64 ``str`` at 1.33x,
574
+ and the factory's ``encode(charset)`` of that at 1.33x again. Base64 has no streaming path here
575
+ -- the parameter is ``bytes``, so a caller cannot hand over a stream to begin with. It *is*
576
+ chunkable at any multiple of three bytes should a spec ever need it; the arm is absent rather
577
+ than impossible (ADR-0051 decision 6). The two
578
+ keywords come from the operation's content key, split once at emission: the type and any other
579
+ parameters in ``media_type``, the ``charset`` parameter on its own -- and each is written only
580
+ where the spec differs from the default (``text/plain``, ``utf-8``), the rule ``param``'s
581
+ ``serialization_format`` set."""
582
+
583
+ factory_name = "text_body"
584
+ spelling = "text_body[T](value), e.g. text_body[Base64AsciiEncodedBytes](data)"
585
+
586
+ def __getitem__(self, declared: TypeForm[T]) -> _DeclaredTextBody[T]:
587
+ return _DeclaredTextBody(adapter_for(validation_target(declared)))
588
+
589
+
590
+ text_body: Final = _TextBodyFactory()
@@ -0,0 +1,68 @@
1
+ """Wire-format converters a generated model names in its annotations.
2
+
3
+ A datetime/date field names one of the four ``Annotated`` aliases -- ``RFC3339DateTime``,
4
+ ``RFC1123DateTime``, ``UnixSecondsDateTime``, ``Date``. A binary value declares two facts instead of
5
+ one, its RFC 4648 alphabet and the charset that renders it: a **model property** states both as
6
+ arguments to :func:`ByteField`, beside its wire key, while an **endpoint** names one of the twenty
7
+ ``Base64AsciiEncodedBytes``-style aliases in a subscript, as every other parameter does. An open enum
8
+ field pairs its type with :func:`open_enum_validator`. The bare parse/dump pairs that build the
9
+ aliases stay internal to their modules."""
10
+
11
+ from .date_time import Date, RFC1123DateTime, RFC3339DateTime, UnixSecondsDateTime
12
+ from .encoded_bytes import (
13
+ Base16AsciiEncodedBytes,
14
+ Base16Cp1252EncodedBytes,
15
+ Base16Latin1EncodedBytes,
16
+ Base16Utf8EncodedBytes,
17
+ Base32AsciiEncodedBytes,
18
+ Base32Cp1252EncodedBytes,
19
+ Base32HexAsciiEncodedBytes,
20
+ Base32HexCp1252EncodedBytes,
21
+ Base32HexLatin1EncodedBytes,
22
+ Base32HexUtf8EncodedBytes,
23
+ Base32Latin1EncodedBytes,
24
+ Base32Utf8EncodedBytes,
25
+ Base64AsciiEncodedBytes,
26
+ Base64Cp1252EncodedBytes,
27
+ Base64Latin1EncodedBytes,
28
+ Base64UrlAsciiEncodedBytes,
29
+ Base64UrlCp1252EncodedBytes,
30
+ Base64UrlLatin1EncodedBytes,
31
+ Base64UrlUtf8EncodedBytes,
32
+ Base64Utf8EncodedBytes,
33
+ ByteField,
34
+ )
35
+ from .open_enum import open_enum_validator
36
+
37
+ __all__ = [
38
+ # Wire-format datetime/date aliases
39
+ "Date",
40
+ "RFC3339DateTime",
41
+ "RFC1123DateTime",
42
+ "UnixSecondsDateTime",
43
+ # Wire-format binary aliases -- one per (RFC 4648 alphabet, charset) pair
44
+ "Base64AsciiEncodedBytes",
45
+ "Base64Utf8EncodedBytes",
46
+ "Base64Latin1EncodedBytes",
47
+ "Base64Cp1252EncodedBytes",
48
+ "Base64UrlAsciiEncodedBytes",
49
+ "Base64UrlUtf8EncodedBytes",
50
+ "Base64UrlLatin1EncodedBytes",
51
+ "Base64UrlCp1252EncodedBytes",
52
+ "Base32AsciiEncodedBytes",
53
+ "Base32Utf8EncodedBytes",
54
+ "Base32Latin1EncodedBytes",
55
+ "Base32Cp1252EncodedBytes",
56
+ "Base32HexAsciiEncodedBytes",
57
+ "Base32HexUtf8EncodedBytes",
58
+ "Base32HexLatin1EncodedBytes",
59
+ "Base32HexCp1252EncodedBytes",
60
+ "Base16AsciiEncodedBytes",
61
+ "Base16Utf8EncodedBytes",
62
+ "Base16Latin1EncodedBytes",
63
+ "Base16Cp1252EncodedBytes",
64
+ # Binary property declaration
65
+ "ByteField",
66
+ # Open enums
67
+ "open_enum_validator",
68
+ ]