cliV1 0.0.6__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.6.dist-info/METADATA +212 -0
- cliv1-0.0.6.dist-info/RECORD +92 -0
- cliv1-0.0.6.dist-info/WHEEL +4 -0
- cliv1-0.0.6.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,157 @@
|
|
|
1
|
+
"""Request parameters and the unresolved URL they are attached to.
|
|
2
|
+
|
|
3
|
+
A :class:`Param` is one query, header, path or form parameter: a key, the value the endpoint
|
|
4
|
+
received, and the adapter for the type it was declared as. The declared type decides how the value
|
|
5
|
+
reaches the wire -- a temporal alias carries its own serializer, so ``RFC1123DateTime`` and
|
|
6
|
+
``UnixSecondsDateTime`` render the same ``datetime`` as a string and as an int -- and is what
|
|
7
|
+
dict-shaped input is validated against. How a *collection* explodes into keys is the one choice a
|
|
8
|
+
type cannot express, so it stays a field (:class:`SerializationFormat`)."""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any, Final, Generic, TypeVar
|
|
16
|
+
|
|
17
|
+
from pydantic import TypeAdapter
|
|
18
|
+
from typing_extensions import TypeForm
|
|
19
|
+
|
|
20
|
+
from ._internal.subscripts import SubscriptOnly
|
|
21
|
+
from .adapters import adapter_for, validation_target
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SerializationFormat(Enum):
|
|
27
|
+
"""How a multi-valued *query or form* parameter explodes into keys.
|
|
28
|
+
|
|
29
|
+
Only those two destinations can honour it, because only they can repeat or subscript a key. A
|
|
30
|
+
path array folds into one comma-separated segment and a header array into one comma-separated
|
|
31
|
+
field value -- OpenAPI's default ``style: simple`` for both, and RFC 9110's own rule for a
|
|
32
|
+
repeated header -- so a path or header :class:`Param` carries this field and nothing reads it.
|
|
33
|
+
That is the price of one ``Param`` and one :func:`param` factory serving every location a
|
|
34
|
+
parameter can occupy, and it is a price rather than a defect: the alternative is a parameter
|
|
35
|
+
type per destination, and with it a generator branch deciding which to emit -- the conditional
|
|
36
|
+
emission ADR-0012 removed.
|
|
37
|
+
|
|
38
|
+
The remaining path and header styles -- ``label``, ``matrix``, ``explode: true`` -- are not
|
|
39
|
+
modelled, and cannot be from here: nothing below this module carries ``style`` or ``explode``,
|
|
40
|
+
so the default is the whole of what there is to render."""
|
|
41
|
+
|
|
42
|
+
INDEXED = "indexed"
|
|
43
|
+
"""Example: variableName[0]=value1"""
|
|
44
|
+
|
|
45
|
+
UNINDEXED = "unindexed"
|
|
46
|
+
"""Example: variableName[]=value1"""
|
|
47
|
+
|
|
48
|
+
PLAIN = "plain"
|
|
49
|
+
"""Example: variableName=value1&variableName=value2"""
|
|
50
|
+
|
|
51
|
+
CSV = "csv"
|
|
52
|
+
"""Example: variableName=value1,value2"""
|
|
53
|
+
|
|
54
|
+
TSV = "tsv"
|
|
55
|
+
r"""Example: variableName=value1\tvalue2"""
|
|
56
|
+
|
|
57
|
+
PSV = "psv"
|
|
58
|
+
"""Example: variableName=value1|value2"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True, slots=True)
|
|
62
|
+
class Param(Generic[T]):
|
|
63
|
+
"""One request parameter, not yet flattened onto the wire.
|
|
64
|
+
|
|
65
|
+
``T`` is the endpoint's declared type in full, dict-shaped companion included -- exactly what
|
|
66
|
+
``param[...]`` accepts -- while the adapter targets :func:`validation_target` of it, which is
|
|
67
|
+
what the value is validated *into*. ``value`` is typed ``object`` all the same, deliberately:
|
|
68
|
+
every consumer holds a ``Param[Any]``, where a ``T``-typed field would read as ``Any`` and be
|
|
69
|
+
checked as nothing, so ``object`` leaves the adapter as the only route to the value. The
|
|
70
|
+
*resolved* adapter is held rather than the type expression it came from, so the one cache
|
|
71
|
+
lookup happens when the parameter is built rather than on every flatten.
|
|
72
|
+
|
|
73
|
+
Unlike a request body, a parameter is deliberately *not* serialized when it is built: query,
|
|
74
|
+
header and path parameters are merged per key with the API-wide ``global_*`` sets while the
|
|
75
|
+
request is being built, so they have to stay inspectable until then. ``wire_value``
|
|
76
|
+
(``_internal/wire.py``) performs the same validate-then-dump through this ``adapter`` at that
|
|
77
|
+
point, for every destination; each destination then decides only how the result is arranged --
|
|
78
|
+
into query pairs, path segments, or one header value.
|
|
79
|
+
|
|
80
|
+
Construct it through ``param[...]``, whose subscript is where ``T`` is bound.
|
|
81
|
+
|
|
82
|
+
Two parameters compare equal when key, value and format match *and* they share an adapter.
|
|
83
|
+
Adapters are cached per type by :func:`adapter_for`, so that holds for any cacheable type; an
|
|
84
|
+
unhashable type form bypasses the cache and would make two otherwise-identical parameters
|
|
85
|
+
unequal. Nothing depends on it -- resolution keys on ``key`` alone."""
|
|
86
|
+
|
|
87
|
+
key: str
|
|
88
|
+
value: object
|
|
89
|
+
adapter: TypeAdapter[T]
|
|
90
|
+
serialization_format: SerializationFormat = SerializationFormat.INDEXED
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True, slots=True)
|
|
94
|
+
class _DeclaredParam(Generic[T]):
|
|
95
|
+
"""A parameter's declared type, bound to its adapter; call it with the key and value.
|
|
96
|
+
|
|
97
|
+
Built by ``param[T]``; resolving the adapter there is what lets the parameter hold a typed
|
|
98
|
+
``TypeAdapter[T]``."""
|
|
99
|
+
|
|
100
|
+
adapter: TypeAdapter[T]
|
|
101
|
+
|
|
102
|
+
def __call__(
|
|
103
|
+
self,
|
|
104
|
+
key: str,
|
|
105
|
+
value: T,
|
|
106
|
+
*,
|
|
107
|
+
serialization_format: SerializationFormat = SerializationFormat.INDEXED,
|
|
108
|
+
) -> Param[T]:
|
|
109
|
+
return Param(key, value, self.adapter, serialization_format)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class _ParamFactory(SubscriptOnly):
|
|
113
|
+
"""``param[T](key, value)`` -- name a parameter's declared type in the subscript.
|
|
114
|
+
|
|
115
|
+
Subscripted for the reason ``json_body`` documents in full: ``T`` is solved from the
|
|
116
|
+
subscript alone, before the value is compared against it, so ``param[bool]("array",
|
|
117
|
+
employee)`` is a build failure where a one-call ``param("array", employee, bool)`` would
|
|
118
|
+
have inference absorb the disagreement. And because the factory declares no ``__call__``,
|
|
119
|
+
*omitting* the declared type is a build failure too: ``param("array", True)`` is rejected
|
|
120
|
+
statically (``[operator]``); the runtime ``__call__`` inherited from :class:`SubscriptOnly` is
|
|
121
|
+
invisible to type checkers and exists only to turn that same mistake into a guided ``TypeError``.
|
|
122
|
+
|
|
123
|
+
The subscript is a ``TypeForm``, exactly as in ``json_body[...]``, so a runtime union alias
|
|
124
|
+
(``Person``, which is ``Employee | Boss``) binds ``T`` as precisely as a concrete class
|
|
125
|
+
does. It is the endpoint's declared type in full, companion included --
|
|
126
|
+
``param[Person | PersonDict]`` -- and :func:`validation_target` keeps the companion out of
|
|
127
|
+
the adapter."""
|
|
128
|
+
|
|
129
|
+
factory_name = "param"
|
|
130
|
+
spelling = 'param[T](key, value), e.g. param[bool]("array", True)'
|
|
131
|
+
|
|
132
|
+
def __getitem__(self, declared: TypeForm[T]) -> _DeclaredParam[T]:
|
|
133
|
+
return _DeclaredParam(adapter_for(validation_target(declared)))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
param: Final = _ParamFactory()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass(frozen=True, slots=True)
|
|
140
|
+
class UrlTemplate:
|
|
141
|
+
"""An unresolved endpoint URL.
|
|
142
|
+
|
|
143
|
+
Holds a (possibly templated) base URL, the endpoint-relative path, and the
|
|
144
|
+
server variables that fill the base URL's ``{placeholder}`` tokens. The base
|
|
145
|
+
URL and the path are expanded separately when the request is built.
|
|
146
|
+
|
|
147
|
+
A server variable is *configuration* rather than a spec-declared parameter --
|
|
148
|
+
it is resolved from ``ServerConfig`` before any request exists -- but it fills
|
|
149
|
+
a placeholder exactly as a path parameter does, so it is carried as a
|
|
150
|
+
:class:`Param` and rendered by the same function. Naming its declared type is
|
|
151
|
+
what retires the hand ``.value`` unwrap an enum-typed variable used to need,
|
|
152
|
+
and what makes forgetting one a type error rather than a base URL reading
|
|
153
|
+
``Region.EU``."""
|
|
154
|
+
|
|
155
|
+
base_url: str
|
|
156
|
+
path: str
|
|
157
|
+
variables: Sequence[Param[Any]] = ()
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"""The seams every endpoint crosses: build a request, send it, shape a result.
|
|
2
|
+
|
|
3
|
+
Pagination does not live here. ``pagination.paginate`` and ``async_paginate`` run that loop, calling the
|
|
4
|
+
endpoint's ``execute`` once per page.
|
|
5
|
+
|
|
6
|
+
The twins live in one module so the few ``await``s they differ by stay visible on one screen."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from typing import Any, Generic, TypeVar
|
|
16
|
+
|
|
17
|
+
from ._internal.headers import resolve_headers
|
|
18
|
+
from ._internal.urls import build_url, resolve_params
|
|
19
|
+
from .auth import AsyncAuthScheme, AuthParams, AuthScheme, invalidate, no_auth, resolve_auth
|
|
20
|
+
from .bodies import RequestBody, content_headers
|
|
21
|
+
from .decoding import AsyncResponseDecoder, ErrorMapper, ResponseDecoder
|
|
22
|
+
from .exceptions import TransportError
|
|
23
|
+
from .logging import HttpLog, LoggingOptions
|
|
24
|
+
from .params import Param, UrlTemplate
|
|
25
|
+
from .request_options import RequestOptions, RequestOptionsOrDict, apply_retry_overrides
|
|
26
|
+
from .results import ApiResult, Failure, Success
|
|
27
|
+
from .retries import RetryOptions, backoff_seconds, is_retryable_body, retry_after_seconds
|
|
28
|
+
from .transport import (
|
|
29
|
+
AsyncHttpClient,
|
|
30
|
+
AsyncHttpResponse,
|
|
31
|
+
HttpClient,
|
|
32
|
+
HttpMethod,
|
|
33
|
+
HttpRequest,
|
|
34
|
+
HttpResponse,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
T = TypeVar("T")
|
|
38
|
+
E = TypeVar("E")
|
|
39
|
+
TransportT = TypeVar("TransportT", bound=HttpClient | AsyncHttpClient)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class BaseRawClient(Generic[TransportT]):
|
|
44
|
+
"""Shared state for the raw clients, and the one place a request is composed from it.
|
|
45
|
+
|
|
46
|
+
The type parameter pins the one transport a flavor can use, so :class:`RawClient` is provably
|
|
47
|
+
sync and :class:`AsyncRawClient` provably async. The ``global_*`` fields are spec-derived -- the
|
|
48
|
+
generator writes them, an API declaring none leaves them empty -- and each is combined with a
|
|
49
|
+
call's own parameters by the matching ``resolve_*`` function, so this class decides nothing
|
|
50
|
+
about precedence. For headers and query parameters alike that order is::
|
|
51
|
+
|
|
52
|
+
global_* -> the endpoint's own -> auth -> the caller's extra_headers
|
|
53
|
+
|
|
54
|
+
Auth outranks the endpoint so an operation parameter cannot clobber a credential, and loses to
|
|
55
|
+
``extra_headers`` because that is how a caller deliberately overrides one. ``_build_request`` takes
|
|
56
|
+
the scheme already resolved, as :class:`~.auth.AuthParams`, because resolving it may await; it
|
|
57
|
+
never sends and never logs."""
|
|
58
|
+
|
|
59
|
+
http_client: TransportT
|
|
60
|
+
|
|
61
|
+
retry_options: RetryOptions
|
|
62
|
+
|
|
63
|
+
logging_options: LoggingOptions
|
|
64
|
+
|
|
65
|
+
global_headers: Sequence[Param[Any]] = ()
|
|
66
|
+
global_query_params: Sequence[Param[Any]] = ()
|
|
67
|
+
global_path_params: Sequence[Param[Any]] = ()
|
|
68
|
+
|
|
69
|
+
def _build_request(
|
|
70
|
+
self,
|
|
71
|
+
*,
|
|
72
|
+
http_method: HttpMethod,
|
|
73
|
+
url_template: UrlTemplate,
|
|
74
|
+
path_params: Sequence[Param[Any]] | None,
|
|
75
|
+
query_params: Sequence[Param[Any]] | None,
|
|
76
|
+
headers: Sequence[Param[Any]] | None,
|
|
77
|
+
body: RequestBody | None,
|
|
78
|
+
auth: AuthParams,
|
|
79
|
+
options: RequestOptions,
|
|
80
|
+
) -> HttpRequest:
|
|
81
|
+
"""Resolve the API-wide, endpoint, auth and caller layers into one request, the body's own
|
|
82
|
+
label beneath them all."""
|
|
83
|
+
return HttpRequest(
|
|
84
|
+
method=http_method,
|
|
85
|
+
url=build_url(
|
|
86
|
+
url_template,
|
|
87
|
+
path_params=resolve_params(self.global_path_params, path_params),
|
|
88
|
+
query_params=resolve_params(self.global_query_params, (*(query_params or ()), *auth.query_params)),
|
|
89
|
+
),
|
|
90
|
+
headers={
|
|
91
|
+
**content_headers(body),
|
|
92
|
+
**resolve_headers(
|
|
93
|
+
self.global_headers,
|
|
94
|
+
(*(headers or ()), *auth.request_headers()),
|
|
95
|
+
extra_headers=options.extra_headers,
|
|
96
|
+
),
|
|
97
|
+
},
|
|
98
|
+
body=body,
|
|
99
|
+
timeout=options.timeout,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True, slots=True)
|
|
104
|
+
class RawClient(BaseRawClient[HttpClient]):
|
|
105
|
+
"""Synchronous raw client: builds the request, sends it, shapes the result."""
|
|
106
|
+
|
|
107
|
+
def execute(
|
|
108
|
+
self,
|
|
109
|
+
*,
|
|
110
|
+
http_method: HttpMethod,
|
|
111
|
+
url_template: UrlTemplate,
|
|
112
|
+
path_params: Sequence[Param[Any]] | None = None,
|
|
113
|
+
query_params: Sequence[Param[Any]] | None = None,
|
|
114
|
+
headers: Sequence[Param[Any]] | None = None,
|
|
115
|
+
body: RequestBody | None = None,
|
|
116
|
+
auth_scheme: AuthScheme = no_auth,
|
|
117
|
+
decoder: ResponseDecoder[T],
|
|
118
|
+
error_mapper: ErrorMapper[E],
|
|
119
|
+
request_options: RequestOptionsOrDict | None = None,
|
|
120
|
+
) -> ApiResult[T, E]:
|
|
121
|
+
"""Send a request and shape the response into a result.
|
|
122
|
+
|
|
123
|
+
Returns once the response head has arrived; the body is the ``decoder``'s from there -- a
|
|
124
|
+
buffered one reads and releases it, ``file_decoder`` hands the open connection to the
|
|
125
|
+
:class:`FileResponse` it builds -- and ``T`` is solved from that argument. A non-2xx body is
|
|
126
|
+
read and the connection released *before* the mapper runs, so a :class:`Failure` never holds
|
|
127
|
+
a connection, even when the mapper raises.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
http_method: The HTTP verb to send.
|
|
131
|
+
url_template: The unresolved URL for the configured environment.
|
|
132
|
+
path_params: The endpoint's path parameters, if any.
|
|
133
|
+
query_params: The endpoint's query parameters, if any.
|
|
134
|
+
headers: The endpoint's header parameters, if any.
|
|
135
|
+
body: The request body, already wire-ready, or ``None``.
|
|
136
|
+
auth_scheme: The scheme the operation declares; ``no_auth`` when it declares none.
|
|
137
|
+
decoder: The operation's 2xx decoder; ``empty_response`` when it declares no body.
|
|
138
|
+
error_mapper: The operation's error mapper; ``raw_error_response`` when it declares none.
|
|
139
|
+
request_options: The caller's per-call overrides, if any.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
A :class:`Success` carrying the decoded payload for a 2xx status, otherwise a
|
|
143
|
+
:class:`Failure` carrying the decoded error body.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
ValueError: If the body does not deserialize; not an API error, so it propagates in both
|
|
147
|
+
response modes.
|
|
148
|
+
Exception: What the transport raised when the last attempt produced no response, or when
|
|
149
|
+
the body failed after the head arrived -- never the :class:`TransportError` wrapper."""
|
|
150
|
+
options = RequestOptions.coerce(request_options)
|
|
151
|
+
retry_options = apply_retry_overrides(options, self.retry_options)
|
|
152
|
+
log = HttpLog(
|
|
153
|
+
options=self.logging_options, log_level_override=options.log_level, started_at=time.perf_counter()
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def send() -> HttpResponse:
|
|
157
|
+
request = self._build_request(
|
|
158
|
+
http_method=http_method,
|
|
159
|
+
url_template=url_template,
|
|
160
|
+
path_params=path_params,
|
|
161
|
+
query_params=query_params,
|
|
162
|
+
headers=headers,
|
|
163
|
+
body=body,
|
|
164
|
+
auth=auth_scheme.apply(),
|
|
165
|
+
options=options,
|
|
166
|
+
)
|
|
167
|
+
log.request_sending(request)
|
|
168
|
+
return self.http_client.send(request)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
response = self._attempt_send(
|
|
172
|
+
auth_scheme=auth_scheme,
|
|
173
|
+
retry_options=retry_options,
|
|
174
|
+
is_retryable=is_retryable_body(body) and http_method in retry_options.http_methods_to_retry,
|
|
175
|
+
send=send,
|
|
176
|
+
log=log,
|
|
177
|
+
)
|
|
178
|
+
except Exception as error:
|
|
179
|
+
# No response was kept: the last attempt's transport error, already unwrapped, or anything raised while
|
|
180
|
+
# an attempt was built, sent or its head handled. ``failed`` writes nothing when no request was reported.
|
|
181
|
+
# A BaseException -- the caller's cancellation -- is not this call failing, and passes untouched.
|
|
182
|
+
log.failed(error)
|
|
183
|
+
raise
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
status_code, response_headers = response.status_code, dict(response.headers)
|
|
187
|
+
if 200 <= status_code < 300:
|
|
188
|
+
# Returns before the read: the body is the decoder's now. The excepts below must never
|
|
189
|
+
# become a finally -- they would close the connection a FileResponse was just handed.
|
|
190
|
+
return Success(
|
|
191
|
+
payload=decoder.decode(response),
|
|
192
|
+
status_code=status_code,
|
|
193
|
+
headers=response_headers,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
content = response.read()
|
|
197
|
+
response.close()
|
|
198
|
+
# Mapped after the close, so a mapper raise cannot leak a connection; the second close it
|
|
199
|
+
# causes is a no-op, since close is required to be idempotent.
|
|
200
|
+
return Failure(
|
|
201
|
+
error=error_mapper.map(status_code, content),
|
|
202
|
+
status_code=status_code,
|
|
203
|
+
headers=response_headers,
|
|
204
|
+
)
|
|
205
|
+
except Exception as error:
|
|
206
|
+
# After the head: a body read the transport refused, a body the decoder rejected, an error body the
|
|
207
|
+
# mapper rejected -- each this call producing no result. Closed before the line is written.
|
|
208
|
+
response.close()
|
|
209
|
+
log.failed(error)
|
|
210
|
+
raise
|
|
211
|
+
except BaseException:
|
|
212
|
+
# The caller's cancellation or interrupt: closed, and not logged as this call's failure.
|
|
213
|
+
response.close()
|
|
214
|
+
raise
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def _attempt_send(
|
|
218
|
+
*,
|
|
219
|
+
auth_scheme: AuthScheme,
|
|
220
|
+
retry_options: RetryOptions,
|
|
221
|
+
is_retryable: bool,
|
|
222
|
+
send: Callable[[], HttpResponse],
|
|
223
|
+
log: HttpLog,
|
|
224
|
+
) -> HttpResponse:
|
|
225
|
+
"""Send, and send again while every gate agrees; hand back the attempt that was kept.
|
|
226
|
+
|
|
227
|
+
``send`` is called once per attempt: it builds the request -- resolving auth afresh, so a
|
|
228
|
+
credential a 401 invalidated is re-obtained rather than resent -- reports it to ``log`` and
|
|
229
|
+
sends it. Each head is reported to ``log`` as it arrives, with whether it is a success by the
|
|
230
|
+
same 2xx rule ``execute`` shapes the result by, so the scope holds no status rule of its own;
|
|
231
|
+
if that report, or the 401 invalidation after it, raises -- a log handler or the caller's
|
|
232
|
+
scheme -- the response is closed before the exception leaves. The retry line is written after
|
|
233
|
+
the discarded response is closed and before the wait, so nothing a log handler does there can
|
|
234
|
+
hold a connection. A 401 invalidates the scheme's cache per attempt but is not retried under
|
|
235
|
+
the shipped defaults.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
auth_scheme: The scheme this operation declares; invalidated where the server answers 401.
|
|
239
|
+
retry_options: The policy in force for this call, per-call terms already merged.
|
|
240
|
+
is_retryable: Whether this request may be repeated at all -- the method gate and the
|
|
241
|
+
body's replayability, folded once per call since neither changes between attempts.
|
|
242
|
+
send: Builds, reports and sends one attempt's request.
|
|
243
|
+
log: The call's log scope.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
The kept attempt's response, open and its body unread.
|
|
247
|
+
|
|
248
|
+
Raises:
|
|
249
|
+
Exception: What the transport raised when the last attempt produced no response; the
|
|
250
|
+
:class:`TransportError` wrapper is unwrapped and its ``inner_exception`` raised in
|
|
251
|
+
its place."""
|
|
252
|
+
for retries_taken in range(retry_options.max_retries + 1):
|
|
253
|
+
try:
|
|
254
|
+
response = send()
|
|
255
|
+
except TransportError as error:
|
|
256
|
+
# No response arrived, so there is nothing to close.
|
|
257
|
+
inner = error.inner_exception
|
|
258
|
+
if retries_taken >= retry_options.max_retries or not is_retryable:
|
|
259
|
+
raise inner from inner.__cause__
|
|
260
|
+
wait_seconds = backoff_seconds(retries_taken, retry_options)
|
|
261
|
+
log.retrying(max_retries=retry_options.max_retries, delay=wait_seconds, reason=inner)
|
|
262
|
+
time.sleep(wait_seconds)
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
status_code, response_headers = response.status_code, dict(response.headers)
|
|
266
|
+
|
|
267
|
+
# The two calls here run code the SDK does not own -- a log handler, the caller's scheme -- while the
|
|
268
|
+
# response is open; one that raises must not leave the connection held.
|
|
269
|
+
try:
|
|
270
|
+
log.response_received(status_code, response_headers, is_success=200 <= status_code < 300)
|
|
271
|
+
if status_code == 401:
|
|
272
|
+
invalidate(auth_scheme)
|
|
273
|
+
except BaseException:
|
|
274
|
+
response.close()
|
|
275
|
+
raise
|
|
276
|
+
|
|
277
|
+
if not (
|
|
278
|
+
is_retryable
|
|
279
|
+
and status_code in retry_options.status_codes_to_retry
|
|
280
|
+
and retries_taken < retry_options.max_retries
|
|
281
|
+
):
|
|
282
|
+
return response
|
|
283
|
+
|
|
284
|
+
retry_after_header = retry_after_seconds(response_headers, datetime.now(timezone.utc))
|
|
285
|
+
wait_seconds = (
|
|
286
|
+
backoff_seconds(retries_taken, retry_options)
|
|
287
|
+
if retry_after_header is None
|
|
288
|
+
else min(retry_after_header, retry_options.max_delay)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Closed before the retry line and the wait, so neither can hold a connection.
|
|
292
|
+
response.close()
|
|
293
|
+
log.retrying(max_retries=retry_options.max_retries, delay=wait_seconds, reason=status_code)
|
|
294
|
+
time.sleep(wait_seconds)
|
|
295
|
+
|
|
296
|
+
raise AssertionError("the retry loop ended without a response to shape")
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@dataclass(frozen=True, slots=True)
|
|
300
|
+
class AsyncRawClient(BaseRawClient[AsyncHttpClient]):
|
|
301
|
+
"""Asynchronous raw client.
|
|
302
|
+
|
|
303
|
+
The method is named ``execute``, not ``execute_async``: sync and async peers
|
|
304
|
+
carry identical names throughout this SDK, and the transport already makes the
|
|
305
|
+
flavor unambiguous."""
|
|
306
|
+
|
|
307
|
+
async def execute(
|
|
308
|
+
self,
|
|
309
|
+
*,
|
|
310
|
+
http_method: HttpMethod,
|
|
311
|
+
url_template: UrlTemplate,
|
|
312
|
+
path_params: Sequence[Param[Any]] | None = None,
|
|
313
|
+
query_params: Sequence[Param[Any]] | None = None,
|
|
314
|
+
headers: Sequence[Param[Any]] | None = None,
|
|
315
|
+
body: RequestBody | None = None,
|
|
316
|
+
auth_scheme: AsyncAuthScheme = no_auth,
|
|
317
|
+
decoder: AsyncResponseDecoder[T],
|
|
318
|
+
error_mapper: ErrorMapper[E],
|
|
319
|
+
request_options: RequestOptionsOrDict | None = None,
|
|
320
|
+
) -> ApiResult[T, E]:
|
|
321
|
+
"""Send a request and shape the response into a result.
|
|
322
|
+
|
|
323
|
+
Returns once the response head has arrived; the body is the ``decoder``'s from there -- a
|
|
324
|
+
buffered one reads and releases it, ``file_decoder`` hands the open connection to the
|
|
325
|
+
:class:`FileResponse` it builds -- and ``T`` is solved from that argument. A non-2xx body is
|
|
326
|
+
read and the connection released *before* the mapper runs, so a :class:`Failure` never holds
|
|
327
|
+
a connection, even when the mapper raises.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
http_method: The HTTP verb to send.
|
|
331
|
+
url_template: The unresolved URL for the configured environment.
|
|
332
|
+
path_params: The endpoint's path parameters, if any.
|
|
333
|
+
query_params: The endpoint's query parameters, if any.
|
|
334
|
+
headers: The endpoint's header parameters, if any.
|
|
335
|
+
body: The request body, already wire-ready, or ``None``.
|
|
336
|
+
auth_scheme: The scheme the operation declares; ``no_auth`` when it declares none.
|
|
337
|
+
decoder: The operation's 2xx decoder; ``empty_response`` when it declares no body.
|
|
338
|
+
error_mapper: The operation's error mapper; ``raw_error_response`` when it declares none.
|
|
339
|
+
request_options: The caller's per-call overrides, if any.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
A :class:`Success` carrying the decoded payload for a 2xx status, otherwise a
|
|
343
|
+
:class:`Failure` carrying the decoded error body.
|
|
344
|
+
|
|
345
|
+
Raises:
|
|
346
|
+
ValueError: If the body does not deserialize; not an API error, so it propagates in both
|
|
347
|
+
response modes.
|
|
348
|
+
Exception: What the transport raised when the last attempt produced no response, or when
|
|
349
|
+
the body failed after the head arrived -- never the :class:`TransportError` wrapper."""
|
|
350
|
+
options = RequestOptions.coerce(request_options)
|
|
351
|
+
retry_options = apply_retry_overrides(options, self.retry_options)
|
|
352
|
+
log = HttpLog(
|
|
353
|
+
options=self.logging_options, log_level_override=options.log_level, started_at=time.perf_counter()
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
async def send() -> AsyncHttpResponse:
|
|
357
|
+
request = self._build_request(
|
|
358
|
+
http_method=http_method,
|
|
359
|
+
url_template=url_template,
|
|
360
|
+
path_params=path_params,
|
|
361
|
+
query_params=query_params,
|
|
362
|
+
headers=headers,
|
|
363
|
+
body=body,
|
|
364
|
+
auth=await resolve_auth(auth_scheme),
|
|
365
|
+
options=options,
|
|
366
|
+
)
|
|
367
|
+
log.request_sending(request)
|
|
368
|
+
return await self.http_client.send(request)
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
response = await self._attempt_send(
|
|
372
|
+
auth_scheme=auth_scheme,
|
|
373
|
+
retry_options=retry_options,
|
|
374
|
+
is_retryable=is_retryable_body(body) and http_method in retry_options.http_methods_to_retry,
|
|
375
|
+
send=send,
|
|
376
|
+
log=log,
|
|
377
|
+
)
|
|
378
|
+
except Exception as error:
|
|
379
|
+
# No response was kept: the last attempt's transport error, already unwrapped, or anything raised while
|
|
380
|
+
# an attempt was built, sent or its head handled. ``failed`` writes nothing when no request was reported.
|
|
381
|
+
# A BaseException -- the caller's cancellation -- is not this call failing, and passes untouched.
|
|
382
|
+
log.failed(error)
|
|
383
|
+
raise
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
status_code, response_headers = response.status_code, dict(response.headers)
|
|
387
|
+
if 200 <= status_code < 300:
|
|
388
|
+
# Returns before the read: the body is the decoder's now. The excepts below must never
|
|
389
|
+
# become a finally -- they would close the connection an AsyncFileResponse was just handed.
|
|
390
|
+
return Success(
|
|
391
|
+
payload=await decoder.decode(response),
|
|
392
|
+
status_code=status_code,
|
|
393
|
+
headers=response_headers,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
content = await response.aread()
|
|
397
|
+
await response.aclose()
|
|
398
|
+
# Mapped after the close, so a mapper raise cannot leak a connection; the second close it
|
|
399
|
+
# causes is a no-op, since aclose is required to be idempotent.
|
|
400
|
+
return Failure(
|
|
401
|
+
error=error_mapper.map(status_code, content),
|
|
402
|
+
status_code=status_code,
|
|
403
|
+
headers=response_headers,
|
|
404
|
+
)
|
|
405
|
+
except Exception as error:
|
|
406
|
+
# After the head: a body read the transport refused, a body the decoder rejected, an error body the
|
|
407
|
+
# mapper rejected -- each this call producing no result. Closed before the line is written.
|
|
408
|
+
await response.aclose()
|
|
409
|
+
log.failed(error)
|
|
410
|
+
raise
|
|
411
|
+
except BaseException:
|
|
412
|
+
# The caller's cancellation or interrupt: closed, and not logged as this call's failure.
|
|
413
|
+
await response.aclose()
|
|
414
|
+
raise
|
|
415
|
+
|
|
416
|
+
@staticmethod
|
|
417
|
+
async def _attempt_send(
|
|
418
|
+
*,
|
|
419
|
+
auth_scheme: AsyncAuthScheme,
|
|
420
|
+
retry_options: RetryOptions,
|
|
421
|
+
is_retryable: bool,
|
|
422
|
+
send: Callable[[], Awaitable[AsyncHttpResponse]],
|
|
423
|
+
log: HttpLog,
|
|
424
|
+
) -> AsyncHttpResponse:
|
|
425
|
+
"""The awaited twin of :meth:`RawClient._attempt_send`, with that contract exactly.
|
|
426
|
+
|
|
427
|
+
Three tokens differ: ``send`` is awaited, the close is ``aclose``, and the wait is
|
|
428
|
+
``asyncio.sleep``, so it yields the event loop instead of blocking it."""
|
|
429
|
+
for retries_taken in range(retry_options.max_retries + 1):
|
|
430
|
+
try:
|
|
431
|
+
response = await send()
|
|
432
|
+
except TransportError as error:
|
|
433
|
+
# No response arrived, so there is nothing to close.
|
|
434
|
+
inner = error.inner_exception
|
|
435
|
+
if retries_taken >= retry_options.max_retries or not is_retryable:
|
|
436
|
+
raise inner from inner.__cause__
|
|
437
|
+
wait_seconds = backoff_seconds(retries_taken, retry_options)
|
|
438
|
+
log.retrying(max_retries=retry_options.max_retries, delay=wait_seconds, reason=inner)
|
|
439
|
+
await asyncio.sleep(wait_seconds)
|
|
440
|
+
continue
|
|
441
|
+
|
|
442
|
+
status_code, response_headers = response.status_code, dict(response.headers)
|
|
443
|
+
|
|
444
|
+
# The two calls here run code the SDK does not own -- a log handler, the caller's scheme -- while the
|
|
445
|
+
# response is open; one that raises must not leave the connection held.
|
|
446
|
+
try:
|
|
447
|
+
log.response_received(status_code, response_headers, is_success=200 <= status_code < 300)
|
|
448
|
+
if status_code == 401:
|
|
449
|
+
invalidate(auth_scheme)
|
|
450
|
+
except BaseException:
|
|
451
|
+
await response.aclose()
|
|
452
|
+
raise
|
|
453
|
+
|
|
454
|
+
if not (
|
|
455
|
+
is_retryable
|
|
456
|
+
and status_code in retry_options.status_codes_to_retry
|
|
457
|
+
and retries_taken < retry_options.max_retries
|
|
458
|
+
):
|
|
459
|
+
return response
|
|
460
|
+
|
|
461
|
+
retry_after_header = retry_after_seconds(response_headers, datetime.now(timezone.utc))
|
|
462
|
+
wait_seconds = (
|
|
463
|
+
backoff_seconds(retries_taken, retry_options)
|
|
464
|
+
if retry_after_header is None
|
|
465
|
+
else min(retry_after_header, retry_options.max_delay)
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# Closed before the retry line and the wait, so neither can hold a connection.
|
|
469
|
+
await response.aclose()
|
|
470
|
+
log.retrying(max_retries=retry_options.max_retries, delay=wait_seconds, reason=status_code)
|
|
471
|
+
await asyncio.sleep(wait_seconds)
|
|
472
|
+
|
|
473
|
+
raise AssertionError("the retry loop ended without a response to shape")
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
RawClientT = TypeVar("RawClientT", bound=RawClient | AsyncRawClient)
|
|
477
|
+
"""Fixes which raw client a generic holder carries -- ``RawClient`` or ``AsyncRawClient``.
|
|
478
|
+
|
|
479
|
+
Bounded by the two concrete clients, not ``BaseRawClient[Any]``: the base has no ``execute``, so the
|
|
480
|
+
looser bound could not prove a holder's ``self._client.execute(...)`` exists."""
|