cliV1 0.0.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. cliv1-0.0.7.dist-info/METADATA +212 -0
  2. cliv1-0.0.7.dist-info/RECORD +92 -0
  3. cliv1-0.0.7.dist-info/WHEEL +4 -0
  4. cliv1-0.0.7.dist-info/licenses/LICENSE +21 -0
  5. swagger_petstore_open_api_3_1_0/__init__.py +15 -0
  6. swagger_petstore_open_api_3_1_0/apis/__init__.py +5 -0
  7. swagger_petstore_open_api_3_1_0/apis/pet_api.py +922 -0
  8. swagger_petstore_open_api_3_1_0/apis/store.py +432 -0
  9. swagger_petstore_open_api_3_1_0/apis/user_api.py +829 -0
  10. swagger_petstore_open_api_3_1_0/async_client.py +87 -0
  11. swagger_petstore_open_api_3_1_0/auth.py +17 -0
  12. swagger_petstore_open_api_3_1_0/base_client.py +27 -0
  13. swagger_petstore_open_api_3_1_0/client.py +85 -0
  14. swagger_petstore_open_api_3_1_0/core/__init__.py +395 -0
  15. swagger_petstore_open_api_3_1_0/core/_internal/__init__.py +9 -0
  16. swagger_petstore_open_api_3_1_0/core/_internal/content_disposition.py +154 -0
  17. swagger_petstore_open_api_3_1_0/core/_internal/flattening.py +104 -0
  18. swagger_petstore_open_api_3_1_0/core/_internal/headers.py +243 -0
  19. swagger_petstore_open_api_3_1_0/core/_internal/redaction.py +208 -0
  20. swagger_petstore_open_api_3_1_0/core/_internal/sse_parser.py +124 -0
  21. swagger_petstore_open_api_3_1_0/core/_internal/subscripts.py +50 -0
  22. swagger_petstore_open_api_3_1_0/core/_internal/urls.py +172 -0
  23. swagger_petstore_open_api_3_1_0/core/_internal/wire.py +201 -0
  24. swagger_petstore_open_api_3_1_0/core/adapters.py +177 -0
  25. swagger_petstore_open_api_3_1_0/core/auth/__init__.py +70 -0
  26. swagger_petstore_open_api_3_1_0/core/auth/composition.py +175 -0
  27. swagger_petstore_open_api_3_1_0/core/auth/models.py +216 -0
  28. swagger_petstore_open_api_3_1_0/core/auth/protocols.py +103 -0
  29. swagger_petstore_open_api_3_1_0/core/auth/schemes/__init__.py +94 -0
  30. swagger_petstore_open_api_3_1_0/core/auth/schemes/api_key_auth.py +53 -0
  31. swagger_petstore_open_api_3_1_0/core/auth/schemes/basic_auth.py +73 -0
  32. swagger_petstore_open_api_3_1_0/core/auth/schemes/bearer_auth.py +21 -0
  33. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_authorization_code.py +395 -0
  34. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_client_credentials.py +130 -0
  35. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_password.py +145 -0
  36. swagger_petstore_open_api_3_1_0/core/auth/schemes/oauth2_schemes.py +300 -0
  37. swagger_petstore_open_api_3_1_0/core/base_raw_response.py +49 -0
  38. swagger_petstore_open_api_3_1_0/core/bodies.py +590 -0
  39. swagger_petstore_open_api_3_1_0/core/converters/__init__.py +68 -0
  40. swagger_petstore_open_api_3_1_0/core/converters/date_time.py +237 -0
  41. swagger_petstore_open_api_3_1_0/core/converters/encoded_bytes.py +273 -0
  42. swagger_petstore_open_api_3_1_0/core/converters/open_enum.py +35 -0
  43. swagger_petstore_open_api_3_1_0/core/decoding.py +498 -0
  44. swagger_petstore_open_api_3_1_0/core/discriminators.py +129 -0
  45. swagger_petstore_open_api_3_1_0/core/event_streams.py +225 -0
  46. swagger_petstore_open_api_3_1_0/core/exceptions.py +78 -0
  47. swagger_petstore_open_api_3_1_0/core/file_responses.py +337 -0
  48. swagger_petstore_open_api_3_1_0/core/files.py +174 -0
  49. swagger_petstore_open_api_3_1_0/core/httpx2_transport.py +748 -0
  50. swagger_petstore_open_api_3_1_0/core/logging.py +472 -0
  51. swagger_petstore_open_api_3_1_0/core/models.py +143 -0
  52. swagger_petstore_open_api_3_1_0/core/optionality.py +144 -0
  53. swagger_petstore_open_api_3_1_0/core/pagination.py +452 -0
  54. swagger_petstore_open_api_3_1_0/core/paginator.py +55 -0
  55. swagger_petstore_open_api_3_1_0/core/params.py +157 -0
  56. swagger_petstore_open_api_3_1_0/core/raw_client.py +480 -0
  57. swagger_petstore_open_api_3_1_0/core/request_options.py +172 -0
  58. swagger_petstore_open_api_3_1_0/core/results.py +178 -0
  59. swagger_petstore_open_api_3_1_0/core/retries.py +237 -0
  60. swagger_petstore_open_api_3_1_0/core/runtime_env.py +43 -0
  61. swagger_petstore_open_api_3_1_0/core/servers.py +33 -0
  62. swagger_petstore_open_api_3_1_0/core/transport.py +218 -0
  63. swagger_petstore_open_api_3_1_0/errors/__init__.py +48 -0
  64. swagger_petstore_open_api_3_1_0/errors/add_pet_error.py +21 -0
  65. swagger_petstore_open_api_3_1_0/errors/delete_order_error.py +21 -0
  66. swagger_petstore_open_api_3_1_0/errors/delete_pet_error.py +21 -0
  67. swagger_petstore_open_api_3_1_0/errors/delete_user_error.py +21 -0
  68. swagger_petstore_open_api_3_1_0/errors/find_pets_by_status_error.py +21 -0
  69. swagger_petstore_open_api_3_1_0/errors/find_pets_by_tags_error.py +21 -0
  70. swagger_petstore_open_api_3_1_0/errors/get_order_by_id_error.py +21 -0
  71. swagger_petstore_open_api_3_1_0/errors/get_pet_by_id_error.py +21 -0
  72. swagger_petstore_open_api_3_1_0/errors/get_user_by_name_error.py +21 -0
  73. swagger_petstore_open_api_3_1_0/errors/login_user_error.py +21 -0
  74. swagger_petstore_open_api_3_1_0/errors/place_order_error.py +21 -0
  75. swagger_petstore_open_api_3_1_0/errors/update_pet_error.py +21 -0
  76. swagger_petstore_open_api_3_1_0/errors/update_pet_with_form_error.py +21 -0
  77. swagger_petstore_open_api_3_1_0/errors/update_user_error.py +21 -0
  78. swagger_petstore_open_api_3_1_0/errors/upload_file_error.py +21 -0
  79. swagger_petstore_open_api_3_1_0/models/__init__.py +23 -0
  80. swagger_petstore_open_api_3_1_0/models/api_response.py +18 -0
  81. swagger_petstore_open_api_3_1_0/models/category.py +15 -0
  82. swagger_petstore_open_api_3_1_0/models/enums/__init__.py +4 -0
  83. swagger_petstore_open_api_3_1_0/models/enums/order_status.py +17 -0
  84. swagger_petstore_open_api_3_1_0/models/enums/pet_status.py +17 -0
  85. swagger_petstore_open_api_3_1_0/models/order.py +27 -0
  86. swagger_petstore_open_api_3_1_0/models/pet.py +28 -0
  87. swagger_petstore_open_api_3_1_0/models/tag_model.py +15 -0
  88. swagger_petstore_open_api_3_1_0/models/user.py +29 -0
  89. swagger_petstore_open_api_3_1_0/py.typed +0 -0
  90. swagger_petstore_open_api_3_1_0/server/__init__.py +21 -0
  91. swagger_petstore_open_api_3_1_0/server/server.py +17 -0
  92. swagger_petstore_open_api_3_1_0/server/server_config.py +52 -0
