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.
Files changed (92) hide show
  1. cliv1-0.0.7.dist-info/METADATA +212 -0
  2. cliv1-0.0.7.dist-info/RECORD +92 -0
  3. cliv1-0.0.7.dist-info/WHEEL +4 -0
  4. cliv1-0.0.7.dist-info/licenses/LICENSE +21 -0
  5. swagger_petstore_open_api_3_1_0/__init__.py +15 -0
  6. swagger_petstore_open_api_3_1_0/apis/__init__.py +5 -0
  7. swagger_petstore_open_api_3_1_0/apis/pet_api.py +922 -0
  8. swagger_petstore_open_api_3_1_0/apis/store.py +432 -0
  9. swagger_petstore_open_api_3_1_0/apis/user_api.py +829 -0
  10. swagger_petstore_open_api_3_1_0/async_client.py +87 -0
  11. swagger_petstore_open_api_3_1_0/auth.py +17 -0
  12. swagger_petstore_open_api_3_1_0/base_client.py +27 -0
  13. swagger_petstore_open_api_3_1_0/client.py +85 -0
  14. swagger_petstore_open_api_3_1_0/core/__init__.py +395 -0
  15. swagger_petstore_open_api_3_1_0/core/_internal/__init__.py +9 -0
  16. swagger_petstore_open_api_3_1_0/core/_internal/content_disposition.py +154 -0
  17. swagger_petstore_open_api_3_1_0/core/_internal/flattening.py +104 -0
  18. swagger_petstore_open_api_3_1_0/core/_internal/headers.py +243 -0
  19. swagger_petstore_open_api_3_1_0/core/_internal/redaction.py +208 -0
  20. swagger_petstore_open_api_3_1_0/core/_internal/sse_parser.py +124 -0
  21. swagger_petstore_open_api_3_1_0/core/_internal/subscripts.py +50 -0
  22. swagger_petstore_open_api_3_1_0/core/_internal/urls.py +172 -0
  23. swagger_petstore_open_api_3_1_0/core/_internal/wire.py +201 -0
  24. swagger_petstore_open_api_3_1_0/core/adapters.py +177 -0
  25. swagger_petstore_open_api_3_1_0/core/auth/__init__.py +70 -0
  26. swagger_petstore_open_api_3_1_0/core/auth/composition.py +175 -0
  27. swagger_petstore_open_api_3_1_0/core/auth/models.py +216 -0
  28. swagger_petstore_open_api_3_1_0/core/auth/protocols.py +103 -0
  29. swagger_petstore_open_api_3_1_0/core/auth/schemes/__init__.py +94 -0
  30. swagger_petstore_open_api_3_1_0/core/auth/schemes/api_key_auth.py +53 -0
  31. swagger_petstore_open_api_3_1_0/core/auth/schemes/basic_auth.py +73 -0
  32. swagger_petstore_open_api_3_1_0/core/auth/schemes/bearer_auth.py +21 -0
  33. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_authorization_code.py +395 -0
  34. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_client_credentials.py +130 -0
  35. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_password.py +145 -0
  36. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_schemes.py +300 -0
  37. swagger_petstore_open_api_3_1_0/core/base_raw_response.py +49 -0
  38. swagger_petstore_open_api_3_1_0/core/bodies.py +590 -0
  39. swagger_petstore_open_api_3_1_0/core/converters/__init__.py +68 -0
  40. swagger_petstore_open_api_3_1_0/core/converters/date_time.py +237 -0
  41. swagger_petstore_open_api_3_1_0/core/converters/encoded_bytes.py +273 -0
  42. swagger_petstore_open_api_3_1_0/core/converters/open_enum.py +35 -0
  43. swagger_petstore_open_api_3_1_0/core/decoding.py +498 -0
  44. swagger_petstore_open_api_3_1_0/core/discriminators.py +129 -0
  45. swagger_petstore_open_api_3_1_0/core/event_streams.py +225 -0
  46. swagger_petstore_open_api_3_1_0/core/exceptions.py +78 -0
  47. swagger_petstore_open_api_3_1_0/core/file_responses.py +337 -0
  48. swagger_petstore_open_api_3_1_0/core/files.py +174 -0
  49. swagger_petstore_open_api_3_1_0/core/httpx2_transport.py +748 -0
  50. swagger_petstore_open_api_3_1_0/core/logging.py +472 -0
  51. swagger_petstore_open_api_3_1_0/core/models.py +143 -0
  52. swagger_petstore_open_api_3_1_0/core/optionality.py +144 -0
  53. swagger_petstore_open_api_3_1_0/core/pagination.py +452 -0
  54. swagger_petstore_open_api_3_1_0/core/paginator.py +55 -0
  55. swagger_petstore_open_api_3_1_0/core/params.py +157 -0
  56. swagger_petstore_open_api_3_1_0/core/raw_client.py +480 -0
  57. swagger_petstore_open_api_3_1_0/core/request_options.py +172 -0
  58. swagger_petstore_open_api_3_1_0/core/results.py +178 -0
  59. swagger_petstore_open_api_3_1_0/core/retries.py +237 -0
  60. swagger_petstore_open_api_3_1_0/core/runtime_env.py +43 -0
  61. swagger_petstore_open_api_3_1_0/core/servers.py +33 -0
  62. swagger_petstore_open_api_3_1_0/core/transport.py +218 -0
  63. swagger_petstore_open_api_3_1_0/errors/__init__.py +48 -0
  64. swagger_petstore_open_api_3_1_0/errors/add_pet_error.py +21 -0
  65. swagger_petstore_open_api_3_1_0/errors/delete_order_error.py +21 -0
  66. swagger_petstore_open_api_3_1_0/errors/delete_pet_error.py +21 -0
  67. swagger_petstore_open_api_3_1_0/errors/delete_user_error.py +21 -0
  68. swagger_petstore_open_api_3_1_0/errors/find_pets_by_status_error.py +21 -0
  69. swagger_petstore_open_api_3_1_0/errors/find_pets_by_tags_error.py +21 -0
  70. swagger_petstore_open_api_3_1_0/errors/get_order_by_id_error.py +21 -0
  71. swagger_petstore_open_api_3_1_0/errors/get_pet_by_id_error.py +21 -0
  72. swagger_petstore_open_api_3_1_0/errors/get_user_by_name_error.py +21 -0
  73. swagger_petstore_open_api_3_1_0/errors/login_user_error.py +21 -0
  74. swagger_petstore_open_api_3_1_0/errors/place_order_error.py +21 -0
  75. swagger_petstore_open_api_3_1_0/errors/update_pet_error.py +21 -0
  76. swagger_petstore_open_api_3_1_0/errors/update_pet_with_form_error.py +21 -0
  77. swagger_petstore_open_api_3_1_0/errors/update_user_error.py +21 -0
  78. swagger_petstore_open_api_3_1_0/errors/upload_file_error.py +21 -0
  79. swagger_petstore_open_api_3_1_0/models/__init__.py +23 -0
  80. swagger_petstore_open_api_3_1_0/models/api_response.py +18 -0
  81. swagger_petstore_open_api_3_1_0/models/category.py +15 -0
  82. swagger_petstore_open_api_3_1_0/models/enums/__init__.py +4 -0
  83. swagger_petstore_open_api_3_1_0/models/enums/order_status.py +17 -0
  84. swagger_petstore_open_api_3_1_0/models/enums/pet_status.py +17 -0
  85. swagger_petstore_open_api_3_1_0/models/order.py +27 -0
  86. swagger_petstore_open_api_3_1_0/models/pet.py +28 -0
  87. swagger_petstore_open_api_3_1_0/models/tag_model.py +15 -0
  88. swagger_petstore_open_api_3_1_0/models/user.py +29 -0
  89. swagger_petstore_open_api_3_1_0/py.typed +0 -0
  90. swagger_petstore_open_api_3_1_0/server/__init__.py +21 -0
  91. swagger_petstore_open_api_3_1_0/server/server.py +17 -0
  92. 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