cliV1 0.0.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. cliv1-0.0.8.dist-info/METADATA +168 -0
  2. cliv1-0.0.8.dist-info/RECORD +87 -0
  3. cliv1-0.0.8.dist-info/WHEEL +4 -0
  4. cliv1-0.0.8.dist-info/licenses/LICENSE +21 -0
  5. swagger_petstore_open_api_3_0/__init__.py +15 -0
  6. swagger_petstore_open_api_3_0/apis/__init__.py +5 -0
  7. swagger_petstore_open_api_3_0/apis/pet_api.py +883 -0
  8. swagger_petstore_open_api_3_0/apis/store.py +413 -0
  9. swagger_petstore_open_api_3_0/apis/user_api.py +794 -0
  10. swagger_petstore_open_api_3_0/async_client.py +79 -0
  11. swagger_petstore_open_api_3_0/auth.py +17 -0
  12. swagger_petstore_open_api_3_0/base_client.py +18 -0
  13. swagger_petstore_open_api_3_0/client.py +77 -0
  14. swagger_petstore_open_api_3_0/core/__init__.py +358 -0
  15. swagger_petstore_open_api_3_0/core/_internal/__init__.py +9 -0
  16. swagger_petstore_open_api_3_0/core/_internal/content_disposition.py +154 -0
  17. swagger_petstore_open_api_3_0/core/_internal/flattening.py +139 -0
  18. swagger_petstore_open_api_3_0/core/_internal/headers.py +144 -0
  19. swagger_petstore_open_api_3_0/core/_internal/sse_parser.py +124 -0
  20. swagger_petstore_open_api_3_0/core/_internal/subscripts.py +50 -0
  21. swagger_petstore_open_api_3_0/core/_internal/urls.py +186 -0
  22. swagger_petstore_open_api_3_0/core/_internal/wire.py +198 -0
  23. swagger_petstore_open_api_3_0/core/adapters.py +177 -0
  24. swagger_petstore_open_api_3_0/core/auth/__init__.py +70 -0
  25. swagger_petstore_open_api_3_0/core/auth/composition.py +175 -0
  26. swagger_petstore_open_api_3_0/core/auth/models.py +216 -0
  27. swagger_petstore_open_api_3_0/core/auth/protocols.py +103 -0
  28. swagger_petstore_open_api_3_0/core/auth/schemes/__init__.py +94 -0
  29. swagger_petstore_open_api_3_0/core/auth/schemes/api_key_auth.py +53 -0
  30. swagger_petstore_open_api_3_0/core/auth/schemes/basic_auth.py +73 -0
  31. swagger_petstore_open_api_3_0/core/auth/schemes/bearer_auth.py +21 -0
  32. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_authorization_code.py +393 -0
  33. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_client_credentials.py +127 -0
  34. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_password.py +142 -0
  35. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_schemes.py +300 -0
  36. swagger_petstore_open_api_3_0/core/base_raw_response.py +49 -0
  37. swagger_petstore_open_api_3_0/core/bodies.py +550 -0
  38. swagger_petstore_open_api_3_0/core/converters/__init__.py +68 -0
  39. swagger_petstore_open_api_3_0/core/converters/date_time.py +237 -0
  40. swagger_petstore_open_api_3_0/core/converters/encoded_bytes.py +273 -0
  41. swagger_petstore_open_api_3_0/core/converters/open_enum.py +35 -0
  42. swagger_petstore_open_api_3_0/core/decoding.py +498 -0
  43. swagger_petstore_open_api_3_0/core/discriminators.py +129 -0
  44. swagger_petstore_open_api_3_0/core/event_streams.py +225 -0
  45. swagger_petstore_open_api_3_0/core/exceptions.py +38 -0
  46. swagger_petstore_open_api_3_0/core/file_responses.py +335 -0
  47. swagger_petstore_open_api_3_0/core/files.py +173 -0
  48. swagger_petstore_open_api_3_0/core/httpx_transport.py +748 -0
  49. swagger_petstore_open_api_3_0/core/models.py +143 -0
  50. swagger_petstore_open_api_3_0/core/optionality.py +130 -0
  51. swagger_petstore_open_api_3_0/core/params.py +157 -0
  52. swagger_petstore_open_api_3_0/core/raw_client.py +338 -0
  53. swagger_petstore_open_api_3_0/core/request_options.py +94 -0
  54. swagger_petstore_open_api_3_0/core/results.py +123 -0
  55. swagger_petstore_open_api_3_0/core/runtime_env.py +43 -0
  56. swagger_petstore_open_api_3_0/core/servers.py +33 -0
  57. swagger_petstore_open_api_3_0/core/transport.py +191 -0
  58. swagger_petstore_open_api_3_0/errors/__init__.py +48 -0
  59. swagger_petstore_open_api_3_0/errors/add_pet_error.py +21 -0
  60. swagger_petstore_open_api_3_0/errors/delete_order_error.py +21 -0
  61. swagger_petstore_open_api_3_0/errors/delete_pet_error.py +21 -0
  62. swagger_petstore_open_api_3_0/errors/delete_user_error.py +21 -0
  63. swagger_petstore_open_api_3_0/errors/find_pets_by_status_error.py +21 -0
  64. swagger_petstore_open_api_3_0/errors/find_pets_by_tags_error.py +21 -0
  65. swagger_petstore_open_api_3_0/errors/get_order_by_id_error.py +21 -0
  66. swagger_petstore_open_api_3_0/errors/get_pet_by_id_error.py +21 -0
  67. swagger_petstore_open_api_3_0/errors/get_user_by_name_error.py +21 -0
  68. swagger_petstore_open_api_3_0/errors/login_user_error.py +21 -0
  69. swagger_petstore_open_api_3_0/errors/place_order_error.py +21 -0
  70. swagger_petstore_open_api_3_0/errors/update_pet_error.py +21 -0
  71. swagger_petstore_open_api_3_0/errors/update_pet_with_form_error.py +21 -0
  72. swagger_petstore_open_api_3_0/errors/update_user_error.py +21 -0
  73. swagger_petstore_open_api_3_0/errors/upload_file_error.py +21 -0
  74. swagger_petstore_open_api_3_0/models/__init__.py +23 -0
  75. swagger_petstore_open_api_3_0/models/api_response.py +18 -0
  76. swagger_petstore_open_api_3_0/models/category.py +15 -0
  77. swagger_petstore_open_api_3_0/models/enums/__init__.py +4 -0
  78. swagger_petstore_open_api_3_0/models/enums/order_status.py +17 -0
  79. swagger_petstore_open_api_3_0/models/enums/pet_status.py +17 -0
  80. swagger_petstore_open_api_3_0/models/order.py +27 -0
  81. swagger_petstore_open_api_3_0/models/pet.py +28 -0
  82. swagger_petstore_open_api_3_0/models/tag_model.py +15 -0
  83. swagger_petstore_open_api_3_0/models/user.py +29 -0
  84. swagger_petstore_open_api_3_0/py.typed +0 -0
  85. swagger_petstore_open_api_3_0/server/__init__.py +21 -0
  86. swagger_petstore_open_api_3_0/server/server.py +17 -0
  87. swagger_petstore_open_api_3_0/server/server_config.py +52 -0
@@ -0,0 +1,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