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,186 @@
1
+ """Turning a :class:`UrlTemplate` plus parameters into a request URL.
2
+
3
+ Six pure functions in four groups. :func:`resolve_params` combines the parameters an API applies
4
+ to every request with the ones a single call declares; :func:`render_segments` turns each of those
5
+ into the URL segments it contributes, reading its declared type; :func:`build_url` then renders the
6
+ result, with server variables filling the base URL's placeholders, path parameters filling the
7
+ path's, and query parameters flattened and encoded onto the end. Resolution stays separate from
8
+ rendering so each can be tested on its own, and none of the six holds state -- there is nothing to
9
+ configure, so there is nothing to construct.
10
+
11
+ :func:`strip_query` is the one that unbuilds rather than builds: the form of a URL that may be
12
+ printed. It lives here because it is a fact about a URL's shape, and it has more than one reader."""
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from collections.abc import Mapping, Sequence
18
+ from typing import Any, Final
19
+ from urllib.parse import quote, urlencode
20
+
21
+ from ..params import Param, UrlTemplate
22
+ from .flattening import flatten
23
+ from .wire import text_values
24
+
25
+ _PLACEHOLDER: Final = re.compile(r"\{([^{}]+)\}")
26
+
27
+
28
+ def expand_template(template: str, segments: Mapping[str, tuple[str, ...]]) -> str:
29
+ """Substitute every ``{placeholder}`` in ``template`` from ``segments``.
30
+
31
+ Each value is the sequence of *unencoded* members its placeholder expands from. Members are
32
+ percent-encoded with no safe characters and joined with ``,``, so a collection fills one path
33
+ segment rather than several -- OpenAPI's ``style: simple`` with ``explode: false``, the default
34
+ for a path parameter, and the same rule ``headers.py`` applies to a header array.
35
+
36
+ Encoding happens per member, *before* the join, which is what keeps the two commas apart: the
37
+ separator is written here and stays literal (a sub-delimiter under RFC 3986), while a comma
38
+ carried by a value is encoded to ``%2C`` along with everything else non-unreserved. A value
39
+ still cannot inject a path separator, for the same reason.
40
+
41
+ Any ``{name}`` is a placeholder -- the braces delimit it, so the name is not restricted to word
42
+ characters and a kebab-case or dotted path parameter resolves like any other. A name with no
43
+ segment raises rather than passing through: unencoded braces are not valid in a URL anyway.
44
+
45
+ Deciding what a value's members *are* belongs to :func:`render_segments`, which is the only
46
+ thing holding a declared type. This function therefore has one rule rather than four, and both
47
+ the join and the encoding live in exactly one place.
48
+
49
+ Args:
50
+ template: The URL or path holding the ``{placeholder}`` tokens.
51
+ segments: Each placeholder name mapped to its unencoded members.
52
+
53
+ Returns:
54
+ ``template`` with every placeholder substituted, each member percent-encoded.
55
+
56
+ Raises:
57
+ ValueError: If ``template`` is empty, or names a placeholder ``segments`` has no entry
58
+ for."""
59
+ if not template:
60
+ raise ValueError("URL template must not be empty")
61
+
62
+ def replace(match: re.Match[str]) -> str:
63
+ key = match.group(1)
64
+ if key not in segments:
65
+ raise ValueError(f"Missing template parameter: {key}")
66
+ return ",".join(quote(segment, safe="") for segment in segments[key])
67
+
68
+ return _PLACEHOLDER.sub(replace, template)
69
+
70
+
71
+ def render_segments(params: Sequence[Param[Any]] | None) -> dict[str, tuple[str, ...]]:
72
+ """Render each parameter into the URL segments its ``{placeholder}`` expands to.
73
+
74
+ Serves a path parameter and a base URL's server variable alike -- both fill a placeholder and
75
+ both name the type they were declared as, so there is one rule rather than two.
76
+
77
+ Members, not finished text: :func:`expand_template` owns the ``,`` join and the
78
+ percent-encoding, so neither is written twice and no value can inject a path separator.
79
+
80
+ Args:
81
+ params: The path parameters or server variables; ``None`` is treated as none at all.
82
+
83
+ Returns:
84
+ Each parameter's key mapped to the unencoded members its placeholder expands to.
85
+
86
+ Raises:
87
+ ValueError: If a parameter contributes no value at all -- a placeholder cannot be left
88
+ unfilled -- or if its value does not match its declared type."""
89
+ rendered: dict[str, tuple[str, ...]] = {}
90
+ for param in params or ():
91
+ segments = text_values(param)
92
+ if not segments:
93
+ raise ValueError(f"URL parameter has no value: {param.key}")
94
+ rendered[param.key] = segments
95
+ return rendered
96
+
97
+
98
+ def encode_query(params: Sequence[Param[Any]] | None) -> str:
99
+ """Flatten ``params`` and percent-encode them into a query string (no leading ``?``).
100
+
101
+ Args:
102
+ params: The query parameters; ``None`` is treated as none at all.
103
+
104
+ Returns:
105
+ The encoded query string, empty when nothing was contributed.
106
+
107
+ Raises:
108
+ ValueError: If a parameter's value does not match its declared type."""
109
+ return urlencode(flatten(params))
110
+
111
+
112
+ def resolve_params(
113
+ global_params: Sequence[Param[Any]], call_params: Sequence[Param[Any]] | None
114
+ ) -> tuple[Param[Any], ...]:
115
+ """Return the parameters for one call, the call's winning per key.
116
+
117
+ One function for both kinds that need it: a path parameter and a query parameter are the same
118
+ type, so they are the same rule -- a call's parameter replaces a global of the same key but
119
+ keeps the global's position, so what an endpoint produces does not depend on which side supplied
120
+ a parameter. Keys are unique within either side -- a collection is carried by a single
121
+ :class:`Param` and expanded by its destination, never by repeating the key -- so replacing
122
+ rather than appending is unambiguous.
123
+
124
+ Both arguments share a type, so the precedence lives in their names rather than in the function
125
+ name: ``global_params`` is what the API prescribes, ``call_params`` what this one call declares.
126
+
127
+ Headers resolve separately, since they merge case-insensitively -- which ``render_headers`` gets
128
+ by lowercasing as it renders. Placeholders in a *base URL* are server variables, carried on the
129
+ :class:`UrlTemplate` itself and rendered by :func:`build_url`.
130
+
131
+ Args:
132
+ global_params: What the API prescribes for every call.
133
+ call_params: What this one call declares; wins per key.
134
+
135
+ Returns:
136
+ The effective parameters, each key once, in the order the global set established."""
137
+ by_key = {param.key: param for param in (*global_params, *(call_params or ()))}
138
+ return tuple(by_key.values())
139
+
140
+
141
+ def build_url(
142
+ url_template: UrlTemplate,
143
+ *,
144
+ path_params: Sequence[Param[Any]] | None = None,
145
+ query_params: Sequence[Param[Any]] | None = None,
146
+ ) -> str:
147
+ """Resolve ``url_template`` into a complete request URL.
148
+
149
+ Args:
150
+ url_template: The unresolved URL, carrying its base URL, path and server variables.
151
+ path_params: The call's path parameters.
152
+ query_params: The call's query parameters.
153
+
154
+ Returns:
155
+ The absolute URL, base and path joined on exactly one ``/``, query appended when non-empty.
156
+
157
+ Raises:
158
+ ValueError: If a placeholder is unfilled or a parameter's value does not match its
159
+ declared type."""
160
+ base = expand_template(url_template.base_url, render_segments(url_template.variables))
161
+ path = expand_template(url_template.path, render_segments(path_params))
162
+
163
+ # Join on exactly one separator. Plain urljoin would be wrong here: it treats the base as a
164
+ # document reference and discards its last path segment, so a base of ".../v2" with path
165
+ # "users" would resolve to ".../users", silently dropping the version.
166
+ url = f"{base.rstrip('/')}/{path.lstrip('/')}"
167
+
168
+ query = encode_query(query_params)
169
+ return f"{url}?{query}" if query else url
170
+
171
+
172
+ def strip_query(url: str) -> str:
173
+ """Return ``url`` without its query string -- the form of it that may be printed.
174
+
175
+ A query-placed api key lives in that string, folded onto the URL by :func:`resolve_params` like
176
+ any other query parameter, so no diagnostic carries one: not a request's repr, not the warning
177
+ naming a response nobody closed. A string form rather than redaction -- the URL a request is
178
+ sent to is never altered, only what is said about it afterwards.
179
+
180
+ Args:
181
+ url: The URL to describe.
182
+
183
+ Returns:
184
+ Everything before the first ``?``, or the whole of ``url`` when it has none."""
185
+ path, _, _ = url.partition("?")
186
+ return path
@@ -0,0 +1,198 @@
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) -> str:
165
+ """JSON-encode a structured value with no insignificant whitespace.
166
+
167
+ ``ensure_ascii`` stays at its default: a header field value is ISO-8859-1 by RFC 9110 and the
168
+ transport rejects anything outside it, so escaping non-ASCII is the only encoding that
169
+ survives. ``allow_nan`` is turned off: at its default a non-finite float encodes as
170
+ ``Infinity`` or ``NaN`` -- a Python dialect no RFC 8259 parser accepts -- so the call would
171
+ fail server-side, far from the line that produced the value.
172
+
173
+ Args:
174
+ value: The structured value to encode.
175
+
176
+ Returns:
177
+ Its JSON text, non-ASCII escaped and no insignificant whitespace.
178
+
179
+ Raises:
180
+ ValueError: If ``value`` holds a non-finite float, which has no RFC 8259 spelling."""
181
+ return json.dumps(value, separators=(",", ":"), allow_nan=False)
182
+
183
+
184
+ def is_collection(value: object) -> TypeGuard[Iterable[object]]:
185
+ """True for an iterable that should expand into several wire values.
186
+
187
+ ``TypeGuard``, deliberately not the newer ``TypeIs``: ``TypeIs`` also narrows the *negative*
188
+ branch, which would be unsound here -- ``str`` is ``Iterable`` yet this returns ``False`` for
189
+ it, so a checker told "not an ``Iterable``" would wrongly rule out strings. ``TypeGuard``
190
+ narrows only where this returns ``True``, which is exactly what the caller needs to iterate.
191
+
192
+ Args:
193
+ value: The dumped value to classify.
194
+
195
+ Returns:
196
+ ``True`` for an iterable that expands into several wire values -- a ``str``, ``bytes`` or
197
+ ``Mapping`` is one value, not several."""
198
+ 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
+ ]