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.
Files changed (83) hide show
  1. cliv1-0.1.1.dist-info/METADATA +156 -0
  2. cliv1-0.1.1.dist-info/RECORD +83 -0
  3. cliv1-0.1.1.dist-info/WHEEL +4 -0
  4. cliv1-0.1.1.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 +881 -0
  8. swagger_petstore_open_api_3_0/apis/store.py +411 -0
  9. swagger_petstore_open_api_3_0/apis/user_api.py +792 -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 +342 -0
  15. swagger_petstore_open_api_3_0/core/_internal/__init__.py +7 -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/urls.py +166 -0
  20. swagger_petstore_open_api_3_0/core/_internal/wire.py +173 -0
  21. swagger_petstore_open_api_3_0/core/adapters.py +177 -0
  22. swagger_petstore_open_api_3_0/core/auth/__init__.py +70 -0
  23. swagger_petstore_open_api_3_0/core/auth/composition.py +175 -0
  24. swagger_petstore_open_api_3_0/core/auth/models.py +217 -0
  25. swagger_petstore_open_api_3_0/core/auth/protocols.py +103 -0
  26. swagger_petstore_open_api_3_0/core/auth/schemes/__init__.py +94 -0
  27. swagger_petstore_open_api_3_0/core/auth/schemes/api_key_auth.py +53 -0
  28. swagger_petstore_open_api_3_0/core/auth/schemes/basic_auth.py +73 -0
  29. swagger_petstore_open_api_3_0/core/auth/schemes/bearer_auth.py +21 -0
  30. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_authorization_code.py +393 -0
  31. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_client_credentials.py +127 -0
  32. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_password.py +142 -0
  33. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_schemes.py +300 -0
  34. swagger_petstore_open_api_3_0/core/base_raw_response.py +49 -0
  35. swagger_petstore_open_api_3_0/core/bodies.py +563 -0
  36. swagger_petstore_open_api_3_0/core/converters/__init__.py +68 -0
  37. swagger_petstore_open_api_3_0/core/converters/date_time.py +237 -0
  38. swagger_petstore_open_api_3_0/core/converters/encoded_bytes.py +271 -0
  39. swagger_petstore_open_api_3_0/core/converters/open_enum.py +35 -0
  40. swagger_petstore_open_api_3_0/core/decoding.py +263 -0
  41. swagger_petstore_open_api_3_0/core/exceptions.py +41 -0
  42. swagger_petstore_open_api_3_0/core/file_responses.py +324 -0
  43. swagger_petstore_open_api_3_0/core/files.py +173 -0
  44. swagger_petstore_open_api_3_0/core/httpx_transport.py +766 -0
  45. swagger_petstore_open_api_3_0/core/models.py +143 -0
  46. swagger_petstore_open_api_3_0/core/optionality.py +130 -0
  47. swagger_petstore_open_api_3_0/core/params.py +161 -0
  48. swagger_petstore_open_api_3_0/core/raw_client.py +388 -0
  49. swagger_petstore_open_api_3_0/core/request_options.py +94 -0
  50. swagger_petstore_open_api_3_0/core/results.py +110 -0
  51. swagger_petstore_open_api_3_0/core/runtime_env.py +43 -0
  52. swagger_petstore_open_api_3_0/core/servers.py +33 -0
  53. swagger_petstore_open_api_3_0/core/transport.py +248 -0
  54. swagger_petstore_open_api_3_0/errors/__init__.py +48 -0
  55. swagger_petstore_open_api_3_0/errors/add_pet_error.py +21 -0
  56. swagger_petstore_open_api_3_0/errors/delete_order_error.py +21 -0
  57. swagger_petstore_open_api_3_0/errors/delete_pet_error.py +21 -0
  58. swagger_petstore_open_api_3_0/errors/delete_user_error.py +21 -0
  59. swagger_petstore_open_api_3_0/errors/find_pets_by_status_error.py +21 -0
  60. swagger_petstore_open_api_3_0/errors/find_pets_by_tags_error.py +21 -0
  61. swagger_petstore_open_api_3_0/errors/get_order_by_id_error.py +21 -0
  62. swagger_petstore_open_api_3_0/errors/get_pet_by_id_error.py +21 -0
  63. swagger_petstore_open_api_3_0/errors/get_user_by_name_error.py +21 -0
  64. swagger_petstore_open_api_3_0/errors/login_user_error.py +21 -0
  65. swagger_petstore_open_api_3_0/errors/place_order_error.py +21 -0
  66. swagger_petstore_open_api_3_0/errors/update_pet_error.py +21 -0
  67. swagger_petstore_open_api_3_0/errors/update_pet_with_form_error.py +21 -0
  68. swagger_petstore_open_api_3_0/errors/update_user_error.py +21 -0
  69. swagger_petstore_open_api_3_0/errors/upload_file_error.py +21 -0
  70. swagger_petstore_open_api_3_0/models/__init__.py +23 -0
  71. swagger_petstore_open_api_3_0/models/api_response.py +18 -0
  72. swagger_petstore_open_api_3_0/models/category.py +15 -0
  73. swagger_petstore_open_api_3_0/models/enums/__init__.py +4 -0
  74. swagger_petstore_open_api_3_0/models/enums/order_status.py +17 -0
  75. swagger_petstore_open_api_3_0/models/enums/pet_status.py +17 -0
  76. swagger_petstore_open_api_3_0/models/order.py +27 -0
  77. swagger_petstore_open_api_3_0/models/pet.py +28 -0
  78. swagger_petstore_open_api_3_0/models/tag.py +15 -0
  79. swagger_petstore_open_api_3_0/models/user.py +29 -0
  80. swagger_petstore_open_api_3_0/py.typed +0 -0
  81. swagger_petstore_open_api_3_0/server/__init__.py +21 -0
  82. swagger_petstore_open_api_3_0/server/server.py +17 -0
  83. 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