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,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)