cliV1 0.0.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. cliv1-0.0.8.dist-info/METADATA +168 -0
  2. cliv1-0.0.8.dist-info/RECORD +87 -0
  3. cliv1-0.0.8.dist-info/WHEEL +4 -0
  4. cliv1-0.0.8.dist-info/licenses/LICENSE +21 -0
  5. swagger_petstore_open_api_3_0/__init__.py +15 -0
  6. swagger_petstore_open_api_3_0/apis/__init__.py +5 -0
  7. swagger_petstore_open_api_3_0/apis/pet_api.py +883 -0
  8. swagger_petstore_open_api_3_0/apis/store.py +413 -0
  9. swagger_petstore_open_api_3_0/apis/user_api.py +794 -0
  10. swagger_petstore_open_api_3_0/async_client.py +79 -0
  11. swagger_petstore_open_api_3_0/auth.py +17 -0
  12. swagger_petstore_open_api_3_0/base_client.py +18 -0
  13. swagger_petstore_open_api_3_0/client.py +77 -0
  14. swagger_petstore_open_api_3_0/core/__init__.py +358 -0
  15. swagger_petstore_open_api_3_0/core/_internal/__init__.py +9 -0
  16. swagger_petstore_open_api_3_0/core/_internal/content_disposition.py +154 -0
  17. swagger_petstore_open_api_3_0/core/_internal/flattening.py +139 -0
  18. swagger_petstore_open_api_3_0/core/_internal/headers.py +144 -0
  19. swagger_petstore_open_api_3_0/core/_internal/sse_parser.py +124 -0
  20. swagger_petstore_open_api_3_0/core/_internal/subscripts.py +50 -0
  21. swagger_petstore_open_api_3_0/core/_internal/urls.py +186 -0
  22. swagger_petstore_open_api_3_0/core/_internal/wire.py +198 -0
  23. swagger_petstore_open_api_3_0/core/adapters.py +177 -0
  24. swagger_petstore_open_api_3_0/core/auth/__init__.py +70 -0
  25. swagger_petstore_open_api_3_0/core/auth/composition.py +175 -0
  26. swagger_petstore_open_api_3_0/core/auth/models.py +216 -0
  27. swagger_petstore_open_api_3_0/core/auth/protocols.py +103 -0
  28. swagger_petstore_open_api_3_0/core/auth/schemes/__init__.py +94 -0
  29. swagger_petstore_open_api_3_0/core/auth/schemes/api_key_auth.py +53 -0
  30. swagger_petstore_open_api_3_0/core/auth/schemes/basic_auth.py +73 -0
  31. swagger_petstore_open_api_3_0/core/auth/schemes/bearer_auth.py +21 -0
  32. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_authorization_code.py +393 -0
  33. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_client_credentials.py +127 -0
  34. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_password.py +142 -0
  35. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_schemes.py +300 -0
  36. swagger_petstore_open_api_3_0/core/base_raw_response.py +49 -0
  37. swagger_petstore_open_api_3_0/core/bodies.py +550 -0
  38. swagger_petstore_open_api_3_0/core/converters/__init__.py +68 -0
  39. swagger_petstore_open_api_3_0/core/converters/date_time.py +237 -0
  40. swagger_petstore_open_api_3_0/core/converters/encoded_bytes.py +273 -0
  41. swagger_petstore_open_api_3_0/core/converters/open_enum.py +35 -0
  42. swagger_petstore_open_api_3_0/core/decoding.py +498 -0
  43. swagger_petstore_open_api_3_0/core/discriminators.py +129 -0
  44. swagger_petstore_open_api_3_0/core/event_streams.py +225 -0
  45. swagger_petstore_open_api_3_0/core/exceptions.py +38 -0
  46. swagger_petstore_open_api_3_0/core/file_responses.py +335 -0
  47. swagger_petstore_open_api_3_0/core/files.py +173 -0
  48. swagger_petstore_open_api_3_0/core/httpx_transport.py +748 -0
  49. swagger_petstore_open_api_3_0/core/models.py +143 -0
  50. swagger_petstore_open_api_3_0/core/optionality.py +130 -0
  51. swagger_petstore_open_api_3_0/core/params.py +157 -0
  52. swagger_petstore_open_api_3_0/core/raw_client.py +338 -0
  53. swagger_petstore_open_api_3_0/core/request_options.py +94 -0
  54. swagger_petstore_open_api_3_0/core/results.py +123 -0
  55. swagger_petstore_open_api_3_0/core/runtime_env.py +43 -0
  56. swagger_petstore_open_api_3_0/core/servers.py +33 -0
  57. swagger_petstore_open_api_3_0/core/transport.py +191 -0
  58. swagger_petstore_open_api_3_0/errors/__init__.py +48 -0
  59. swagger_petstore_open_api_3_0/errors/add_pet_error.py +21 -0
  60. swagger_petstore_open_api_3_0/errors/delete_order_error.py +21 -0
  61. swagger_petstore_open_api_3_0/errors/delete_pet_error.py +21 -0
  62. swagger_petstore_open_api_3_0/errors/delete_user_error.py +21 -0
  63. swagger_petstore_open_api_3_0/errors/find_pets_by_status_error.py +21 -0
  64. swagger_petstore_open_api_3_0/errors/find_pets_by_tags_error.py +21 -0
  65. swagger_petstore_open_api_3_0/errors/get_order_by_id_error.py +21 -0
  66. swagger_petstore_open_api_3_0/errors/get_pet_by_id_error.py +21 -0
  67. swagger_petstore_open_api_3_0/errors/get_user_by_name_error.py +21 -0
  68. swagger_petstore_open_api_3_0/errors/login_user_error.py +21 -0
  69. swagger_petstore_open_api_3_0/errors/place_order_error.py +21 -0
  70. swagger_petstore_open_api_3_0/errors/update_pet_error.py +21 -0
  71. swagger_petstore_open_api_3_0/errors/update_pet_with_form_error.py +21 -0
  72. swagger_petstore_open_api_3_0/errors/update_user_error.py +21 -0
  73. swagger_petstore_open_api_3_0/errors/upload_file_error.py +21 -0
  74. swagger_petstore_open_api_3_0/models/__init__.py +23 -0
  75. swagger_petstore_open_api_3_0/models/api_response.py +18 -0
  76. swagger_petstore_open_api_3_0/models/category.py +15 -0
  77. swagger_petstore_open_api_3_0/models/enums/__init__.py +4 -0
  78. swagger_petstore_open_api_3_0/models/enums/order_status.py +17 -0
  79. swagger_petstore_open_api_3_0/models/enums/pet_status.py +17 -0
  80. swagger_petstore_open_api_3_0/models/order.py +27 -0
  81. swagger_petstore_open_api_3_0/models/pet.py +28 -0
  82. swagger_petstore_open_api_3_0/models/tag_model.py +15 -0
  83. swagger_petstore_open_api_3_0/models/user.py +29 -0
  84. swagger_petstore_open_api_3_0/py.typed +0 -0
  85. swagger_petstore_open_api_3_0/server/__init__.py +21 -0
  86. swagger_petstore_open_api_3_0/server/server.py +17 -0
  87. swagger_petstore_open_api_3_0/server/server_config.py +52 -0
