cliV1 0.0.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cliv1-0.0.8.dist-info/METADATA +168 -0
- cliv1-0.0.8.dist-info/RECORD +87 -0
- cliv1-0.0.8.dist-info/WHEEL +4 -0
- cliv1-0.0.8.dist-info/licenses/LICENSE +21 -0
- swagger_petstore_open_api_3_0/__init__.py +15 -0
- swagger_petstore_open_api_3_0/apis/__init__.py +5 -0
- swagger_petstore_open_api_3_0/apis/pet_api.py +883 -0
- swagger_petstore_open_api_3_0/apis/store.py +413 -0
- swagger_petstore_open_api_3_0/apis/user_api.py +794 -0
- swagger_petstore_open_api_3_0/async_client.py +79 -0
- swagger_petstore_open_api_3_0/auth.py +17 -0
- swagger_petstore_open_api_3_0/base_client.py +18 -0
- swagger_petstore_open_api_3_0/client.py +77 -0
- swagger_petstore_open_api_3_0/core/__init__.py +358 -0
- swagger_petstore_open_api_3_0/core/_internal/__init__.py +9 -0
- swagger_petstore_open_api_3_0/core/_internal/content_disposition.py +154 -0
- swagger_petstore_open_api_3_0/core/_internal/flattening.py +139 -0
- swagger_petstore_open_api_3_0/core/_internal/headers.py +144 -0
- swagger_petstore_open_api_3_0/core/_internal/sse_parser.py +124 -0
- swagger_petstore_open_api_3_0/core/_internal/subscripts.py +50 -0
- swagger_petstore_open_api_3_0/core/_internal/urls.py +186 -0
- swagger_petstore_open_api_3_0/core/_internal/wire.py +198 -0
- swagger_petstore_open_api_3_0/core/adapters.py +177 -0
- swagger_petstore_open_api_3_0/core/auth/__init__.py +70 -0
- swagger_petstore_open_api_3_0/core/auth/composition.py +175 -0
- swagger_petstore_open_api_3_0/core/auth/models.py +216 -0
- swagger_petstore_open_api_3_0/core/auth/protocols.py +103 -0
- swagger_petstore_open_api_3_0/core/auth/schemes/__init__.py +94 -0
- swagger_petstore_open_api_3_0/core/auth/schemes/api_key_auth.py +53 -0
- swagger_petstore_open_api_3_0/core/auth/schemes/basic_auth.py +73 -0
- swagger_petstore_open_api_3_0/core/auth/schemes/bearer_auth.py +21 -0
- swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_authorization_code.py +393 -0
- swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_client_credentials.py +127 -0
- swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_password.py +142 -0
- swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_schemes.py +300 -0
- swagger_petstore_open_api_3_0/core/base_raw_response.py +49 -0
- swagger_petstore_open_api_3_0/core/bodies.py +550 -0
- swagger_petstore_open_api_3_0/core/converters/__init__.py +68 -0
- swagger_petstore_open_api_3_0/core/converters/date_time.py +237 -0
- swagger_petstore_open_api_3_0/core/converters/encoded_bytes.py +273 -0
- swagger_petstore_open_api_3_0/core/converters/open_enum.py +35 -0
- swagger_petstore_open_api_3_0/core/decoding.py +498 -0
- swagger_petstore_open_api_3_0/core/discriminators.py +129 -0
- swagger_petstore_open_api_3_0/core/event_streams.py +225 -0
- swagger_petstore_open_api_3_0/core/exceptions.py +38 -0
- swagger_petstore_open_api_3_0/core/file_responses.py +335 -0
- swagger_petstore_open_api_3_0/core/files.py +173 -0
- swagger_petstore_open_api_3_0/core/httpx_transport.py +748 -0
- swagger_petstore_open_api_3_0/core/models.py +143 -0
- swagger_petstore_open_api_3_0/core/optionality.py +130 -0
- swagger_petstore_open_api_3_0/core/params.py +157 -0
- swagger_petstore_open_api_3_0/core/raw_client.py +338 -0
- swagger_petstore_open_api_3_0/core/request_options.py +94 -0
- swagger_petstore_open_api_3_0/core/results.py +123 -0
- swagger_petstore_open_api_3_0/core/runtime_env.py +43 -0
- swagger_petstore_open_api_3_0/core/servers.py +33 -0
- swagger_petstore_open_api_3_0/core/transport.py +191 -0
- swagger_petstore_open_api_3_0/errors/__init__.py +48 -0
- swagger_petstore_open_api_3_0/errors/add_pet_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/delete_order_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/delete_pet_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/delete_user_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/find_pets_by_status_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/find_pets_by_tags_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/get_order_by_id_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/get_pet_by_id_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/get_user_by_name_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/login_user_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/place_order_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/update_pet_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/update_pet_with_form_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/update_user_error.py +21 -0
- swagger_petstore_open_api_3_0/errors/upload_file_error.py +21 -0
- swagger_petstore_open_api_3_0/models/__init__.py +23 -0
- swagger_petstore_open_api_3_0/models/api_response.py +18 -0
- swagger_petstore_open_api_3_0/models/category.py +15 -0
- swagger_petstore_open_api_3_0/models/enums/__init__.py +4 -0
- swagger_petstore_open_api_3_0/models/enums/order_status.py +17 -0
- swagger_petstore_open_api_3_0/models/enums/pet_status.py +17 -0
- swagger_petstore_open_api_3_0/models/order.py +27 -0
- swagger_petstore_open_api_3_0/models/pet.py +28 -0
- swagger_petstore_open_api_3_0/models/tag_model.py +15 -0
- swagger_petstore_open_api_3_0/models/user.py +29 -0
- swagger_petstore_open_api_3_0/py.typed +0 -0
- swagger_petstore_open_api_3_0/server/__init__.py +21 -0
- swagger_petstore_open_api_3_0/server/server.py +17 -0
- swagger_petstore_open_api_3_0/server/server_config.py +52 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""The seams every endpoint crosses: build a request, send it, shape a result.
|
|
2
|
+
|
|
3
|
+
One pipeline, two flavors, one seam. :class:`BaseRawClient` holds the half that does not depend
|
|
4
|
+
on concurrency -- resolving the API-wide, endpoint and caller parameter layers into a URL and
|
|
5
|
+
headers -- and each concrete client adds only the transport-touching ``execute``. Both flavors spell
|
|
6
|
+
it identically, so which flavor is in play is carried by the client a caller holds rather than by a
|
|
7
|
+
suffix on the method.
|
|
8
|
+
|
|
9
|
+
There is no ``_build_result`` beside ``_build_request`` any more. Shaping a result now needs the
|
|
10
|
+
response *and* its lifetime in the same place -- the 2xx return has to escape without closing, while
|
|
11
|
+
every other path closes -- and a static helper that could be handed a connection is one that can
|
|
12
|
+
leak it. The decoder owns the body it is given (ADR-0063), so what is left here is one status
|
|
13
|
+
branch, written out in each flavor because the two await differently.
|
|
14
|
+
|
|
15
|
+
The two live in one module, with no ``async_raw_client.py`` beside it, because they are a peer pair:
|
|
16
|
+
the handful of ``await``s they differ by are visible on one screen, which is what keeps the sync and
|
|
17
|
+
async paths from drifting apart."""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Any, Generic, TypeVar
|
|
24
|
+
|
|
25
|
+
from ._internal.headers import resolve_headers
|
|
26
|
+
from ._internal.urls import build_url, resolve_params
|
|
27
|
+
from .auth import AsyncAuthScheme, AuthParams, AuthScheme, invalidate, no_auth, resolve_auth
|
|
28
|
+
from .bodies import RequestBody
|
|
29
|
+
from .decoding import AsyncResponseDecoder, ErrorMapper, ResponseDecoder
|
|
30
|
+
from .params import Param, UrlTemplate
|
|
31
|
+
from .request_options import RequestOptions, RequestOptionsOrDict
|
|
32
|
+
from .results import ApiResult, Failure, Success
|
|
33
|
+
from .transport import (
|
|
34
|
+
AsyncHttpClient,
|
|
35
|
+
HttpClient,
|
|
36
|
+
HttpRequest,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
T = TypeVar("T")
|
|
40
|
+
E = TypeVar("E")
|
|
41
|
+
TransportT = TypeVar("TransportT", bound=HttpClient | AsyncHttpClient)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class BaseRawClient(Generic[TransportT]):
|
|
46
|
+
"""Shared request-building and result-shaping for the raw clients.
|
|
47
|
+
|
|
48
|
+
Holds the one transport its concurrency flavor can actually use -- the type
|
|
49
|
+
parameter is what makes :class:`RawClient` provably sync and
|
|
50
|
+
:class:`AsyncRawClient` provably async, so neither can be handed the other's
|
|
51
|
+
transport. Subclasses add only the ``execute`` seam, which each writes out in its
|
|
52
|
+
own flavor because the two await differently; the pipeline below is not duplicated
|
|
53
|
+
and the sync/async boundary is never crossed.
|
|
54
|
+
|
|
55
|
+
The three ``global_*`` fields carry the parameters the API applies to *every*
|
|
56
|
+
request. They are derived from the API description, not from a caller: whoever
|
|
57
|
+
generates this SDK writes them here, and an API that declares none leaves them
|
|
58
|
+
empty. Each is combined with a single call's own parameters by the matching
|
|
59
|
+
``resolve_*`` function, so this class decides nothing about precedence -- it only
|
|
60
|
+
says which two sides go in. All three are the same shape -- a sequence of
|
|
61
|
+
parameters, each naming the type it was declared as -- which is what lets one
|
|
62
|
+
``param`` factory serve every location a request parameter can occupy, and what
|
|
63
|
+
makes a wrongly-typed API-wide header a build failure rather than a wire bug.
|
|
64
|
+
|
|
65
|
+
A single call's ``request_options`` is the third source: not what the API
|
|
66
|
+
prescribes and not what the endpoint declares, but what the caller asked for this
|
|
67
|
+
once. It is validated here, at the one place a request is built, so no emitted
|
|
68
|
+
endpoint ever inspects an option.
|
|
69
|
+
|
|
70
|
+
Authentication is a fourth source, and the full order for both headers and query parameters is::
|
|
71
|
+
|
|
72
|
+
global_* -> the endpoint's own -> auth -> the caller's extra_headers
|
|
73
|
+
|
|
74
|
+
Auth outranks the endpoint's own parameters because an operation parameter must not be able to
|
|
75
|
+
clobber a credential, and loses to ``extra_headers`` because that is how a caller deliberately
|
|
76
|
+
overrides one -- or blanks it, for a call meant to go out anonymous. Note that ``_build_request``
|
|
77
|
+
takes already-resolved :class:`~.auth.AuthParams`, not a scheme: resolving a scheme may require
|
|
78
|
+
I/O, so it happens in ``execute``, which is the one place the two flavors already differ.
|
|
79
|
+
|
|
80
|
+
A **401 invalidates whatever the scheme cached**, so a credential the server revoked ahead of its
|
|
81
|
+
own expiry is re-obtained on the next call rather than resent until it times out. For every scheme
|
|
82
|
+
that holds a constant this is one ``isinstance`` and nothing more. The failing request is
|
|
83
|
+
deliberately **not** retried: a caller sees one 401 and then recovery, which keeps this seam free
|
|
84
|
+
of the retry policy ADR-0001 rules out. Like scheme resolution, it lives in ``execute`` --
|
|
85
|
+
``_build_request`` never sees the scheme, only the params it already resolved to."""
|
|
86
|
+
|
|
87
|
+
http_client: TransportT
|
|
88
|
+
global_headers: Sequence[Param[Any]] = ()
|
|
89
|
+
global_query_params: Sequence[Param[Any]] = ()
|
|
90
|
+
global_path_params: Sequence[Param[Any]] = ()
|
|
91
|
+
|
|
92
|
+
def _build_request(
|
|
93
|
+
self,
|
|
94
|
+
*,
|
|
95
|
+
http_method: str,
|
|
96
|
+
url_template: UrlTemplate,
|
|
97
|
+
path_params: Sequence[Param[Any]] | None,
|
|
98
|
+
query_params: Sequence[Param[Any]] | None,
|
|
99
|
+
headers: Sequence[Param[Any]] | None,
|
|
100
|
+
body: RequestBody | None,
|
|
101
|
+
auth: AuthParams,
|
|
102
|
+
request_options: RequestOptionsOrDict | None,
|
|
103
|
+
) -> HttpRequest:
|
|
104
|
+
options = RequestOptions.coerce(request_options)
|
|
105
|
+
return HttpRequest(
|
|
106
|
+
method=http_method,
|
|
107
|
+
url=build_url(
|
|
108
|
+
url_template,
|
|
109
|
+
path_params=resolve_params(self.global_path_params, path_params),
|
|
110
|
+
query_params=resolve_params(self.global_query_params, (*(query_params or ()), *auth.query_params)),
|
|
111
|
+
),
|
|
112
|
+
headers=resolve_headers(
|
|
113
|
+
self.global_headers,
|
|
114
|
+
(*(headers or ()), *auth.request_headers()),
|
|
115
|
+
extra_headers=options.extra_headers,
|
|
116
|
+
),
|
|
117
|
+
body=body,
|
|
118
|
+
timeout=options.timeout,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True, slots=True)
|
|
123
|
+
class RawClient(BaseRawClient[HttpClient]):
|
|
124
|
+
"""Synchronous raw client: builds the request, sends it, shapes the result."""
|
|
125
|
+
|
|
126
|
+
def execute(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
http_method: str,
|
|
130
|
+
url_template: UrlTemplate,
|
|
131
|
+
path_params: Sequence[Param[Any]] | None = None,
|
|
132
|
+
query_params: Sequence[Param[Any]] | None = None,
|
|
133
|
+
headers: Sequence[Param[Any]] | None = None,
|
|
134
|
+
body: RequestBody | None = None,
|
|
135
|
+
auth_scheme: AuthScheme = no_auth,
|
|
136
|
+
decoder: ResponseDecoder[T],
|
|
137
|
+
error_mapper: ErrorMapper[E],
|
|
138
|
+
request_options: RequestOptionsOrDict | None = None,
|
|
139
|
+
) -> ApiResult[T, E]:
|
|
140
|
+
"""Send a request and shape the response into a result.
|
|
141
|
+
|
|
142
|
+
Returns once the response head has arrived. What happens to the body from there is the
|
|
143
|
+
``decoder``'s decision, not this method's: a buffered one reads it, releases the connection
|
|
144
|
+
and parses what it read; ``file_decoder`` hands the open connection to the
|
|
145
|
+
:class:`FileResponse` it builds. Either way ``T`` is solved from that argument rather than
|
|
146
|
+
from the declared return type, and this method owns no connection on the success path.
|
|
147
|
+
|
|
148
|
+
Every operation names a decoder -- an empty 2xx body is ``empty_response``, not an omitted
|
|
149
|
+
argument -- so there is no "was one supplied?" branch and no empty-body special case. An
|
|
150
|
+
operation that declares a payload and receives no body raises out of its decoder rather than
|
|
151
|
+
quietly succeeding with ``None``, which is the rule every other response-deserialization
|
|
152
|
+
failure already follows.
|
|
153
|
+
|
|
154
|
+
A non-2xx body is read in full and the connection released *before* the mapper runs, so a
|
|
155
|
+
:class:`Failure` never carries a lifetime obligation -- even when the mapper itself raises.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
http_method: The HTTP verb to send.
|
|
159
|
+
url_template: The unresolved URL for the configured environment.
|
|
160
|
+
path_params: The endpoint's path parameters, if any.
|
|
161
|
+
query_params: The endpoint's query parameters, if any.
|
|
162
|
+
headers: The endpoint's header parameters, if any.
|
|
163
|
+
body: The request body, already wire-ready, or ``None``.
|
|
164
|
+
auth_scheme: The scheme the operation declares; ``no_auth`` when it declares none.
|
|
165
|
+
decoder: The operation's 2xx decoder; ``empty_response`` when it declares no body,
|
|
166
|
+
``file_decoder`` when the body is a file.
|
|
167
|
+
error_mapper: The operation's error mapper; ``raw_error_response`` when it declares
|
|
168
|
+
no error schemas.
|
|
169
|
+
request_options: The caller's per-call overrides, if any.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
A :class:`Success` carrying the decoded payload for a 2xx status, otherwise a
|
|
173
|
+
:class:`Failure` carrying the decoded error body.
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
ValueError: If the body does not deserialize. A deserialization failure is not an API
|
|
177
|
+
error, so it propagates in both response modes rather than becoming a failure."""
|
|
178
|
+
request = self._build_request(
|
|
179
|
+
http_method=http_method,
|
|
180
|
+
url_template=url_template,
|
|
181
|
+
path_params=path_params,
|
|
182
|
+
query_params=query_params,
|
|
183
|
+
headers=headers,
|
|
184
|
+
body=body,
|
|
185
|
+
auth=auth_scheme.apply(),
|
|
186
|
+
request_options=request_options,
|
|
187
|
+
)
|
|
188
|
+
response = self.http_client.send(request)
|
|
189
|
+
# The head, once, on arrival -- a fact as soon as the response exists. Read here, no path
|
|
190
|
+
# below has to touch a response it has already closed or handed to a decoder. Spelled
|
|
191
|
+
# response_headers because ``headers`` in this scope is the endpoint's own request params.
|
|
192
|
+
status_code, response_headers = response.status_code, dict(response.headers)
|
|
193
|
+
try:
|
|
194
|
+
if status_code == 401:
|
|
195
|
+
invalidate(auth_scheme)
|
|
196
|
+
if 200 <= status_code < 300:
|
|
197
|
+
# The 2xx return leaves before the read below: the body now belongs to the decoder,
|
|
198
|
+
# which either reads and releases it or hands it to the payload it builds. A decoder
|
|
199
|
+
# that raises instead is caught below, which is why nothing on this line has to be
|
|
200
|
+
# total. The handler below must never become a finally -- it would close the
|
|
201
|
+
# connection a FileResponse was just handed, on every download and silently.
|
|
202
|
+
return Success(
|
|
203
|
+
payload=decoder.decode(response),
|
|
204
|
+
status_code=status_code,
|
|
205
|
+
headers=response_headers,
|
|
206
|
+
)
|
|
207
|
+
# Read once: re-reading a consumed stream raises, and read is what both releases
|
|
208
|
+
# the connection and preserves the bytes.
|
|
209
|
+
content = response.read()
|
|
210
|
+
response.close()
|
|
211
|
+
# Mapped AFTER the close, so a mapper raise cannot leak -- a Failure never holds a
|
|
212
|
+
# connection. Such a raise re-enters the handler below and closes a second time, which
|
|
213
|
+
# is a no-op: HttpResponse.close is required to be idempotent, and the 2xx decoder-raise
|
|
214
|
+
# path already relies on that.
|
|
215
|
+
return Failure(
|
|
216
|
+
error=error_mapper.map(status_code, content),
|
|
217
|
+
status_code=status_code,
|
|
218
|
+
headers=response_headers,
|
|
219
|
+
)
|
|
220
|
+
except BaseException:
|
|
221
|
+
response.close()
|
|
222
|
+
raise
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@dataclass(frozen=True, slots=True)
|
|
226
|
+
class AsyncRawClient(BaseRawClient[AsyncHttpClient]):
|
|
227
|
+
"""Asynchronous raw client.
|
|
228
|
+
|
|
229
|
+
The method is named ``execute``, not ``execute_async``: sync and async peers
|
|
230
|
+
carry identical names throughout this SDK, and the transport already makes the
|
|
231
|
+
flavor unambiguous."""
|
|
232
|
+
|
|
233
|
+
async def execute(
|
|
234
|
+
self,
|
|
235
|
+
*,
|
|
236
|
+
http_method: str,
|
|
237
|
+
url_template: UrlTemplate,
|
|
238
|
+
path_params: Sequence[Param[Any]] | None = None,
|
|
239
|
+
query_params: Sequence[Param[Any]] | None = None,
|
|
240
|
+
headers: Sequence[Param[Any]] | None = None,
|
|
241
|
+
body: RequestBody | None = None,
|
|
242
|
+
auth_scheme: AsyncAuthScheme = no_auth,
|
|
243
|
+
decoder: AsyncResponseDecoder[T],
|
|
244
|
+
error_mapper: ErrorMapper[E],
|
|
245
|
+
request_options: RequestOptionsOrDict | None = None,
|
|
246
|
+
) -> ApiResult[T, E]:
|
|
247
|
+
"""Send a request and shape the response into a result.
|
|
248
|
+
|
|
249
|
+
Returns once the response head has arrived. What happens to the body from there is the
|
|
250
|
+
``decoder``'s decision, not this method's: a buffered one reads it, releases the connection
|
|
251
|
+
and parses what it read; ``file_decoder`` hands the open connection to the
|
|
252
|
+
:class:`FileResponse` it builds. Either way ``T`` is solved from that argument rather than
|
|
253
|
+
from the declared return type, and this method owns no connection on the success path.
|
|
254
|
+
|
|
255
|
+
Every operation names a decoder -- an empty 2xx body is ``empty_response``, not an omitted
|
|
256
|
+
argument -- so there is no "was one supplied?" branch and no empty-body special case. An
|
|
257
|
+
operation that declares a payload and receives no body raises out of its decoder rather than
|
|
258
|
+
quietly succeeding with ``None``, which is the rule every other response-deserialization
|
|
259
|
+
failure already follows.
|
|
260
|
+
|
|
261
|
+
A non-2xx body is read in full and the connection released *before* the mapper runs, so a
|
|
262
|
+
:class:`Failure` never carries a lifetime obligation -- even when the mapper itself raises.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
http_method: The HTTP verb to send.
|
|
266
|
+
url_template: The unresolved URL for the configured environment.
|
|
267
|
+
path_params: The endpoint's path parameters, if any.
|
|
268
|
+
query_params: The endpoint's query parameters, if any.
|
|
269
|
+
headers: The endpoint's header parameters, if any.
|
|
270
|
+
body: The request body, already wire-ready, or ``None``.
|
|
271
|
+
auth_scheme: The scheme the operation declares; ``no_auth`` when it declares none.
|
|
272
|
+
decoder: The operation's 2xx decoder; ``empty_response`` when it declares no body,
|
|
273
|
+
``file_decoder`` when the body is a file.
|
|
274
|
+
error_mapper: The operation's error mapper; ``raw_error_response`` when it declares
|
|
275
|
+
no error schemas.
|
|
276
|
+
request_options: The caller's per-call overrides, if any.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
A :class:`Success` carrying the decoded payload for a 2xx status, otherwise a
|
|
280
|
+
:class:`Failure` carrying the decoded error body.
|
|
281
|
+
|
|
282
|
+
Raises:
|
|
283
|
+
ValueError: If the body does not deserialize. A deserialization failure is not an API
|
|
284
|
+
error, so it propagates in both response modes rather than becoming a failure."""
|
|
285
|
+
request = self._build_request(
|
|
286
|
+
http_method=http_method,
|
|
287
|
+
url_template=url_template,
|
|
288
|
+
path_params=path_params,
|
|
289
|
+
query_params=query_params,
|
|
290
|
+
headers=headers,
|
|
291
|
+
body=body,
|
|
292
|
+
auth=await resolve_auth(auth_scheme),
|
|
293
|
+
request_options=request_options,
|
|
294
|
+
)
|
|
295
|
+
response = await self.http_client.send(request)
|
|
296
|
+
# The head, once, on arrival -- a fact as soon as the response exists. Read here, no path
|
|
297
|
+
# below has to touch a response it has already closed or handed to a decoder. Spelled
|
|
298
|
+
# response_headers because ``headers`` in this scope is the endpoint's own request params.
|
|
299
|
+
status_code, response_headers = response.status_code, dict(response.headers)
|
|
300
|
+
try:
|
|
301
|
+
if status_code == 401:
|
|
302
|
+
invalidate(auth_scheme)
|
|
303
|
+
if 200 <= status_code < 300:
|
|
304
|
+
# The 2xx return leaves before the read below: the body now belongs to the decoder,
|
|
305
|
+
# which either reads and releases it or hands it to the payload it builds. A decoder
|
|
306
|
+
# that raises instead is caught below, which is why nothing on this line has to be
|
|
307
|
+
# total. The handler below must never become a finally -- it would close the
|
|
308
|
+
# connection an AsyncFileResponse was just handed, on every download and silently.
|
|
309
|
+
return Success(
|
|
310
|
+
payload=await decoder.decode(response),
|
|
311
|
+
status_code=status_code,
|
|
312
|
+
headers=response_headers,
|
|
313
|
+
)
|
|
314
|
+
# Read once: re-reading a consumed stream raises, and aread is what both releases
|
|
315
|
+
# the connection and preserves the bytes.
|
|
316
|
+
content = await response.aread()
|
|
317
|
+
await response.aclose()
|
|
318
|
+
# Mapped AFTER the close, so a mapper raise cannot leak -- a Failure never holds a
|
|
319
|
+
# connection. Such a raise re-enters the handler below and closes a second time, which
|
|
320
|
+
# is a no-op: AsyncHttpResponse.aclose is required to be idempotent, and the 2xx
|
|
321
|
+
# decoder-raise path already relies on that.
|
|
322
|
+
return Failure(
|
|
323
|
+
error=error_mapper.map(status_code, content),
|
|
324
|
+
status_code=status_code,
|
|
325
|
+
headers=response_headers,
|
|
326
|
+
)
|
|
327
|
+
except BaseException:
|
|
328
|
+
await response.aclose()
|
|
329
|
+
raise
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
RawClientT = TypeVar("RawClientT", bound=RawClient | AsyncRawClient)
|
|
333
|
+
"""Fixes which raw client a generic holder carries -- ``RawClient`` or ``AsyncRawClient``.
|
|
334
|
+
|
|
335
|
+
Bounded by the two concrete clients rather than by ``BaseRawClient[Any]``: the base defines only
|
|
336
|
+
the request/result pipeline, not ``execute``, so the looser bound could not prove that a holder's
|
|
337
|
+
``self._client.execute(...)`` exists -- it type-checked only because each subclass substitutes a
|
|
338
|
+
concrete argument. This bound also removes the ``Any``."""
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Per-call request options -- the SDK's one override channel.
|
|
2
|
+
|
|
3
|
+
Every endpoint takes an optional ``request_options``, so a caller can override request-scoped
|
|
4
|
+
behaviour for a single call without re-configuring the client::
|
|
5
|
+
|
|
6
|
+
client.body_params.send_long(5, request_options={"timeout": 5.0})
|
|
7
|
+
|
|
8
|
+
One bundled parameter rather than a keyword per knob. That is what keeps the set of names a
|
|
9
|
+
generated endpoint reserves at exactly one, however many options are added later: a new option is a
|
|
10
|
+
field here and collides with nobody's parameter.
|
|
11
|
+
|
|
12
|
+
Accepted either typed or dict-shaped, the same either-spelling convention the model input companions
|
|
13
|
+
use. :meth:`RequestOptions.coerce` is the one place the dict form is resolved, so the two spellings
|
|
14
|
+
cannot produce different requests.
|
|
15
|
+
|
|
16
|
+
A validated model rather than a plain frozen dataclass, because of who supplies the value: this is
|
|
17
|
+
caller input, so its constraint and its unknown-key rejection are declared here rather than
|
|
18
|
+
hand-written, exactly as they are for the configuration models. What the runtime *builds* --
|
|
19
|
+
including the resolved timeout that reaches a transport on :class:`HttpRequest` -- stays a plain
|
|
20
|
+
dataclass field, so no model crosses the transport boundary."""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from collections.abc import Mapping
|
|
25
|
+
from typing import Final, TypeAlias
|
|
26
|
+
|
|
27
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
28
|
+
from typing_extensions import NotRequired, TypedDict
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RequestOptions(BaseModel):
|
|
32
|
+
"""Overrides that apply to a single request.
|
|
33
|
+
|
|
34
|
+
``frozen`` matches every other value type in the runtime; ``extra="forbid"`` rejects a
|
|
35
|
+
misspelled option that reached here without a type checker having seen the call."""
|
|
36
|
+
|
|
37
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
38
|
+
|
|
39
|
+
timeout: float | None = Field(default=None, gt=0)
|
|
40
|
+
"""Seconds to wait for this one request. ``None`` leaves the client's own timeout in force."""
|
|
41
|
+
|
|
42
|
+
extra_headers: Mapping[str, str] | None = None
|
|
43
|
+
"""Headers added to this one request, winning over both the API's and the endpoint's own."""
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def coerce(cls, options: RequestOptionsOrDict | None) -> RequestOptions:
|
|
47
|
+
"""Return ``options`` as a :class:`RequestOptions`, dict-shaped input included.
|
|
48
|
+
|
|
49
|
+
An instance passes straight through -- it validated itself when it was built -- and ``None``
|
|
50
|
+
yields the shared empty options rather than ``None`` itself, so the path every call that
|
|
51
|
+
overrides nothing takes neither validates nor allocates, and every read downstream stays
|
|
52
|
+
unconditional as fields are added.
|
|
53
|
+
|
|
54
|
+
Everything else is validated, an empty mapping and a falsy value of the wrong type included.
|
|
55
|
+
``None`` is the only short circuit deliberately: a caller no type checker saw is exactly who
|
|
56
|
+
this validation is for, and absorbing their ``False`` or ``()`` as "no overrides" would
|
|
57
|
+
leave them believing an override took effect.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
options: The caller's per-call overrides, in either spelling, or ``None`` for none.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
A validated :class:`RequestOptions`; the shared empty instance when ``options`` is
|
|
64
|
+
``None``.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ValidationError: If ``options`` carries an unknown key or an out-of-range value. It
|
|
68
|
+
subclasses ``ValueError``, so a caller catching that still covers this."""
|
|
69
|
+
if isinstance(options, cls):
|
|
70
|
+
return options
|
|
71
|
+
if options is None:
|
|
72
|
+
return _NO_OPTIONS
|
|
73
|
+
return cls.model_validate(options)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class RequestOptionsDict(TypedDict):
|
|
77
|
+
"""The dict-shaped spelling of :class:`RequestOptions`, mirroring it field for field.
|
|
78
|
+
|
|
79
|
+
Closed and typed per key, which is what makes ``request_options={"timeuot": 1}`` a type error at
|
|
80
|
+
the call site rather than a surprise at runtime."""
|
|
81
|
+
|
|
82
|
+
timeout: NotRequired[float | None]
|
|
83
|
+
extra_headers: NotRequired[Mapping[str, str] | None]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
RequestOptionsOrDict: TypeAlias = RequestOptions | RequestOptionsDict
|
|
87
|
+
"""What an endpoint accepts: the typed options, or a dict carrying the same keys.
|
|
88
|
+
|
|
89
|
+
Aliased rather than spelled out at each call site -- unlike a model and its companion, which a
|
|
90
|
+
generator writes as a pair per schema, there is exactly one options type, so a single alias is what
|
|
91
|
+
keeps every emitted signature on one line instead of four."""
|
|
92
|
+
|
|
93
|
+
_NO_OPTIONS: Final = RequestOptions()
|
|
94
|
+
"""The empty options, built once -- the default path for every call that overrides nothing."""
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""The result of one API call, as a value rather than an exception.
|
|
2
|
+
|
|
3
|
+
``ApiResult`` is a tagged union: a call yields exactly one of :class:`Success` or :class:`Failure`,
|
|
4
|
+
never both and never neither, so which one you hold is a fact the type checker can see. Narrow it
|
|
5
|
+
with ``match`` or ``isinstance`` and the payload or error comes with it:
|
|
6
|
+
|
|
7
|
+
match client.body_params.with_raw_response.send_model(employee):
|
|
8
|
+
case Success(payload=confirmation):
|
|
9
|
+
...
|
|
10
|
+
case Failure(error=err):
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
Both variants are frozen dataclasses, so pattern matching needs nothing added. ``.unwrap()`` is the
|
|
14
|
+
shortcut for callers that would rather not branch -- it returns the payload or raises.
|
|
15
|
+
|
|
16
|
+
Each carries the response's **head** -- its status and headers -- as fields of its own rather than a
|
|
17
|
+
response object. There is no body beside them: the body *is* the payload on a success, and on a
|
|
18
|
+
failure it is the error, reachable through :class:`RawError` where the operation documents no schema
|
|
19
|
+
for it. A no-content operation therefore cannot show a caller the body it discarded; reaching one
|
|
20
|
+
deliberately is future work, and an empty ``content`` on a response object was the wrong way to
|
|
21
|
+
advertise its absence.
|
|
22
|
+
|
|
23
|
+
:class:`RawError` lives here too: it is the error type a :class:`Failure` carries when the operation
|
|
24
|
+
does not document the status that came back."""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from collections.abc import Mapping
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from typing import Any, Generic, NoReturn, TypeAlias, TypeVar
|
|
31
|
+
|
|
32
|
+
from ._internal.wire import parse_json
|
|
33
|
+
from .exceptions import ApiError
|
|
34
|
+
|
|
35
|
+
T = TypeVar("T")
|
|
36
|
+
E = TypeVar("E")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class Success(Generic[T]):
|
|
41
|
+
"""A successful (2xx) API call carrying the decoded ``payload``."""
|
|
42
|
+
|
|
43
|
+
payload: T
|
|
44
|
+
status_code: int
|
|
45
|
+
headers: Mapping[str, str] = field(repr=False)
|
|
46
|
+
"""Header names lowercased, per the transports' obligation -- look keys up in lowercase.
|
|
47
|
+
|
|
48
|
+
Kept out of the generated repr: a response's headers carry ``set-cookie``, so a result printed
|
|
49
|
+
in a log line or a debugger pane would otherwise hand over a session secret. The status stays,
|
|
50
|
+
because identifying the result is the whole job of the string form (ADR-0017)."""
|
|
51
|
+
|
|
52
|
+
def unwrap(self) -> T:
|
|
53
|
+
"""Collapse this result to its parsed value.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The decoded payload. This variant never raises."""
|
|
57
|
+
return self.payload
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, slots=True)
|
|
61
|
+
class Failure(Generic[E]):
|
|
62
|
+
"""A failed (non-2xx) API call carrying the decoded error body ``error``.
|
|
63
|
+
|
|
64
|
+
``error`` is the typed error payload directly (a union of the operation's
|
|
65
|
+
documented schemas, or :class:`RawError` for an unmapped status) -- not a wrapper."""
|
|
66
|
+
|
|
67
|
+
error: E
|
|
68
|
+
status_code: int
|
|
69
|
+
headers: Mapping[str, str] = field(repr=False)
|
|
70
|
+
"""Header names lowercased, per the transports' obligation -- look keys up in lowercase.
|
|
71
|
+
|
|
72
|
+
Kept out of the generated repr: a response's headers carry ``set-cookie``, so a result printed
|
|
73
|
+
in a log line or a debugger pane would otherwise hand over a session secret. The status stays,
|
|
74
|
+
because identifying the result is the whole job of the string form (ADR-0017)."""
|
|
75
|
+
|
|
76
|
+
def unwrap(self) -> NoReturn:
|
|
77
|
+
"""Collapse this result to its parsed value, which for a failure means raising.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
ApiError: Always, carrying this result's ``error`` and the response head."""
|
|
81
|
+
raise ApiError(error=self.error, status_code=self.status_code, headers=self.headers)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# One call yields exactly one of these; narrow with ``match`` or ``isinstance``,
|
|
85
|
+
# or collapse to the parsed value with ``.unwrap()``.
|
|
86
|
+
ApiResult: TypeAlias = Success[T] | Failure[E]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True, slots=True)
|
|
90
|
+
class RawError:
|
|
91
|
+
"""Undecoded fallback body (unmapped status / no declared schema).
|
|
92
|
+
|
|
93
|
+
Holds the status and the bytes themselves rather than a response, so the one object that *is*
|
|
94
|
+
the error carries everything it needs to describe itself. Decoded on demand --
|
|
95
|
+
``RawError(status_code, content)``."""
|
|
96
|
+
|
|
97
|
+
status_code: int
|
|
98
|
+
content: bytes
|
|
99
|
+
|
|
100
|
+
def text(self, encoding: str = "utf-8") -> str:
|
|
101
|
+
"""Decode the undecoded body as text, for a log line or a diagnostic.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
encoding: Character encoding to decode with.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
The body as text, undecodable bytes replaced rather than raising."""
|
|
108
|
+
return self.content.decode(encoding, errors="replace")
|
|
109
|
+
|
|
110
|
+
def json(self) -> Any:
|
|
111
|
+
"""Parse the undecoded body as JSON.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Whatever the body parses to.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ValueError: If the body is not valid JSON."""
|
|
118
|
+
return parse_json(self.content)
|
|
119
|
+
|
|
120
|
+
def __repr__(self) -> str:
|
|
121
|
+
# Identify by status only -- the (undecoded, possibly large, binary, or sensitive) body is
|
|
122
|
+
# deliberately kept out of the string form. Read it on demand via ``text``/``json``.
|
|
123
|
+
return f"{type(self).__name__}(status_code={self.status_code})"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""The host environment this SDK reports in its identification headers.
|
|
2
|
+
|
|
3
|
+
Two values, both constant for the life of the process: the operating system with its architecture,
|
|
4
|
+
and the Python implementation with its version. They are probed once at import rather than per
|
|
5
|
+
request -- neither can change while the process runs, and a request-path probe would pay for that
|
|
6
|
+
certainty on every call.
|
|
7
|
+
|
|
8
|
+
The architecture is reported exactly as the host spells it. ``platform.machine()`` is what the
|
|
9
|
+
machine answered, so the header is ground truth for the host that sent it, and no table here can
|
|
10
|
+
fall behind an architecture that ships after this SDK does. A consumer comparing hosts across
|
|
11
|
+
operating systems folds the spellings on its own side -- ``AMD64`` and ``x86_64`` are one machine
|
|
12
|
+
described by two kernels, and only the collector knows which spellings it wants joined.
|
|
13
|
+
|
|
14
|
+
Every probe is guarded. These values are computed while the package is being imported, so an
|
|
15
|
+
unhandled failure here would not degrade a header, it would stop the SDK from importing at all."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import platform
|
|
20
|
+
from typing import Final
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _operating_system() -> str:
|
|
24
|
+
"""Return the host OS, its release and its architecture, or ``unknown`` if a probe fails."""
|
|
25
|
+
try:
|
|
26
|
+
return f"{platform.system()} {platform.release()} {platform.machine()}".strip()
|
|
27
|
+
except Exception:
|
|
28
|
+
return "unknown"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _python_runtime() -> str:
|
|
32
|
+
"""Return the interpreter and its version, or ``unknown`` if a probe fails."""
|
|
33
|
+
try:
|
|
34
|
+
return f"{platform.python_implementation()} {platform.python_version()}"
|
|
35
|
+
except Exception:
|
|
36
|
+
return "unknown"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
OPERATING_SYSTEM: Final = _operating_system()
|
|
40
|
+
"""The host OS, its release and its architecture -- ``Windows 11 AMD64``, ``Darwin 23.5.0 arm64``."""
|
|
41
|
+
|
|
42
|
+
PYTHON_RUNTIME: Final = _python_runtime()
|
|
43
|
+
"""The interpreter and its version -- ``CPython 3.13.14``, ``PyPy 3.10.14``."""
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Runtime helpers for the generated ``server/`` layer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import TypeVar
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def validate_one_of(value: T, allowed: Sequence[T], label: str) -> T:
|
|
12
|
+
"""Return ``value`` unchanged, or raise a guided ``ValueError`` naming the valid names.
|
|
13
|
+
|
|
14
|
+
The generated ``server/environment.py`` calls this with the arms of its ``Environment``
|
|
15
|
+
literal alias, which is why the check is a membership test rather than an enum lookup: the
|
|
16
|
+
environment is an internal selector for a URL variant and never reaches the wire, so it is
|
|
17
|
+
spelled as a plain string. There is no case or whitespace coercion -- with one spelling per
|
|
18
|
+
environment there is nothing to reconcile.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
value: The candidate the caller supplied.
|
|
22
|
+
allowed: Every accepted value, in the order the message should name them.
|
|
23
|
+
label: What ``value`` names, used in the error message (e.g. ``"environment"``).
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
``value`` unchanged.
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
ValueError: If ``value`` is not in ``allowed``; the message names every accepted value."""
|
|
30
|
+
if value not in allowed:
|
|
31
|
+
valid = ", ".join(repr(name) for name in allowed)
|
|
32
|
+
raise ValueError(f"{value!r} is not a valid {label}; expected one of {valid}")
|
|
33
|
+
return value
|