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