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,216 @@
|
|
|
1
|
+
"""What every security scheme trades in, and the OAuth 2.0 vocabulary its grants share.
|
|
2
|
+
|
|
3
|
+
:class:`AuthParams` is what one scheme contributes to a request. The rest is managed OAuth 2.0's
|
|
4
|
+
grant-agnostic half: the token endpoint's success and error bodies (RFC 6749 §5.1 and §5.2, fixed for
|
|
5
|
+
every provider, which is why they are runtime types at all), §2.3.1's client-authentication placements,
|
|
6
|
+
§3.3's scope delimiter, and the scope vocabulary parameter every grant's credentials carry.
|
|
7
|
+
|
|
8
|
+
The bottom of the auth layer. It names no protocol and no scheme, so ``protocols``, ``composition`` and
|
|
9
|
+
``schemes/`` all import from here and nothing imports back. It performs no token request either --
|
|
10
|
+
which is why no ``RawClient``, ``form_body`` or ``UrlTemplate`` appears below."""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from base64 import b64encode
|
|
15
|
+
from collections.abc import Callable, Sequence
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any, Final, TypeAlias
|
|
18
|
+
from urllib.parse import quote
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
21
|
+
|
|
22
|
+
# ``TypeVar`` from here and not ``typing``: PEP 696 defaults reach ``typing`` at 3.13, and ``ScopeT``
|
|
23
|
+
# below needs one at the 3.10 floor.
|
|
24
|
+
from typing_extensions import TypeVar
|
|
25
|
+
|
|
26
|
+
from .._internal.wire import parse_json, text_values
|
|
27
|
+
from ..decoding import ErrorMapper
|
|
28
|
+
from ..params import Param, param
|
|
29
|
+
from ..results import RawError
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True)
|
|
33
|
+
class AuthParams:
|
|
34
|
+
"""What one scheme contributes to a request.
|
|
35
|
+
|
|
36
|
+
Held as :class:`Param` rather than plain strings, so a credential is validated, dumped and
|
|
37
|
+
encoded by the same code as an endpoint's own parameters."""
|
|
38
|
+
|
|
39
|
+
headers: tuple[Param[Any], ...] = ()
|
|
40
|
+
query_params: tuple[Param[Any], ...] = ()
|
|
41
|
+
cookies: tuple[Param[Any], ...] = ()
|
|
42
|
+
|
|
43
|
+
def request_headers(self) -> tuple[Param[Any], ...]:
|
|
44
|
+
"""Every header this contributes, cookie pairs folded into one ``Cookie`` (RFC 6265).
|
|
45
|
+
|
|
46
|
+
Folding here rather than at each scheme is what lets two cookie-bearing schemes compose;
|
|
47
|
+
two ``Cookie`` parameters would see the second overwrite the first. This covers only what
|
|
48
|
+
*this request's* schemes contributed -- folding the jar across the API's, the endpoint's and
|
|
49
|
+
the caller's layers is ``_internal/headers.py``'s.
|
|
50
|
+
|
|
51
|
+
The jar is tested rather than the tuple, so cookies that all render to nothing contribute no
|
|
52
|
+
header at all instead of an empty one that would clobber a lower layer.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The headers, plus one folded ``Cookie`` when any cookie was contributed."""
|
|
56
|
+
jar = "; ".join(f"{cookie.key}={value}" for cookie in self.cookies for value in text_values(cookie))
|
|
57
|
+
if not jar:
|
|
58
|
+
return self.headers
|
|
59
|
+
|
|
60
|
+
return *self.headers, param[str]("Cookie", jar)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
ScopeT = TypeVar("ScopeT", bound=str, default=str)
|
|
64
|
+
"""A grant's scope vocabulary, as its flow declares it.
|
|
65
|
+
|
|
66
|
+
Bound to ``str`` because RFC 6749 §3.3 puts scopes on the wire as space-delimited strings, and
|
|
67
|
+
defaulted to it so an unparametrized credential means what it always did: any scope the provider
|
|
68
|
+
accepts, which is also what a flow declaring no scopes emits."""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class OAuth2Credentials(BaseModel):
|
|
72
|
+
"""The base every OAuth 2.0 grant's credentials share, carrying one thing: their repr.
|
|
73
|
+
|
|
74
|
+
Each grant's credentials are generic over :data:`ScopeT`, and pydantic names a parametrized model
|
|
75
|
+
after its parametrization -- so an unhelped repr prints the flow's whole scope vocabulary in place
|
|
76
|
+
of the class a caller wrote. This restores the latter.
|
|
77
|
+
|
|
78
|
+
⚠️ **It declares no fields, and must not gain one.** Pydantic orders a base's fields ahead of a
|
|
79
|
+
subclass's, so a field here would move to the front of every credential's repr and ``model_dump``.
|
|
80
|
+
A grant's own fields stay in its own model, which is also where ``model_config``, its ``coerce``
|
|
81
|
+
and its validators stay."""
|
|
82
|
+
|
|
83
|
+
def __repr_name__(self) -> str:
|
|
84
|
+
"""The class name without its parametrization.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
The unsubscripted class's name, or this class's own when it was never subscripted."""
|
|
88
|
+
origin = type(self).__pydantic_generic_metadata__["origin"]
|
|
89
|
+
return origin.__name__ if origin is not None else type(self).__name__
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class OAuthToken(BaseModel):
|
|
93
|
+
"""The token endpoint's success body (RFC 6749 §5.1), and what a token source returns.
|
|
94
|
+
|
|
95
|
+
``expires_in`` is RECOMMENDED rather than required, so its absence means "no client-side
|
|
96
|
+
deadline". ``token_type`` is parsed because the RFC requires it and deliberately **not** used
|
|
97
|
+
when applying: echoing a wire keyword into ``Authorization`` would put unvalidated data there.
|
|
98
|
+
|
|
99
|
+
No ``refresh_token``: RFC 6749 §4.4.3 says the client-credentials grant SHOULD NOT be issued
|
|
100
|
+
one. A grant that may be returns :class:`OAuthTokenRefreshable` instead."""
|
|
101
|
+
|
|
102
|
+
model_config = ConfigDict(frozen=True)
|
|
103
|
+
|
|
104
|
+
access_token: str = Field(repr=False)
|
|
105
|
+
token_type: str
|
|
106
|
+
expires_in: int | None = None
|
|
107
|
+
scope: str | None = None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class OAuthTokenRefreshable(OAuthToken):
|
|
111
|
+
"""The token endpoint's success body for a grant that may issue a refresh token.
|
|
112
|
+
|
|
113
|
+
RFC 6749 §4.1.4 and §4.3.3 both make ``refresh_token`` OPTIONAL. A subclass rather than an
|
|
114
|
+
optional field, so a source's declared return type says which tier it belongs to."""
|
|
115
|
+
|
|
116
|
+
refresh_token: str | None = Field(default=None, repr=False)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class OAuthProviderError(BaseModel):
|
|
120
|
+
"""The token endpoint's error body (RFC 6749 §5.2).
|
|
121
|
+
|
|
122
|
+
Typed, unlike the rest of a failed token fetch, because §5.2 fixes these three members for every
|
|
123
|
+
provider."""
|
|
124
|
+
|
|
125
|
+
model_config = ConfigDict(frozen=True)
|
|
126
|
+
|
|
127
|
+
error: str
|
|
128
|
+
error_description: str | None = None
|
|
129
|
+
error_uri: str | None = None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
OAuthError: TypeAlias = OAuthProviderError | RawError
|
|
133
|
+
"""What a failed token fetch carries: the RFC 6749 §5.2 body, or the raw response without one."""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True, slots=True)
|
|
137
|
+
class OAuthErrorResponse:
|
|
138
|
+
"""Error mapper for the token endpoint.
|
|
139
|
+
|
|
140
|
+
The only mapper here that selects a *schema*, and it can: RFC 6749 §5.2 fixes one error body for
|
|
141
|
+
every provider. A response that does not conform becomes a :class:`RawError` rather than raising
|
|
142
|
+
out of the mapper, because a failed fetch must surface the provider's answer, not a parse error."""
|
|
143
|
+
|
|
144
|
+
def map(self, status_code: int, content: bytes) -> OAuthError:
|
|
145
|
+
try:
|
|
146
|
+
return OAuthProviderError.model_validate(parse_json(content))
|
|
147
|
+
except ValueError:
|
|
148
|
+
return RawError(status_code, content)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
oauth_error_response: Final[ErrorMapper[OAuthError]] = OAuthErrorResponse()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
OAuthParams: TypeAlias = tuple[Param[Any], ...]
|
|
155
|
+
"""One destination's worth of parameters: half a token request, or an authorization URL's query."""
|
|
156
|
+
|
|
157
|
+
CredentialsPlacement: TypeAlias = Callable[[str, str | None], tuple[OAuthParams, OAuthParams]]
|
|
158
|
+
"""Where a client's own credentials travel on a token request: its headers, and its form fields.
|
|
159
|
+
|
|
160
|
+
Named after the specification extension it is read from, ``x-oauth2-credentials-placement``.
|
|
161
|
+
Grant-agnostic: RFC 6749 §2.3.1 authenticates the *client*, which every grant does identically."""
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def scope_params(scopes: Sequence[str] | None) -> OAuthParams:
|
|
165
|
+
"""The ``scope`` field, or nothing at all when no scopes were asked for.
|
|
166
|
+
|
|
167
|
+
RFC 6749 §3.3's space delimiter, written once, so a caller passes a list and never learns it.
|
|
168
|
+
Takes a ``Sequence`` so a credential's narrowed ``list[ScopeT]`` reaches it without a copy.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
scopes: The scopes to request; absent or empty asks for the client's defaults.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
One ``scope`` parameter, or an empty tuple."""
|
|
175
|
+
if not scopes:
|
|
176
|
+
return ()
|
|
177
|
+
return (param[str]("scope", " ".join(scopes)),)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def client_secret_basic(client_id: str, client_secret: str | None) -> tuple[OAuthParams, OAuthParams]:
|
|
181
|
+
"""RFC 6749 §2.3.1: the client authenticates with HTTP Basic on the token request.
|
|
182
|
+
|
|
183
|
+
Both halves are percent-encoded before base64, as §2.3.1 requires. A client with **no secret**
|
|
184
|
+
identifies itself with a ``client_id`` form field instead, which is §3.2.1's requirement exactly
|
|
185
|
+
then rather than a degraded fallback.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
client_id: The client's identifier, as issued by the provider.
|
|
189
|
+
client_secret: Its secret, or ``None`` for a public client.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
The headers and the form fields the client's credentials contribute, in that order."""
|
|
193
|
+
if client_secret is None:
|
|
194
|
+
return (), (param[str]("client_id", client_id),)
|
|
195
|
+
raw = f"{quote(client_id, safe='')}:{quote(client_secret, safe='')}"
|
|
196
|
+
return (param[str]("Authorization", f"Basic {b64encode(raw.encode()).decode()}"),), ()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def client_secret_post(client_id: str, client_secret: str | None) -> tuple[OAuthParams, OAuthParams]:
|
|
200
|
+
"""RFC 6749 §2.3.1's alternative: the credentials travel in the form body instead.
|
|
201
|
+
|
|
202
|
+
The empty header tuple is the invariant, not an oversight -- carrying the pair in both places
|
|
203
|
+
would authenticate the client twice.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
client_id: The client's identifier, as issued by the provider.
|
|
207
|
+
client_secret: Its secret, omitted entirely when ``None`` (§3.2.1).
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
An empty header tuple, and the form fields the client's credentials contribute."""
|
|
211
|
+
if client_secret is None:
|
|
212
|
+
return (), (param[str]("client_id", client_id),)
|
|
213
|
+
return (), (
|
|
214
|
+
param[str]("client_id", client_id),
|
|
215
|
+
param[str]("client_secret", client_secret),
|
|
216
|
+
)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Every protocol the authentication layer defines: what a scheme is, and where a token comes from.
|
|
2
|
+
|
|
3
|
+
Two families, one file. :class:`AuthScheme` and its async twin are the whole contract for applying a
|
|
4
|
+
credential to a request, so a scheme this runtime does not ship is a class a *caller* writes; ``apply``
|
|
5
|
+
runs once per request, which is what makes rotation and just-in-time signing work.
|
|
6
|
+
:class:`TokenSource` and its three relatives are the seam managed OAuth 2.0 fetches through, so a
|
|
7
|
+
consumer replaces the built-in source without touching the caching scheme that calls it.
|
|
8
|
+
|
|
9
|
+
Nothing here has a body, so nothing here reaches a scheme, a composite or a transport -- only the
|
|
10
|
+
models its members name."""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Awaitable
|
|
15
|
+
from typing import Protocol, TypeVar, runtime_checkable
|
|
16
|
+
|
|
17
|
+
from .models import AuthParams, OAuthToken, OAuthTokenRefreshable
|
|
18
|
+
|
|
19
|
+
CredentialsT_contra = TypeVar("CredentialsT_contra", contravariant=True)
|
|
20
|
+
"""A grant's credentials, as a source accepts them.
|
|
21
|
+
|
|
22
|
+
Contravariant because the protocols below take it in an *argument* position and nowhere else."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AuthScheme(Protocol):
|
|
26
|
+
"""A security scheme, as the synchronous client sees it.
|
|
27
|
+
|
|
28
|
+
Implementations:
|
|
29
|
+
|
|
30
|
+
- MUST be safe to call repeatedly -- ``apply`` runs once per request.
|
|
31
|
+
- SHOULD be frozen: one scheme instance serves every request a client makes.
|
|
32
|
+
- MAY block, but only if the synchronous client is the only one that will hold it."""
|
|
33
|
+
|
|
34
|
+
def apply(self) -> AuthParams: ...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AsyncAuthScheme(Protocol):
|
|
38
|
+
"""A security scheme, as the asynchronous client sees it.
|
|
39
|
+
|
|
40
|
+
``apply`` may return the parameters directly *or* an awaitable of them, so one synchronous method
|
|
41
|
+
satisfies this protocol **and** :class:`AuthScheme` and no async twin of a static scheme is
|
|
42
|
+
needed. A scheme that must reach the network declares ``async def apply`` and satisfies only this
|
|
43
|
+
one, which is what keeps it off the synchronous transport it could not use."""
|
|
44
|
+
|
|
45
|
+
def apply(self) -> AuthParams | Awaitable[AuthParams]: ...
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@runtime_checkable
|
|
49
|
+
class RevocableAuthScheme(Protocol):
|
|
50
|
+
"""A scheme whose credential can go stale before it expires.
|
|
51
|
+
|
|
52
|
+
``invalidate`` is a **hint, not a barrier**: it runs after a 401 and must not block, so a fetch
|
|
53
|
+
already in flight may repopulate the cache immediately afterwards. ``runtime_checkable`` because
|
|
54
|
+
``composition.invalidate`` has to ask a scheme whose type it does not know."""
|
|
55
|
+
|
|
56
|
+
def invalidate(self) -> None: ...
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TokenSource(Protocol[CredentialsT_contra]):
|
|
60
|
+
"""Where an OAuth 2.0 scheme gets its token, given that grant's credentials.
|
|
61
|
+
|
|
62
|
+
Generic because the credentials differ between grants, so a mismatch is reported at the client
|
|
63
|
+
constructor rather than at the first request. ``fetch`` always receives the validated credentials
|
|
64
|
+
model, never the mapping form.
|
|
65
|
+
|
|
66
|
+
Implementations:
|
|
67
|
+
|
|
68
|
+
- MUST be safe to call repeatedly, and MAY be called concurrently.
|
|
69
|
+
- MUST NOT issue a request that itself uses the scheme holding them -- ``fetch`` runs inside that
|
|
70
|
+
scheme's lock, so doing so deadlocks.
|
|
71
|
+
- SHOULD bound their own I/O. The built-in source inherits the client's timeout."""
|
|
72
|
+
|
|
73
|
+
def fetch(self, credentials: CredentialsT_contra) -> OAuthToken: ...
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class AsyncTokenSource(Protocol[CredentialsT_contra]):
|
|
77
|
+
""":class:`TokenSource` as the asynchronous client sees it.
|
|
78
|
+
|
|
79
|
+
``fetch`` is a coroutine, deliberately unlike :meth:`AsyncAuthScheme.apply`'s union: nothing about
|
|
80
|
+
obtaining a token is static, so declaring it makes a synchronous source a **type** error rather
|
|
81
|
+
than a ``TypeError`` at the first request."""
|
|
82
|
+
|
|
83
|
+
async def fetch(self, credentials: CredentialsT_contra) -> OAuthToken: ...
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class RefreshableTokenSource(TokenSource[CredentialsT_contra], Protocol[CredentialsT_contra]):
|
|
87
|
+
"""A :class:`TokenSource` that can also exchange a refresh token for a new access token.
|
|
88
|
+
|
|
89
|
+
Extends rather than replaces: ``fetch`` still runs when there is nothing to refresh, and again
|
|
90
|
+
when a refresh is refused. ``refresh`` returns ``None`` for a *provider* refusal rather than
|
|
91
|
+
raising; a transport failure still propagates, as does a success whose body will not decode."""
|
|
92
|
+
|
|
93
|
+
def fetch(self, credentials: CredentialsT_contra) -> OAuthTokenRefreshable: ...
|
|
94
|
+
|
|
95
|
+
def refresh(self, credentials: CredentialsT_contra, refresh_token: str) -> OAuthTokenRefreshable | None: ...
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class AsyncRefreshableTokenSource(AsyncTokenSource[CredentialsT_contra], Protocol[CredentialsT_contra]):
|
|
99
|
+
""":class:`RefreshableTokenSource` as the asynchronous client sees it: both members are awaited."""
|
|
100
|
+
|
|
101
|
+
async def fetch(self, credentials: CredentialsT_contra) -> OAuthTokenRefreshable: ...
|
|
102
|
+
|
|
103
|
+
async def refresh(self, credentials: CredentialsT_contra, refresh_token: str) -> OAuthTokenRefreshable | None: ...
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Every scheme this runtime ships, plus the two caching tiers a managed OAuth 2.0 grant applies through.
|
|
2
|
+
|
|
3
|
+
One module per scheme *type* -- together the whole of OpenAPI's ``securitySchemes`` surface:
|
|
4
|
+
``http``/``basic`` and ``http``/``bearer``, ``apiKey`` in a header, a query parameter or a cookie, and
|
|
5
|
+
three ``oauth2`` flows. ``openIdConnect`` and ``implicit`` need no module of their own -- both reach this
|
|
6
|
+
runtime as a plain bearer token. Adding a fourth grant is one more module here and nothing anywhere else.
|
|
7
|
+
|
|
8
|
+
``oauth2_schemes`` is the exception that earns its place: it names no grant, holding the two
|
|
9
|
+
grant-agnostic tiers every flow's credentials and source are handed to."""
|
|
10
|
+
|
|
11
|
+
from .api_key_auth import ApiKeyCookieScheme, ApiKeyHeaderScheme, ApiKeyQueryScheme
|
|
12
|
+
from .basic_auth import (
|
|
13
|
+
BasicAuthCredentials,
|
|
14
|
+
BasicAuthCredentialsDict,
|
|
15
|
+
BasicAuthCredentialsOrDict,
|
|
16
|
+
BasicAuthScheme,
|
|
17
|
+
)
|
|
18
|
+
from .bearer_auth import BearerAuthScheme
|
|
19
|
+
from .oauth2_authorization_code import (
|
|
20
|
+
AsyncAuthorizationCodeCredentials,
|
|
21
|
+
AsyncAuthorizationCodeCredentialsDict,
|
|
22
|
+
AsyncAuthorizationCodeCredentialsOrDict,
|
|
23
|
+
AsyncAuthorizationCodePrompt,
|
|
24
|
+
AsyncAuthorizationCodeTokenSource,
|
|
25
|
+
AuthorizationCodeCredentials,
|
|
26
|
+
AuthorizationCodeCredentialsDict,
|
|
27
|
+
AuthorizationCodeCredentialsOrDict,
|
|
28
|
+
AuthorizationCodePrompt,
|
|
29
|
+
AuthorizationCodeTokenSource,
|
|
30
|
+
PkceMethod,
|
|
31
|
+
)
|
|
32
|
+
from .oauth2_client_credentials import (
|
|
33
|
+
AsyncClientCredentialsTokenSource,
|
|
34
|
+
ClientCredentials,
|
|
35
|
+
ClientCredentialsDict,
|
|
36
|
+
ClientCredentialsOrDict,
|
|
37
|
+
ClientCredentialsTokenSource,
|
|
38
|
+
)
|
|
39
|
+
from .oauth2_password import (
|
|
40
|
+
AsyncPasswordTokenSource,
|
|
41
|
+
PasswordCredentials,
|
|
42
|
+
PasswordCredentialsDict,
|
|
43
|
+
PasswordCredentialsOrDict,
|
|
44
|
+
PasswordTokenSource,
|
|
45
|
+
)
|
|
46
|
+
from .oauth2_schemes import (
|
|
47
|
+
AsyncOAuth2RefreshableScheme,
|
|
48
|
+
AsyncOAuth2Scheme,
|
|
49
|
+
OAuth2RefreshableScheme,
|
|
50
|
+
OAuth2Scheme,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
# http / basic
|
|
55
|
+
"BasicAuthCredentials",
|
|
56
|
+
"BasicAuthCredentialsDict",
|
|
57
|
+
"BasicAuthCredentialsOrDict",
|
|
58
|
+
"BasicAuthScheme",
|
|
59
|
+
# http / bearer
|
|
60
|
+
"BearerAuthScheme",
|
|
61
|
+
# apiKey
|
|
62
|
+
"ApiKeyHeaderScheme",
|
|
63
|
+
"ApiKeyQueryScheme",
|
|
64
|
+
"ApiKeyCookieScheme",
|
|
65
|
+
# oauth2 -- the two caching tiers, grant-agnostic
|
|
66
|
+
"OAuth2Scheme",
|
|
67
|
+
"AsyncOAuth2Scheme",
|
|
68
|
+
"OAuth2RefreshableScheme",
|
|
69
|
+
"AsyncOAuth2RefreshableScheme",
|
|
70
|
+
# oauth2 / clientCredentials
|
|
71
|
+
"ClientCredentials",
|
|
72
|
+
"ClientCredentialsDict",
|
|
73
|
+
"ClientCredentialsOrDict",
|
|
74
|
+
"ClientCredentialsTokenSource",
|
|
75
|
+
"AsyncClientCredentialsTokenSource",
|
|
76
|
+
# oauth2 / password
|
|
77
|
+
"PasswordCredentials",
|
|
78
|
+
"PasswordCredentialsDict",
|
|
79
|
+
"PasswordCredentialsOrDict",
|
|
80
|
+
"PasswordTokenSource",
|
|
81
|
+
"AsyncPasswordTokenSource",
|
|
82
|
+
# oauth2 / authorizationCode
|
|
83
|
+
"AuthorizationCodeCredentials",
|
|
84
|
+
"AuthorizationCodeCredentialsDict",
|
|
85
|
+
"AuthorizationCodeCredentialsOrDict",
|
|
86
|
+
"AsyncAuthorizationCodeCredentials",
|
|
87
|
+
"AsyncAuthorizationCodeCredentialsDict",
|
|
88
|
+
"AsyncAuthorizationCodeCredentialsOrDict",
|
|
89
|
+
"AuthorizationCodePrompt",
|
|
90
|
+
"AsyncAuthorizationCodePrompt",
|
|
91
|
+
"PkceMethod",
|
|
92
|
+
"AuthorizationCodeTokenSource",
|
|
93
|
+
"AsyncAuthorizationCodeTokenSource",
|
|
94
|
+
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""``apiKey`` -- one credential, three destinations.
|
|
2
|
+
|
|
3
|
+
OpenAPI's ``apiKey`` is a single type parameterised by ``in:``, so the three classes below differ
|
|
4
|
+
only in which bucket of :class:`AuthParams` they fill."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
|
|
10
|
+
from ...params import param
|
|
11
|
+
from ..models import AuthParams
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class ApiKeyHeaderScheme:
|
|
16
|
+
"""``apiKey`` with ``in: header``.
|
|
17
|
+
|
|
18
|
+
Also every custom ``Authorization`` keyword an API invents, which needs no scheme of its own:
|
|
19
|
+
pass ``name="Authorization"`` and a value that already carries the keyword."""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
value: str = field(repr=False)
|
|
23
|
+
|
|
24
|
+
def apply(self) -> AuthParams:
|
|
25
|
+
return AuthParams(headers=(param[str](self.name, self.value),))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class ApiKeyQueryScheme:
|
|
30
|
+
"""``apiKey`` with ``in: query``.
|
|
31
|
+
|
|
32
|
+
The value is percent-encoded, because :class:`Param` renders it exactly as an endpoint's own
|
|
33
|
+
query parameter -- the opposite obligation from the cookie below."""
|
|
34
|
+
|
|
35
|
+
name: str
|
|
36
|
+
value: str = field(repr=False)
|
|
37
|
+
|
|
38
|
+
def apply(self) -> AuthParams:
|
|
39
|
+
return AuthParams(query_params=(param[str](self.name, self.value),))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class ApiKeyCookieScheme:
|
|
44
|
+
"""``apiKey`` with ``in: cookie``.
|
|
45
|
+
|
|
46
|
+
The value is sent verbatim. RFC 6265's ``cookie-value`` alphabet already admits every character
|
|
47
|
+
a base64 key uses, so percent-encoding it would corrupt the credential rather than protect it."""
|
|
48
|
+
|
|
49
|
+
name: str
|
|
50
|
+
value: str = field(repr=False)
|
|
51
|
+
|
|
52
|
+
def apply(self) -> AuthParams:
|
|
53
|
+
return AuthParams(cookies=(param[str](self.name, self.value),))
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""``http`` / ``basic`` -- RFC 7617."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from base64 import b64encode
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TypeAlias
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
10
|
+
from typing_extensions import TypedDict
|
|
11
|
+
|
|
12
|
+
from ...params import param
|
|
13
|
+
from ..models import AuthParams
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BasicAuthCredentials(BaseModel):
|
|
17
|
+
"""The user-id and password of an ``http``/``basic`` scheme (RFC 7617)."""
|
|
18
|
+
|
|
19
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
20
|
+
|
|
21
|
+
username: str
|
|
22
|
+
password: str = Field(repr=False)
|
|
23
|
+
|
|
24
|
+
@field_validator("username")
|
|
25
|
+
@classmethod
|
|
26
|
+
def _reject_colon(cls, value: str) -> str:
|
|
27
|
+
if ":" in value:
|
|
28
|
+
raise ValueError(
|
|
29
|
+
"username must not contain ':' -- RFC 7617 encodes 'user-id:password', so a colon "
|
|
30
|
+
f"in the user-id cannot be decoded unambiguously; got {value!r}"
|
|
31
|
+
)
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def coerce(cls, value: BasicAuthCredentialsOrDict) -> BasicAuthCredentials:
|
|
36
|
+
"""Accept either spelling, validating the mapping form.
|
|
37
|
+
|
|
38
|
+
Unlike the other coercion seams in this runtime, no credential model's ``coerce`` takes
|
|
39
|
+
``None``: an absent credential means *no scheme*, which the client decides before this.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
value: The credential, as the typed model or as a mapping.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The validated model, passed straight through when it already is one.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
ValidationError: If the mapping is missing a key, carries an unknown one, or holds a
|
|
49
|
+
user-id with a colon. It subclasses ``ValueError``."""
|
|
50
|
+
if isinstance(value, cls):
|
|
51
|
+
return value
|
|
52
|
+
return cls.model_validate(value)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class BasicAuthCredentialsDict(TypedDict):
|
|
56
|
+
username: str
|
|
57
|
+
password: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
BasicAuthCredentialsOrDict: TypeAlias = BasicAuthCredentials | BasicAuthCredentialsDict
|
|
61
|
+
"""What a Basic credential accepts: the typed model, or a mapping carrying the same two keys."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True, slots=True)
|
|
65
|
+
class BasicAuthScheme:
|
|
66
|
+
"""``http``/``basic``: ``Authorization: Basic base64(user-id:password)``."""
|
|
67
|
+
|
|
68
|
+
credentials: BasicAuthCredentials
|
|
69
|
+
|
|
70
|
+
def apply(self) -> AuthParams:
|
|
71
|
+
raw = f"{self.credentials.username}:{self.credentials.password}"
|
|
72
|
+
encoded = b64encode(raw.encode()).decode()
|
|
73
|
+
return AuthParams(headers=(param[str]("Authorization", f"Basic {encoded}"),))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""``http`` / ``bearer`` -- and, on the wire, ``openIdConnect`` too."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from ...params import param
|
|
8
|
+
from ..models import AuthParams
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class BearerAuthScheme:
|
|
13
|
+
"""``http``/``bearer``: ``Authorization: Bearer <token>``.
|
|
14
|
+
|
|
15
|
+
Also the wire form of an ``openIdConnect`` scheme, whose discovery document says how a token is
|
|
16
|
+
obtained but not how it is sent."""
|
|
17
|
+
|
|
18
|
+
token: str = field(repr=False)
|
|
19
|
+
|
|
20
|
+
def apply(self) -> AuthParams:
|
|
21
|
+
return AuthParams(headers=(param[str]("Authorization", f"Bearer {self.token}"),))
|