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,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}"),))
@@ -0,0 +1,393 @@
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
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 self.client.execute(
289
+ http_method="POST",
290
+ url_template=self.token_url,
291
+ headers=headers,
292
+ body=form_body(
293
+ param[str]("grant_type", "authorization_code"),
294
+ param[str]("code", code),
295
+ param[str]("redirect_uri", credentials.redirect_uri),
296
+ *_code_verifier_params(pkce),
297
+ *client_params,
298
+ ),
299
+ decoder=json_decoder[OAuthTokenRefreshable],
300
+ error_mapper=oauth_error_response,
301
+ ).unwrap()
302
+
303
+ def refresh(
304
+ self, credentials: AuthorizationCodeCredentials[ScopeT], refresh_token: str
305
+ ) -> OAuthTokenRefreshable | None:
306
+ """RFC 6749 §6, and ``None`` when the provider refuses.
307
+
308
+ The one operation in this runtime that reads an ``ApiResult`` instead of unwrapping it: a
309
+ refused refresh is a step in the flow, not an error, so the scheme re-acquires instead.
310
+
311
+ Args:
312
+ credentials: The configured credentials.
313
+ refresh_token: The refresh token to spend.
314
+
315
+ Returns:
316
+ The new token, or ``None`` for a provider refusal. A transport failure propagates, as
317
+ does a success whose body will not decode."""
318
+ headers, client_params = self.placement(credentials.client_id, credentials.client_secret)
319
+ result = self.client.execute(
320
+ http_method="POST",
321
+ url_template=self.refresh_url,
322
+ headers=headers,
323
+ body=form_body(
324
+ param[str]("grant_type", "refresh_token"),
325
+ param[str]("refresh_token", refresh_token),
326
+ *client_params,
327
+ ),
328
+ decoder=json_decoder[OAuthTokenRefreshable],
329
+ error_mapper=oauth_error_response,
330
+ )
331
+ return result.payload if isinstance(result, Success) else None
332
+
333
+
334
+ @dataclass(frozen=True, slots=True, kw_only=True)
335
+ class AsyncAuthorizationCodeTokenSource(Generic[ScopeT]):
336
+ """:class:`AuthorizationCodeTokenSource` for the asynchronous client.
337
+
338
+ The prompt is awaited, as are both requests."""
339
+
340
+ client: AsyncRawClient
341
+ authorization_url: UrlTemplate
342
+ token_url: UrlTemplate
343
+ refresh_url: UrlTemplate
344
+ placement: CredentialsPlacement
345
+
346
+ async def fetch(self, credentials: AsyncAuthorizationCodeCredentials[ScopeT]) -> OAuthTokenRefreshable:
347
+ pkce = _generate_pkce(credentials.pkce) if credentials.pkce is not None else None
348
+ code = await credentials.prompt_for_authorization_code(
349
+ build_url(self.authorization_url, query_params=_authorization_params(credentials, pkce))
350
+ )
351
+ headers, client_params = self.placement(credentials.client_id, credentials.client_secret)
352
+ return (
353
+ await self.client.execute(
354
+ http_method="POST",
355
+ url_template=self.token_url,
356
+ headers=headers,
357
+ body=form_body(
358
+ param[str]("grant_type", "authorization_code"),
359
+ param[str]("code", code),
360
+ param[str]("redirect_uri", credentials.redirect_uri),
361
+ *_code_verifier_params(pkce),
362
+ *client_params,
363
+ ),
364
+ decoder=async_json_decoder[OAuthTokenRefreshable],
365
+ error_mapper=oauth_error_response,
366
+ )
367
+ ).unwrap()
368
+
369
+ async def refresh(
370
+ self, credentials: AsyncAuthorizationCodeCredentials[ScopeT], refresh_token: str
371
+ ) -> OAuthTokenRefreshable | None:
372
+ """See :meth:`AuthorizationCodeTokenSource.refresh`.
373
+
374
+ Args:
375
+ credentials: The configured credentials.
376
+ refresh_token: The refresh token to spend.
377
+
378
+ Returns:
379
+ The new token, or ``None`` for a provider refusal."""
380
+ headers, client_params = self.placement(credentials.client_id, credentials.client_secret)
381
+ result = await self.client.execute(
382
+ http_method="POST",
383
+ url_template=self.refresh_url,
384
+ headers=headers,
385
+ body=form_body(
386
+ param[str]("grant_type", "refresh_token"),
387
+ param[str]("refresh_token", refresh_token),
388
+ *client_params,
389
+ ),
390
+ decoder=async_json_decoder[OAuthTokenRefreshable],
391
+ error_mapper=oauth_error_response,
392
+ )
393
+ return result.payload if isinstance(result, Success) else None
@@ -0,0 +1,127 @@
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 ..models import (
19
+ CredentialsPlacement,
20
+ OAuth2Credentials,
21
+ OAuthParams,
22
+ OAuthToken,
23
+ ScopeT,
24
+ oauth_error_response,
25
+ scope_params,
26
+ )
27
+
28
+
29
+ class ClientCredentials(OAuth2Credentials, Generic[ScopeT]):
30
+ """The client identifier and secret of an OAuth 2.0 ``clientCredentials`` flow (RFC 6749 §4.4).
31
+
32
+ ``scopes`` is a list this SDK joins for the wire, so a caller never writes §3.3's delimiter, and
33
+ it is typed by the flow's declared vocabulary -- so a scope the flow does not document fails the
34
+ build at the call site instead of the request at the provider."""
35
+
36
+ model_config = ConfigDict(frozen=True, extra="forbid")
37
+
38
+ client_id: str
39
+ client_secret: str = Field(repr=False)
40
+ scopes: list[ScopeT] | None = None
41
+
42
+ @classmethod
43
+ def coerce(cls, value: ClientCredentialsOrDict[ScopeT]) -> ClientCredentials[ScopeT]:
44
+ """Accept either spelling, validating the mapping form.
45
+
46
+ Args:
47
+ value: The credential, as the typed model or as a mapping.
48
+
49
+ Returns:
50
+ The validated model; see :meth:`BasicAuthCredentials.coerce` for why it takes no
51
+ ``None``.
52
+
53
+ Raises:
54
+ ValidationError: If the mapping is missing a key, carries an unknown one, or names a
55
+ scope this flow does not declare."""
56
+ if isinstance(value, cls):
57
+ return value
58
+ return cls.model_validate(value)
59
+
60
+
61
+ class ClientCredentialsDict(TypedDict, Generic[ScopeT]):
62
+ client_id: str
63
+ client_secret: str
64
+ scopes: NotRequired[list[ScopeT] | None]
65
+
66
+
67
+ ClientCredentialsOrDict: TypeAlias = ClientCredentials[ScopeT] | ClientCredentialsDict[ScopeT]
68
+ """What a client-credentials credential accepts: the typed model, or a mapping of the same keys."""
69
+
70
+
71
+ def _token_params(credentials: ClientCredentials[ScopeT]) -> OAuthParams:
72
+ """RFC 6749 §4.4.2's token request, minus the client's own credentials.
73
+
74
+ Shared by both flavours rather than duplicated, so an edit cannot land in one only.
75
+
76
+ Args:
77
+ credentials: The configured client credentials.
78
+
79
+ Returns:
80
+ The ``grant_type`` and the scopes; the placement's half is spliced in by each ``fetch``."""
81
+ return param[str]("grant_type", "client_credentials"), *scope_params(credentials.scopes)
82
+
83
+
84
+ @dataclass(frozen=True, slots=True, kw_only=True)
85
+ class ClientCredentialsTokenSource(Generic[ScopeT]):
86
+ """The built-in :class:`TokenSource` for the client-credentials grant: POST the token request.
87
+
88
+ ``placement`` is a field rather than two subclasses, because the placements differ in *data* and
89
+ not in behaviour: :meth:`fetch` is identical for both. Carries no ``auth_scheme=`` -- the
90
+ credentials are on this request already, so the fetch provably cannot recurse into itself."""
91
+
92
+ client: RawClient
93
+ token_url: UrlTemplate
94
+ placement: CredentialsPlacement
95
+
96
+ def fetch(self, credentials: ClientCredentials[ScopeT]) -> OAuthToken:
97
+ headers, client_params = self.placement(credentials.client_id, credentials.client_secret)
98
+ return self.client.execute(
99
+ http_method="POST",
100
+ url_template=self.token_url,
101
+ headers=headers,
102
+ body=form_body(*_token_params(credentials), *client_params),
103
+ decoder=json_decoder[OAuthToken],
104
+ error_mapper=oauth_error_response,
105
+ ).unwrap()
106
+
107
+
108
+ @dataclass(frozen=True, slots=True, kw_only=True)
109
+ class AsyncClientCredentialsTokenSource(Generic[ScopeT]):
110
+ """:class:`ClientCredentialsTokenSource` for the asynchronous client: the request is awaited."""
111
+
112
+ client: AsyncRawClient
113
+ token_url: UrlTemplate
114
+ placement: CredentialsPlacement
115
+
116
+ async def fetch(self, credentials: ClientCredentials[ScopeT]) -> OAuthToken:
117
+ headers, client_params = self.placement(credentials.client_id, credentials.client_secret)
118
+ return (
119
+ await self.client.execute(
120
+ http_method="POST",
121
+ url_template=self.token_url,
122
+ headers=headers,
123
+ body=form_body(*_token_params(credentials), *client_params),
124
+ decoder=async_json_decoder[OAuthToken],
125
+ error_mapper=oauth_error_response,
126
+ )
127
+ ).unwrap()