@@ -0,0 +1,395 @@
1
+ """``oauth2`` / ``authorizationCode`` -- RFC 6749 §4.1, with RFC 7636 PKCE.
2
+
3
+ The only grant that cannot complete without a human, and the only one needing the refreshable tier.
4
+
5
+ **The human leg is a callback on the credentials**, not a two-step surface on the client: the source
6
+ calls it in the middle of ``fetch`` -- build the URL, hand it over, exchange the code that comes
7
+ back -- so the whole flow stays behind the one ``fetch(credentials)`` seam every grant shares.
8
+
9
+ **The credentials are twinned** because that callback's flavour must match the transport awaiting
10
+ it; a prompt blocking on a human is the last thing that should run inside an event loop."""
11
+
12
+ from __future__ import annotations
13
+
14
+ import secrets
15
+ from base64 import urlsafe_b64encode
16
+ from collections.abc import Awaitable, Callable
17
+ from dataclasses import dataclass
18
+ from hashlib import sha256
19
+ from typing import Generic, Literal, TypeAlias
20
+
21
+ from pydantic import ConfigDict, Field, model_validator
22
+ from typing_extensions import NotRequired, Self, TypedDict
23
+
24
+ from ..._internal.urls import build_url
25
+ from ...bodies import form_body
26
+ from ...decoding import async_json_decoder, json_decoder
27
+ from ...params import UrlTemplate, param
28
+ from ...raw_client import AsyncRawClient, RawClient
29
+ from ...results import Success, unwrap
30
+ from ..models import (
31
+ CredentialsPlacement,
32
+ OAuth2Credentials,
33
+ OAuthParams,
34
+ OAuthTokenRefreshable,
35
+ ScopeT,
36
+ oauth_error_response,
37
+ scope_params,
38
+ )
39
+
40
+ AuthorizationCodePrompt: TypeAlias = Callable[[str], str]
41
+ """How the SDK obtains an authorization code: hand a URL to a human, get a code back.
42
+
43
+ Given the fully built authorization URL, an implementation navigates the user to it, receives the
44
+ redirect, and returns its ``code`` query parameter. Opening a browser and listening on the redirect
45
+ URI are the caller's business: what is correct differs between a desktop app, a CLI and a test.
46
+
47
+ Called on the first acquisition, and again whenever a refresh is refused. ⚠️ It runs **inside the
48
+ scheme's lock**, so it must not issue a request that itself uses this scheme."""
49
+
50
+ AsyncAuthorizationCodePrompt: TypeAlias = Callable[[str], Awaitable[str]]
51
+ """:data:`AuthorizationCodePrompt` for the asynchronous client: awaited rather than called.
52
+
53
+ A coroutine and not a union, so a synchronous prompt is a **type** error on the asynchronous
54
+ credentials rather than a whole event loop discovered blocked on a human in production."""
55
+
56
+ PkceMethod: TypeAlias = Literal["S256", "plain"]
57
+ """RFC 7636 §4.2's code-challenge transformations.
58
+
59
+ A ``Literal`` and **not** a ``(str, Enum)``: these two members are fixed by an RFC, not derived from
60
+ a description, so the set cannot grow after generation and a call site is checked statically."""
61
+
62
+
63
+ @dataclass(frozen=True, slots=True)
64
+ class _Pkce:
65
+ """One authorization's verifier, its derived challenge, and which transformation derived it."""
66
+
67
+ verifier: str
68
+ challenge: str
69
+ method: PkceMethod
70
+
71
+
72
+ def _generate_pkce(method: PkceMethod) -> _Pkce:
73
+ """A fresh RFC 7636 §4.1 verifier and its §4.2 challenge -- one pair per authorization.
74
+
75
+ Per acquisition rather than per client, because a verifier reused across authorizations is one
76
+ an observer of the first can replay against the second.
77
+
78
+ Args:
79
+ method: ``S256``, or ``plain`` where §7.2 permits it -- the verifier as its own challenge.
80
+
81
+ Returns:
82
+ The verifier, its challenge, and the method that derived it."""
83
+ verifier = secrets.token_urlsafe(32)
84
+ if method == "plain":
85
+ return _Pkce(verifier, verifier, method)
86
+ digest = sha256(verifier.encode("ascii")).digest()
87
+ return _Pkce(verifier, urlsafe_b64encode(digest).rstrip(b"=").decode("ascii"), method)
88
+
89
+
90
+ class AuthorizationCodeCredentials(OAuth2Credentials, Generic[ScopeT]):
91
+ """What the authorization-code grant needs, for the synchronous client (RFC 6749 §4.1).
92
+
93
+ ``prompt_for_authorization_code`` is **required**, so a configured credential provably has a way
94
+ to obtain a code, checked when this model is built rather than at the first request.
95
+
96
+ ``state`` is **pass-through only**: sent on the authorization request, never generated or
97
+ validated here -- the SDK never sees the redirect, so producing and verifying it are the
98
+ caller's one job. ``pkce`` defaults to ``"S256"``; ``None`` disables it and then requires
99
+ ``client_secret``, which the validator below refuses at construction. ``scopes`` is typed by
100
+ the flow's declared vocabulary, as every grant's is."""
101
+
102
+ model_config = ConfigDict(frozen=True, extra="forbid")
103
+
104
+ client_id: str
105
+ client_secret: str | None = Field(default=None, repr=False)
106
+ redirect_uri: str
107
+ scopes: list[ScopeT] | None = None
108
+ state: str | None = None
109
+ pkce: PkceMethod | None = "S256"
110
+ prompt_for_authorization_code: AuthorizationCodePrompt
111
+
112
+ @model_validator(mode="after")
113
+ def _require_a_secret_without_pkce(self) -> Self:
114
+ if self.pkce is None and self.client_secret is None:
115
+ raise ValueError(
116
+ "client_secret is required when pkce is None: a client that neither authenticates "
117
+ "nor proves possession of a code verifier cannot use the authorization-code grant "
118
+ 'safely. Set pkce="S256" for a public client, or supply client_secret.'
119
+ )
120
+ return self
121
+
122
+ @classmethod
123
+ def coerce(cls, value: AuthorizationCodeCredentialsOrDict[ScopeT]) -> AuthorizationCodeCredentials[ScopeT]:
124
+ """Accept either spelling, validating the mapping form.
125
+
126
+ Args:
127
+ value: The credential, as the typed model or as a mapping.
128
+
129
+ Returns:
130
+ The validated model; see :meth:`BasicAuthCredentials.coerce` for why it takes no
131
+ ``None``.
132
+
133
+ Raises:
134
+ ValidationError: If a key is missing or unknown, if ``scopes`` names one this flow does
135
+ not declare, or if ``pkce`` is ``None`` without a ``client_secret``."""
136
+ if isinstance(value, cls):
137
+ return value
138
+ return cls.model_validate(value)
139
+
140
+
141
+ class AuthorizationCodeCredentialsDict(TypedDict, Generic[ScopeT]):
142
+ client_id: str
143
+ client_secret: NotRequired[str | None]
144
+ redirect_uri: str
145
+ scopes: NotRequired[list[ScopeT] | None]
146
+ state: NotRequired[str | None]
147
+ pkce: NotRequired[PkceMethod | None]
148
+ prompt_for_authorization_code: AuthorizationCodePrompt
149
+
150
+
151
+ AuthorizationCodeCredentialsOrDict: TypeAlias = (
152
+ AuthorizationCodeCredentials[ScopeT] | AuthorizationCodeCredentialsDict[ScopeT]
153
+ )
154
+ """What an authorization-code credential accepts: the typed model, or a mapping of the same keys."""
155
+
156
+
157
+ class AsyncAuthorizationCodeCredentials(OAuth2Credentials, Generic[ScopeT]):
158
+ """:class:`AuthorizationCodeCredentials` for the asynchronous client.
159
+
160
+ Identical but for ``prompt_for_authorization_code``, which is awaited. Written out rather than
161
+ sharing a base: the two would still need their own ``coerce``, ``TypedDict`` and validator."""
162
+
163
+ model_config = ConfigDict(frozen=True, extra="forbid")
164
+
165
+ client_id: str
166
+ client_secret: str | None = Field(default=None, repr=False)
167
+ redirect_uri: str
168
+ scopes: list[ScopeT] | None = None
169
+ state: str | None = None
170
+ pkce: PkceMethod | None = "S256"
171
+ prompt_for_authorization_code: AsyncAuthorizationCodePrompt
172
+
173
+ @model_validator(mode="after")
174
+ def _require_a_secret_without_pkce(self) -> Self:
175
+ if self.pkce is None and self.client_secret is None:
176
+ raise ValueError(
177
+ "client_secret is required when pkce is None: a client that neither authenticates "
178
+ "nor proves possession of a code verifier cannot use the authorization-code grant "
179
+ 'safely. Set pkce="S256" for a public client, or supply client_secret.'
180
+ )
181
+ return self
182
+
183
+ @classmethod
184
+ def coerce(
185
+ cls, value: AsyncAuthorizationCodeCredentialsOrDict[ScopeT]
186
+ ) -> AsyncAuthorizationCodeCredentials[ScopeT]:
187
+ """See :meth:`AuthorizationCodeCredentials.coerce`.
188
+
189
+ Args:
190
+ value: The credential, as the typed model or as a mapping.
191
+
192
+ Returns:
193
+ The validated model.
194
+
195
+ Raises:
196
+ ValidationError: If a key is missing or unknown, if ``scopes`` names one this flow does
197
+ not declare, or if ``pkce`` is ``None`` without a ``client_secret``."""
198
+ if isinstance(value, cls):
199
+ return value
200
+ return cls.model_validate(value)
201
+
202
+
203
+ class AsyncAuthorizationCodeCredentialsDict(TypedDict, Generic[ScopeT]):
204
+ client_id: str
205
+ client_secret: NotRequired[str | None]
206
+ redirect_uri: str
207
+ scopes: NotRequired[list[ScopeT] | None]
208
+ state: NotRequired[str | None]
209
+ pkce: NotRequired[PkceMethod | None]
210
+ prompt_for_authorization_code: AsyncAuthorizationCodePrompt
211
+
212
+
213
+ AsyncAuthorizationCodeCredentialsOrDict: TypeAlias = (
214
+ AsyncAuthorizationCodeCredentials[ScopeT] | AsyncAuthorizationCodeCredentialsDict[ScopeT]
215
+ )
216
+ """:data:`AuthorizationCodeCredentialsOrDict` for the asynchronous client."""
217
+
218
+
219
+ def _authorization_params(
220
+ credentials: AuthorizationCodeCredentials[ScopeT] | AsyncAuthorizationCodeCredentials[ScopeT],
221
+ pkce: _Pkce | None,
222
+ ) -> OAuthParams:
223
+ """RFC 6749 §4.1.1's authorization request, as query parameters.
224
+
225
+ Shared by both flavours: what goes on an authorization URL does not depend on which transport
226
+ later exchanges the code.
227
+
228
+ Args:
229
+ credentials: The configured credentials, in either flavour.
230
+ pkce: This acquisition's PKCE pair, or ``None`` when PKCE is disabled.
231
+
232
+ Returns:
233
+ The query parameters for the authorization URL. ``state`` is omitted entirely when unset
234
+ rather than sent empty, since §4.1.1 only RECOMMENDS it."""
235
+ params = (
236
+ param[str]("response_type", "code"),
237
+ param[str]("client_id", credentials.client_id),
238
+ param[str]("redirect_uri", credentials.redirect_uri),
239
+ *scope_params(credentials.scopes),
240
+ )
241
+ if credentials.state is not None:
242
+ params = (*params, param[str]("state", credentials.state))
243
+ if pkce is None:
244
+ return params
245
+ return (
246
+ *params,
247
+ param[str]("code_challenge", pkce.challenge),
248
+ param[PkceMethod]("code_challenge_method", pkce.method),
249
+ )
250
+
251
+
252
+ def _code_verifier_params(pkce: _Pkce | None) -> OAuthParams:
253
+ """RFC 7636 §4.5's ``code_verifier`` on the exchange.
254
+
255
+ Args:
256
+ pkce: This acquisition's PKCE pair, or ``None`` when PKCE is disabled.
257
+
258
+ Returns:
259
+ One ``code_verifier`` parameter, or an empty tuple."""
260
+ if pkce is None:
261
+ return ()
262
+ return (param[str]("code_verifier", pkce.verifier),)
263
+
264
+
265
+ @dataclass(frozen=True, slots=True, kw_only=True)
266
+ class AuthorizationCodeTokenSource(Generic[ScopeT]):
267
+ """The built-in :class:`RefreshableTokenSource` for the authorization-code grant.
268
+
269
+ Two requests with a human between them: :meth:`fetch` builds the authorization URL, hands it to
270
+ the credentials' prompt and exchanges the code; :meth:`refresh` skips both, which is the entire
271
+ reason this grant needs the refreshable tier.
272
+
273
+ ``refresh_url`` is its own field because the flow declares ``refreshUrl`` beside ``tokenUrl``;
274
+ §6 defaults it to the token endpoint, but a description naming another is read, not guessed."""
275
+
276
+ client: RawClient
277
+ authorization_url: UrlTemplate
278
+ token_url: UrlTemplate
279
+ refresh_url: UrlTemplate
280
+ placement: CredentialsPlacement
281
+
282
+ def fetch(self, credentials: AuthorizationCodeCredentials[ScopeT]) -> OAuthTokenRefreshable:
283
+ pkce = _generate_pkce(credentials.pkce) if credentials.pkce is not None else None
284
+ code = credentials.prompt_for_authorization_code(
285
+ build_url(self.authorization_url, query_params=_authorization_params(credentials, pkce))
286
+ )
287
+ headers, client_params = self.placement(credentials.client_id, credentials.client_secret)
288
+ return unwrap(
289
+ self.client.execute(
290
+ http_method="POST",
291
+ url_template=self.token_url,
292
+ headers=headers,
293
+ body=form_body(
294
+ param[str]("grant_type", "authorization_code"),
295
+ param[str]("code", code),
296
+ param[str]("redirect_uri", credentials.redirect_uri),
297
+ *_code_verifier_params(pkce),
298
+ *client_params,
299
+ ),
300
+ decoder=json_decoder[OAuthTokenRefreshable],
301
+ error_mapper=oauth_error_response,
302
+ )
303
+ )
304
+
305
+ def refresh(
306
+ self, credentials: AuthorizationCodeCredentials[ScopeT], refresh_token: str
307
+ ) -> OAuthTokenRefreshable | None:
308
+ """RFC 6749 §6, and ``None`` when the provider refuses.
309
+
310
+ The one operation in this runtime that reads an ``ApiResult`` instead of unwrapping it: a
311
+ refused refresh is a step in the flow, not an error, so the scheme re-acquires instead.
312
+
313
+ Args:
314
+ credentials: The configured credentials.
315
+ refresh_token: The refresh token to spend.
316
+
317
+ Returns:
318
+ The new token, or ``None`` for a provider refusal. A transport failure propagates, as
319
+ does a success whose body will not decode."""
320
+ headers, client_params = self.placement(credentials.client_id, credentials.client_secret)
321
+ result = self.client.execute(
322
+ http_method="POST",
323
+ url_template=self.refresh_url,
324
+ headers=headers,
325
+ body=form_body(
326
+ param[str]("grant_type", "refresh_token"),
327
+ param[str]("refresh_token", refresh_token),
328
+ *client_params,
329
+ ),
330
+ decoder=json_decoder[OAuthTokenRefreshable],
331
+ error_mapper=oauth_error_response,
332
+ )
333
+ return result.payload if isinstance(result, Success) else None
334
+
335
+
336
+ @dataclass(frozen=True, slots=True, kw_only=True)
337
+ class AsyncAuthorizationCodeTokenSource(Generic[ScopeT]):
338
+ """:class:`AuthorizationCodeTokenSource` for the asynchronous client.
339
+
340
+ The prompt is awaited, as are both requests."""
341
+
342
+ client: AsyncRawClient
343
+ authorization_url: UrlTemplate
344
+ token_url: UrlTemplate
345
+ refresh_url: UrlTemplate
346
+ placement: CredentialsPlacement
347
+
348
+ async def fetch(self, credentials: AsyncAuthorizationCodeCredentials[ScopeT]) -> OAuthTokenRefreshable:
349
+ pkce = _generate_pkce(credentials.pkce) if credentials.pkce is not None else None
350
+ code = await credentials.prompt_for_authorization_code(
351
+ build_url(self.authorization_url, query_params=_authorization_params(credentials, pkce))
352
+ )
353
+ headers, client_params = self.placement(credentials.client_id, credentials.client_secret)
354
+ return unwrap(
355
+ await self.client.execute(
356
+ http_method="POST",
357
+ url_template=self.token_url,
358
+ headers=headers,
359
+ body=form_body(
360
+ param[str]("grant_type", "authorization_code"),
361
+ param[str]("code", code),
362
+ param[str]("redirect_uri", credentials.redirect_uri),
363
+ *_code_verifier_params(pkce),
364
+ *client_params,
365
+ ),
366
+ decoder=async_json_decoder[OAuthTokenRefreshable],
367
+ error_mapper=oauth_error_response,
368
+ )
369
+ )
370
+
371
+ async def refresh(
372
+ self, credentials: AsyncAuthorizationCodeCredentials[ScopeT], refresh_token: str
373
+ ) -> OAuthTokenRefreshable | None:
374
+ """See :meth:`AuthorizationCodeTokenSource.refresh`.
375
+
376
+ Args:
377
+ credentials: The configured credentials.
378
+ refresh_token: The refresh token to spend.
379
+
380
+ Returns:
381
+ The new token, or ``None`` for a provider refusal."""
382
+ headers, client_params = self.placement(credentials.client_id, credentials.client_secret)
383
+ result = await self.client.execute(
384
+ http_method="POST",
385
+ url_template=self.refresh_url,
386
+ headers=headers,
387
+ body=form_body(
388
+ param[str]("grant_type", "refresh_token"),
389
+ param[str]("refresh_token", refresh_token),
390
+ *client_params,
391
+ ),
392
+ decoder=async_json_decoder[OAuthTokenRefreshable],
393
+ error_mapper=oauth_error_response,
394
+ )
395
+ return result.payload if isinstance(result, Success) else None
@@ -0,0 +1,130 @@
1
+ """``oauth2`` / ``clientCredentials`` -- RFC 6749 §4.4.
2
+
3
+ The grant with the least to it: no resource owner, so no second credential pair and no human. The
4
+ whole request is a ``grant_type``, the scopes, and whatever the placement contributes."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Generic, TypeAlias
10
+
11
+ from pydantic import ConfigDict, Field
12
+ from typing_extensions import NotRequired, TypedDict
13
+
14
+ from ...bodies import form_body
15
+ from ...decoding import async_json_decoder, json_decoder
16
+ from ...params import UrlTemplate, param
17
+ from ...raw_client import AsyncRawClient, RawClient
18
+ from ...results import unwrap
19
+ from ..models import (
20
+ CredentialsPlacement,
21
+ OAuth2Credentials,
22
+ OAuthParams,
23
+ OAuthToken,
24
+ ScopeT,
25
+ oauth_error_response,
26
+ scope_params,
27
+ )
28
+
29
+
30
+ class ClientCredentials(OAuth2Credentials, Generic[ScopeT]):
31
+ """The client identifier and secret of an OAuth 2.0 ``clientCredentials`` flow (RFC 6749 §4.4).
32
+
33
+ ``scopes`` is a list this SDK joins for the wire, so a caller never writes §3.3's delimiter, and
34
+ it is typed by the flow's declared vocabulary -- so a scope the flow does not document fails the
35
+ build at the call site instead of the request at the provider."""
36
+
37
+ model_config = ConfigDict(frozen=True, extra="forbid")
38
+
39
+ client_id: str
40
+ client_secret: str = Field(repr=False)
41
+ scopes: list[ScopeT] | None = None
42
+
43
+ @classmethod
44
+ def coerce(cls, value: ClientCredentialsOrDict[ScopeT]) -> ClientCredentials[ScopeT]:
45
+ """Accept either spelling, validating the mapping form.
46
+
47
+ Args:
48
+ value: The credential, as the typed model or as a mapping.
49
+
50
+ Returns:
51
+ The validated model; see :meth:`BasicAuthCredentials.coerce` for why it takes no
52
+ ``None``.
53
+
54
+ Raises:
55
+ ValidationError: If the mapping is missing a key, carries an unknown one, or names a
56
+ scope this flow does not declare."""
57
+ if isinstance(value, cls):
58
+ return value
59
+ return cls.model_validate(value)
60
+
61
+
62
+ class ClientCredentialsDict(TypedDict, Generic[ScopeT]):
63
+ client_id: str
64
+ client_secret: str
65
+ scopes: NotRequired[list[ScopeT] | None]
66
+
67
+
68
+ ClientCredentialsOrDict: TypeAlias = ClientCredentials[ScopeT] | ClientCredentialsDict[ScopeT]
69
+ """What a client-credentials credential accepts: the typed model, or a mapping of the same keys."""
70
+
71
+
72
+ def _token_params(credentials: ClientCredentials[ScopeT]) -> OAuthParams:
73
+ """RFC 6749 §4.4.2's token request, minus the client's own credentials.
74
+
75
+ Shared by both flavours rather than duplicated, so an edit cannot land in one only.
76
+
77
+ Args:
78
+ credentials: The configured client credentials.
79
+
80
+ Returns:
81
+ The ``grant_type`` and the scopes; the placement's half is spliced in by each ``fetch``."""
82
+ return param[str]("grant_type", "client_credentials"), *scope_params(credentials.scopes)
83
+
84
+
85
+ @dataclass(frozen=True, slots=True, kw_only=True)
86
+ class ClientCredentialsTokenSource(Generic[ScopeT]):
87
+ """The built-in :class:`TokenSource` for the client-credentials grant: POST the token request.
88
+
89
+ ``placement`` is a field rather than two subclasses, because the placements differ in *data* and
90
+ not in behaviour: :meth:`fetch` is identical for both. Carries no ``auth_scheme=`` -- the
91
+ credentials are on this request already, so the fetch provably cannot recurse into itself."""
92
+
93
+ client: RawClient
94
+ token_url: UrlTemplate
95
+ placement: CredentialsPlacement
96
+
97
+ def fetch(self, credentials: ClientCredentials[ScopeT]) -> OAuthToken:
98
+ headers, client_params = self.placement(credentials.client_id, credentials.client_secret)
99
+ return unwrap(
100
+ self.client.execute(
101
+ http_method="POST",
102
+ url_template=self.token_url,
103
+ headers=headers,
104
+ body=form_body(*_token_params(credentials), *client_params),
105
+ decoder=json_decoder[OAuthToken],
106
+ error_mapper=oauth_error_response,
107
+ )
108
+ )
109
+
110
+
111
+ @dataclass(frozen=True, slots=True, kw_only=True)
112
+ class AsyncClientCredentialsTokenSource(Generic[ScopeT]):
113
+ """:class:`ClientCredentialsTokenSource` for the asynchronous client: the request is awaited."""
114
+
115
+ client: AsyncRawClient
116
+ token_url: UrlTemplate
117
+ placement: CredentialsPlacement
118
+
119
+ async def fetch(self, credentials: ClientCredentials[ScopeT]) -> OAuthToken:
120
+ headers, client_params = self.placement(credentials.client_id, credentials.client_secret)
121
+ return unwrap(
122
+ await self.client.execute(
123
+ http_method="POST",
124
+ url_template=self.token_url,
125
+ headers=headers,
126
+ body=form_body(*_token_params(credentials), *client_params),
127
+ decoder=async_json_decoder[OAuthToken],
128
+ error_mapper=oauth_error_response,
129
+ )
130
+ )
@@ -0,0 +1,145 @@
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 ...results import unwrap
24
+ from ..models import (
25
+ CredentialsPlacement,
26
+ OAuth2Credentials,
27
+ OAuthParams,
28
+ OAuthToken,
29
+ ScopeT,
30
+ oauth_error_response,
31
+ scope_params,
32
+ )
33
+
34
+
35
+ class PasswordCredentials(OAuth2Credentials, Generic[ScopeT]):
36
+ """The resource owner's credentials, plus the client's own (RFC 6749 §4.3.2).
37
+
38
+ ``client_secret`` is optional because a public client has none -- §3.2.1 then makes ``client_id``
39
+ the required identification, which the placement function writes. ``scopes`` is typed by the
40
+ flow's declared vocabulary, as every grant's is."""
41
+
42
+ model_config = ConfigDict(frozen=True, extra="forbid")
43
+
44
+ client_id: str
45
+ client_secret: str | None = Field(default=None, repr=False)
46
+ username: str
47
+ password: str = Field(repr=False)
48
+ scopes: list[ScopeT] | None = None
49
+
50
+ @classmethod
51
+ def coerce(cls, value: PasswordCredentialsOrDict[ScopeT]) -> PasswordCredentials[ScopeT]:
52
+ """Accept either spelling, validating the mapping form.
53
+
54
+ Args:
55
+ value: The credential, as the typed model or as a mapping.
56
+
57
+ Returns:
58
+ The validated model; see :meth:`BasicAuthCredentials.coerce` for why it takes no
59
+ ``None``.
60
+
61
+ Raises:
62
+ ValidationError: If the mapping is missing a key, carries an unknown one, or names a
63
+ scope this flow does not declare."""
64
+ if isinstance(value, cls):
65
+ return value
66
+ return cls.model_validate(value)
67
+
68
+
69
+ class PasswordCredentialsDict(TypedDict, Generic[ScopeT]):
70
+ client_id: str
71
+ client_secret: NotRequired[str | None]
72
+ username: str
73
+ password: str
74
+ scopes: NotRequired[list[ScopeT] | None]
75
+
76
+
77
+ PasswordCredentialsOrDict: TypeAlias = PasswordCredentials[ScopeT] | PasswordCredentialsDict[ScopeT]
78
+ """What a password credential accepts: the typed model, or a mapping of the same keys."""
79
+
80
+
81
+ def _token_params(credentials: PasswordCredentials[ScopeT]) -> OAuthParams:
82
+ """RFC 6749 §4.3.2's token request, less the client's own half.
83
+
84
+ Shared by both flavours rather than duplicated, so an edit cannot land in one only.
85
+
86
+ Args:
87
+ credentials: The configured password credentials.
88
+
89
+ Returns:
90
+ The ``grant_type``, the resource owner's pair and the scopes. The client's half is
91
+ deliberately absent: it comes from the source's ``placement``."""
92
+ return (
93
+ param[str]("grant_type", "password"),
94
+ param[str]("username", credentials.username),
95
+ param[str]("password", credentials.password),
96
+ *scope_params(credentials.scopes),
97
+ )
98
+
99
+
100
+ @dataclass(frozen=True, slots=True, kw_only=True)
101
+ class PasswordTokenSource(Generic[ScopeT]):
102
+ """The built-in :class:`TokenSource` for the password grant: POST the token request.
103
+
104
+ ``placement`` is read from the flow's own ``x-oauth2-credentials-placement`` by the generated
105
+ client, exactly as the client-credentials source's is. Carries no ``auth_scheme=`` -- the
106
+ credentials are on this request already, so the fetch provably cannot recurse into itself."""
107
+
108
+ client: RawClient
109
+ token_url: UrlTemplate
110
+ placement: CredentialsPlacement
111
+
112
+ def fetch(self, credentials: PasswordCredentials[ScopeT]) -> OAuthToken:
113
+ headers, client_params = self.placement(credentials.client_id, credentials.client_secret)
114
+ return unwrap(
115
+ self.client.execute(
116
+ http_method="POST",
117
+ url_template=self.token_url,
118
+ headers=headers,
119
+ body=form_body(*_token_params(credentials), *client_params),
120
+ decoder=json_decoder[OAuthToken],
121
+ error_mapper=oauth_error_response,
122
+ )
123
+ )
124
+
125
+
126
+ @dataclass(frozen=True, slots=True, kw_only=True)
127
+ class AsyncPasswordTokenSource(Generic[ScopeT]):
128
+ """:class:`PasswordTokenSource` for the asynchronous client: the request is awaited."""
129
+
130
+ client: AsyncRawClient
131
+ token_url: UrlTemplate
132
+ placement: CredentialsPlacement
133
+
134
+ async def fetch(self, credentials: PasswordCredentials[ScopeT]) -> OAuthToken:
135
+ headers, client_params = self.placement(credentials.client_id, credentials.client_secret)
136
+ return unwrap(
137
+ await self.client.execute(
138
+ http_method="POST",
139
+ url_template=self.token_url,
140
+ headers=headers,
141
+ body=form_body(*_token_params(credentials), *client_params),
142
+ decoder=async_json_decoder[OAuthToken],
143
+ error_mapper=oauth_error_response,
144
+ )
145
+ )