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.
- cliv1-0.0.8.dist-info/METADATA +168 -0
- cliv1-0.0.8.dist-info/RECORD +87 -0
- cliv1-0.0.8.dist-info/WHEEL +4 -0
- cliv1-0.0.8.dist-info/licenses/LICENSE +21 -0
- swagger_petstore_open_api_3_0/__init__.py +15 -0
- swagger_petstore_open_api_3_0/apis/__init__.py +5 -0
- swagger_petstore_open_api_3_0/apis/pet_api.py +883 -0
- swagger_petstore_open_api_3_0/apis/store.py +413 -0
- swagger_petstore_open_api_3_0/apis/user_api.py +794 -0
- swagger_petstore_open_api_3_0/async_client.py +79 -0
- swagger_petstore_open_api_3_0/auth.py +17 -0
- swagger_petstore_open_api_3_0/base_client.py +18 -0
- swagger_petstore_open_api_3_0/client.py +77 -0
- swagger_petstore_open_api_3_0/core/__init__.py +358 -0
- swagger_petstore_open_api_3_0/core/_internal/__init__.py +9 -0
- swagger_petstore_open_api_3_0/core/_internal/content_disposition.py +154 -0
- swagger_petstore_open_api_3_0/core/_internal/flattening.py +139 -0
- swagger_petstore_open_api_3_0/core/_internal/headers.py +144 -0
- swagger_petstore_open_api_3_0/core/_internal/sse_parser.py +124 -0
- swagger_petstore_open_api_3_0/core/_internal/subscripts.py +50 -0
- swagger_petstore_open_api_3_0/core/_internal/urls.py +186 -0
- swagger_petstore_open_api_3_0/core/_internal/wire.py +198 -0
- swagger_petstore_open_api_3_0/core/adapters.py +177 -0
- swagger_petstore_open_api_3_0/core/auth/__init__.py +70 -0
- swagger_petstore_open_api_3_0/core/auth/composition.py +175 -0
- swagger_petstore_open_api_3_0/core/auth/models.py +216 -0
- swagger_petstore_open_api_3_0/core/auth/protocols.py +103 -0
- swagger_petstore_open_api_3_0/core/auth/schemes/__init__.py +94 -0
- swagger_petstore_open_api_3_0/core/auth/schemes/api_key_auth.py +53 -0
- swagger_petstore_open_api_3_0/core/auth/schemes/basic_auth.py +73 -0
- swagger_petstore_open_api_3_0/core/auth/schemes/bearer_auth.py +21 -0
- swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_authorization_code.py +393 -0
- swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_client_credentials.py +127 -0
- swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_password.py +142 -0
- swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_schemes.py +300 -0
- swagger_petstore_open_api_3_0/core/base_raw_response.py +49 -0
- swagger_petstore_open_api_3_0/core/bodies.py +550 -0
- swagger_petstore_open_api_3_0/core/converters/__init__.py +68 -0
- swagger_petstore_open_api_3_0/core/converters/date_time.py +237 -0
- swagger_petstore_open_api_3_0/core/converters/encoded_bytes.py +273 -0
- swagger_petstore_open_api_3_0/core/converters/open_enum.py +35 -0
- swagger_petstore_open_api_3_0/core/decoding.py +498 -0
- swagger_petstore_open_api_3_0/core/discriminators.py +129 -0
- swagger_petstore_open_api_3_0/core/event_streams.py +225 -0
- swagger_petstore_open_api_3_0/core/exceptions.py +38 -0
- swagger_petstore_open_api_3_0/core/file_responses.py +335 -0
- swagger_petstore_open_api_3_0/core/files.py +173 -0
- swagger_petstore_open_api_3_0/core/httpx_transport.py +748 -0
- swagger_petstore_open_api_3_0/core/models.py +143 -0
- swagger_petstore_open_api_3_0/core/optionality.py +130 -0
- swagger_petstore_open_api_3_0/core/params.py +157 -0
- swagger_petstore_open_api_3_0/core/raw_client.py +338 -0
- swagger_petstore_open_api_3_0/core/request_options.py +94 -0
- swagger_petstore_open_api_3_0/core/results.py +123 -0
- swagger_petstore_open_api_3_0/core/runtime_env.py +43 -0
- swagger_petstore_open_api_3_0/core/servers.py +33 -0
- swagger_petstore_open_api_3_0/core/transport.py +191 -0
- swagger_petstore_open_api_3_0/errors/__init__.py +48 -0
- swagger_petstore_open_api_3_0/errors/add_pet_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/delete_order_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/delete_pet_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/delete_user_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/find_pets_by_status_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/find_pets_by_tags_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/get_order_by_id_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/get_pet_by_id_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/get_user_by_name_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/login_user_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/place_order_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/update_pet_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/update_pet_with_form_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/update_user_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/upload_file_error.py +21 -0
- swagger_petstore_open_api_3_0/models/__init__.py +23 -0
- swagger_petstore_open_api_3_0/models/api_response.py +18 -0
- swagger_petstore_open_api_3_0/models/category.py +15 -0
- swagger_petstore_open_api_3_0/models/enums/__init__.py +4 -0
- swagger_petstore_open_api_3_0/models/enums/order_status.py +17 -0
- swagger_petstore_open_api_3_0/models/enums/pet_status.py +17 -0
- swagger_petstore_open_api_3_0/models/order.py +27 -0
- swagger_petstore_open_api_3_0/models/pet.py +28 -0
- swagger_petstore_open_api_3_0/models/tag_model.py +15 -0
- swagger_petstore_open_api_3_0/models/user.py +29 -0
- swagger_petstore_open_api_3_0/py.typed +0 -0
- swagger_petstore_open_api_3_0/server/__init__.py +21 -0
- swagger_petstore_open_api_3_0/server/server.py +17 -0
- 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
|
+
]
|