cliV1 0.0.7__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.7.dist-info/METADATA +212 -0
- cliv1-0.0.7.dist-info/RECORD +92 -0
- cliv1-0.0.7.dist-info/WHEEL +4 -0
- cliv1-0.0.7.dist-info/licenses/LICENSE +21 -0
- swagger_petstore_open_api_3_1_0/__init__.py +15 -0
- swagger_petstore_open_api_3_1_0/apis/__init__.py +5 -0
- swagger_petstore_open_api_3_1_0/apis/pet_api.py +922 -0
- swagger_petstore_open_api_3_1_0/apis/store.py +432 -0
- swagger_petstore_open_api_3_1_0/apis/user_api.py +829 -0
- swagger_petstore_open_api_3_1_0/async_client.py +87 -0
- swagger_petstore_open_api_3_1_0/auth.py +17 -0
- swagger_petstore_open_api_3_1_0/base_client.py +27 -0
- swagger_petstore_open_api_3_1_0/client.py +85 -0
- swagger_petstore_open_api_3_1_0/core/__init__.py +395 -0
- swagger_petstore_open_api_3_1_0/core/_internal/__init__.py +9 -0
- swagger_petstore_open_api_3_1_0/core/_internal/content_disposition.py +154 -0
- swagger_petstore_open_api_3_1_0/core/_internal/flattening.py +104 -0
- swagger_petstore_open_api_3_1_0/core/_internal/headers.py +243 -0
- swagger_petstore_open_api_3_1_0/core/_internal/redaction.py +208 -0
- swagger_petstore_open_api_3_1_0/core/_internal/sse_parser.py +124 -0
- swagger_petstore_open_api_3_1_0/core/_internal/subscripts.py +50 -0
- swagger_petstore_open_api_3_1_0/core/_internal/urls.py +172 -0
- swagger_petstore_open_api_3_1_0/core/_internal/wire.py +201 -0
- swagger_petstore_open_api_3_1_0/core/adapters.py +177 -0
- swagger_petstore_open_api_3_1_0/core/auth/__init__.py +70 -0
- swagger_petstore_open_api_3_1_0/core/auth/composition.py +175 -0
- swagger_petstore_open_api_3_1_0/core/auth/models.py +216 -0
- swagger_petstore_open_api_3_1_0/core/auth/protocols.py +103 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/__init__.py +94 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/api_key_auth.py +53 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/basic_auth.py +73 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/bearer_auth.py +21 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_authorization_code.py +395 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_client_credentials.py +130 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_password.py +145 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_schemes.py +300 -0
- swagger_petstore_open_api_3_1_0/core/base_raw_response.py +49 -0
- swagger_petstore_open_api_3_1_0/core/bodies.py +590 -0
- swagger_petstore_open_api_3_1_0/core/converters/__init__.py +68 -0
- swagger_petstore_open_api_3_1_0/core/converters/date_time.py +237 -0
- swagger_petstore_open_api_3_1_0/core/converters/encoded_bytes.py +273 -0
- swagger_petstore_open_api_3_1_0/core/converters/open_enum.py +35 -0
- swagger_petstore_open_api_3_1_0/core/decoding.py +498 -0
- swagger_petstore_open_api_3_1_0/core/discriminators.py +129 -0
- swagger_petstore_open_api_3_1_0/core/event_streams.py +225 -0
- swagger_petstore_open_api_3_1_0/core/exceptions.py +78 -0
- swagger_petstore_open_api_3_1_0/core/file_responses.py +337 -0
- swagger_petstore_open_api_3_1_0/core/files.py +174 -0
- swagger_petstore_open_api_3_1_0/core/httpx2_transport.py +748 -0
- swagger_petstore_open_api_3_1_0/core/logging.py +472 -0
- swagger_petstore_open_api_3_1_0/core/models.py +143 -0
- swagger_petstore_open_api_3_1_0/core/optionality.py +144 -0
- swagger_petstore_open_api_3_1_0/core/pagination.py +452 -0
- swagger_petstore_open_api_3_1_0/core/paginator.py +55 -0
- swagger_petstore_open_api_3_1_0/core/params.py +157 -0
- swagger_petstore_open_api_3_1_0/core/raw_client.py +480 -0
- swagger_petstore_open_api_3_1_0/core/request_options.py +172 -0
- swagger_petstore_open_api_3_1_0/core/results.py +178 -0
- swagger_petstore_open_api_3_1_0/core/retries.py +237 -0
- swagger_petstore_open_api_3_1_0/core/runtime_env.py +43 -0
- swagger_petstore_open_api_3_1_0/core/servers.py +33 -0
- swagger_petstore_open_api_3_1_0/core/transport.py +218 -0
- swagger_petstore_open_api_3_1_0/errors/__init__.py +48 -0
- swagger_petstore_open_api_3_1_0/errors/add_pet_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/delete_order_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/delete_pet_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/delete_user_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/find_pets_by_status_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/find_pets_by_tags_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/get_order_by_id_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/get_pet_by_id_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/get_user_by_name_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/login_user_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/place_order_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/update_pet_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/update_pet_with_form_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/update_user_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/upload_file_error.py +21 -0
- swagger_petstore_open_api_3_1_0/models/__init__.py +23 -0
- swagger_petstore_open_api_3_1_0/models/api_response.py +18 -0
- swagger_petstore_open_api_3_1_0/models/category.py +15 -0
- swagger_petstore_open_api_3_1_0/models/enums/__init__.py +4 -0
- swagger_petstore_open_api_3_1_0/models/enums/order_status.py +17 -0
- swagger_petstore_open_api_3_1_0/models/enums/pet_status.py +17 -0
- swagger_petstore_open_api_3_1_0/models/order.py +27 -0
- swagger_petstore_open_api_3_1_0/models/pet.py +28 -0
- swagger_petstore_open_api_3_1_0/models/tag_model.py +15 -0
- swagger_petstore_open_api_3_1_0/models/user.py +29 -0
- swagger_petstore_open_api_3_1_0/py.typed +0 -0
- swagger_petstore_open_api_3_1_0/server/__init__.py +21 -0
- swagger_petstore_open_api_3_1_0/server/server.py +17 -0
- swagger_petstore_open_api_3_1_0/server/server_config.py +52 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""One declared value, one wire-ready form.
|
|
2
|
+
|
|
3
|
+
Every request parameter -- query, form, path, header, or a base URL's server variable -- reaches
|
|
4
|
+
the wire the same way: validated against the type the endpoint declared, dumped through that
|
|
5
|
+
type's adapter, and turned into text. Only the *arrangement* differs, and that stays with each
|
|
6
|
+
destination: :mod:`.form` explodes values into ``key[sub]`` pairs, :mod:`.urls` joins them into
|
|
7
|
+
``/``-separated segments, :mod:`.headers` folds them into one field value.
|
|
8
|
+
|
|
9
|
+
That shared prefix lives here, which is why a ``datetime`` declared ``RFC1123DateTime`` renders as
|
|
10
|
+
its GMT string at every destination without any of them knowing what a datetime is -- and why a
|
|
11
|
+
fifth destination would need no serialization code of its own."""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from collections.abc import Iterable, Mapping
|
|
17
|
+
from typing import Any, TypeGuard
|
|
18
|
+
|
|
19
|
+
from pydantic import ValidationError
|
|
20
|
+
|
|
21
|
+
from ..optionality import strip_unset
|
|
22
|
+
from ..params import Param
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def wire_value(param: Param[Any]) -> object:
|
|
26
|
+
"""``param``'s value in wire form: validated against its declared type, then dumped.
|
|
27
|
+
|
|
28
|
+
The same two steps, in the same order, as :func:`json_body` -- and the reason a parameter
|
|
29
|
+
holds its resolved adapter rather than the type expression it came from. Validation coerces
|
|
30
|
+
dict-shaped input (``Model | FooDict`` companions) through the model's own rules; the dump
|
|
31
|
+
applies the type's serializer, which is what renders an ``RFC1123DateTime`` as its GMT string,
|
|
32
|
+
an enum as its value and a model under its wire aliases.
|
|
33
|
+
|
|
34
|
+
A validation failure is re-raised naming the parameter: pydantic titles the error by the
|
|
35
|
+
*type* (``1 validation error for int``), which on an endpoint with five parameters leaves the
|
|
36
|
+
caller guessing which one failed. ``ValidationError`` subclasses ``ValueError``, so a caller's
|
|
37
|
+
``except ValueError`` covers both spellings, and the full per-field detail rides ``__cause__``.
|
|
38
|
+
|
|
39
|
+
``strip_unset`` is what drops a field the caller never touched, and the only thing that can:
|
|
40
|
+
``UnsetType`` is not ``None``, so the dump keeps the key and it serializes as ``null``, which a
|
|
41
|
+
JSON-encoded path segment or header value would carry verbatim. An *explicit* ``None`` is
|
|
42
|
+
kept -- see docs/designs/optional-nullable-fields.md. Which of those nulls survives is the
|
|
43
|
+
destination's own decision, taken in :func:`text_values` and ``flattening.flatten``: compact
|
|
44
|
+
JSON can spell one, a query pair or a flat path segment cannot.
|
|
45
|
+
|
|
46
|
+
The result is still structured -- a collection stays a list, a model stays a mapping -- because
|
|
47
|
+
the query and form destination needs that shape to explode into ``key[sub]`` keys.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
param: The parameter to render, carrying its declared type's adapter.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
The dumped value, still structured; a top-level set is sorted so one call yields one URL.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ValueError: If the value does not match its declared type. The message names the
|
|
57
|
+
parameter; pydantic's per-field detail rides ``__cause__``."""
|
|
58
|
+
try:
|
|
59
|
+
validated = param.adapter.validate_python(param.value)
|
|
60
|
+
except ValidationError as error:
|
|
61
|
+
raise ValueError(f"Parameter value does not match its declared type: {param.key}") from error
|
|
62
|
+
|
|
63
|
+
dumped: object = strip_unset(validated, param.adapter.dump_python(validated, mode="json"))
|
|
64
|
+
|
|
65
|
+
# JSON has no set, so the dump has already flattened one into a list in hash-iteration order.
|
|
66
|
+
# Sorting is what keeps one call producing one URL across runs. A *top-level* set is the whole
|
|
67
|
+
# reach on purpose: a set nested inside a container is not an emittable declared type --
|
|
68
|
+
# ``adapters.py``'s ``_CONTAINERS`` and ``strip_unset`` close at ``list``/``dict`` for the same
|
|
69
|
+
# reason -- so walking deeper would be speculative surface for a shape no spec yields.
|
|
70
|
+
if isinstance(validated, (set, frozenset)) and isinstance(dumped, list):
|
|
71
|
+
return sorted(dumped, key=str)
|
|
72
|
+
|
|
73
|
+
return dumped
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def text_values(param: Param[Any]) -> tuple[str, ...]:
|
|
77
|
+
"""``param`` as the flat texts it contributes -- one per path segment or header list member.
|
|
78
|
+
|
|
79
|
+
Empty means it contributes nothing: its value was ``None``, or its collection was. A scalar is
|
|
80
|
+
one text, a flat collection is one text per member, and anything structured -- a mapping, a
|
|
81
|
+
model, or a collection holding either -- is one text of compact JSON, since neither a path
|
|
82
|
+
segment nor a header value can carry a nested shape any other way.
|
|
83
|
+
|
|
84
|
+
The arrangement is settled *before* the ``None`` rule, because the two arrangements can
|
|
85
|
+
represent different things: compact JSON spells ``null``, so a structured collection carries a
|
|
86
|
+
``None`` member as one, while a flat collection has no such spelling and refuses it.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
param: The parameter to render.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
One text per segment or header member; empty when the parameter contributes nothing.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
ValueError: If the value does not match its declared type, or if a *flat* collection holds
|
|
96
|
+
a ``None`` -- neither a path segment nor a header member can spell one."""
|
|
97
|
+
value = wire_value(param)
|
|
98
|
+
if value is None:
|
|
99
|
+
return ()
|
|
100
|
+
|
|
101
|
+
if isinstance(value, Mapping):
|
|
102
|
+
return (compact_json(value),)
|
|
103
|
+
|
|
104
|
+
if not is_collection(value):
|
|
105
|
+
return (to_text(value),)
|
|
106
|
+
|
|
107
|
+
members = list(value)
|
|
108
|
+
if any(isinstance(member, Mapping) or is_collection(member) for member in members):
|
|
109
|
+
return (compact_json(members),)
|
|
110
|
+
|
|
111
|
+
if any(member is None for member in members):
|
|
112
|
+
# Query and form encoding can drop a member, because it can also repeat or omit a key.
|
|
113
|
+
# Neither of these destinations can: a path would address a different route and a header
|
|
114
|
+
# would carry an empty list slot, both silently.
|
|
115
|
+
raise ValueError(f"Parameter value contains None: {param.key}")
|
|
116
|
+
|
|
117
|
+
return tuple(to_text(member) for member in members)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def to_text(value: object) -> str:
|
|
121
|
+
"""One dumped scalar as the text a URL or a header carries.
|
|
122
|
+
|
|
123
|
+
The adapter has already applied wire aliases, enum values and temporal formats, so only
|
|
124
|
+
booleans are left: every destination here is text and expects JSON's ``true``/``false``,
|
|
125
|
+
whereas ``str(True)`` would emit ``True``. Called only where a value actually becomes text,
|
|
126
|
+
which is what lets the structured branches above keep real booleans for :func:`compact_json`.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
value: One already-dumped scalar.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Its text form, with booleans lowercased to JSON's spelling."""
|
|
133
|
+
if isinstance(value, bool):
|
|
134
|
+
return "true" if value else "false"
|
|
135
|
+
|
|
136
|
+
return str(value)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def parse_json(content: bytes) -> Any:
|
|
140
|
+
"""Parse a response body as JSON, normalising both ways it can fail.
|
|
141
|
+
|
|
142
|
+
Parsed from the bytes rather than from a decoded ``str``, so RFC 8259 encoding detection applies
|
|
143
|
+
(UTF-8/16/32, BOM-tolerant) and an undecodable byte is a failure rather than a replacement
|
|
144
|
+
character smuggled into a successful payload.
|
|
145
|
+
|
|
146
|
+
``json.JSONDecodeError`` and ``UnicodeDecodeError`` are both ``ValueError``s already, but they
|
|
147
|
+
read as two unrelated faults; a caller who reached a body that will not parse is owed one answer.
|
|
148
|
+
The inverse of :func:`compact_json`, and here for the same reason -- one spelling, every caller.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
content: The raw body.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Whatever the body parses to -- an object, array or scalar.
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
ValueError: If the body is not valid JSON, or is not decodable at all."""
|
|
158
|
+
try:
|
|
159
|
+
return json.loads(content)
|
|
160
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
161
|
+
raise ValueError("Response body is not valid JSON") from exc
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def compact_json(value: object, *, ensure_ascii: bool = True) -> str:
|
|
165
|
+
"""JSON-encode a structured value with no insignificant whitespace.
|
|
166
|
+
|
|
167
|
+
``ensure_ascii`` defaults on: a header field value is ISO-8859-1 by RFC 9110 and the transport
|
|
168
|
+
rejects anything outside it, so a header, a path segment or a multipart text part keeps every
|
|
169
|
+
non-ASCII character escaped. A JSON *body* passes ``False`` and goes out as UTF-8 -- the bytes
|
|
170
|
+
httpx's own encoder produced before a body carried its own, so the wire did not change.
|
|
171
|
+
``allow_nan`` is turned off: at its default a non-finite float encodes as ``Infinity`` or
|
|
172
|
+
``NaN`` -- a Python dialect no RFC 8259 parser accepts -- so the call would fail server-side,
|
|
173
|
+
far from the line that produced the value.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
value: The structured value to encode.
|
|
177
|
+
ensure_ascii: Whether non-ASCII characters are escaped; off for a body, on everywhere else.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Its JSON text with no insignificant whitespace.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
ValueError: If ``value`` holds a non-finite float, which has no RFC 8259 spelling."""
|
|
184
|
+
return json.dumps(value, separators=(",", ":"), ensure_ascii=ensure_ascii, allow_nan=False)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def is_collection(value: object) -> TypeGuard[Iterable[object]]:
|
|
188
|
+
"""True for an iterable that should expand into several wire values.
|
|
189
|
+
|
|
190
|
+
``TypeGuard``, deliberately not the newer ``TypeIs``: ``TypeIs`` also narrows the *negative*
|
|
191
|
+
branch, which would be unsound here -- ``str`` is ``Iterable`` yet this returns ``False`` for
|
|
192
|
+
it, so a checker told "not an ``Iterable``" would wrongly rule out strings. ``TypeGuard``
|
|
193
|
+
narrows only where this returns ``True``, which is exactly what the caller needs to iterate.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
value: The dumped value to classify.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
``True`` for an iterable that expands into several wire values -- a ``str``, ``bytes`` or
|
|
200
|
+
``Mapping`` is one value, not several."""
|
|
201
|
+
return isinstance(value, Iterable) and not isinstance(value, (str, bytes, Mapping))
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""The one place a ``TypeAdapter`` is built, and what a value is validated *into*.
|
|
2
|
+
|
|
3
|
+
Shared by the request path (:func:`json_body`, :func:`param`) and the response path (the decoders),
|
|
4
|
+
so the expensive step in pydantic v2 -- building the adapter -- happens once per type for the life
|
|
5
|
+
of the process rather than once per request.
|
|
6
|
+
|
|
7
|
+
It sits on its own, below both paths, because both need it and neither owns it: a request-body
|
|
8
|
+
module reaching into the response-decoding module for it would put a cycle in the import graph."""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from functools import reduce
|
|
13
|
+
from operator import or_
|
|
14
|
+
from threading import RLock
|
|
15
|
+
from types import UnionType
|
|
16
|
+
from typing import Any, Final, TypeVar, Union, get_args, get_origin
|
|
17
|
+
|
|
18
|
+
from pydantic import TypeAdapter
|
|
19
|
+
from typing_extensions import TypeForm, is_typeddict
|
|
20
|
+
|
|
21
|
+
T = TypeVar("T")
|
|
22
|
+
|
|
23
|
+
_CONTAINERS: Final = (list, dict)
|
|
24
|
+
"""The container origins a spec can express -- an array and a map -- so the only two emitted."""
|
|
25
|
+
|
|
26
|
+
_ADAPTER_CACHE_MAX: Final = 512
|
|
27
|
+
_adapter_lock: Final = RLock()
|
|
28
|
+
_adapters: Final[dict[object, TypeAdapter[Any]]] = {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def validation_target(type_: TypeForm[T]) -> TypeForm[T]:
|
|
32
|
+
"""Drop the dict-shaped companions from ``type_``, leaving what a value is validated *into*.
|
|
33
|
+
|
|
34
|
+
An endpoint accepts ``Employee | EmployeeDict``, and that whole union is what it declares and
|
|
35
|
+
hands the factory -- but only ``Employee`` may reach :func:`adapter_for`. The adapter both
|
|
36
|
+
validates *and* dumps, and a dict matching the ``TypedDict`` arm is dumped verbatim: wire
|
|
37
|
+
aliases lost (``address_line_1`` for ``addressLine1``), defaulted fields absent, unknown keys
|
|
38
|
+
silently dropped. A companion is an *input* shape, never a wire shape, so the type describing
|
|
39
|
+
what a caller may pass and the type a value is validated into are two different things.
|
|
40
|
+
|
|
41
|
+
The strip reaches wherever a companion can be written -- through union members and into
|
|
42
|
+
container parameters -- so ``list[Employee | EmployeeDict]`` and
|
|
43
|
+
``list[Employee] | list[EmployeeDict]`` both resolve to ``list[Employee]``. Generic over ``T``
|
|
44
|
+
so that ``adapter_for(validation_target(type_))`` keeps the adapter's type instead of erasing
|
|
45
|
+
it to ``Any`` at the one hop between two otherwise precise signatures.
|
|
46
|
+
|
|
47
|
+
A type form that is dict-shaped *throughout* has no target at all. Only a generator defect
|
|
48
|
+
emits one, and it type-checks, so it is named here rather than surfacing as ``reduce`` failing
|
|
49
|
+
on an empty sequence at the first request.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
type_: The declared type expression, dict-shaped companions included.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The same expression with every companion arm removed -- what a value validates into.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
TypeError: If ``type_`` is dict-shaped throughout, leaving no model arm to validate into."""
|
|
59
|
+
if _is_companion(type_):
|
|
60
|
+
raise TypeError(
|
|
61
|
+
f"{type_!r} names only dict-shaped companions, which are input shapes and never wire "
|
|
62
|
+
f"shapes -- a validation target needs the model arm each companion accompanies"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
target: TypeForm[T] = _strip_companions(type_)
|
|
66
|
+
return target
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_companion(type_: TypeForm[Any]) -> bool:
|
|
70
|
+
"""Whether ``type_`` is dict-shaped throughout, so no model arm survives the strip.
|
|
71
|
+
|
|
72
|
+
``all`` for a union and ``any`` for a container, and the asymmetry is the rule itself: a union
|
|
73
|
+
keeps a target while *one* member has one, whereas a container whose parameter has none has none
|
|
74
|
+
either -- ``list[EmployeeDict]`` cannot be narrowed to a list of anything.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
type_: The type expression to classify.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
``True`` when nothing in ``type_`` would survive the companion strip."""
|
|
81
|
+
if is_typeddict(type_):
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
origin = get_origin(type_)
|
|
85
|
+
if origin in (Union, UnionType):
|
|
86
|
+
return all(_is_companion(arg) for arg in get_args(type_))
|
|
87
|
+
if origin in _CONTAINERS:
|
|
88
|
+
return any(_is_companion(arg) for arg in get_args(type_))
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _strip_companions(type_: TypeForm[Any]) -> TypeForm[Any]:
|
|
93
|
+
"""``type_`` with every companion dropped, rebuilt only where something actually changed.
|
|
94
|
+
|
|
95
|
+
Two properties, each load-bearing:
|
|
96
|
+
|
|
97
|
+
- the origin checks, because ``get_args(list[str])`` is ``(str,)`` -- descending is only sound
|
|
98
|
+
for the origins whose parameters *are* element types, so everything else is returned
|
|
99
|
+
untouched. Notably ``Annotated``, which is how a discriminated union carries its
|
|
100
|
+
``WireDiscriminator``: descending would silently untag it.
|
|
101
|
+
``_CONTAINERS`` is closed at ``list`` and ``dict`` rather than "every container Python has" --
|
|
102
|
+
a spec expresses arrays and maps, and descending a shape no generator emits would be
|
|
103
|
+
speculative surface. A ``tuple`` or ``set`` parameter passes through like any other origin;
|
|
104
|
+
- the nothing-changed short-circuits, which hand back the expression *as written* rather than a
|
|
105
|
+
rebuilt one, so a companion-free union such as ``EmailStr | str`` cannot lose a member to
|
|
106
|
+
``Union``'s deduplication, and one declared type keeps one :func:`adapter_for` cache entry.
|
|
107
|
+
|
|
108
|
+
Rebuilt with ``|`` rather than ``Union[tuple(targets)]``: mypy types that subscript as a typing
|
|
109
|
+
special form rather than a ``TypeForm``, so it cannot be returned from this signature. The
|
|
110
|
+
annotated locals are what keep the imprecision of ``get_args`` here instead of at every caller.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
type_: The type expression to strip.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
``type_`` itself when no companion was found, otherwise the rebuilt expression."""
|
|
117
|
+
origin = get_origin(type_)
|
|
118
|
+
args = get_args(type_)
|
|
119
|
+
|
|
120
|
+
if origin in (Union, UnionType):
|
|
121
|
+
targets = [_strip_companions(arg) for arg in args if not _is_companion(arg)]
|
|
122
|
+
if len(targets) == len(args) and all(target is arg for target, arg in zip(targets, args, strict=True)):
|
|
123
|
+
return type_
|
|
124
|
+
rebuilt: TypeForm[Any] = reduce(or_, targets)
|
|
125
|
+
return rebuilt
|
|
126
|
+
|
|
127
|
+
if origin in _CONTAINERS:
|
|
128
|
+
stripped = [_strip_companions(arg) for arg in args]
|
|
129
|
+
if all(target is arg for target, arg in zip(stripped, args, strict=True)):
|
|
130
|
+
return type_
|
|
131
|
+
reparametrized: TypeForm[Any] = origin[tuple(stripped)]
|
|
132
|
+
return reparametrized
|
|
133
|
+
|
|
134
|
+
return type_
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def adapter_for(type_: TypeForm[T]) -> TypeAdapter[T]:
|
|
138
|
+
"""Return the process-wide cached ``TypeAdapter`` for ``type_``, building it once.
|
|
139
|
+
|
|
140
|
+
The cache must be module-level to work at all: a decoder is constructed per call, so
|
|
141
|
+
an instance-owned cache would be discarded with it and never see a second lookup.
|
|
142
|
+
|
|
143
|
+
Unhashable type forms bypass the cache. At ``_ADAPTER_CACHE_MAX`` entries it is cleared
|
|
144
|
+
wholesale -- predictable, and far beyond the number of distinct types one generated SDK
|
|
145
|
+
produces.
|
|
146
|
+
|
|
147
|
+
A hit is served without the lock, which is sound because a dict read is atomic under the GIL and
|
|
148
|
+
internally locked on free-threaded builds; a stale read racing the wholesale ``clear()`` still
|
|
149
|
+
returns a correct adapter for that type. The lock is there to make each *build* happen once --
|
|
150
|
+
the contract ``test_adapter_cache.py`` pins -- not to serialize reads, so the build stays inside
|
|
151
|
+
it and the re-check inside catches a type another thread built while this one waited.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
type_: The validation target, already stripped of companions by :func:`validation_target`.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
The adapter for ``type_``, shared across the process unless ``type_`` is unhashable."""
|
|
158
|
+
try:
|
|
159
|
+
hash(type_)
|
|
160
|
+
except TypeError:
|
|
161
|
+
return TypeAdapter(type_)
|
|
162
|
+
|
|
163
|
+
cached = _adapters.get(type_)
|
|
164
|
+
if cached is not None:
|
|
165
|
+
return cached
|
|
166
|
+
|
|
167
|
+
with _adapter_lock:
|
|
168
|
+
cached = _adapters.get(type_)
|
|
169
|
+
if cached is not None:
|
|
170
|
+
return cached
|
|
171
|
+
|
|
172
|
+
if len(_adapters) >= _ADAPTER_CACHE_MAX:
|
|
173
|
+
_adapters.clear()
|
|
174
|
+
|
|
175
|
+
adapter: TypeAdapter[T] = TypeAdapter(type_)
|
|
176
|
+
_adapters[type_] = adapter
|
|
177
|
+
return adapter
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""The authentication layer's curated surface: its models, its contract, its composites.
|
|
2
|
+
|
|
3
|
+
The route the runtime facade and ``raw_client`` take, so neither needs to know how this package is laid
|
|
4
|
+
out. What only ``schemes/`` needs -- ``ScopeT``, ``OAuth2Credentials``, ``CredentialsPlacement``,
|
|
5
|
+
``OAuthParams`` and ``scope_params`` -- stays behind, reached at ``models`` directly.
|
|
6
|
+
|
|
7
|
+
⚠️ **``schemes/`` is deliberately not re-exported here.** A scheme module reaches ``raw_client``, which
|
|
8
|
+
reaches this package -- so importing ``.schemes`` from this file would close that loop into an
|
|
9
|
+
``ImportError`` on whichever of the two is imported first. The facade names ``.schemes`` itself."""
|
|
10
|
+
|
|
11
|
+
from .composition import (
|
|
12
|
+
AllSchemes,
|
|
13
|
+
AnySchemes,
|
|
14
|
+
AsyncAllSchemes,
|
|
15
|
+
AsyncAnySchemes,
|
|
16
|
+
invalidate,
|
|
17
|
+
is_configured,
|
|
18
|
+
no_auth,
|
|
19
|
+
resolve_auth,
|
|
20
|
+
)
|
|
21
|
+
from .models import (
|
|
22
|
+
AuthParams,
|
|
23
|
+
OAuthError,
|
|
24
|
+
OAuthProviderError,
|
|
25
|
+
OAuthToken,
|
|
26
|
+
OAuthTokenRefreshable,
|
|
27
|
+
client_secret_basic,
|
|
28
|
+
client_secret_post,
|
|
29
|
+
oauth_error_response,
|
|
30
|
+
)
|
|
31
|
+
from .protocols import (
|
|
32
|
+
AsyncAuthScheme,
|
|
33
|
+
AsyncRefreshableTokenSource,
|
|
34
|
+
AsyncTokenSource,
|
|
35
|
+
AuthScheme,
|
|
36
|
+
RefreshableTokenSource,
|
|
37
|
+
RevocableAuthScheme,
|
|
38
|
+
TokenSource,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
# What one scheme contributes to a request
|
|
43
|
+
"AuthParams",
|
|
44
|
+
# The contract
|
|
45
|
+
"AuthScheme",
|
|
46
|
+
"AsyncAuthScheme",
|
|
47
|
+
"RevocableAuthScheme",
|
|
48
|
+
# The absent credential, and composition
|
|
49
|
+
"no_auth",
|
|
50
|
+
"is_configured",
|
|
51
|
+
"resolve_auth",
|
|
52
|
+
"invalidate",
|
|
53
|
+
"AllSchemes",
|
|
54
|
+
"AnySchemes",
|
|
55
|
+
"AsyncAllSchemes",
|
|
56
|
+
"AsyncAnySchemes",
|
|
57
|
+
# Managed OAuth 2.0 -- the token source seam
|
|
58
|
+
"TokenSource",
|
|
59
|
+
"AsyncTokenSource",
|
|
60
|
+
"RefreshableTokenSource",
|
|
61
|
+
"AsyncRefreshableTokenSource",
|
|
62
|
+
# Managed OAuth 2.0 -- the provider's vocabulary
|
|
63
|
+
"OAuthToken",
|
|
64
|
+
"OAuthTokenRefreshable",
|
|
65
|
+
"OAuthProviderError",
|
|
66
|
+
"OAuthError",
|
|
67
|
+
"oauth_error_response",
|
|
68
|
+
"client_secret_basic",
|
|
69
|
+
"client_secret_post",
|
|
70
|
+
]
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""The absent credential, and the two ways an operation's ``security`` composes several schemes.
|
|
2
|
+
|
|
3
|
+
:data:`no_auth` is what an unconfigured credential becomes, and :func:`is_configured` tests identity
|
|
4
|
+
against that one instance -- never "applying it did not raise", so a *blank* credential is configured
|
|
5
|
+
and is sent. :class:`AllSchemes` and :class:`AnySchemes`, with their async twins, are OpenAPI's AND and
|
|
6
|
+
OR requirements. Neither withholds a request over a partial set: the SDK never refuses to send because
|
|
7
|
+
a credential is missing, the server decides.
|
|
8
|
+
|
|
9
|
+
All four composites carry ``invalidate``, which makes a composite itself revocable so nesting
|
|
10
|
+
propagates."""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Final
|
|
16
|
+
|
|
17
|
+
from .models import AuthParams
|
|
18
|
+
from .protocols import AsyncAuthScheme, AuthScheme, RevocableAuthScheme
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def resolve_auth(scheme: AsyncAuthScheme) -> AuthParams:
|
|
22
|
+
"""The async client's counterpart to a plain ``scheme.apply()``.
|
|
23
|
+
|
|
24
|
+
Narrowing is on :class:`AuthParams` rather than on awaitability, so the awaited branch keeps its
|
|
25
|
+
precise type instead of widening to ``Any``.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
scheme: The scheme to apply, whether or not it needs I/O to produce its parameters.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
What the scheme contributes to this request."""
|
|
32
|
+
applied = scheme.apply()
|
|
33
|
+
return applied if isinstance(applied, AuthParams) else await applied
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class _NoAuth:
|
|
38
|
+
"""The absent credential: a scheme that contributes nothing.
|
|
39
|
+
|
|
40
|
+
Private so that :func:`is_configured` can test identity against the one instance below; a second
|
|
41
|
+
instance would be indistinguishable from a configured scheme by that test."""
|
|
42
|
+
|
|
43
|
+
def apply(self) -> AuthParams:
|
|
44
|
+
return AuthParams()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
no_auth: Final[AuthScheme] = _NoAuth()
|
|
48
|
+
"""What the client writes when a credential is absent, and ``execute``'s default.
|
|
49
|
+
|
|
50
|
+
An endpoint whose operation declares no ``security`` omits ``auth=`` entirely and takes this. The SDK
|
|
51
|
+
never refuses to send a request because a credential is missing -- the server decides."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_configured(scheme: AuthScheme | AsyncAuthScheme) -> bool:
|
|
55
|
+
"""Whether a credential was supplied for ``scheme``.
|
|
56
|
+
|
|
57
|
+
Identity against the one null object, never "applying it did not raise". A *blank* credential is
|
|
58
|
+
configured and is sent: ``None`` is the only sentinel.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
scheme: The scheme to test, a caller's own included.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
``True`` unless this is the null object :data:`no_auth`."""
|
|
65
|
+
return scheme is not no_auth
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def invalidate(scheme: AuthScheme | AsyncAuthScheme) -> None:
|
|
69
|
+
"""Drop whatever ``scheme`` cached, if it caches anything at all.
|
|
70
|
+
|
|
71
|
+
A no-op for every static scheme, so on an API with no managed OAuth the 401 branch in ``execute``
|
|
72
|
+
costs one ``isinstance`` and nothing else.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
scheme: The scheme a 401 was received against."""
|
|
76
|
+
if isinstance(scheme, RevocableAuthScheme):
|
|
77
|
+
scheme.invalidate()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AllSchemes:
|
|
81
|
+
"""AND-composition: every configured member applies.
|
|
82
|
+
|
|
83
|
+
An unconfigured member contributes nothing and drops out on its own; the request still goes out
|
|
84
|
+
with whatever *was* configured. This layer never withholds a request over a partial set."""
|
|
85
|
+
|
|
86
|
+
__slots__ = ("_schemes",)
|
|
87
|
+
|
|
88
|
+
def __init__(self, *schemes: AuthScheme) -> None:
|
|
89
|
+
self._schemes = schemes
|
|
90
|
+
|
|
91
|
+
def apply(self) -> AuthParams:
|
|
92
|
+
applied = [scheme.apply() for scheme in self._schemes]
|
|
93
|
+
return AuthParams(
|
|
94
|
+
headers=tuple(p for a in applied for p in a.headers),
|
|
95
|
+
query_params=tuple(p for a in applied for p in a.query_params),
|
|
96
|
+
cookies=tuple(p for a in applied for p in a.cookies),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def invalidate(self) -> None:
|
|
100
|
+
"""Drop every member's cached credential, via the module-level :func:`invalidate`.
|
|
101
|
+
|
|
102
|
+
Having this makes a composite itself revocable, so nesting one inside another propagates."""
|
|
103
|
+
for scheme in self._schemes:
|
|
104
|
+
invalidate(scheme)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class AnySchemes:
|
|
108
|
+
"""OR-composition: the first *configured* member applies.
|
|
109
|
+
|
|
110
|
+
Selection is on credential presence, never on "applying it did not fail" -- an unconfigured member
|
|
111
|
+
listed ahead of a configured one must not win and send the request unauthenticated. With nothing
|
|
112
|
+
configured the request goes out unauthenticated, as a single unconfigured scheme would."""
|
|
113
|
+
|
|
114
|
+
__slots__ = ("_schemes",)
|
|
115
|
+
|
|
116
|
+
def __init__(self, *schemes: AuthScheme) -> None:
|
|
117
|
+
self._schemes = schemes
|
|
118
|
+
|
|
119
|
+
def apply(self) -> AuthParams:
|
|
120
|
+
for scheme in self._schemes:
|
|
121
|
+
if is_configured(scheme):
|
|
122
|
+
return scheme.apply()
|
|
123
|
+
return AuthParams()
|
|
124
|
+
|
|
125
|
+
def invalidate(self) -> None:
|
|
126
|
+
"""Drop every member's cached credential; see :meth:`AllSchemes.invalidate`.
|
|
127
|
+
|
|
128
|
+
Over-invalidates on purpose: which arm won the last :meth:`apply` is not tracked, and tracking
|
|
129
|
+
it would mean per-request state on an object every request shares."""
|
|
130
|
+
for scheme in self._schemes:
|
|
131
|
+
invalidate(scheme)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class AsyncAllSchemes:
|
|
135
|
+
""":class:`AllSchemes` for the asynchronous client: a member may await its credential."""
|
|
136
|
+
|
|
137
|
+
__slots__ = ("_schemes",)
|
|
138
|
+
|
|
139
|
+
def __init__(self, *schemes: AsyncAuthScheme) -> None:
|
|
140
|
+
self._schemes = schemes
|
|
141
|
+
|
|
142
|
+
async def apply(self) -> AuthParams:
|
|
143
|
+
applied = [await resolve_auth(scheme) for scheme in self._schemes]
|
|
144
|
+
return AuthParams(
|
|
145
|
+
headers=tuple(p for a in applied for p in a.headers),
|
|
146
|
+
query_params=tuple(p for a in applied for p in a.query_params),
|
|
147
|
+
cookies=tuple(p for a in applied for p in a.cookies),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def invalidate(self) -> None:
|
|
151
|
+
"""See :meth:`AllSchemes.invalidate`. Not a coroutine: dropping a cache never awaits."""
|
|
152
|
+
for scheme in self._schemes:
|
|
153
|
+
invalidate(scheme)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class AsyncAnySchemes:
|
|
157
|
+
""":class:`AnySchemes` for the asynchronous client: the chosen member may await its credential.
|
|
158
|
+
|
|
159
|
+
Selection still happens without I/O, so only the one member that wins is ever resolved."""
|
|
160
|
+
|
|
161
|
+
__slots__ = ("_schemes",)
|
|
162
|
+
|
|
163
|
+
def __init__(self, *schemes: AsyncAuthScheme) -> None:
|
|
164
|
+
self._schemes = schemes
|
|
165
|
+
|
|
166
|
+
async def apply(self) -> AuthParams:
|
|
167
|
+
for scheme in self._schemes:
|
|
168
|
+
if is_configured(scheme):
|
|
169
|
+
return await resolve_auth(scheme)
|
|
170
|
+
return AuthParams()
|
|
171
|
+
|
|
172
|
+
def invalidate(self) -> None:
|
|
173
|
+
"""See :meth:`AnySchemes.invalidate`, including why it over-invalidates."""
|
|
174
|
+
for scheme in self._schemes:
|
|
175
|
+
invalidate(scheme)
|