cliV1 0.1.1__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.1.1.dist-info/METADATA +156 -0
- cliv1-0.1.1.dist-info/RECORD +83 -0
- cliv1-0.1.1.dist-info/WHEEL +4 -0
- cliv1-0.1.1.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 +881 -0
- swagger_petstore_open_api_3_0/apis/store.py +411 -0
- swagger_petstore_open_api_3_0/apis/user_api.py +792 -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 +342 -0
- swagger_petstore_open_api_3_0/core/_internal/__init__.py +7 -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/urls.py +166 -0
- swagger_petstore_open_api_3_0/core/_internal/wire.py +173 -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 +217 -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 +563 -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 +271 -0
- swagger_petstore_open_api_3_0/core/converters/open_enum.py +35 -0
- swagger_petstore_open_api_3_0/core/decoding.py +263 -0
- swagger_petstore_open_api_3_0/core/exceptions.py +41 -0
- swagger_petstore_open_api_3_0/core/file_responses.py +324 -0
- swagger_petstore_open_api_3_0/core/files.py +173 -0
- swagger_petstore_open_api_3_0/core/httpx_transport.py +766 -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 +161 -0
- swagger_petstore_open_api_3_0/core/raw_client.py +388 -0
- swagger_petstore_open_api_3_0/core/request_options.py +94 -0
- swagger_petstore_open_api_3_0/core/results.py +110 -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 +248 -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.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,388 @@
|
|
|
1
|
+
"""The seams every endpoint crosses: build a request, send it, shape a result.
|
|
2
|
+
|
|
3
|
+
One pipeline, two flavors. :class:`BaseRawClient` holds the halves that do not depend on
|
|
4
|
+
concurrency -- resolving the API-wide, endpoint and caller parameter layers into a URL and headers,
|
|
5
|
+
and deciding whether a status code makes a :class:`Success` or a :class:`Failure` -- and each
|
|
6
|
+
concrete client adds only the transport-touching seams: ``execute`` for a buffered response,
|
|
7
|
+
``stream`` for one whose body stays unread. Both flavors spell both seams identically, so which
|
|
8
|
+
flavor is in play is carried by the client a caller holds rather than by a suffix on the method.
|
|
9
|
+
|
|
10
|
+
The two live in one module, with no ``async_raw_client.py`` beside it, because they are a peer pair:
|
|
11
|
+
the two lines they differ by -- resolving the auth scheme and sending -- are visible on one screen,
|
|
12
|
+
which is what keeps the sync and async paths from drifting apart."""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Sequence
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Any, Generic, TypeVar
|
|
19
|
+
|
|
20
|
+
from ._internal.headers import resolve_headers
|
|
21
|
+
from ._internal.urls import build_url, resolve_params
|
|
22
|
+
from .auth import AsyncAuthScheme, AuthParams, AuthScheme, invalidate, no_auth, resolve_auth
|
|
23
|
+
from .bodies import RequestBody
|
|
24
|
+
from .decoding import AsyncStreamDecoder, ErrorMapper, ResponseDecoder, StreamDecoder
|
|
25
|
+
from .params import Param, UrlTemplate
|
|
26
|
+
from .request_options import RequestOptions, RequestOptionsOrDict
|
|
27
|
+
from .results import ApiResult, Failure, Success
|
|
28
|
+
from .transport import (
|
|
29
|
+
AsyncHttpClient,
|
|
30
|
+
AsyncStreamedResponse,
|
|
31
|
+
HttpClient,
|
|
32
|
+
HttpRequest,
|
|
33
|
+
HttpResponse,
|
|
34
|
+
StreamedResponse,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
T = TypeVar("T")
|
|
38
|
+
E = TypeVar("E")
|
|
39
|
+
TransportT = TypeVar("TransportT", bound=HttpClient | AsyncHttpClient)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class BaseRawClient(Generic[TransportT]):
|
|
44
|
+
"""Shared request-building and result-shaping for the raw clients.
|
|
45
|
+
|
|
46
|
+
Holds the one transport its concurrency flavor can actually use -- the type
|
|
47
|
+
parameter is what makes :class:`RawClient` provably sync and
|
|
48
|
+
:class:`AsyncRawClient` provably async, so neither can be handed the other's
|
|
49
|
+
transport. Subclasses add only the ``execute`` and ``stream`` seams, which differ
|
|
50
|
+
solely by how they resolve the auth scheme and by ``send`` vs ``await …send``; the
|
|
51
|
+
pipeline below is not duplicated and the sync/async boundary is never crossed.
|
|
52
|
+
|
|
53
|
+
The three ``global_*`` fields carry the parameters the API applies to *every*
|
|
54
|
+
request. They are derived from the API description, not from a caller: whoever
|
|
55
|
+
generates this SDK writes them here, and an API that declares none leaves them
|
|
56
|
+
empty. Each is combined with a single call's own parameters by the matching
|
|
57
|
+
``resolve_*`` function, so this class decides nothing about precedence -- it only
|
|
58
|
+
says which two sides go in. All three are the same shape -- a sequence of
|
|
59
|
+
parameters, each naming the type it was declared as -- which is what lets one
|
|
60
|
+
``param`` factory serve every location a request parameter can occupy, and what
|
|
61
|
+
makes a wrongly-typed API-wide header a build failure rather than a wire bug.
|
|
62
|
+
|
|
63
|
+
A single call's ``request_options`` is the third source: not what the API
|
|
64
|
+
prescribes and not what the endpoint declares, but what the caller asked for this
|
|
65
|
+
once. It is validated here, at the one place a request is built, so no emitted
|
|
66
|
+
endpoint ever inspects an option.
|
|
67
|
+
|
|
68
|
+
Authentication is a fourth source, and the full order for both headers and query parameters is::
|
|
69
|
+
|
|
70
|
+
global_* -> the endpoint's own -> auth -> the caller's extra_headers
|
|
71
|
+
|
|
72
|
+
Auth outranks the endpoint's own parameters because an operation parameter must not be able to
|
|
73
|
+
clobber a credential, and loses to ``extra_headers`` because that is how a caller deliberately
|
|
74
|
+
overrides one -- or blanks it, for a call meant to go out anonymous. Note that ``_build_request``
|
|
75
|
+
takes already-resolved :class:`~.auth.AuthParams`, not a scheme: resolving a scheme may require
|
|
76
|
+
I/O, so it happens in ``execute``, which is the one place the two flavors already differ.
|
|
77
|
+
|
|
78
|
+
A **401 invalidates whatever the scheme cached**, so a credential the server revoked ahead of its
|
|
79
|
+
own expiry is re-obtained on the next call rather than resent until it times out. For every scheme
|
|
80
|
+
that holds a constant this is one ``isinstance`` and nothing more. The failing request is
|
|
81
|
+
deliberately **not** retried: a caller sees one 401 and then recovery, which keeps this seam free
|
|
82
|
+
of the retry policy ADR-0001 rules out. Like scheme resolution, it lives in ``execute`` --
|
|
83
|
+
``_build_result`` is a ``staticmethod`` that never sees the scheme."""
|
|
84
|
+
|
|
85
|
+
http_client: TransportT
|
|
86
|
+
global_headers: Sequence[Param[Any]] = ()
|
|
87
|
+
global_query_params: Sequence[Param[Any]] = ()
|
|
88
|
+
global_path_params: Sequence[Param[Any]] = ()
|
|
89
|
+
|
|
90
|
+
def _build_request(
|
|
91
|
+
self,
|
|
92
|
+
*,
|
|
93
|
+
http_method: str,
|
|
94
|
+
url_template: UrlTemplate,
|
|
95
|
+
path_params: Sequence[Param[Any]] | None,
|
|
96
|
+
query_params: Sequence[Param[Any]] | None,
|
|
97
|
+
headers: Sequence[Param[Any]] | None,
|
|
98
|
+
body: RequestBody | None,
|
|
99
|
+
auth: AuthParams,
|
|
100
|
+
request_options: RequestOptionsOrDict | None,
|
|
101
|
+
) -> HttpRequest:
|
|
102
|
+
options = RequestOptions.coerce(request_options)
|
|
103
|
+
return HttpRequest(
|
|
104
|
+
method=http_method,
|
|
105
|
+
url=build_url(
|
|
106
|
+
url_template,
|
|
107
|
+
path_params=resolve_params(self.global_path_params, path_params),
|
|
108
|
+
query_params=resolve_params(self.global_query_params, (*(query_params or ()), *auth.query_params)),
|
|
109
|
+
),
|
|
110
|
+
headers=resolve_headers(
|
|
111
|
+
self.global_headers,
|
|
112
|
+
(*(headers or ()), *auth.request_headers()),
|
|
113
|
+
extra_headers=options.extra_headers,
|
|
114
|
+
),
|
|
115
|
+
body=body,
|
|
116
|
+
timeout=options.timeout,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _build_result(
|
|
121
|
+
response: HttpResponse,
|
|
122
|
+
*,
|
|
123
|
+
decoder: ResponseDecoder[T],
|
|
124
|
+
error_mapper: ErrorMapper[E],
|
|
125
|
+
) -> ApiResult[T, E]:
|
|
126
|
+
"""Shape a response into a result. 2xx succeeds, everything else fails.
|
|
127
|
+
|
|
128
|
+
Every operation names a decoder -- an empty 2xx body is ``empty_response``, not an omitted
|
|
129
|
+
argument -- so there is no "was one supplied?" branch here, and no empty-body special case
|
|
130
|
+
either. ``T`` therefore means what it says: a declared ``ApiResult[T, E]`` yields a ``T``,
|
|
131
|
+
and no cast or ``Any`` stands between the two.
|
|
132
|
+
|
|
133
|
+
An operation that declares a payload and receives no body raises out of its decoder rather
|
|
134
|
+
than quietly succeeding with ``None``, which is the rule every other
|
|
135
|
+
response-deserialization failure already follows.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
response: The response as the transport returned it.
|
|
139
|
+
decoder: The operation's 2xx decoder; ``empty_response`` when it declares no body.
|
|
140
|
+
error_mapper: The operation's error mapper; ``raw_error_response`` when it declares
|
|
141
|
+
no error schemas.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
A :class:`Success` for a 2xx status, otherwise a :class:`Failure`.
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
ValueError: If the body does not deserialize. A deserialization failure is not an API
|
|
148
|
+
error, so it propagates in both response modes rather than becoming a failure."""
|
|
149
|
+
if 200 <= response.status_code < 300:
|
|
150
|
+
return Success(payload=decoder.decode(response), response=response)
|
|
151
|
+
|
|
152
|
+
return Failure(error=error_mapper.map(response), response=response)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _head_response(streamed: StreamedResponse | AsyncStreamedResponse) -> HttpResponse:
|
|
156
|
+
# status/headers are synchronous properties on both streamed protocols, so one helper serves
|
|
157
|
+
# both flavours. content stays b"": on a streamed success the body IS the payload.
|
|
158
|
+
return HttpResponse(status_code=streamed.status_code, headers=dict(streamed.headers), content=b"")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass(frozen=True, slots=True)
|
|
162
|
+
class RawClient(BaseRawClient[HttpClient]):
|
|
163
|
+
"""Synchronous raw client: builds the request, sends it, shapes the result."""
|
|
164
|
+
|
|
165
|
+
def execute(
|
|
166
|
+
self,
|
|
167
|
+
*,
|
|
168
|
+
http_method: str,
|
|
169
|
+
url_template: UrlTemplate,
|
|
170
|
+
path_params: Sequence[Param[Any]] | None = None,
|
|
171
|
+
query_params: Sequence[Param[Any]] | None = None,
|
|
172
|
+
headers: Sequence[Param[Any]] | None = None,
|
|
173
|
+
body: RequestBody | None = None,
|
|
174
|
+
auth_scheme: AuthScheme = no_auth,
|
|
175
|
+
decoder: ResponseDecoder[T],
|
|
176
|
+
error_mapper: ErrorMapper[E],
|
|
177
|
+
request_options: RequestOptionsOrDict | None = None,
|
|
178
|
+
) -> ApiResult[T, E]:
|
|
179
|
+
request = self._build_request(
|
|
180
|
+
http_method=http_method,
|
|
181
|
+
url_template=url_template,
|
|
182
|
+
path_params=path_params,
|
|
183
|
+
query_params=query_params,
|
|
184
|
+
headers=headers,
|
|
185
|
+
body=body,
|
|
186
|
+
auth=auth_scheme.apply(),
|
|
187
|
+
request_options=request_options,
|
|
188
|
+
)
|
|
189
|
+
response = self.http_client.send(request)
|
|
190
|
+
if response.status_code == 401:
|
|
191
|
+
invalidate(auth_scheme)
|
|
192
|
+
return self._build_result(response, decoder=decoder, error_mapper=error_mapper)
|
|
193
|
+
|
|
194
|
+
def stream(
|
|
195
|
+
self,
|
|
196
|
+
*,
|
|
197
|
+
http_method: str,
|
|
198
|
+
url_template: UrlTemplate,
|
|
199
|
+
path_params: Sequence[Param[Any]] | None = None,
|
|
200
|
+
query_params: Sequence[Param[Any]] | None = None,
|
|
201
|
+
headers: Sequence[Param[Any]] | None = None,
|
|
202
|
+
body: RequestBody | None = None,
|
|
203
|
+
auth_scheme: AuthScheme = no_auth,
|
|
204
|
+
decoder: StreamDecoder[T],
|
|
205
|
+
error_mapper: ErrorMapper[E],
|
|
206
|
+
request_options: RequestOptionsOrDict | None = None,
|
|
207
|
+
) -> ApiResult[T, E]:
|
|
208
|
+
"""Send a request and return its body unread.
|
|
209
|
+
|
|
210
|
+
The ``decoder`` receives the body **unread**, unlike a :class:`ResponseDecoder`, and the
|
|
211
|
+
payload it builds owns the connection from that moment. A file names ``file_decoder``,
|
|
212
|
+
which carries no subscript because a file bypasses the model layer; a payload type that
|
|
213
|
+
varies per operation names it in a subscript, exactly as ``json_decoder[T]`` does. Either
|
|
214
|
+
way ``T`` is solved from an argument, never from the declared return type.
|
|
215
|
+
|
|
216
|
+
A non-2xx body is read in full and the connection released *before* the mapper runs, so an
|
|
217
|
+
error schema deserializes exactly as it does on the buffered path and a :class:`Failure`
|
|
218
|
+
never carries a lifetime obligation -- even when the error decoder itself raises. A
|
|
219
|
+
:class:`Success` owns the connection through its payload; see *File lifetime* in
|
|
220
|
+
docs/plans/file-handling.md.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
http_method: The HTTP verb to send.
|
|
224
|
+
url_template: The unresolved URL for the configured environment.
|
|
225
|
+
path_params: The endpoint's path parameters, if any.
|
|
226
|
+
query_params: The endpoint's query parameters, if any.
|
|
227
|
+
headers: The endpoint's header parameters, if any.
|
|
228
|
+
body: The request body, already wire-ready, or ``None``.
|
|
229
|
+
auth_scheme: The scheme the operation declares; ``no_auth`` when it declares none.
|
|
230
|
+
decoder: The operation's streamed-payload decoder; ``file_decoder`` for a file.
|
|
231
|
+
error_mapper: The operation's error mapper; ``raw_error_response`` when it declares
|
|
232
|
+
no error schemas.
|
|
233
|
+
request_options: The caller's per-call overrides, if any.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
A :class:`Success` carrying the payload built over the unread body for a 2xx status,
|
|
237
|
+
otherwise a :class:`Failure` carrying the decoded error body."""
|
|
238
|
+
request = self._build_request(
|
|
239
|
+
http_method=http_method,
|
|
240
|
+
url_template=url_template,
|
|
241
|
+
path_params=path_params,
|
|
242
|
+
query_params=query_params,
|
|
243
|
+
headers=headers,
|
|
244
|
+
body=body,
|
|
245
|
+
auth=auth_scheme.apply(),
|
|
246
|
+
request_options=request_options,
|
|
247
|
+
)
|
|
248
|
+
streamed = self.http_client.stream(request)
|
|
249
|
+
try:
|
|
250
|
+
if streamed.status_code == 401:
|
|
251
|
+
invalidate(auth_scheme)
|
|
252
|
+
if 200 <= streamed.status_code < 300:
|
|
253
|
+
# The 2xx return exits the try without running else: the connection now belongs to
|
|
254
|
+
# the payload the decoder builds. A decoder that raises instead is caught below, which
|
|
255
|
+
# is why nothing on this line has to be total.
|
|
256
|
+
return Success(payload=decoder.decode(streamed, request.url), response=_head_response(streamed))
|
|
257
|
+
# Buffered once, then shared by both Failure fields -- re-reading a consumed stream
|
|
258
|
+
# raises, and read is what both releases the connection and preserves the bytes.
|
|
259
|
+
buffered = HttpResponse(
|
|
260
|
+
status_code=streamed.status_code,
|
|
261
|
+
headers=dict(streamed.headers),
|
|
262
|
+
content=streamed.read(),
|
|
263
|
+
)
|
|
264
|
+
except BaseException:
|
|
265
|
+
streamed.close()
|
|
266
|
+
raise
|
|
267
|
+
else:
|
|
268
|
+
streamed.close()
|
|
269
|
+
# Mapped AFTER the close, so a decoder raise cannot leak -- a Failure never holds a
|
|
270
|
+
# connection.
|
|
271
|
+
return Failure(error=error_mapper.map(buffered), response=buffered)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@dataclass(frozen=True, slots=True)
|
|
275
|
+
class AsyncRawClient(BaseRawClient[AsyncHttpClient]):
|
|
276
|
+
"""Asynchronous raw client.
|
|
277
|
+
|
|
278
|
+
The method is named ``execute``, not ``execute_async``: sync and async peers
|
|
279
|
+
carry identical names throughout this SDK, and the transport already makes the
|
|
280
|
+
flavor unambiguous."""
|
|
281
|
+
|
|
282
|
+
async def execute(
|
|
283
|
+
self,
|
|
284
|
+
*,
|
|
285
|
+
http_method: str,
|
|
286
|
+
url_template: UrlTemplate,
|
|
287
|
+
path_params: Sequence[Param[Any]] | None = None,
|
|
288
|
+
query_params: Sequence[Param[Any]] | None = None,
|
|
289
|
+
headers: Sequence[Param[Any]] | None = None,
|
|
290
|
+
body: RequestBody | None = None,
|
|
291
|
+
auth_scheme: AsyncAuthScheme = no_auth,
|
|
292
|
+
decoder: ResponseDecoder[T],
|
|
293
|
+
error_mapper: ErrorMapper[E],
|
|
294
|
+
request_options: RequestOptionsOrDict | None = None,
|
|
295
|
+
) -> ApiResult[T, E]:
|
|
296
|
+
request = self._build_request(
|
|
297
|
+
http_method=http_method,
|
|
298
|
+
url_template=url_template,
|
|
299
|
+
path_params=path_params,
|
|
300
|
+
query_params=query_params,
|
|
301
|
+
headers=headers,
|
|
302
|
+
body=body,
|
|
303
|
+
auth=await resolve_auth(auth_scheme),
|
|
304
|
+
request_options=request_options,
|
|
305
|
+
)
|
|
306
|
+
response = await self.http_client.send(request)
|
|
307
|
+
if response.status_code == 401:
|
|
308
|
+
invalidate(auth_scheme)
|
|
309
|
+
return self._build_result(response, decoder=decoder, error_mapper=error_mapper)
|
|
310
|
+
|
|
311
|
+
async def stream(
|
|
312
|
+
self,
|
|
313
|
+
*,
|
|
314
|
+
http_method: str,
|
|
315
|
+
url_template: UrlTemplate,
|
|
316
|
+
path_params: Sequence[Param[Any]] | None = None,
|
|
317
|
+
query_params: Sequence[Param[Any]] | None = None,
|
|
318
|
+
headers: Sequence[Param[Any]] | None = None,
|
|
319
|
+
body: RequestBody | None = None,
|
|
320
|
+
auth_scheme: AsyncAuthScheme = no_auth,
|
|
321
|
+
decoder: AsyncStreamDecoder[T],
|
|
322
|
+
error_mapper: ErrorMapper[E],
|
|
323
|
+
request_options: RequestOptionsOrDict | None = None,
|
|
324
|
+
) -> ApiResult[T, E]:
|
|
325
|
+
"""Send a request and return its body unread; the awaited twin of ``RawClient.stream``.
|
|
326
|
+
|
|
327
|
+
The same contract, with the same two-line delta ``execute`` already has: the auth scheme
|
|
328
|
+
is resolved rather than applied, and the transport is awaited.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
http_method: The HTTP verb to send.
|
|
332
|
+
url_template: The unresolved URL for the configured environment.
|
|
333
|
+
path_params: The endpoint's path parameters, if any.
|
|
334
|
+
query_params: The endpoint's query parameters, if any.
|
|
335
|
+
headers: The endpoint's header parameters, if any.
|
|
336
|
+
body: The request body, already wire-ready, or ``None``.
|
|
337
|
+
auth_scheme: The scheme the operation declares; ``no_auth`` when it declares none.
|
|
338
|
+
decoder: The operation's streamed-payload decoder; ``async_file_decoder`` for a file.
|
|
339
|
+
error_mapper: The operation's error mapper; ``raw_error_response`` when it declares
|
|
340
|
+
no error schemas.
|
|
341
|
+
request_options: The caller's per-call overrides, if any.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
A :class:`Success` carrying the payload built over the unread body for a 2xx status,
|
|
345
|
+
otherwise a :class:`Failure` carrying the decoded error body."""
|
|
346
|
+
request = self._build_request(
|
|
347
|
+
http_method=http_method,
|
|
348
|
+
url_template=url_template,
|
|
349
|
+
path_params=path_params,
|
|
350
|
+
query_params=query_params,
|
|
351
|
+
headers=headers,
|
|
352
|
+
body=body,
|
|
353
|
+
auth=await resolve_auth(auth_scheme),
|
|
354
|
+
request_options=request_options,
|
|
355
|
+
)
|
|
356
|
+
streamed = await self.http_client.stream(request)
|
|
357
|
+
try:
|
|
358
|
+
if streamed.status_code == 401:
|
|
359
|
+
invalidate(auth_scheme)
|
|
360
|
+
if 200 <= streamed.status_code < 300:
|
|
361
|
+
# The 2xx return exits the try without running else: the connection now belongs to
|
|
362
|
+
# the payload the decoder builds. A decoder that raises instead is caught below, which
|
|
363
|
+
# is why nothing on this line has to be total.
|
|
364
|
+
return Success(payload=decoder.decode(streamed, request.url), response=_head_response(streamed))
|
|
365
|
+
# Buffered once, then shared by both Failure fields -- re-reading a consumed stream
|
|
366
|
+
# raises, and aread is what both releases the connection and preserves the bytes.
|
|
367
|
+
buffered = HttpResponse(
|
|
368
|
+
status_code=streamed.status_code,
|
|
369
|
+
headers=dict(streamed.headers),
|
|
370
|
+
content=await streamed.aread(),
|
|
371
|
+
)
|
|
372
|
+
except BaseException:
|
|
373
|
+
await streamed.aclose()
|
|
374
|
+
raise
|
|
375
|
+
else:
|
|
376
|
+
await streamed.aclose()
|
|
377
|
+
# Mapped AFTER the close, so a decoder raise cannot leak -- a Failure never holds a
|
|
378
|
+
# connection.
|
|
379
|
+
return Failure(error=error_mapper.map(buffered), response=buffered)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
RawClientT = TypeVar("RawClientT", bound=RawClient | AsyncRawClient)
|
|
383
|
+
"""Fixes which raw client a generic holder carries -- ``RawClient`` or ``AsyncRawClient``.
|
|
384
|
+
|
|
385
|
+
Bounded by the two concrete clients rather than by ``BaseRawClient[Any]``: the base defines only
|
|
386
|
+
the request/result pipeline, not ``execute``, so the looser bound could not prove that a holder's
|
|
387
|
+
``self._client.execute(...)`` exists -- it type-checked only because each subclass substitutes a
|
|
388
|
+
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,110 @@
|
|
|
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
|
+
:class:`RawError` lives here too: it is the error type a :class:`Failure` carries when the operation
|
|
17
|
+
does not document the status that came back."""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Any, Generic, NoReturn, TypeAlias, TypeVar
|
|
23
|
+
|
|
24
|
+
from .exceptions import ApiError
|
|
25
|
+
from .transport import HttpResponse
|
|
26
|
+
|
|
27
|
+
T = TypeVar("T")
|
|
28
|
+
E = TypeVar("E")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class Success(Generic[T]):
|
|
33
|
+
"""A successful (2xx) API call carrying the decoded ``payload``."""
|
|
34
|
+
|
|
35
|
+
payload: T
|
|
36
|
+
response: HttpResponse
|
|
37
|
+
|
|
38
|
+
def unwrap(self) -> T:
|
|
39
|
+
"""Collapse this result to its parsed value.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
The decoded payload. This variant never raises."""
|
|
43
|
+
return self.payload
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class Failure(Generic[E]):
|
|
48
|
+
"""A failed (non-2xx) API call carrying the decoded error body ``error``.
|
|
49
|
+
|
|
50
|
+
``error`` is the typed error payload directly (a union of the operation's
|
|
51
|
+
documented schemas, or :class:`RawError` for an unmapped status) -- not a wrapper."""
|
|
52
|
+
|
|
53
|
+
error: E
|
|
54
|
+
response: HttpResponse
|
|
55
|
+
|
|
56
|
+
def unwrap(self) -> NoReturn:
|
|
57
|
+
"""Collapse this result to its parsed value, which for a failure means raising.
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
ApiError: Always, carrying this result's ``error`` and ``response``."""
|
|
61
|
+
raise ApiError(error=self.error, response=self.response)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# One call yields exactly one of these; narrow with ``match`` or ``isinstance``,
|
|
65
|
+
# or collapse to the parsed value with ``.unwrap()``.
|
|
66
|
+
ApiResult: TypeAlias = Success[T] | Failure[E]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True, slots=True)
|
|
70
|
+
class RawError:
|
|
71
|
+
"""Undecoded fallback body (unmapped status / no declared schema).
|
|
72
|
+
|
|
73
|
+
Wraps the raw response so the status and bytes are available and decoded on
|
|
74
|
+
demand. Constructed directly -- ``RawError(response)``."""
|
|
75
|
+
|
|
76
|
+
response: HttpResponse
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def status_code(self) -> int:
|
|
80
|
+
return self.response.status_code
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def content(self) -> bytes:
|
|
84
|
+
return self.response.content
|
|
85
|
+
|
|
86
|
+
def text(self, encoding: str = "utf-8") -> str:
|
|
87
|
+
"""Decode the undecoded body as text, for a log line or a diagnostic.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
encoding: Character encoding to decode with.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The body as text, undecodable bytes replaced rather than raising."""
|
|
94
|
+
return self.response.text(encoding)
|
|
95
|
+
|
|
96
|
+
def json(self) -> Any:
|
|
97
|
+
"""Parse the undecoded body as JSON.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Whatever the body parses to.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
ValueError: If the body is not valid JSON."""
|
|
104
|
+
return self.response.json()
|
|
105
|
+
|
|
106
|
+
def __repr__(self) -> str:
|
|
107
|
+
# Identify the response by status only -- the (undecoded, possibly large,
|
|
108
|
+
# binary, or sensitive) body and the headers/request are deliberately kept
|
|
109
|
+
# out of the string form. Read the body on demand via ``text``/``json``.
|
|
110
|
+
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
|