@@ -0,0 +1,142 @@
1
+ """``oauth2`` / ``password`` -- RFC 6749 §4.3, the resource owner password credentials grant.
2
+
3
+ Two credential pairs on one request: the resource owner's, always as form fields, and the client's
4
+ own, wherever ``x-oauth2-credentials-placement`` puts them.
5
+
6
+ **Plain tier, not refreshable.** §4.3.3 permits a refresh token here and this ignores it, so the
7
+ resource owner's password is re-sent at every expiry -- the cost §10.7 warns about, taken
8
+ deliberately (ADR-0019 decision 6). The refreshable tier is available to a generator that emits it;
9
+ this API's client does not."""
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+ from typing import Generic, TypeAlias
15
+
16
+ from pydantic import ConfigDict, Field
17
+ from typing_extensions import NotRequired, TypedDict
18
+
19
+ from ...bodies import form_body
20
+ from ...decoding import async_json_decoder, json_decoder
21
+ from ...params import UrlTemplate, param
22
+ from ...raw_client import AsyncRawClient, RawClient
23
+ from ..models import (
24
+ CredentialsPlacement,
25
+ OAuth2Credentials,
26
+ OAuthParams,
27
+ OAuthToken,
28
+ ScopeT,
29
+ oauth_error_response,
30
+ scope_params,
31
+ )
32
+
33
+
34
+ class PasswordCredentials(OAuth2Credentials, Generic[ScopeT]):
35
+ """The resource owner's credentials, plus the client's own (RFC 6749 §4.3.2).
36
+
37
+ ``client_secret`` is optional because a public client has none -- §3.2.1 then makes ``client_id``
38
+ the required identification, which the placement function writes. ``scopes`` is typed by the
39
+ flow's declared vocabulary, as every grant's is."""
40
+
41
+ model_config = ConfigDict(frozen=True, extra="forbid")
42
+
43
+ client_id: str
44
+ client_secret: str | None = Field(default=None, repr=False)
45
+ username: str
46
+ password: str = Field(repr=False)
47
+ scopes: list[ScopeT] | None = None
48
+
49
+ @classmethod
50
+ def coerce(cls, value: PasswordCredentialsOrDict[ScopeT]) -> PasswordCredentials[ScopeT]:
51
+ """Accept either spelling, validating the mapping form.
52
+
53
+ Args:
54
+ value: The credential, as the typed model or as a mapping.
55
+
56
+ Returns:
57
+ The validated model; see :meth:`BasicAuthCredentials.coerce` for why it takes no
58
+ ``None``.
59
+
60
+ Raises:
61
+ ValidationError: If the mapping is missing a key, carries an unknown one, or names a
62
+ scope this flow does not declare."""
63
+ if isinstance(value, cls):
64
+ return value
65
+ return cls.model_validate(value)
66
+
67
+
68
+ class PasswordCredentialsDict(TypedDict, Generic[ScopeT]):
69
+ client_id: str
70
+ client_secret: NotRequired[str | None]
71
+ username: str
72
+ password: str
73
+ scopes: NotRequired[list[ScopeT] | None]
74
+
75
+
76
+ PasswordCredentialsOrDict: TypeAlias = PasswordCredentials[ScopeT] | PasswordCredentialsDict[ScopeT]
77
+ """What a password credential accepts: the typed model, or a mapping of the same keys."""
78
+
79
+
80
+ def _token_params(credentials: PasswordCredentials[ScopeT]) -> OAuthParams:
81
+ """RFC 6749 §4.3.2's token request, less the client's own half.
82
+
83
+ Shared by both flavours rather than duplicated, so an edit cannot land in one only.
84
+
85
+ Args:
86
+ credentials: The configured password credentials.
87
+
88
+ Returns:
89
+ The ``grant_type``, the resource owner's pair and the scopes. The client's half is
90
+ deliberately absent: it comes from the source's ``placement``."""
91
+ return (
92
+ param[str]("grant_type", "password"),
93
+ param[str]("username", credentials.username),
94
+ param[str]("password", credentials.password),
95
+ *scope_params(credentials.scopes),
96
+ )
97
+
98
+
99
+ @dataclass(frozen=True, slots=True, kw_only=True)
100
+ class PasswordTokenSource(Generic[ScopeT]):
101
+ """The built-in :class:`TokenSource` for the password grant: POST the token request.
102
+
103
+ ``placement`` is read from the flow's own ``x-oauth2-credentials-placement`` by the generated
104
+ client, exactly as the client-credentials source's is. Carries no ``auth_scheme=`` -- the
105
+ credentials are on this request already, so the fetch provably cannot recurse into itself."""
106
+
107
+ client: RawClient
108
+ token_url: UrlTemplate
109
+ placement: CredentialsPlacement
110
+
111
+ def fetch(self, credentials: PasswordCredentials[ScopeT]) -> OAuthToken:
112
+ headers, client_params = self.placement(credentials.client_id, credentials.client_secret)
113
+ return self.client.execute(
114
+ http_method="POST",
115
+ url_template=self.token_url,
116
+ headers=headers,
117
+ body=form_body(*_token_params(credentials), *client_params),
118
+ decoder=json_decoder[OAuthToken],
119
+ error_mapper=oauth_error_response,
120
+ ).unwrap()
121
+
122
+
123
+ @dataclass(frozen=True, slots=True, kw_only=True)
124
+ class AsyncPasswordTokenSource(Generic[ScopeT]):
125
+ """:class:`PasswordTokenSource` for the asynchronous client: the request is awaited."""
126
+
127
+ client: AsyncRawClient
128
+ token_url: UrlTemplate
129
+ placement: CredentialsPlacement
130
+
131
+ async def fetch(self, credentials: PasswordCredentials[ScopeT]) -> OAuthToken:
132
+ headers, client_params = self.placement(credentials.client_id, credentials.client_secret)
133
+ return (
134
+ await self.client.execute(
135
+ http_method="POST",
136
+ url_template=self.token_url,
137
+ headers=headers,
138
+ body=form_body(*_token_params(credentials), *client_params),
139
+ decoder=async_json_decoder[OAuthToken],
140
+ error_mapper=oauth_error_response,
141
+ )
142
+ ).unwrap()
@@ -0,0 +1,300 @@
1
+ """Managed OAuth 2.0's caching schemes: two tiers, four classes, and no fetching code at all.
2
+
3
+ :class:`OAuth2Scheme` owns the token cache, the deadline, ``invalidate`` and the ``Bearer``
4
+ application, and obtains a token by calling ``source.fetch(credentials)``.
5
+ :class:`OAuth2RefreshableScheme` is its sibling for a grant whose endpoint may issue a refresh token,
6
+ and prefers a refresh over a re-acquisition. Two tiers and not one with a flag: client credentials can
7
+ never reach a refresh branch (RFC 6749 §4.4.3), so making that branch reachable would need an
8
+ ``isinstance`` on the token.
9
+
10
+ The one module in this directory that names no grant, which is what lets four classes serve every one."""
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import threading
16
+ import time
17
+ from dataclasses import dataclass, field
18
+ from math import inf
19
+ from typing import Final, Generic, TypeVar
20
+
21
+ from ...params import param
22
+ from ..models import AuthParams, OAuthToken, OAuthTokenRefreshable
23
+ from ..protocols import (
24
+ AsyncRefreshableTokenSource,
25
+ AsyncTokenSource,
26
+ RefreshableTokenSource,
27
+ TokenSource,
28
+ )
29
+
30
+ CredentialsT = TypeVar("CredentialsT")
31
+ """A grant's credentials, as the scheme holds them.
32
+
33
+ Declared here and not beside its contravariant twin in ``protocols``: the four classes below are its
34
+ only consumers."""
35
+
36
+ _EXPIRY_BUFFER_SECONDS: Final = 30.0
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class _CachedToken:
41
+ """An access token and the monotonic instant it stops being usable.
42
+
43
+ One object rather than two fields on the scheme, so the lock-free fast path reads a *single*
44
+ attribute and can never observe a new token beside a stale deadline."""
45
+
46
+ access_token: str = field(repr=False)
47
+ expires_at: float
48
+
49
+ def is_expired(self) -> bool:
50
+ return time.monotonic() >= self.expires_at
51
+
52
+
53
+ def _expires_at(expires_in: int | None) -> float:
54
+ """The monotonic instant a token issued *now* stops being usable.
55
+
56
+ ``time.monotonic``, because ``expires_in`` is a duration and a wall clock an NTP step can move
57
+ does not measure one. Named for the conversion it performs: a duration in, an instant out.
58
+
59
+ Args:
60
+ expires_in: RFC 6749 §5.1's lifetime in seconds; ``None`` or non-positive means no
61
+ client-side deadline, and the token is cached until a 401 dislodges it.
62
+
63
+ Returns:
64
+ The monotonic deadline, or ``inf`` when there is none."""
65
+ if expires_in is None or expires_in <= 0:
66
+ return inf
67
+
68
+ # ``max``, not a flat subtraction: subtracting outright is non-monotonic near the buffer, so a
69
+ # longer-lived token would be cached for less time. The two cross at 60 s.
70
+ return time.monotonic() + max(expires_in - _EXPIRY_BUFFER_SECONDS, expires_in / 2)
71
+
72
+
73
+ def _cache(token: OAuthToken) -> _CachedToken:
74
+ """Deadline a token on the monotonic clock.
75
+
76
+ Args:
77
+ token: The token endpoint's success body.
78
+
79
+ Returns:
80
+ The cache entry: the token, plus the deadline :func:`_expires_at` computes for it."""
81
+ return _CachedToken(token.access_token, _expires_at(token.expires_in))
82
+
83
+
84
+ @dataclass(frozen=True, slots=True)
85
+ class _CachedRefreshableToken:
86
+ """:class:`_CachedToken` plus the refresh token that outlives it.
87
+
88
+ A sibling rather than a third field, so a scheme over a grant that cannot be issued a refresh
89
+ token has none to read. Frozen and fully built before publication, for its sibling's reason."""
90
+
91
+ access_token: str = field(repr=False)
92
+ expires_at: float
93
+ refresh_token: str | None = field(repr=False)
94
+
95
+ def is_expired(self) -> bool:
96
+ return time.monotonic() >= self.expires_at
97
+
98
+
99
+ def _cache_refreshable(token: OAuthTokenRefreshable, previous: str | None) -> _CachedRefreshableToken:
100
+ """Deadline a refreshable token, keeping ``previous`` when the response omitted one.
101
+
102
+ RFC 6749 §6: a refresh response MAY omit ``refresh_token``, and the previously-issued one stays
103
+ valid when it does.
104
+
105
+ Args:
106
+ token: The token endpoint's success body.
107
+ previous: The refresh token that bought this response; ``None`` on a first acquisition.
108
+
109
+ Returns:
110
+ The cache entry, carrying whichever refresh token is still valid."""
111
+ return _CachedRefreshableToken(
112
+ token.access_token,
113
+ _expires_at(token.expires_in),
114
+ token.refresh_token if token.refresh_token is not None else previous,
115
+ )
116
+
117
+
118
+ class OAuth2Scheme(Generic[CredentialsT]):
119
+ """A bearer token obtained through a :class:`TokenSource` and cached until it expires.
120
+
121
+ Grant-agnostic, and holds no fetching code of its own: it calls ``source.fetch(credentials)``.
122
+ Acquisition is lazy and one instance serves a client's whole life, so the cache does too. Not
123
+ frozen -- the token cache is the one thing a scheme legitimately mutates."""
124
+
125
+ __slots__ = ("_cached", "_credentials", "_lock", "_source")
126
+
127
+ def __init__(self, *, credentials: CredentialsT, source: TokenSource[CredentialsT]) -> None:
128
+ self._credentials = credentials
129
+ self._source = source
130
+ self._lock = threading.Lock()
131
+ self._cached: _CachedToken | None = None
132
+
133
+ def apply(self) -> AuthParams:
134
+ token = self._get_and_cache_token()
135
+ return AuthParams(headers=(param[str]("Authorization", f"Bearer {token}"),))
136
+
137
+ def invalidate(self) -> None:
138
+ """Drop the cached token, so the next request obtains a new one."""
139
+ self._cached = None
140
+
141
+ def _get_and_cache_token(self) -> str:
142
+ """The cached access token, obtaining and caching one when none is fresh.
143
+
144
+ Double-checked: the fast path is one lock-free attribute read, and the second check inside
145
+ the lock is what makes a burst of concurrent requests pay for **one** acquisition.
146
+
147
+ Returns:
148
+ The access token to apply."""
149
+ cached = self._cached
150
+ if cached is not None and not cached.is_expired():
151
+ return cached.access_token
152
+
153
+ with self._lock:
154
+ cached = self._cached
155
+ if cached is not None and not cached.is_expired():
156
+ return cached.access_token
157
+
158
+ cached = _cache(self._source.fetch(self._credentials))
159
+ self._cached = cached
160
+ return cached.access_token
161
+
162
+
163
+ class AsyncOAuth2Scheme(Generic[CredentialsT]):
164
+ """:class:`OAuth2Scheme` for the asynchronous client: the token is awaited.
165
+
166
+ The lock is an ``asyncio.Lock``, which binds to the running loop on first *use* rather than at
167
+ construction, so building the client outside a loop is fine. Thereafter the scheme belongs to
168
+ one loop -- the same constraint the connection pool beside it already imposes."""
169
+
170
+ __slots__ = ("_cached", "_credentials", "_lock", "_source")
171
+
172
+ def __init__(self, *, credentials: CredentialsT, source: AsyncTokenSource[CredentialsT]) -> None:
173
+ self._credentials = credentials
174
+ self._source = source
175
+ self._lock = asyncio.Lock()
176
+ self._cached: _CachedToken | None = None
177
+
178
+ async def apply(self) -> AuthParams:
179
+ token = await self._get_and_cache_token()
180
+ return AuthParams(headers=(param[str]("Authorization", f"Bearer {token}"),))
181
+
182
+ def invalidate(self) -> None:
183
+ """Drop the cached token, so the next request obtains a new one."""
184
+ self._cached = None
185
+
186
+ async def _get_and_cache_token(self) -> str:
187
+ """See :meth:`OAuth2Scheme._get_and_cache_token`; the source may need to be awaited.
188
+
189
+ Returns:
190
+ The access token to apply."""
191
+ cached = self._cached
192
+ if cached is not None and not cached.is_expired():
193
+ return cached.access_token
194
+
195
+ async with self._lock:
196
+ cached = self._cached
197
+ if cached is not None and not cached.is_expired():
198
+ return cached.access_token
199
+
200
+ cached = _cache(await self._source.fetch(self._credentials))
201
+ self._cached = cached
202
+ return cached.access_token
203
+
204
+
205
+ class OAuth2RefreshableScheme(Generic[CredentialsT]):
206
+ """:class:`OAuth2Scheme` for a grant whose token endpoint may issue a refresh token.
207
+
208
+ **Refreshing is preferred over re-acquiring**, which is the whole value of the tier: for the
209
+ authorization-code grant a re-acquisition runs the caller's prompt again. :meth:`invalidate`
210
+ drops the refresh token too, so a 401 is answered by a full re-acquisition."""
211
+
212
+ __slots__ = ("_cached", "_credentials", "_lock", "_source")
213
+
214
+ def __init__(self, *, credentials: CredentialsT, source: RefreshableTokenSource[CredentialsT]) -> None:
215
+ self._credentials = credentials
216
+ self._source = source
217
+ self._lock = threading.Lock()
218
+ self._cached: _CachedRefreshableToken | None = None
219
+
220
+ def apply(self) -> AuthParams:
221
+ token = self._get_and_cache_token()
222
+ return AuthParams(headers=(param[str]("Authorization", f"Bearer {token}"),))
223
+
224
+ def invalidate(self) -> None:
225
+ """Drop the cached token, so the next request acquires a new one from scratch."""
226
+ self._cached = None
227
+
228
+ def _get_and_cache_token(self) -> str:
229
+ """The cached access token, refreshing or re-acquiring when none is fresh.
230
+
231
+ Double-checked as :meth:`OAuth2Scheme._get_and_cache_token` is, with one branch added: a
232
+ stale entry still carrying a refresh token is worth one attempt first. The fallback calls
233
+ ``fetch``, which for an interactive grant blocks on a human *while this lock is held*.
234
+
235
+ Returns:
236
+ The access token to apply."""
237
+ cached = self._cached
238
+ if cached is not None and not cached.is_expired():
239
+ return cached.access_token
240
+
241
+ with self._lock:
242
+ cached = self._cached
243
+ if cached is not None and not cached.is_expired():
244
+ return cached.access_token
245
+
246
+ if cached is not None and cached.refresh_token is not None:
247
+ refreshed = self._source.refresh(self._credentials, cached.refresh_token)
248
+ if refreshed is not None:
249
+ acquired = _cache_refreshable(refreshed, cached.refresh_token)
250
+ self._cached = acquired
251
+ return acquired.access_token
252
+
253
+ acquired = _cache_refreshable(self._source.fetch(self._credentials), None)
254
+ self._cached = acquired
255
+ return acquired.access_token
256
+
257
+
258
+ class AsyncOAuth2RefreshableScheme(Generic[CredentialsT]):
259
+ """:class:`OAuth2RefreshableScheme` for the asynchronous client: both source calls are awaited."""
260
+
261
+ __slots__ = ("_cached", "_credentials", "_lock", "_source")
262
+
263
+ def __init__(self, *, credentials: CredentialsT, source: AsyncRefreshableTokenSource[CredentialsT]) -> None:
264
+ self._credentials = credentials
265
+ self._source = source
266
+ self._lock = asyncio.Lock()
267
+ self._cached: _CachedRefreshableToken | None = None
268
+
269
+ async def apply(self) -> AuthParams:
270
+ token = await self._get_and_cache_token()
271
+ return AuthParams(headers=(param[str]("Authorization", f"Bearer {token}"),))
272
+
273
+ def invalidate(self) -> None:
274
+ """Drop the cached token, so the next request acquires a new one from scratch."""
275
+ self._cached = None
276
+
277
+ async def _get_and_cache_token(self) -> str:
278
+ """See :meth:`OAuth2RefreshableScheme._get_and_cache_token`; both source calls are awaited.
279
+
280
+ Returns:
281
+ The access token to apply."""
282
+ cached = self._cached
283
+ if cached is not None and not cached.is_expired():
284
+ return cached.access_token
285
+
286
+ async with self._lock:
287
+ cached = self._cached
288
+ if cached is not None and not cached.is_expired():
289
+ return cached.access_token
290
+
291
+ if cached is not None and cached.refresh_token is not None:
292
+ refreshed = await self._source.refresh(self._credentials, cached.refresh_token)
293
+ if refreshed is not None:
294
+ acquired = _cache_refreshable(refreshed, cached.refresh_token)
295
+ self._cached = acquired
296
+ return acquired.access_token
297
+
298
+ acquired = _cache_refreshable(await self._source.fetch(self._credentials), None)
299
+ self._cached = acquired
300
+ return acquired.access_token
@@ -0,0 +1,49 @@
1
+ """What the generated per-group raw-response classes share: a raw client, a server, its auth."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Generic, TypeVar
6
+
7
+ from .raw_client import RawClientT
8
+
9
+ ServerT = TypeVar("ServerT")
10
+ """The generated ``Server`` type a subclass carries.
11
+
12
+ Unbounded and never touched by the runtime: its only purpose is to let this class hold the server
13
+ without *naming* it, since the runtime is copied verbatim into every generated SDK. A plain
14
+ attribute typed ``Any`` would lose what subclasses need -- that ``self._server.prism(...)``
15
+ type-checks."""
16
+
17
+ AuthT = TypeVar("AuthT")
18
+ """The generated scheme holder a *secured* subclass carries.
19
+
20
+ Unbounded for the same reason as ``ServerT``, so ``self._auth.basic_auth`` type-checks in a
21
+ generated endpoint while the runtime stays free of any upward import. It is also what keeps the two
22
+ flavors' schemes apart, so a scheme that can only work on one transport cannot be handed to the
23
+ other."""
24
+
25
+
26
+ class BaseRawResponse(Generic[RawClientT, ServerT]):
27
+ """Base for the internal per-group raw-response classes.
28
+
29
+ Holds the raw client and the server, both type parameters a subclass fixes, so
30
+ ``self._client.execute(...)`` resolves to the right flavor and ``self._server`` to the concrete
31
+ generated ``Server``. A group whose operations declare no ``security`` extends **this**, and so
32
+ has no ``self._auth`` to name; one that does extends :class:`SecuredRawResponse`."""
33
+
34
+ def __init__(self, client: RawClientT, server: ServerT) -> None:
35
+ self._client = client
36
+ self._server = server
37
+
38
+
39
+ class SecuredRawResponse(BaseRawResponse[RawClientT, ServerT], Generic[RawClientT, ServerT, AuthT]):
40
+ """Base for a group at least one of whose operations declares ``security``.
41
+
42
+ Adds the API's security schemes as a third parameter a subclass fixes. Two classes rather than
43
+ one optional argument, because the split makes the reach of a credential checkable: a group that
44
+ declares no ``security`` cannot name ``self._auth``, so a generator writing ``auth=self._auth.``
45
+ into the wrong file fails to type-check instead of shipping a credential."""
46
+
47
+ def __init__(self, client: RawClientT, server: ServerT, auth: AuthT) -> None:
48
+ super().__init__(client, server)
49
+ self._auth = auth