cliV1 0.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cliv1-0.0.7.dist-info/METADATA +212 -0
- cliv1-0.0.7.dist-info/RECORD +92 -0
- cliv1-0.0.7.dist-info/WHEEL +4 -0
- cliv1-0.0.7.dist-info/licenses/LICENSE +21 -0
- swagger_petstore_open_api_3_1_0/__init__.py +15 -0
- swagger_petstore_open_api_3_1_0/apis/__init__.py +5 -0
- swagger_petstore_open_api_3_1_0/apis/pet_api.py +922 -0
- swagger_petstore_open_api_3_1_0/apis/store.py +432 -0
- swagger_petstore_open_api_3_1_0/apis/user_api.py +829 -0
- swagger_petstore_open_api_3_1_0/async_client.py +87 -0
- swagger_petstore_open_api_3_1_0/auth.py +17 -0
- swagger_petstore_open_api_3_1_0/base_client.py +27 -0
- swagger_petstore_open_api_3_1_0/client.py +85 -0
- swagger_petstore_open_api_3_1_0/core/__init__.py +395 -0
- swagger_petstore_open_api_3_1_0/core/_internal/__init__.py +9 -0
- swagger_petstore_open_api_3_1_0/core/_internal/content_disposition.py +154 -0
- swagger_petstore_open_api_3_1_0/core/_internal/flattening.py +104 -0
- swagger_petstore_open_api_3_1_0/core/_internal/headers.py +243 -0
- swagger_petstore_open_api_3_1_0/core/_internal/redaction.py +208 -0
- swagger_petstore_open_api_3_1_0/core/_internal/sse_parser.py +124 -0
- swagger_petstore_open_api_3_1_0/core/_internal/subscripts.py +50 -0
- swagger_petstore_open_api_3_1_0/core/_internal/urls.py +172 -0
- swagger_petstore_open_api_3_1_0/core/_internal/wire.py +201 -0
- swagger_petstore_open_api_3_1_0/core/adapters.py +177 -0
- swagger_petstore_open_api_3_1_0/core/auth/__init__.py +70 -0
- swagger_petstore_open_api_3_1_0/core/auth/composition.py +175 -0
- swagger_petstore_open_api_3_1_0/core/auth/models.py +216 -0
- swagger_petstore_open_api_3_1_0/core/auth/protocols.py +103 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/__init__.py +94 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/api_key_auth.py +53 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/basic_auth.py +73 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/bearer_auth.py +21 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_authorization_code.py +395 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_client_credentials.py +130 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_password.py +145 -0
- swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_schemes.py +300 -0
- swagger_petstore_open_api_3_1_0/core/base_raw_response.py +49 -0
- swagger_petstore_open_api_3_1_0/core/bodies.py +590 -0
- swagger_petstore_open_api_3_1_0/core/converters/__init__.py +68 -0
- swagger_petstore_open_api_3_1_0/core/converters/date_time.py +237 -0
- swagger_petstore_open_api_3_1_0/core/converters/encoded_bytes.py +273 -0
- swagger_petstore_open_api_3_1_0/core/converters/open_enum.py +35 -0
- swagger_petstore_open_api_3_1_0/core/decoding.py +498 -0
- swagger_petstore_open_api_3_1_0/core/discriminators.py +129 -0
- swagger_petstore_open_api_3_1_0/core/event_streams.py +225 -0
- swagger_petstore_open_api_3_1_0/core/exceptions.py +78 -0
- swagger_petstore_open_api_3_1_0/core/file_responses.py +337 -0
- swagger_petstore_open_api_3_1_0/core/files.py +174 -0
- swagger_petstore_open_api_3_1_0/core/httpx2_transport.py +748 -0
- swagger_petstore_open_api_3_1_0/core/logging.py +472 -0
- swagger_petstore_open_api_3_1_0/core/models.py +143 -0
- swagger_petstore_open_api_3_1_0/core/optionality.py +144 -0
- swagger_petstore_open_api_3_1_0/core/pagination.py +452 -0
- swagger_petstore_open_api_3_1_0/core/paginator.py +55 -0
- swagger_petstore_open_api_3_1_0/core/params.py +157 -0
- swagger_petstore_open_api_3_1_0/core/raw_client.py +480 -0
- swagger_petstore_open_api_3_1_0/core/request_options.py +172 -0
- swagger_petstore_open_api_3_1_0/core/results.py +178 -0
- swagger_petstore_open_api_3_1_0/core/retries.py +237 -0
- swagger_petstore_open_api_3_1_0/core/runtime_env.py +43 -0
- swagger_petstore_open_api_3_1_0/core/servers.py +33 -0
- swagger_petstore_open_api_3_1_0/core/transport.py +218 -0
- swagger_petstore_open_api_3_1_0/errors/__init__.py +48 -0
- swagger_petstore_open_api_3_1_0/errors/add_pet_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/delete_order_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/delete_pet_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/delete_user_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/find_pets_by_status_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/find_pets_by_tags_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/get_order_by_id_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/get_pet_by_id_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/get_user_by_name_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/login_user_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/place_order_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/update_pet_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/update_pet_with_form_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/update_user_error.py +21 -0
- swagger_petstore_open_api_3_1_0/errors/upload_file_error.py +21 -0
- swagger_petstore_open_api_3_1_0/models/__init__.py +23 -0
- swagger_petstore_open_api_3_1_0/models/api_response.py +18 -0
- swagger_petstore_open_api_3_1_0/models/category.py +15 -0
- swagger_petstore_open_api_3_1_0/models/enums/__init__.py +4 -0
- swagger_petstore_open_api_3_1_0/models/enums/order_status.py +17 -0
- swagger_petstore_open_api_3_1_0/models/enums/pet_status.py +17 -0
- swagger_petstore_open_api_3_1_0/models/order.py +27 -0
- swagger_petstore_open_api_3_1_0/models/pet.py +28 -0
- swagger_petstore_open_api_3_1_0/models/tag_model.py +15 -0
- swagger_petstore_open_api_3_1_0/models/user.py +29 -0
- swagger_petstore_open_api_3_1_0/py.typed +0 -0
- swagger_petstore_open_api_3_1_0/server/__init__.py +21 -0
- swagger_petstore_open_api_3_1_0/server/server.py +17 -0
- swagger_petstore_open_api_3_1_0/server/server_config.py +52 -0
|
@@ -0,0 +1,172 @@
|
|
|
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 constraints 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 collections.abc import Set as AbstractSet
|
|
26
|
+
from typing import Final, TypeAlias
|
|
27
|
+
|
|
28
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
29
|
+
from typing_extensions import NotRequired, TypedDict
|
|
30
|
+
|
|
31
|
+
from .retries import MaxRetries, RetryOptions
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RequestOptions(BaseModel):
|
|
35
|
+
"""Overrides that apply to a single request.
|
|
36
|
+
|
|
37
|
+
``frozen`` matches every other value type in the runtime; ``extra="forbid"`` rejects a
|
|
38
|
+
misspelled option that reached here without a type checker having seen the call.
|
|
39
|
+
|
|
40
|
+
The retry terms are spelled **flat**, under the names :class:`~.retries.RetryOptions` gives them,
|
|
41
|
+
and each one **merges** onto the client's policy rather than replacing it
|
|
42
|
+
(:func:`apply_retry_overrides`, below): a field left ``None`` is a field the caller did not set,
|
|
43
|
+
and the client's value stands. So overriding one term costs one key -- ``{"max_retries": 0}``
|
|
44
|
+
turns retrying off for this call and moves nothing else -- where a nested policy obliged the
|
|
45
|
+
caller to restate every term they were happy with in order to change one.
|
|
46
|
+
|
|
47
|
+
Of the retry terms, only the two that vary by call site are here: how many times *this* call may
|
|
48
|
+
be retried, and which statuses count as transient for *this* endpoint. The wait curve --
|
|
49
|
+
``initial_delay``, ``backoff_factor``, ``max_delay``, ``max_jitter`` -- describes how the server
|
|
50
|
+
recovers, a fact about the API rather than about one call, and ``http_methods_to_retry`` is fixed
|
|
51
|
+
by the method an endpoint was emitted with, so a per-call set could only agree with it or disable
|
|
52
|
+
this call's retrying by a second route. All five stay the client's alone.
|
|
53
|
+
|
|
54
|
+
``log_level`` is the third per-call term: how loud *this* call may be, read where a call's log
|
|
55
|
+
scope is opened."""
|
|
56
|
+
|
|
57
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
58
|
+
|
|
59
|
+
timeout: float | None = Field(default=None, gt=0)
|
|
60
|
+
"""Seconds to wait for this one request. ``None`` leaves the client's own timeout in force."""
|
|
61
|
+
|
|
62
|
+
extra_headers: Mapping[str, str] | None = None
|
|
63
|
+
"""Headers added to this one request, winning over both the API's and the endpoint's own."""
|
|
64
|
+
|
|
65
|
+
max_retries: MaxRetries | None = None
|
|
66
|
+
"""Retries after the initial attempt of this one request. ``0`` turns retrying off for it."""
|
|
67
|
+
|
|
68
|
+
status_codes_to_retry: AbstractSet[int] | None = None
|
|
69
|
+
"""Statuses that qualify for a retry of this request, under the client's method gate."""
|
|
70
|
+
|
|
71
|
+
log_level: int | None = Field(default=None, ge=0)
|
|
72
|
+
"""A floor for this one call's log records: a ``logging`` level. Records below it are not written;
|
|
73
|
+
records at or above it are written when the logger's own level and the client's flags allow them. It
|
|
74
|
+
silences, and never turns a dump or the body on. ``None`` leaves every event to the client's options."""
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def coerce(cls, options: RequestOptionsOrDict | None) -> RequestOptions:
|
|
78
|
+
"""Return ``options`` as a :class:`RequestOptions`, dict-shaped input included.
|
|
79
|
+
|
|
80
|
+
An instance passes straight through -- it validated itself when it was built -- and ``None``
|
|
81
|
+
yields the shared empty options rather than ``None`` itself, so the path every call that
|
|
82
|
+
overrides nothing takes neither validates nor allocates, and every read downstream stays
|
|
83
|
+
unconditional as fields are added.
|
|
84
|
+
|
|
85
|
+
Everything else is validated, an empty mapping and a falsy value of the wrong type included.
|
|
86
|
+
``None`` is the only short circuit deliberately: a caller no type checker saw is exactly who
|
|
87
|
+
this validation is for, and absorbing their ``False`` or ``()`` as "no overrides" would
|
|
88
|
+
leave them believing an override took effect.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
options: The caller's per-call overrides, in either spelling, or ``None`` for none.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
A validated :class:`RequestOptions`; the shared empty instance when ``options`` is
|
|
95
|
+
``None``.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
ValidationError: If ``options`` carries an unknown key or an out-of-range value. It
|
|
99
|
+
subclasses ``ValueError``, so a caller catching that still covers this."""
|
|
100
|
+
if isinstance(options, cls):
|
|
101
|
+
return options
|
|
102
|
+
if options is None:
|
|
103
|
+
return _NO_OPTIONS
|
|
104
|
+
return cls.model_validate(options)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class RequestOptionsDict(TypedDict):
|
|
108
|
+
"""The dict-shaped spelling of :class:`RequestOptions`, mirroring it field for field.
|
|
109
|
+
|
|
110
|
+
Closed and typed per key, which is what makes ``request_options={"timeuot": 1}`` a type error at
|
|
111
|
+
the call site rather than a surprise at runtime."""
|
|
112
|
+
|
|
113
|
+
timeout: NotRequired[float | None]
|
|
114
|
+
extra_headers: NotRequired[Mapping[str, str] | None]
|
|
115
|
+
max_retries: NotRequired[int | None]
|
|
116
|
+
status_codes_to_retry: NotRequired[AbstractSet[int] | None]
|
|
117
|
+
log_level: NotRequired[int | None]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
RequestOptionsOrDict: TypeAlias = RequestOptions | RequestOptionsDict
|
|
121
|
+
"""What an endpoint accepts: the typed options, or a dict carrying the same keys.
|
|
122
|
+
|
|
123
|
+
Aliased rather than spelled out at each call site -- unlike a model and its companion, which a
|
|
124
|
+
generator writes as a pair per schema, there is exactly one options type, so a single alias is what
|
|
125
|
+
keeps every emitted signature on one line instead of four."""
|
|
126
|
+
|
|
127
|
+
_NO_OPTIONS: Final = RequestOptions()
|
|
128
|
+
"""The empty options, built once -- the default path for every call that overrides nothing."""
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def apply_retry_overrides(options: RequestOptions, retry_options: RetryOptions) -> RetryOptions:
|
|
132
|
+
"""Return the client's policy with one call's retry overrides applied.
|
|
133
|
+
|
|
134
|
+
A function rather than a method, and not re-exported from ``core/__init__.py``: the two terms are
|
|
135
|
+
a caller's to *write*, but folding them onto a policy is the raw client's business, and nothing a
|
|
136
|
+
caller holds should offer it. The runtime's one caller is ``raw_client.execute``, once per
|
|
137
|
+
request, in both flavours.
|
|
138
|
+
|
|
139
|
+
The terms are a *layer over* ``retry_options``, never a policy of their own: each one the caller
|
|
140
|
+
set replaces the client's value for this request, and each left ``None`` leaves the client's
|
|
141
|
+
standing. ``None`` is therefore **unset rather than a value** -- the same reading ``timeout`` has,
|
|
142
|
+
and the reading the dict spelling requires, since an omitted key and ``{"max_retries": None}``
|
|
143
|
+
cannot be allowed to mean different things once the companion accepts both. The cost of that is
|
|
144
|
+
one thing a caller cannot say: there is no per-call way to put a field *back* to the policy's own
|
|
145
|
+
default, only to name the value they want.
|
|
146
|
+
|
|
147
|
+
The client's policy is copied rather than rebuilt, which is what keeps this merge correct as the
|
|
148
|
+
policy grows: a field this function has never heard of keeps the client's value instead of
|
|
149
|
+
silently falling back to its class default. It is copied rather than revalidated because the
|
|
150
|
+
overriding values were already validated against the very bounds ``RetryOptions`` declares --
|
|
151
|
+
both models name ``MaxRetries``, and a status set has no bound -- so there is nothing left to
|
|
152
|
+
re-check.
|
|
153
|
+
|
|
154
|
+
A call that overrode nothing gets the client's own instance back, unwrapped and unallocated; that
|
|
155
|
+
is the path every request takes.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
options: The caller's per-call overrides, already coerced.
|
|
159
|
+
retry_options: The client's policy -- what governs every term this call is silent about.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
The client's policy where this call is silent and the caller's value at each field they set;
|
|
163
|
+
``retry_options`` itself when they set none."""
|
|
164
|
+
overrides = {
|
|
165
|
+
name: value
|
|
166
|
+
for name, value in (
|
|
167
|
+
("max_retries", options.max_retries),
|
|
168
|
+
("status_codes_to_retry", options.status_codes_to_retry),
|
|
169
|
+
)
|
|
170
|
+
if value is not None
|
|
171
|
+
}
|
|
172
|
+
return retry_options.model_copy(update=overrides) if overrides else retry_options
|
|
@@ -0,0 +1,178 @@
|
|
|
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. To skip the branch,
|
|
14
|
+
call ``unwrap(result)``: it returns the payload or raises ``ApiError``. It is a function, not a method, so
|
|
15
|
+
the same name also covers paginators: ``unwrap(paginator, items_selector)`` returns a paginator of payloads.
|
|
16
|
+
|
|
17
|
+
Each carries the response's **head** -- its status and headers -- as fields of its own rather than a
|
|
18
|
+
response object. There is no body beside them: the body *is* the payload on a success, and on a
|
|
19
|
+
failure it is the error, reachable through :class:`RawError` where the operation documents no schema
|
|
20
|
+
for it. A no-content operation therefore cannot show a caller the body it discarded; reaching one
|
|
21
|
+
deliberately is future work, and an empty ``content`` on a response object was the wrong way to
|
|
22
|
+
advertise its absence.
|
|
23
|
+
|
|
24
|
+
:class:`RawError` lives here too: it is the error type a :class:`Failure` carries when the operation
|
|
25
|
+
does not document the status that came back."""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from collections.abc import AsyncIterator, Callable, Iterator, Mapping, Sequence
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from typing import Any, Generic, NoReturn, TypeAlias, TypeVar, overload
|
|
32
|
+
|
|
33
|
+
from ._internal.wire import parse_json
|
|
34
|
+
from .exceptions import ApiError
|
|
35
|
+
from .paginator import AsyncPaginator, Paginator
|
|
36
|
+
|
|
37
|
+
T = TypeVar("T")
|
|
38
|
+
E = TypeVar("E")
|
|
39
|
+
ItemT = TypeVar("ItemT")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class Success(Generic[T]):
|
|
44
|
+
"""A successful (2xx) API call carrying the decoded ``payload``."""
|
|
45
|
+
|
|
46
|
+
payload: T
|
|
47
|
+
status_code: int
|
|
48
|
+
headers: Mapping[str, str] = field(repr=False)
|
|
49
|
+
"""Header names lowercased, per the transports' obligation -- look keys up in lowercase.
|
|
50
|
+
|
|
51
|
+
Kept out of the generated repr: a response's headers carry ``set-cookie``, so a result printed
|
|
52
|
+
in a log line or a debugger pane would otherwise hand over a session secret. The status stays,
|
|
53
|
+
because identifying the result is the whole job of the string form (ADR-0017)."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class Failure(Generic[E]):
|
|
58
|
+
"""A failed (non-2xx) API call carrying the decoded error body ``error``.
|
|
59
|
+
|
|
60
|
+
``error`` is the typed error payload directly (a union of the operation's
|
|
61
|
+
documented schemas, or :class:`RawError` for an unmapped status) -- not a wrapper."""
|
|
62
|
+
|
|
63
|
+
error: E
|
|
64
|
+
status_code: int
|
|
65
|
+
headers: Mapping[str, str] = field(repr=False)
|
|
66
|
+
"""Header names lowercased, per the transports' obligation -- look keys up in lowercase.
|
|
67
|
+
|
|
68
|
+
Kept out of the generated repr: a response's headers carry ``set-cookie``, so a result printed
|
|
69
|
+
in a log line or a debugger pane would otherwise hand over a session secret. The status stays,
|
|
70
|
+
because identifying the result is the whole job of the string form (ADR-0017)."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# One call returns exactly one of these. Narrow with ``match`` or ``isinstance``,
|
|
74
|
+
# or get the payload with ``unwrap(result)``.
|
|
75
|
+
ApiResult: TypeAlias = Success[T] | Failure[E]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True, slots=True)
|
|
79
|
+
class RawError:
|
|
80
|
+
"""Undecoded fallback body (unmapped status / no declared schema).
|
|
81
|
+
|
|
82
|
+
Holds the status and the bytes themselves rather than a response, so the one object that *is*
|
|
83
|
+
the error carries everything it needs to describe itself. Decoded on demand --
|
|
84
|
+
``RawError(status_code, content)``."""
|
|
85
|
+
|
|
86
|
+
status_code: int
|
|
87
|
+
content: bytes
|
|
88
|
+
|
|
89
|
+
def text(self, encoding: str = "utf-8") -> str:
|
|
90
|
+
"""Decode the undecoded body as text, for a log line or a diagnostic.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
encoding: Character encoding to decode with.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
The body as text, undecodable bytes replaced rather than raising."""
|
|
97
|
+
return self.content.decode(encoding, errors="replace")
|
|
98
|
+
|
|
99
|
+
def json(self) -> Any:
|
|
100
|
+
"""Parse the undecoded body as JSON.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Whatever the body parses to.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
ValueError: If the body is not valid JSON."""
|
|
107
|
+
return parse_json(self.content)
|
|
108
|
+
|
|
109
|
+
def __repr__(self) -> str:
|
|
110
|
+
# Identify by status only -- the (undecoded, possibly large, binary, or sensitive) body is
|
|
111
|
+
# deliberately kept out of the string form. Read it on demand via ``text``/``json``.
|
|
112
|
+
return f"{type(self).__name__}(status_code={self.status_code})"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@overload
|
|
116
|
+
def unwrap(result: Failure[E], /) -> NoReturn: ...
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@overload
|
|
120
|
+
def unwrap(result: ApiResult[T, E], /) -> T: ...
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@overload
|
|
124
|
+
def unwrap(
|
|
125
|
+
paginator: Paginator[ApiResult[T, E], ItemT], items_selector: Callable[[T], Sequence[ItemT]], /
|
|
126
|
+
) -> Paginator[T, ItemT]: ...
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@overload
|
|
130
|
+
def unwrap(
|
|
131
|
+
paginator: AsyncPaginator[ApiResult[T, E], ItemT], items_selector: Callable[[T], Sequence[ItemT]], /
|
|
132
|
+
) -> AsyncPaginator[T, ItemT]: ...
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def unwrap(
|
|
136
|
+
value: ApiResult[T, E] | Paginator[ApiResult[T, E], ItemT] | AsyncPaginator[ApiResult[T, E], ItemT],
|
|
137
|
+
items_selector: Callable[[T], Sequence[ItemT]] | None = None,
|
|
138
|
+
/,
|
|
139
|
+
) -> T | Paginator[T, ItemT] | AsyncPaginator[T, ItemT]:
|
|
140
|
+
"""Return the parsed form of a raw value: a result's payload, or a paginator over payloads.
|
|
141
|
+
|
|
142
|
+
A ``Success`` returns its payload and a ``Failure`` raises. A paginator of ``ApiResult`` pages returns a
|
|
143
|
+
paginator of the same flavour over the payloads: earlier pages arrive, and the first failed page raises.
|
|
144
|
+
The raw paginator's own item selector is not used.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
value: A result, or a paginator whose pages are results.
|
|
148
|
+
items_selector: Returns one payload's items, in order. Required with a paginator; omit it with a result.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
The payload, or a lazy paginator over payloads that can be iterated more than once.
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
ApiError: For a ``Failure``, carrying its ``error`` and response head. For a paginator, raised while
|
|
155
|
+
iterating, at the first failed page.
|
|
156
|
+
TypeError: If the value is neither a result nor a paginator, or a paginator arrives without ``items_selector``.
|
|
157
|
+
The overloads reject both statically."""
|
|
158
|
+
match value, items_selector:
|
|
159
|
+
case Success(payload=payload), _:
|
|
160
|
+
return payload
|
|
161
|
+
case Failure(error=error, status_code=status_code, headers=headers), _:
|
|
162
|
+
raise ApiError(error=error, status_code=status_code, headers=headers)
|
|
163
|
+
case Paginator(), items if items is not None:
|
|
164
|
+
|
|
165
|
+
def fetch_pages() -> Iterator[T]:
|
|
166
|
+
for result in value.iter_pages():
|
|
167
|
+
yield unwrap(result)
|
|
168
|
+
|
|
169
|
+
return Paginator(fetch_pages, items)
|
|
170
|
+
case AsyncPaginator(), items if items is not None:
|
|
171
|
+
|
|
172
|
+
async def async_fetch_pages() -> AsyncIterator[T]:
|
|
173
|
+
async for result in value.iter_pages():
|
|
174
|
+
yield unwrap(result)
|
|
175
|
+
|
|
176
|
+
return AsyncPaginator(async_fetch_pages, items)
|
|
177
|
+
case _:
|
|
178
|
+
raise TypeError("the value cannot be unwrapped")
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""What to retry, how long to wait, and when to stop.
|
|
2
|
+
|
|
3
|
+
Decisions only, never action: the loop that sleeps and sends again is ``raw_client.execute``, and
|
|
4
|
+
this module is what it asks. That is what makes every rule here testable without a transport.
|
|
5
|
+
|
|
6
|
+
A retry is safe only when two things hold, and both are answered here: the request may be repeated
|
|
7
|
+
without knowing whether the server acted on it -- a method the caller nominated, failing in a way
|
|
8
|
+
the policy names -- and its body can go out again byte for byte (:func:`is_retryable_body`)."""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import random
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from collections.abc import Set as AbstractSet
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from email.utils import parsedate_to_datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Annotated, TypeAlias
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
21
|
+
from typing_extensions import NotRequired, TypedDict
|
|
22
|
+
|
|
23
|
+
from ._internal.headers import find_header
|
|
24
|
+
from .bodies import BinaryBody, FormBody, JsonBody, MultipartBody, MultipartFile, RequestBody, TextBody
|
|
25
|
+
from .transport import HttpMethod
|
|
26
|
+
|
|
27
|
+
# This policy's bounds, each declared once as an alias. ``MaxRetries`` is the one the per-call layer
|
|
28
|
+
# shares: ``RequestOptions`` spells the same field as ``MaxRetries | None`` and the two must not be
|
|
29
|
+
# able to drift, because the merge there copies a caller's value onto this policy *without*
|
|
30
|
+
# revalidating it -- sound only while the bound that admitted the value is the bound governing the
|
|
31
|
+
# field it lands on. A value out of range reaching the loop is not a rejected call but a malformed
|
|
32
|
+
# one: a negative ``max_retries`` empties ``range(max_retries + 1)``, so ``execute`` sends nothing at
|
|
33
|
+
# all. The four curve bounds are the client's alone and are aliased for uniformity;
|
|
34
|
+
# ``status_codes_to_retry`` has no alias because it has no bound to single-source -- but it shares a
|
|
35
|
+
# *type* the same way: ``AbstractSet[int]`` at both boundaries, stored by pydantic as a ``frozenset``,
|
|
36
|
+
# so the copy lands a ``frozenset`` on a field that already holds one.
|
|
37
|
+
MaxRetries: TypeAlias = Annotated[int, Field(ge=0)]
|
|
38
|
+
InitialDelay: TypeAlias = Annotated[float, Field(gt=0)]
|
|
39
|
+
BackoffFactor: TypeAlias = Annotated[float, Field(ge=1)]
|
|
40
|
+
MaxDelay: TypeAlias = Annotated[float, Field(gt=0)]
|
|
41
|
+
MaxJitter: TypeAlias = Annotated[float, Field(ge=0)]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class RetryOptions(BaseModel):
|
|
45
|
+
"""How many times to retry, how long to wait, and which failures qualify.
|
|
46
|
+
|
|
47
|
+
Caller input, so its constraints are declared rather than hand-written, exactly as they are on
|
|
48
|
+
:class:`~.request_options.RequestOptions`. The field counts *retries*, not attempts, so
|
|
49
|
+
``max_retries=0`` disables retrying -- the spelling Stainless, Fern and APIMatic v1 all use, so a
|
|
50
|
+
count reads the same wherever a caller met it before. The loop adds the initial attempt.
|
|
51
|
+
|
|
52
|
+
Retries are counted rather than time budgeted. A wall-clock budget makes the elapsed time
|
|
53
|
+
predictable and the attempt count arbitrary; counting retries makes the worst case
|
|
54
|
+
``max_retries + 1`` timeouts plus the delays between them -- every term of which the caller set.
|
|
55
|
+
|
|
56
|
+
One status set under one method gate, and the gate covers a request that produced no response
|
|
57
|
+
too. A 5xx or a 408 says the server may have processed the request; a dropped connection leaves
|
|
58
|
+
that unknown; and a 429, though a refusal, is gated the same way rather than given a set of its
|
|
59
|
+
own. Nothing is repeated for a method outside ``http_methods_to_retry``; a caller whose API honors
|
|
60
|
+
the ``Idempotency-Key`` this SDK already sends on every non-GET declares that by adding ``POST``."""
|
|
61
|
+
|
|
62
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
63
|
+
|
|
64
|
+
max_retries: MaxRetries = 3
|
|
65
|
+
"""Retries after the initial attempt. ``0`` disables retrying."""
|
|
66
|
+
|
|
67
|
+
initial_delay: InitialDelay = 1.0
|
|
68
|
+
"""Seconds to wait before the first retry, before jitter."""
|
|
69
|
+
|
|
70
|
+
backoff_factor: BackoffFactor = 2.0
|
|
71
|
+
"""Multiplier applied per attempt. ``1`` makes the delay constant."""
|
|
72
|
+
|
|
73
|
+
max_delay: MaxDelay = 60.0
|
|
74
|
+
"""Ceiling for one wait: the curve is capped at it, and a longer ``Retry-After`` is trimmed to it."""
|
|
75
|
+
|
|
76
|
+
max_jitter: MaxJitter = 0.5
|
|
77
|
+
"""Widest random addition to one wait, in seconds. ``0`` makes the curve exact."""
|
|
78
|
+
|
|
79
|
+
status_codes_to_retry: AbstractSet[int] = frozenset({408, 429, 500, 502, 503, 504})
|
|
80
|
+
"""Statuses that qualify for a retry -- for a method in ``http_methods_to_retry``, never otherwise."""
|
|
81
|
+
|
|
82
|
+
http_methods_to_retry: AbstractSet[HttpMethod] = frozenset({"GET", "HEAD", "PUT", "OPTIONS"})
|
|
83
|
+
"""Methods safe to repeat blind. Add ``POST`` when the API honours our ``Idempotency-Key``."""
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def coerce(cls, retry: int | RetryOptionsOrDict | None) -> RetryOptions:
|
|
87
|
+
"""Return ``retry`` as a :class:`RetryOptions`, whichever spelling it arrived in.
|
|
88
|
+
|
|
89
|
+
An instance passes straight through -- it validated itself when it was built -- and ``None``
|
|
90
|
+
yields ``RetryOptions()``, because **retrying is on by default**: a caller who
|
|
91
|
+
configured nothing gets the tuned policy, three retries on the idempotent methods. Turning it
|
|
92
|
+
off is ``0`` (or ``{"max_retries": 0}``); asking with a count alone is a bare ``int``, which is
|
|
93
|
+
``max_retries`` and leaves every other field at its default. It is handed to that field
|
|
94
|
+
unexamined, so it coerces exactly as ``max_retries`` does -- a ``bool`` included, ``True`` being
|
|
95
|
+
one retry -- and no rule here is stricter or laxer than the field it stands for.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
retry: A retry count, the policy in either spelling, or ``None`` for the tuned
|
|
99
|
+
defaults. It is the client's own ``retry_options`` argument, whichever spelling it
|
|
100
|
+
arrived in; a single call overrides the policy it yields field by field, through
|
|
101
|
+
the flat terms on :class:`~.request_options.RequestOptions`.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
A validated :class:`RetryOptions`; ``RetryOptions()`` when ``retry`` is ``None``.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
ValidationError: If ``retry`` carries an unknown key or an out-of-range value -- a
|
|
108
|
+
negative count included. It subclasses ``ValueError``, so a caller catching that
|
|
109
|
+
still covers this."""
|
|
110
|
+
if retry is None:
|
|
111
|
+
return cls()
|
|
112
|
+
if isinstance(retry, int):
|
|
113
|
+
return cls(max_retries=retry)
|
|
114
|
+
if isinstance(retry, cls):
|
|
115
|
+
return retry
|
|
116
|
+
return cls.model_validate(retry)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class RetryOptionsDict(TypedDict):
|
|
120
|
+
"""The dict-shaped spelling of :class:`RetryOptions`, mirroring it field for field.
|
|
121
|
+
|
|
122
|
+
Closed and typed per key, which is what makes ``{"max_retires": 2}`` a type error at the call
|
|
123
|
+
site rather than a surprise at runtime."""
|
|
124
|
+
|
|
125
|
+
max_retries: NotRequired[int]
|
|
126
|
+
initial_delay: NotRequired[float]
|
|
127
|
+
backoff_factor: NotRequired[float]
|
|
128
|
+
max_delay: NotRequired[float]
|
|
129
|
+
max_jitter: NotRequired[float]
|
|
130
|
+
status_codes_to_retry: NotRequired[AbstractSet[int]]
|
|
131
|
+
http_methods_to_retry: NotRequired[AbstractSet[HttpMethod]]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
RetryOptionsOrDict: TypeAlias = RetryOptions | RetryOptionsDict
|
|
135
|
+
"""The typed policy, or a dict carrying the same keys -- the two spellings the companion covers."""
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def backoff_seconds(retries_taken: int, retry_options: RetryOptions) -> float:
|
|
139
|
+
"""The curve's wait after ``retries_taken`` retries, plus a random addition up to ``max_jitter``.
|
|
140
|
+
|
|
141
|
+
The exponential curve is the floor and the jitter sits on top of it. ``max_delay`` bounds the
|
|
142
|
+
**sum**, so a wait never exceeds it and the jitter is what the cap swallows first: every attempt
|
|
143
|
+
whose curve has reached the ceiling waits exactly ``max_delay``.
|
|
144
|
+
|
|
145
|
+
That includes a curve too large for a float. ``float ** int`` raises ``OverflowError`` rather
|
|
146
|
+
than answering infinity -- a factor of 2 does from the 1024th retry -- and the loop asks for this
|
|
147
|
+
wait while it still holds the discarded response, outside its close guard. So this never raises:
|
|
148
|
+
an overflow is read as past the ceiling.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
retries_taken: How many retries this call has already made; ``0`` for the first one.
|
|
152
|
+
retry_options: The policy in force for this call.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Seconds to wait: the curve plus ``0.0`` to ``max_jitter``, capped at ``max_delay``."""
|
|
156
|
+
try:
|
|
157
|
+
backoff_sec = retry_options.initial_delay * retry_options.backoff_factor**retries_taken
|
|
158
|
+
except OverflowError:
|
|
159
|
+
backoff_sec = retry_options.max_delay
|
|
160
|
+
return min(backoff_sec + random.uniform(0.0, retry_options.max_jitter), retry_options.max_delay)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def is_retryable_body(body: RequestBody | None) -> bool:
|
|
164
|
+
"""Whether ``body`` can be sent again byte for byte.
|
|
165
|
+
|
|
166
|
+
``None``, JSON, form and text bodies always can: each already holds its wire bytes. The two that
|
|
167
|
+
carry caller-held content -- a raw binary body, and a multipart body's file parts -- are each
|
|
168
|
+
asked about their content.
|
|
169
|
+
|
|
170
|
+
A multipart body's text parts are not consulted: they hold their bytes. One non-retryable file part
|
|
171
|
+
disqualifies the whole body, because the parts are encoded as a single stream.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
body: The body the endpoint built, or ``None`` for a request that carries none.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
``True`` only for a shape proven re-sendable."""
|
|
178
|
+
match body:
|
|
179
|
+
case None | JsonBody() | FormBody() | TextBody():
|
|
180
|
+
return True
|
|
181
|
+
case BinaryBody(content=content):
|
|
182
|
+
return isinstance(content, (bytes, bytearray, Path))
|
|
183
|
+
case MultipartBody(parts=parts):
|
|
184
|
+
return all(
|
|
185
|
+
isinstance(part.content, (bytes, bytearray, Path)) for part in parts if isinstance(part, MultipartFile)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def retry_after_seconds(headers: Mapping[str, str], now: datetime) -> float | None:
|
|
190
|
+
"""Seconds named by ``Retry-After``, or ``None`` if the header is absent or unusable.
|
|
191
|
+
|
|
192
|
+
Both RFC 9110 forms are read: delta-seconds, and an HTTP-date. A date already past floors at
|
|
193
|
+
``0.0`` -- the server is naming a moment, and a moment gone means *now*.
|
|
194
|
+
|
|
195
|
+
Delta-seconds is read exactly as the grammar writes it, ``1*DIGIT``, and never through bare
|
|
196
|
+
``float`` -- which would take ``1_0`` as ten, ``inf`` as a wait beyond any ceiling, and ``1.5``
|
|
197
|
+
and ``-5`` as values the grammar has no room for. So a non-conforming value is *unparseable*
|
|
198
|
+
rather than quietly reinterpreted, and the floor above is the date branch's alone.
|
|
199
|
+
|
|
200
|
+
An unparseable value yields ``None`` rather than raising. A malformed header is the server's
|
|
201
|
+
defect, and turning a retryable failure into an exception would make the SDK's behavior worse
|
|
202
|
+
than if the header had never been sent.
|
|
203
|
+
|
|
204
|
+
The clock is the caller's: ``now`` is read once in ``raw_client.execute``, immediately after the
|
|
205
|
+
head arrives, and handed over. So the one branch that needs a reference moment names it as an
|
|
206
|
+
argument rather than reaching for a global, which keeps this module free of ambient state and
|
|
207
|
+
makes the date branch testable by passing a moment instead of patching ``datetime``.
|
|
208
|
+
|
|
209
|
+
The field is found case-insensitively rather than by ``headers.get("retry-after")``. Both
|
|
210
|
+
response protocols oblige a transport to lowercase its header names, and the shipped one does --
|
|
211
|
+
but a custom transport that forgets would not fail here, it would silently ignore every
|
|
212
|
+
``Retry-After`` a server sends and wait out the curve instead. That is the kind of defect a
|
|
213
|
+
caller never sees, so the one reader of a response header pays a scan of the head to be
|
|
214
|
+
independent of the obligation. The scan runs only on a retryable failure.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
headers: The failing response's head, scanned case-insensitively for the field.
|
|
218
|
+
now: The moment an HTTP-date is measured against -- the loop's own reading of the clock,
|
|
219
|
+
taken when the head arrived. Unused by the delta-seconds form.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Seconds to wait, never negative; ``None`` where the header is absent or unusable."""
|
|
223
|
+
token = find_header(headers, "Retry-After")
|
|
224
|
+
if token is None:
|
|
225
|
+
return None
|
|
226
|
+
# ``isdigit`` alone would take a non-ASCII digit; ``isascii`` fences the pair to ``[0-9]+``.
|
|
227
|
+
if token.isascii() and token.isdigit():
|
|
228
|
+
return float(token)
|
|
229
|
+
try:
|
|
230
|
+
when = parsedate_to_datetime(token)
|
|
231
|
+
except (TypeError, ValueError):
|
|
232
|
+
return None
|
|
233
|
+
# A date with no zone is UTC by RFC 9110: HTTP-dates are always GMT, and the one legacy format
|
|
234
|
+
# parsedate_to_datetime accepts without a zone would otherwise subtract as naive and raise.
|
|
235
|
+
if when.tzinfo is None:
|
|
236
|
+
when = when.replace(tzinfo=timezone.utc)
|
|
237
|
+
return max(0.0, (when - now).total_seconds())
|
|
@@ -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
|