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,237 @@
|
|
|
1
|
+
"""Wire formats for temporal values.
|
|
2
|
+
|
|
3
|
+
A :class:`DateTimeConverter` is a parse/dump pair for one wire format. Four are defined here --
|
|
4
|
+
``RFC3339``, ``RFC1123``, ``UNIX_SECONDS`` and ``DATE`` -- each paired with an ``Annotated`` alias
|
|
5
|
+
(``RFC3339DateTime``, ``Date``, …) that wires it into pydantic so a field or parameter declares its
|
|
6
|
+
wire format by type alone. The aliases are the whole public surface: every value reaching the wire
|
|
7
|
+
is dumped through its declared type's adapter, so the bare converters are only the vehicle that
|
|
8
|
+
builds them and are not exported above this package.
|
|
9
|
+
|
|
10
|
+
Datetimes are required to be timezone-aware in every format that encodes an instant. A naive
|
|
11
|
+
datetime has no single correct wire representation, so it is rejected rather than silently assumed
|
|
12
|
+
to be UTC or local.
|
|
13
|
+
|
|
14
|
+
Every rejection raises ``ValueError``, never ``TypeError`` -- including rejections that are *about* a
|
|
15
|
+
wrong type. Pydantic wraps ``ValueError`` from a validator into ``ValidationError`` but lets
|
|
16
|
+
``TypeError`` escape as a programming error, so a converter that raised ``TypeError`` for unexpected
|
|
17
|
+
wire data would surface a bare ``TypeError`` from deserialization instead of the ``ValidationError``
|
|
18
|
+
callers catch. Bad wire data is a validation failure, not a bug in the caller's code."""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import math
|
|
23
|
+
import re
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from datetime import date, datetime, timedelta, timezone
|
|
27
|
+
from email.utils import format_datetime, parsedate_to_datetime
|
|
28
|
+
from typing import Annotated, Final, Generic, TypeAlias, TypeVar
|
|
29
|
+
|
|
30
|
+
from pydantic.functional_serializers import PlainSerializer
|
|
31
|
+
from pydantic.functional_validators import BeforeValidator
|
|
32
|
+
|
|
33
|
+
# Bounded rather than constrained to (date, datetime): a datetime *is* a date, so the bound
|
|
34
|
+
# admits both while keeping one type parameter.
|
|
35
|
+
T = TypeVar("T", bound=date)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class DateTimeConverter(Generic[T]):
|
|
40
|
+
"""How one temporal wire format is read and written.
|
|
41
|
+
|
|
42
|
+
``parse`` accepts whatever arrived on the wire -- hence ``object``, narrowed inside each
|
|
43
|
+
implementation -- and ``dump`` produces the JSON scalar the format calls for."""
|
|
44
|
+
|
|
45
|
+
parse: Callable[[object], T]
|
|
46
|
+
dump: Callable[[T], str | int]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _require_tzaware(dt: datetime, *, name: str) -> datetime:
|
|
50
|
+
if dt.tzinfo is None or dt.utcoffset() is None:
|
|
51
|
+
raise ValueError(f"{name}: datetime must be timezone-aware (include UTC offset).")
|
|
52
|
+
return dt
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Explicit [0-9] rather than \d, which also matches non-ASCII decimal digits: fromisoformat
|
|
56
|
+
# rejects those anyway, and reshaping a string that is going to be refused only obscures it.
|
|
57
|
+
_SUBSECOND: Final = re.compile(r"\.([0-9]+)")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_rfc3339(value: object) -> datetime:
|
|
61
|
+
if isinstance(value, datetime):
|
|
62
|
+
return _require_tzaware(value, name="RFC3339")
|
|
63
|
+
|
|
64
|
+
if not isinstance(value, str):
|
|
65
|
+
raise ValueError("RFC3339: expected RFC3339/ISO8601 string or tz-aware datetime.")
|
|
66
|
+
|
|
67
|
+
s = value.strip()
|
|
68
|
+
if not s:
|
|
69
|
+
raise ValueError("RFC3339: datetime string cannot be empty.")
|
|
70
|
+
|
|
71
|
+
# Two normalizations, one reason: what fromisoformat accepts is narrower than RFC3339 on the
|
|
72
|
+
# supported floor, and neither gap may be left to vary by interpreter. 'Z' is RFC3339's UTC
|
|
73
|
+
# designator, rejected before 3.11. A fractional second may carry any number of digits
|
|
74
|
+
# (`time-secfrac = "." 1*DIGIT`), where 3.10 accepts exactly three or six -- padding and
|
|
75
|
+
# truncating to six converges on what 3.11+ does natively. Normalizing on every version rather
|
|
76
|
+
# than behind a sys.version_info gate keeps one code path everywhere; a gated branch would
|
|
77
|
+
# also be invisible to mypy, which prunes it at the pinned floor. Both go at the 3.11 floor.
|
|
78
|
+
if s.endswith(("Z", "z")):
|
|
79
|
+
s = s[:-1] + "+00:00"
|
|
80
|
+
s = _SUBSECOND.sub(lambda match: "." + match[1][:6].ljust(6, "0"), s, count=1)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
dt = datetime.fromisoformat(s)
|
|
84
|
+
except ValueError as e:
|
|
85
|
+
raise ValueError(f"RFC3339: invalid RFC3339/ISO8601 datetime: {value!r}") from e
|
|
86
|
+
|
|
87
|
+
return _require_tzaware(dt, name="RFC3339")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _dump_rfc3339(dt: datetime) -> str:
|
|
91
|
+
dt = _require_tzaware(dt, name="RFC3339")
|
|
92
|
+
# Canonicalize UTC -> 'Z'. Compare the offset to zero directly; asking
|
|
93
|
+
# timezone.utc for its offset just to compare against it is a detour.
|
|
94
|
+
if dt.utcoffset() == timedelta(0):
|
|
95
|
+
return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
96
|
+
return dt.isoformat()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
RFC3339: Final = DateTimeConverter(parse=_parse_rfc3339, dump=_dump_rfc3339)
|
|
100
|
+
|
|
101
|
+
RFC3339DateTime: TypeAlias = Annotated[
|
|
102
|
+
datetime,
|
|
103
|
+
BeforeValidator(RFC3339.parse),
|
|
104
|
+
PlainSerializer(RFC3339.dump, return_type=str),
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _parse_rfc1123(value: object) -> datetime:
|
|
109
|
+
if isinstance(value, datetime):
|
|
110
|
+
return _require_tzaware(value, name="RFC1123")
|
|
111
|
+
|
|
112
|
+
if isinstance(value, str):
|
|
113
|
+
# parsedate_to_datetime raises ValueError for an unparseable string but OverflowError for
|
|
114
|
+
# a year too large for C long. Both are wire data, so both become this module's ValueError.
|
|
115
|
+
try:
|
|
116
|
+
dt = parsedate_to_datetime(value)
|
|
117
|
+
except (ValueError, OverflowError) as e:
|
|
118
|
+
raise ValueError(f"RFC1123: invalid RFC1123 datetime: {value!r}") from e
|
|
119
|
+
return _require_tzaware(dt, name="RFC1123")
|
|
120
|
+
|
|
121
|
+
raise ValueError("RFC1123: expected RFC1123 string or tz-aware datetime.")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _dump_rfc1123(dt: datetime) -> str:
|
|
125
|
+
dt = _require_tzaware(dt, name="RFC1123").astimezone(timezone.utc)
|
|
126
|
+
return format_datetime(dt, usegmt=True)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
RFC1123: Final = DateTimeConverter(parse=_parse_rfc1123, dump=_dump_rfc1123)
|
|
130
|
+
|
|
131
|
+
RFC1123DateTime: TypeAlias = Annotated[
|
|
132
|
+
datetime,
|
|
133
|
+
BeforeValidator(RFC1123.parse),
|
|
134
|
+
PlainSerializer(RFC1123.dump, return_type=str),
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _parse_unix_seconds(value: object) -> datetime:
|
|
139
|
+
if isinstance(value, datetime):
|
|
140
|
+
return _require_tzaware(value, name="UNIX_SECONDS")
|
|
141
|
+
|
|
142
|
+
# bool is a subclass of int, so it must be rejected before the numeric branch --
|
|
143
|
+
# otherwise True would quietly parse as epoch second 1.
|
|
144
|
+
if isinstance(value, bool):
|
|
145
|
+
raise ValueError("UNIX_SECONDS: expected unix seconds, not a bool.")
|
|
146
|
+
|
|
147
|
+
# float() is the gate, so a quoted number is accepted exactly where the unquoted number is and
|
|
148
|
+
# the two can never diverge. It also handles sign and surrounding whitespace, which isdigit()
|
|
149
|
+
# did not. Every epoch a datetime can hold (< 2.6e11) is far inside float's exact-integer
|
|
150
|
+
# range, so routing ints through the same path loses nothing.
|
|
151
|
+
if isinstance(value, (int, float)):
|
|
152
|
+
seconds: float = value
|
|
153
|
+
elif isinstance(value, str):
|
|
154
|
+
try:
|
|
155
|
+
seconds = float(value)
|
|
156
|
+
except ValueError:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"UNIX_SECONDS: expected unix seconds (int/float/numeric string) or tz-aware datetime, got {value!r}"
|
|
159
|
+
) from None
|
|
160
|
+
else:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
"UNIX_SECONDS: expected unix seconds (int/float/numeric string) "
|
|
163
|
+
f"or tz-aware datetime, got {type(value).__name__}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# fromtimestamp has three failure modes and only one is a ValueError: NaN raises ValueError,
|
|
167
|
+
# a value beyond time_t raises OverflowError, and one representable in time_t but past
|
|
168
|
+
# datetime.max raises OSError. A milliseconds-for-seconds mixup lands on the latter two.
|
|
169
|
+
try:
|
|
170
|
+
return datetime.fromtimestamp(seconds, tz=timezone.utc)
|
|
171
|
+
except (ValueError, OverflowError, OSError) as e:
|
|
172
|
+
raise ValueError(f"UNIX_SECONDS: not a representable unix-seconds value: {value!r}") from e
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _dump_unix_seconds(dt: datetime) -> int:
|
|
176
|
+
dt = _require_tzaware(dt, name="UNIX_SECONDS").astimezone(timezone.utc)
|
|
177
|
+
# floor, not int(): int() truncates toward zero, which floors positive timestamps but
|
|
178
|
+
# *ceilings* negative ones -- -0.7 would dump as 0, a wire value after the instant encoded.
|
|
179
|
+
return math.floor(dt.timestamp())
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
UNIX_SECONDS: Final = DateTimeConverter(parse=_parse_unix_seconds, dump=_dump_unix_seconds)
|
|
183
|
+
|
|
184
|
+
UnixSecondsDateTime: TypeAlias = Annotated[
|
|
185
|
+
datetime,
|
|
186
|
+
BeforeValidator(UNIX_SECONDS.parse),
|
|
187
|
+
PlainSerializer(UNIX_SECONDS.dump, return_type=int),
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _parse_date(value: object) -> date:
|
|
192
|
+
"""Parse a calendar date from ISO-8601 'YYYY-MM-DD'.
|
|
193
|
+
|
|
194
|
+
Accepts a ``date`` as-is, a ``datetime`` (UTC-normalized first if tz-aware, so the
|
|
195
|
+
calendar day matches the instant rather than the author's locale), or a date-only
|
|
196
|
+
string. Strings carrying a time component are rejected rather than truncated.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
value: A ``date``, a ``datetime``, or an ISO date string.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
The calendar date ``value`` denotes.
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
ValueError: If ``value`` is another type, carries a time component, or is not a valid
|
|
206
|
+
ISO date. Every rejection is a ``ValueError`` so pydantic wraps it rather than
|
|
207
|
+
letting it escape as a broken deserialization contract."""
|
|
208
|
+
if isinstance(value, date) and not isinstance(value, datetime):
|
|
209
|
+
return value
|
|
210
|
+
|
|
211
|
+
if isinstance(value, datetime):
|
|
212
|
+
dt = value.astimezone(timezone.utc) if value.tzinfo is not None else value
|
|
213
|
+
return dt.date()
|
|
214
|
+
|
|
215
|
+
if isinstance(value, str):
|
|
216
|
+
s = value.strip()
|
|
217
|
+
if "T" in s or " " in s:
|
|
218
|
+
raise ValueError(f"DATE: expected ISO date 'YYYY-MM-DD', got datetime-like string: {value!r}")
|
|
219
|
+
try:
|
|
220
|
+
return date.fromisoformat(s)
|
|
221
|
+
except ValueError as e:
|
|
222
|
+
raise ValueError(f"DATE: invalid ISO date (expected 'YYYY-MM-DD'): {value!r}") from e
|
|
223
|
+
|
|
224
|
+
raise ValueError(f"DATE: expected ISO date string, date, or datetime; got {type(value).__name__}")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _dump_date(value: date) -> str:
|
|
228
|
+
return value.isoformat()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
DATE: Final = DateTimeConverter(parse=_parse_date, dump=_dump_date)
|
|
232
|
+
|
|
233
|
+
Date: TypeAlias = Annotated[
|
|
234
|
+
date,
|
|
235
|
+
BeforeValidator(DATE.parse),
|
|
236
|
+
PlainSerializer(DATE.dump, return_type=str),
|
|
237
|
+
]
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Wire formats for binary values carried as text.
|
|
2
|
+
|
|
3
|
+
A :class:`ByteEncodingConverter` is a parse/dump pair for one of RFC 4648's five alphabets -- the
|
|
4
|
+
binary sibling of :mod:`.date_time`'s ``DateTimeConverter``. Two surfaces are built from them, one
|
|
5
|
+
per position a spec can put a binary value in. A **model property** names :func:`ByteField`, which
|
|
6
|
+
carries its encoding and charset beside its wire key exactly as pydantic's own ``Field`` carries an
|
|
7
|
+
alias. An **endpoint** names one of the twenty ``Annotated`` aliases in a subscript, because that is
|
|
8
|
+
how every other parameter, body and decoder declares its type. Both spellings resolve to the same
|
|
9
|
+
converter; only the declaration site differs.
|
|
10
|
+
|
|
11
|
+
**The direction split is the whole point.** A ``str`` can only be the wire spelling, so it is
|
|
12
|
+
decoded; ``bytes`` are the value itself and pass through to pydantic's own ``bytes`` validation.
|
|
13
|
+
This is deliberately not pydantic's ``EncodedBytes``, which is an *after* validator and therefore
|
|
14
|
+
decodes raw input bytes too -- and lax decoding discards non-alphabet characters, so a caller's file
|
|
15
|
+
content is silently corrupted rather than rejected.
|
|
16
|
+
|
|
17
|
+
**Strictness is hand-audited per alphabet, because the stdlib's convenience spellings are not
|
|
18
|
+
uniformly strict.** ``urlsafe_b64decode`` is lax: it reads ``"aGV*sbG8="`` as ``b"hello"``, dropping
|
|
19
|
+
the ``*``, so the base64url codec goes through ``b64decode`` with ``altchars`` and ``validate=True``
|
|
20
|
+
instead. The three base16/base32 decoders refuse a lowercase digit unless told to fold it, while
|
|
21
|
+
their encoders emit the uppercase alphabets the RFC defines, so decoding folds and encoding stays
|
|
22
|
+
canonical.
|
|
23
|
+
|
|
24
|
+
An encoding that cannot *reject* is not offered. ``quoted-printable`` is RFC 2045's other
|
|
25
|
+
``contentEncoding`` value and is absent for that reason: ``quopri.decodestring`` never raises
|
|
26
|
+
(``b"hello=ZZworld"`` passes through unchanged), so it could only ship lax. ``7bit``, ``8bit`` and
|
|
27
|
+
``binary`` mean *not encoded at all*, which is a raw body or a file part rather than a converter.
|
|
28
|
+
|
|
29
|
+
Every decode rejection raises ``ValueError`` with the alphabet's name in caps, never ``TypeError``:
|
|
30
|
+
pydantic wraps ``ValueError`` from a validator into ``ValidationError`` but lets anything else escape
|
|
31
|
+
as a programming error. All five stdlib decoders raise ``binascii.Error``, a ``ValueError`` subclass,
|
|
32
|
+
so one ``except`` arm covers the family. The message deliberately does not repeat the offending
|
|
33
|
+
value -- it may be gigabytes, and it may be a secret."""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import base64
|
|
38
|
+
import binascii
|
|
39
|
+
from collections.abc import Callable
|
|
40
|
+
from dataclasses import dataclass
|
|
41
|
+
from functools import partial
|
|
42
|
+
from typing import Annotated, Any, Final, Literal, TypeAlias
|
|
43
|
+
|
|
44
|
+
from pydantic import Field
|
|
45
|
+
from pydantic.functional_serializers import PlainSerializer
|
|
46
|
+
from pydantic.functional_validators import BeforeValidator
|
|
47
|
+
|
|
48
|
+
ByteEncodingName: TypeAlias = Literal["base64", "base64url", "base32", "base32hex", "base16"]
|
|
49
|
+
"""The alphabets an OpenAPI 3.1 ``contentEncoding`` can name and this module can reject strictly.
|
|
50
|
+
|
|
51
|
+
RFC 4648 defines exactly these five, and OpenAPI 3.1 says ``contentEncoding`` supports all encodings
|
|
52
|
+
defined in RFC 4648. ``hex`` is base16 under its other name, folded onto ``"base16"`` at emission
|
|
53
|
+
time along with the case-insensitive comparison the spec calls for."""
|
|
54
|
+
|
|
55
|
+
ByteCharset: TypeAlias = Literal["ascii", "utf-8", "latin-1", "cp1252"]
|
|
56
|
+
"""The charsets that can render RFC 4648 output back as text.
|
|
57
|
+
|
|
58
|
+
Restricted deliberately, and the restriction is the safety. Those alphabets are ASCII-only -- the
|
|
59
|
+
highest byte any of the five encoders emits is ``z`` -- so all four values produce byte-identical
|
|
60
|
+
text. ``utf-16`` would instead pair the characters into unrelated codepoints and ``utf-32`` cannot
|
|
61
|
+
decode the length at all, so admitting them would only add values that cannot round-trip. Refusing
|
|
62
|
+
them here makes a wrong charset a build failure rather than a wrong wire value.
|
|
63
|
+
|
|
64
|
+
``utf-8`` is the default: it is the charset JSON itself is defined in, and for these alphabets it
|
|
65
|
+
is byte-identical to the alternatives. ``ascii`` remains available and is strictly stricter,
|
|
66
|
+
raising on a non-ASCII byte where ``utf-8`` accepts a well-formed multi-byte sequence; that made
|
|
67
|
+
it the original default, as an assertion that the codec table produced what it promised.
|
|
68
|
+
Declaring it is now how a property asks for that check."""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True, slots=True)
|
|
72
|
+
class ByteEncodingConverter:
|
|
73
|
+
"""How one alphabet is read and written.
|
|
74
|
+
|
|
75
|
+
``decode`` takes the wire ``str`` -- every stdlib decoder accepts one and encodes it to ASCII
|
|
76
|
+
itself -- and ``encode`` produces the alphabet's bytes, which :meth:`dump` renders back to text
|
|
77
|
+
through the declared charset. Frozen so the instance stays hashable, which is what lets the
|
|
78
|
+
aliases below reach the process-wide adapter cache."""
|
|
79
|
+
|
|
80
|
+
name: str
|
|
81
|
+
decode: Callable[[str], bytes]
|
|
82
|
+
encode: Callable[[bytes], bytes]
|
|
83
|
+
|
|
84
|
+
def parse(self, value: object) -> object:
|
|
85
|
+
"""Decode the wire spelling, or pass a value straight through.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
value: Whatever arrived -- a ``str`` from the wire, or ``bytes`` the caller supplied.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The decoded ``bytes`` for a ``str``, and ``value`` unchanged for anything else, which
|
|
92
|
+
leaves pydantic's own ``bytes`` validation to judge it.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
ValueError: If ``value`` is a ``str`` that is not valid text in this alphabet. The
|
|
96
|
+
message names the alphabet and deliberately omits the value."""
|
|
97
|
+
if isinstance(value, str):
|
|
98
|
+
try:
|
|
99
|
+
return self.decode(value)
|
|
100
|
+
except binascii.Error as e:
|
|
101
|
+
raise ValueError(f"{self.name}: not valid RFC 4648 {self.name.lower()} text.") from e
|
|
102
|
+
return value
|
|
103
|
+
|
|
104
|
+
def dump(self, value: bytes | None, charset: ByteCharset) -> str | None:
|
|
105
|
+
"""Encode ``value`` into this alphabet and render it as text.
|
|
106
|
+
|
|
107
|
+
The ``None`` arm is load-bearing for :func:`ByteField` alone: a field's metadata applies to
|
|
108
|
+
its whole annotation, so an optional property hands ``None`` straight here. The aliases below
|
|
109
|
+
sit inside the ``bytes`` arm of a ``bytes | None`` union and never see one.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
value: The bytes to encode, or ``None`` from an optional property.
|
|
113
|
+
charset: The charset to render the encoded bytes through. Every value
|
|
114
|
+
:data:`ByteCharset` admits produces identical text for this ASCII-only output.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
The encoded text, or ``None`` for a ``None`` input."""
|
|
118
|
+
return None if value is None else self.encode(value).decode(charset)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
BASE64: Final = ByteEncodingConverter(
|
|
122
|
+
name="BASE64",
|
|
123
|
+
decode=lambda text: base64.b64decode(text, validate=True),
|
|
124
|
+
encode=base64.b64encode,
|
|
125
|
+
)
|
|
126
|
+
BASE64URL: Final = ByteEncodingConverter(
|
|
127
|
+
name="BASE64URL",
|
|
128
|
+
decode=lambda text: base64.b64decode(text, altchars=b"-_", validate=True),
|
|
129
|
+
encode=base64.urlsafe_b64encode,
|
|
130
|
+
)
|
|
131
|
+
BASE32: Final = ByteEncodingConverter(
|
|
132
|
+
name="BASE32",
|
|
133
|
+
decode=lambda text: base64.b32decode(text, casefold=True),
|
|
134
|
+
encode=base64.b32encode,
|
|
135
|
+
)
|
|
136
|
+
BASE32HEX: Final = ByteEncodingConverter(
|
|
137
|
+
name="BASE32HEX",
|
|
138
|
+
decode=lambda text: base64.b32hexdecode(text, casefold=True),
|
|
139
|
+
encode=base64.b32hexencode,
|
|
140
|
+
)
|
|
141
|
+
BASE16: Final = ByteEncodingConverter(
|
|
142
|
+
name="BASE16",
|
|
143
|
+
decode=lambda text: base64.b16decode(text, casefold=True),
|
|
144
|
+
encode=base64.b16encode,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
_CODECS: Final[dict[ByteEncodingName, ByteEncodingConverter]] = {
|
|
148
|
+
"base64": BASE64,
|
|
149
|
+
"base64url": BASE64URL,
|
|
150
|
+
"base32": BASE32,
|
|
151
|
+
"base32hex": BASE32HEX,
|
|
152
|
+
"base16": BASE16,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def ByteField(
|
|
157
|
+
*,
|
|
158
|
+
encoding: ByteEncodingName = "base64",
|
|
159
|
+
charset: ByteCharset = "utf-8",
|
|
160
|
+
alias: str | None = None,
|
|
161
|
+
description: str | None = None,
|
|
162
|
+
default: Any = ...,
|
|
163
|
+
) -> Any:
|
|
164
|
+
"""A bytes property's wire encoding, declared beside its wire key.
|
|
165
|
+
|
|
166
|
+
Wraps pydantic's ``Field`` and adds the two facts a binary property carries: which RFC 4648
|
|
167
|
+
alphabet it crosses the wire in, and which charset renders that alphabet as text. Both are
|
|
168
|
+
keyword-only and both are typed as ``Literal``, so a mistake is a build failure whose message
|
|
169
|
+
names the accepted values rather than a wrong wire format nobody disagrees with -- and each is
|
|
170
|
+
spelled at a property only where the spec differs from the default (``base64``, ``utf-8``), as
|
|
171
|
+
every other spec-derived default in the emitted layer is (ADR-0048).
|
|
172
|
+
|
|
173
|
+
Named in PascalCase on purpose. ``Field`` is itself a function spelled that way, and this reads
|
|
174
|
+
as its sibling at the declaration site::
|
|
175
|
+
|
|
176
|
+
file_name: str | None = Field(default=None, alias="fileName")
|
|
177
|
+
content: bytes = ByteField()
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
encoding: The RFC 4648 alphabet the value is transmitted in.
|
|
181
|
+
charset: The charset the encoded alphabet is rendered through.
|
|
182
|
+
alias: The wire key, when it differs from the Python name.
|
|
183
|
+
description: The property's description.
|
|
184
|
+
default: The default value; omitted for a required property.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
A pydantic ``FieldInfo`` carrying the alphabet's validator and serializer, typed ``Any`` so
|
|
188
|
+
it can stand as the default of a ``bytes``-annotated property -- exactly as ``Field`` is."""
|
|
189
|
+
codec = _CODECS[encoding]
|
|
190
|
+
info = Field(default=default, alias=alias, description=description)
|
|
191
|
+
info.metadata.append(BeforeValidator(codec.parse))
|
|
192
|
+
info.metadata.append(PlainSerializer(partial(codec.dump, charset=charset), return_type=str))
|
|
193
|
+
return info
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# The twenty aliases an endpoint names in a subscript, one per (alphabet, charset) pair. Each is
|
|
197
|
+
# spelled longhand because a type alias must be a valid type *expression*: a helper returning the
|
|
198
|
+
# ``Annotated`` is rejected as "expression is not a valid type", and would erase ``bytes`` with it.
|
|
199
|
+
# Each is bound once here, at import, which is what keeps one alias to one cached adapter --
|
|
200
|
+
# ``functools.partial`` hashes by identity, so an alias rebuilt per call site would mint a new one.
|
|
201
|
+
|
|
202
|
+
Base64AsciiEncodedBytes: TypeAlias = Annotated[
|
|
203
|
+
bytes, BeforeValidator(BASE64.parse), PlainSerializer(partial(BASE64.dump, charset="ascii"), return_type=str)
|
|
204
|
+
]
|
|
205
|
+
Base64Utf8EncodedBytes: TypeAlias = Annotated[
|
|
206
|
+
bytes, BeforeValidator(BASE64.parse), PlainSerializer(partial(BASE64.dump, charset="utf-8"), return_type=str)
|
|
207
|
+
]
|
|
208
|
+
Base64Latin1EncodedBytes: TypeAlias = Annotated[
|
|
209
|
+
bytes, BeforeValidator(BASE64.parse), PlainSerializer(partial(BASE64.dump, charset="latin-1"), return_type=str)
|
|
210
|
+
]
|
|
211
|
+
Base64Cp1252EncodedBytes: TypeAlias = Annotated[
|
|
212
|
+
bytes, BeforeValidator(BASE64.parse), PlainSerializer(partial(BASE64.dump, charset="cp1252"), return_type=str)
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
Base64UrlAsciiEncodedBytes: TypeAlias = Annotated[
|
|
216
|
+
bytes, BeforeValidator(BASE64URL.parse), PlainSerializer(partial(BASE64URL.dump, charset="ascii"), return_type=str)
|
|
217
|
+
]
|
|
218
|
+
Base64UrlUtf8EncodedBytes: TypeAlias = Annotated[
|
|
219
|
+
bytes, BeforeValidator(BASE64URL.parse), PlainSerializer(partial(BASE64URL.dump, charset="utf-8"), return_type=str)
|
|
220
|
+
]
|
|
221
|
+
Base64UrlLatin1EncodedBytes: TypeAlias = Annotated[
|
|
222
|
+
bytes,
|
|
223
|
+
BeforeValidator(BASE64URL.parse),
|
|
224
|
+
PlainSerializer(partial(BASE64URL.dump, charset="latin-1"), return_type=str),
|
|
225
|
+
]
|
|
226
|
+
Base64UrlCp1252EncodedBytes: TypeAlias = Annotated[
|
|
227
|
+
bytes,
|
|
228
|
+
BeforeValidator(BASE64URL.parse),
|
|
229
|
+
PlainSerializer(partial(BASE64URL.dump, charset="cp1252"), return_type=str),
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
Base32AsciiEncodedBytes: TypeAlias = Annotated[
|
|
233
|
+
bytes, BeforeValidator(BASE32.parse), PlainSerializer(partial(BASE32.dump, charset="ascii"), return_type=str)
|
|
234
|
+
]
|
|
235
|
+
Base32Utf8EncodedBytes: TypeAlias = Annotated[
|
|
236
|
+
bytes, BeforeValidator(BASE32.parse), PlainSerializer(partial(BASE32.dump, charset="utf-8"), return_type=str)
|
|
237
|
+
]
|
|
238
|
+
Base32Latin1EncodedBytes: TypeAlias = Annotated[
|
|
239
|
+
bytes, BeforeValidator(BASE32.parse), PlainSerializer(partial(BASE32.dump, charset="latin-1"), return_type=str)
|
|
240
|
+
]
|
|
241
|
+
Base32Cp1252EncodedBytes: TypeAlias = Annotated[
|
|
242
|
+
bytes, BeforeValidator(BASE32.parse), PlainSerializer(partial(BASE32.dump, charset="cp1252"), return_type=str)
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
Base32HexAsciiEncodedBytes: TypeAlias = Annotated[
|
|
246
|
+
bytes, BeforeValidator(BASE32HEX.parse), PlainSerializer(partial(BASE32HEX.dump, charset="ascii"), return_type=str)
|
|
247
|
+
]
|
|
248
|
+
Base32HexUtf8EncodedBytes: TypeAlias = Annotated[
|
|
249
|
+
bytes, BeforeValidator(BASE32HEX.parse), PlainSerializer(partial(BASE32HEX.dump, charset="utf-8"), return_type=str)
|
|
250
|
+
]
|
|
251
|
+
Base32HexLatin1EncodedBytes: TypeAlias = Annotated[
|
|
252
|
+
bytes,
|
|
253
|
+
BeforeValidator(BASE32HEX.parse),
|
|
254
|
+
PlainSerializer(partial(BASE32HEX.dump, charset="latin-1"), return_type=str),
|
|
255
|
+
]
|
|
256
|
+
Base32HexCp1252EncodedBytes: TypeAlias = Annotated[
|
|
257
|
+
bytes,
|
|
258
|
+
BeforeValidator(BASE32HEX.parse),
|
|
259
|
+
PlainSerializer(partial(BASE32HEX.dump, charset="cp1252"), return_type=str),
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
Base16AsciiEncodedBytes: TypeAlias = Annotated[
|
|
263
|
+
bytes, BeforeValidator(BASE16.parse), PlainSerializer(partial(BASE16.dump, charset="ascii"), return_type=str)
|
|
264
|
+
]
|
|
265
|
+
Base16Utf8EncodedBytes: TypeAlias = Annotated[
|
|
266
|
+
bytes, BeforeValidator(BASE16.parse), PlainSerializer(partial(BASE16.dump, charset="utf-8"), return_type=str)
|
|
267
|
+
]
|
|
268
|
+
Base16Latin1EncodedBytes: TypeAlias = Annotated[
|
|
269
|
+
bytes, BeforeValidator(BASE16.parse), PlainSerializer(partial(BASE16.dump, charset="latin-1"), return_type=str)
|
|
270
|
+
]
|
|
271
|
+
Base16Cp1252EncodedBytes: TypeAlias = Annotated[
|
|
272
|
+
bytes, BeforeValidator(BASE16.parse), PlainSerializer(partial(BASE16.dump, charset="cp1252"), return_type=str)
|
|
273
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Forward-compatible enums: a value the server adds after generation survives as its scalar."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
from pydantic import BeforeValidator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def open_enum_validator(enum_type: type[Enum]) -> BeforeValidator:
|
|
11
|
+
"""Coerce a known wire value to its ``enum_type`` member; pass unknown values through unchanged.
|
|
12
|
+
|
|
13
|
+
Enum-type-agnostic: works for any ``Enum`` via the generic ``enum_type(value)`` lookup. Pair
|
|
14
|
+
with an ``Enum | <scalar>`` annotation whose fallback arm is the enum's underlying scalar
|
|
15
|
+
type --
|
|
16
|
+
``Days | str`` for a ``(str, Enum)``, ``Priority | int`` for an ``(int, Enum)``. Known values
|
|
17
|
+
become ergonomic members; values added by the server after generation survive as that raw
|
|
18
|
+
scalar instead of raising. Resolving here (before the union) sidesteps smart-union mis-scoring,
|
|
19
|
+
which would otherwise collapse every value to the scalar arm (pydantic #7110).
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
enum_type: The closed enum whose members the wire values are looked up in.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
A ``BeforeValidator`` to attach to the open alias with ``Annotated``."""
|
|
26
|
+
|
|
27
|
+
def coerce(value: object) -> object:
|
|
28
|
+
if isinstance(value, enum_type):
|
|
29
|
+
return value
|
|
30
|
+
try:
|
|
31
|
+
return enum_type(value)
|
|
32
|
+
except ValueError:
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
return BeforeValidator(coerce)
|