cliV1 0.1.1__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 (83) hide show
  1. cliv1-0.1.1.dist-info/METADATA +156 -0
  2. cliv1-0.1.1.dist-info/RECORD +83 -0
  3. cliv1-0.1.1.dist-info/WHEEL +4 -0
  4. cliv1-0.1.1.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 +881 -0
  8. swagger_petstore_open_api_3_0/apis/store.py +411 -0
  9. swagger_petstore_open_api_3_0/apis/user_api.py +792 -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 +342 -0
  15. swagger_petstore_open_api_3_0/core/_internal/__init__.py +7 -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/urls.py +166 -0
  20. swagger_petstore_open_api_3_0/core/_internal/wire.py +173 -0
  21. swagger_petstore_open_api_3_0/core/adapters.py +177 -0
  22. swagger_petstore_open_api_3_0/core/auth/__init__.py +70 -0
  23. swagger_petstore_open_api_3_0/core/auth/composition.py +175 -0
  24. swagger_petstore_open_api_3_0/core/auth/models.py +217 -0
  25. swagger_petstore_open_api_3_0/core/auth/protocols.py +103 -0
  26. swagger_petstore_open_api_3_0/core/auth/schemes/__init__.py +94 -0
  27. swagger_petstore_open_api_3_0/core/auth/schemes/api_key_auth.py +53 -0
  28. swagger_petstore_open_api_3_0/core/auth/schemes/basic_auth.py +73 -0
  29. swagger_petstore_open_api_3_0/core/auth/schemes/bearer_auth.py +21 -0
  30. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_authorization_code.py +393 -0
  31. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_client_credentials.py +127 -0
  32. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_password.py +142 -0
  33. swagger_petstore_open_api_3_0/core/auth/schemes/oauth2_schemes.py +300 -0
  34. swagger_petstore_open_api_3_0/core/base_raw_response.py +49 -0
  35. swagger_petstore_open_api_3_0/core/bodies.py +563 -0
  36. swagger_petstore_open_api_3_0/core/converters/__init__.py +68 -0
  37. swagger_petstore_open_api_3_0/core/converters/date_time.py +237 -0
  38. swagger_petstore_open_api_3_0/core/converters/encoded_bytes.py +271 -0
  39. swagger_petstore_open_api_3_0/core/converters/open_enum.py +35 -0
  40. swagger_petstore_open_api_3_0/core/decoding.py +263 -0
  41. swagger_petstore_open_api_3_0/core/exceptions.py +41 -0
  42. swagger_petstore_open_api_3_0/core/file_responses.py +324 -0
  43. swagger_petstore_open_api_3_0/core/files.py +173 -0
  44. swagger_petstore_open_api_3_0/core/httpx_transport.py +766 -0
  45. swagger_petstore_open_api_3_0/core/models.py +143 -0
  46. swagger_petstore_open_api_3_0/core/optionality.py +130 -0
  47. swagger_petstore_open_api_3_0/core/params.py +161 -0
  48. swagger_petstore_open_api_3_0/core/raw_client.py +388 -0
  49. swagger_petstore_open_api_3_0/core/request_options.py +94 -0
  50. swagger_petstore_open_api_3_0/core/results.py +110 -0
  51. swagger_petstore_open_api_3_0/core/runtime_env.py +43 -0
  52. swagger_petstore_open_api_3_0/core/servers.py +33 -0
  53. swagger_petstore_open_api_3_0/core/transport.py +248 -0
  54. swagger_petstore_open_api_3_0/errors/__init__.py +48 -0
  55. swagger_petstore_open_api_3_0/errors/add_pet_error.py +21 -0
  56. swagger_petstore_open_api_3_0/errors/delete_order_error.py +21 -0
  57. swagger_petstore_open_api_3_0/errors/delete_pet_error.py +21 -0
  58. swagger_petstore_open_api_3_0/errors/delete_user_error.py +21 -0
  59. swagger_petstore_open_api_3_0/errors/find_pets_by_status_error.py +21 -0
  60. swagger_petstore_open_api_3_0/errors/find_pets_by_tags_error.py +21 -0
  61. swagger_petstore_open_api_3_0/errors/get_order_by_id_error.py +21 -0
  62. swagger_petstore_open_api_3_0/errors/get_pet_by_id_error.py +21 -0
  63. swagger_petstore_open_api_3_0/errors/get_user_by_name_error.py +21 -0
  64. swagger_petstore_open_api_3_0/errors/login_user_error.py +21 -0
  65. swagger_petstore_open_api_3_0/errors/place_order_error.py +21 -0
  66. swagger_petstore_open_api_3_0/errors/update_pet_error.py +21 -0
  67. swagger_petstore_open_api_3_0/errors/update_pet_with_form_error.py +21 -0
  68. swagger_petstore_open_api_3_0/errors/update_user_error.py +21 -0
  69. swagger_petstore_open_api_3_0/errors/upload_file_error.py +21 -0
  70. swagger_petstore_open_api_3_0/models/__init__.py +23 -0
  71. swagger_petstore_open_api_3_0/models/api_response.py +18 -0
  72. swagger_petstore_open_api_3_0/models/category.py +15 -0
  73. swagger_petstore_open_api_3_0/models/enums/__init__.py +4 -0
  74. swagger_petstore_open_api_3_0/models/enums/order_status.py +17 -0
  75. swagger_petstore_open_api_3_0/models/enums/pet_status.py +17 -0
  76. swagger_petstore_open_api_3_0/models/order.py +27 -0
  77. swagger_petstore_open_api_3_0/models/pet.py +28 -0
  78. swagger_petstore_open_api_3_0/models/tag.py +15 -0
  79. swagger_petstore_open_api_3_0/models/user.py +29 -0
  80. swagger_petstore_open_api_3_0/py.typed +0 -0
  81. swagger_petstore_open_api_3_0/server/__init__.py +21 -0
  82. swagger_petstore_open_api_3_0/server/server.py +17 -0
  83. swagger_petstore_open_api_3_0/server/server_config.py +52 -0
@@ -0,0 +1,177 @@
1
+ """The one place a ``TypeAdapter`` is built, and what a value is validated *into*.
2
+
3
+ Shared by the request path (:func:`json_body`, :func:`param`) and the response path (the decoders),
4
+ so the expensive step in pydantic v2 -- building the adapter -- happens once per type for the life
5
+ of the process rather than once per request.
6
+
7
+ It sits on its own, below both paths, because both need it and neither owns it: a request-body
8
+ module reaching into the response-decoding module for it would put a cycle in the import graph."""
9
+
10
+ from __future__ import annotations
11
+
12
+ from functools import reduce
13
+ from operator import or_
14
+ from threading import RLock
15
+ from types import UnionType
16
+ from typing import Any, Final, TypeVar, Union, get_args, get_origin
17
+
18
+ from pydantic import TypeAdapter
19
+ from typing_extensions import TypeForm, is_typeddict
20
+
21
+ T = TypeVar("T")
22
+
23
+ _CONTAINERS: Final = (list, dict)
24
+ """The container origins a spec can express -- an array and a map -- so the only two emitted."""
25
+
26
+ _ADAPTER_CACHE_MAX: Final = 512
27
+ _adapter_lock: Final = RLock()
28
+ _adapters: Final[dict[object, TypeAdapter[Any]]] = {}
29
+
30
+
31
+ def validation_target(type_: TypeForm[T]) -> TypeForm[T]:
32
+ """Drop the dict-shaped companions from ``type_``, leaving what a value is validated *into*.
33
+
34
+ An endpoint accepts ``Employee | EmployeeDict``, and that whole union is what it declares and
35
+ hands the factory -- but only ``Employee`` may reach :func:`adapter_for`. The adapter both
36
+ validates *and* dumps, and a dict matching the ``TypedDict`` arm is dumped verbatim: wire
37
+ aliases lost (``address_line_1`` for ``addressLine1``), defaulted fields absent, unknown keys
38
+ silently dropped. A companion is an *input* shape, never a wire shape, so the type describing
39
+ what a caller may pass and the type a value is validated into are two different things.
40
+
41
+ The strip reaches wherever a companion can be written -- through union members and into
42
+ container parameters -- so ``list[Employee | EmployeeDict]`` and
43
+ ``list[Employee] | list[EmployeeDict]`` both resolve to ``list[Employee]``. Generic over ``T``
44
+ so that ``adapter_for(validation_target(type_))`` keeps the adapter's type instead of erasing
45
+ it to ``Any`` at the one hop between two otherwise precise signatures.
46
+
47
+ A type form that is dict-shaped *throughout* has no target at all. Only a generator defect
48
+ emits one, and it type-checks, so it is named here rather than surfacing as ``reduce`` failing
49
+ on an empty sequence at the first request.
50
+
51
+ Args:
52
+ type_: The declared type expression, dict-shaped companions included.
53
+
54
+ Returns:
55
+ The same expression with every companion arm removed -- what a value validates into.
56
+
57
+ Raises:
58
+ TypeError: If ``type_`` is dict-shaped throughout, leaving no model arm to validate into."""
59
+ if _is_companion(type_):
60
+ raise TypeError(
61
+ f"{type_!r} names only dict-shaped companions, which are input shapes and never wire "
62
+ f"shapes -- a validation target needs the model arm each companion accompanies"
63
+ )
64
+
65
+ target: TypeForm[T] = _strip_companions(type_)
66
+ return target
67
+
68
+
69
+ def _is_companion(type_: TypeForm[Any]) -> bool:
70
+ """Whether ``type_`` is dict-shaped throughout, so no model arm survives the strip.
71
+
72
+ ``all`` for a union and ``any`` for a container, and the asymmetry is the rule itself: a union
73
+ keeps a target while *one* member has one, whereas a container whose parameter has none has none
74
+ either -- ``list[EmployeeDict]`` cannot be narrowed to a list of anything.
75
+
76
+ Args:
77
+ type_: The type expression to classify.
78
+
79
+ Returns:
80
+ ``True`` when nothing in ``type_`` would survive the companion strip."""
81
+ if is_typeddict(type_):
82
+ return True
83
+
84
+ origin = get_origin(type_)
85
+ if origin in (Union, UnionType):
86
+ return all(_is_companion(arg) for arg in get_args(type_))
87
+ if origin in _CONTAINERS:
88
+ return any(_is_companion(arg) for arg in get_args(type_))
89
+ return False
90
+
91
+
92
+ def _strip_companions(type_: TypeForm[Any]) -> TypeForm[Any]:
93
+ """``type_`` with every companion dropped, rebuilt only where something actually changed.
94
+
95
+ Two properties, each load-bearing:
96
+
97
+ - the origin checks, because ``get_args(list[str])`` is ``(str,)`` -- descending is only sound
98
+ for the origins whose parameters *are* element types, so everything else is returned
99
+ untouched. Notably ``Annotated``, which is how ``Person`` carries its
100
+ ``Field(discriminator=...)``: descending would silently untag a discriminated union.
101
+ ``_CONTAINERS`` is closed at ``list`` and ``dict`` rather than "every container Python has" --
102
+ a spec expresses arrays and maps, and descending a shape no generator emits would be
103
+ speculative surface. A ``tuple`` or ``set`` parameter passes through like any other origin;
104
+ - the nothing-changed short-circuits, which hand back the expression *as written* rather than a
105
+ rebuilt one, so a companion-free union such as ``EmailStr | str`` cannot lose a member to
106
+ ``Union``'s deduplication, and one declared type keeps one :func:`adapter_for` cache entry.
107
+
108
+ Rebuilt with ``|`` rather than ``Union[tuple(targets)]``: mypy types that subscript as a typing
109
+ special form rather than a ``TypeForm``, so it cannot be returned from this signature. The
110
+ annotated locals are what keep the imprecision of ``get_args`` here instead of at every caller.
111
+
112
+ Args:
113
+ type_: The type expression to strip.
114
+
115
+ Returns:
116
+ ``type_`` itself when no companion was found, otherwise the rebuilt expression."""
117
+ origin = get_origin(type_)
118
+ args = get_args(type_)
119
+
120
+ if origin in (Union, UnionType):
121
+ targets = [_strip_companions(arg) for arg in args if not _is_companion(arg)]
122
+ if len(targets) == len(args) and all(target is arg for target, arg in zip(targets, args, strict=True)):
123
+ return type_
124
+ rebuilt: TypeForm[Any] = reduce(or_, targets)
125
+ return rebuilt
126
+
127
+ if origin in _CONTAINERS:
128
+ stripped = [_strip_companions(arg) for arg in args]
129
+ if all(target is arg for target, arg in zip(stripped, args, strict=True)):
130
+ return type_
131
+ reparametrized: TypeForm[Any] = origin[tuple(stripped)]
132
+ return reparametrized
133
+
134
+ return type_
135
+
136
+
137
+ def adapter_for(type_: TypeForm[T]) -> TypeAdapter[T]:
138
+ """Return the process-wide cached ``TypeAdapter`` for ``type_``, building it once.
139
+
140
+ The cache must be module-level to work at all: a decoder is constructed per call, so
141
+ an instance-owned cache would be discarded with it and never see a second lookup.
142
+
143
+ Unhashable type forms bypass the cache. At ``_ADAPTER_CACHE_MAX`` entries it is cleared
144
+ wholesale -- predictable, and far beyond the number of distinct types one generated SDK
145
+ produces.
146
+
147
+ A hit is served without the lock, which is sound because a dict read is atomic under the GIL and
148
+ internally locked on free-threaded builds; a stale read racing the wholesale ``clear()`` still
149
+ returns a correct adapter for that type. The lock is there to make each *build* happen once --
150
+ the contract ``test_adapter_cache.py`` pins -- not to serialize reads, so the build stays inside
151
+ it and the re-check inside catches a type another thread built while this one waited.
152
+
153
+ Args:
154
+ type_: The validation target, already stripped of companions by :func:`validation_target`.
155
+
156
+ Returns:
157
+ The adapter for ``type_``, shared across the process unless ``type_`` is unhashable."""
158
+ try:
159
+ hash(type_)
160
+ except TypeError:
161
+ return TypeAdapter(type_)
162
+
163
+ cached = _adapters.get(type_)
164
+ if cached is not None:
165
+ return cached
166
+
167
+ with _adapter_lock:
168
+ cached = _adapters.get(type_)
169
+ if cached is not None:
170
+ return cached
171
+
172
+ if len(_adapters) >= _ADAPTER_CACHE_MAX:
173
+ _adapters.clear()
174
+
175
+ adapter: TypeAdapter[T] = TypeAdapter(type_)
176
+ _adapters[type_] = adapter
177
+ return adapter
@@ -0,0 +1,70 @@
1
+ """The authentication layer's curated surface: its models, its contract, its composites.
2
+
3
+ The route the runtime facade and ``raw_client`` take, so neither needs to know how this package is laid
4
+ out. What only ``schemes/`` needs -- ``ScopeT``, ``OAuth2Credentials``, ``CredentialsPlacement``,
5
+ ``OAuthParams`` and ``scope_params`` -- stays behind, reached at ``models`` directly.
6
+
7
+ ⚠️ **``schemes/`` is deliberately not re-exported here.** A scheme module reaches ``raw_client``, which
8
+ reaches this package -- so importing ``.schemes`` from this file would close that loop into an
9
+ ``ImportError`` on whichever of the two is imported first. The facade names ``.schemes`` itself."""
10
+
11
+ from .composition import (
12
+ AllSchemes,
13
+ AnySchemes,
14
+ AsyncAllSchemes,
15
+ AsyncAnySchemes,
16
+ invalidate,
17
+ is_configured,
18
+ no_auth,
19
+ resolve_auth,
20
+ )
21
+ from .models import (
22
+ AuthParams,
23
+ OAuthError,
24
+ OAuthProviderError,
25
+ OAuthToken,
26
+ OAuthTokenRefreshable,
27
+ client_secret_basic,
28
+ client_secret_post,
29
+ oauth_error_response,
30
+ )
31
+ from .protocols import (
32
+ AsyncAuthScheme,
33
+ AsyncRefreshableTokenSource,
34
+ AsyncTokenSource,
35
+ AuthScheme,
36
+ RefreshableTokenSource,
37
+ RevocableAuthScheme,
38
+ TokenSource,
39
+ )
40
+
41
+ __all__ = [
42
+ # What one scheme contributes to a request
43
+ "AuthParams",
44
+ # The contract
45
+ "AuthScheme",
46
+ "AsyncAuthScheme",
47
+ "RevocableAuthScheme",
48
+ # The absent credential, and composition
49
+ "no_auth",
50
+ "is_configured",
51
+ "resolve_auth",
52
+ "invalidate",
53
+ "AllSchemes",
54
+ "AnySchemes",
55
+ "AsyncAllSchemes",
56
+ "AsyncAnySchemes",
57
+ # Managed OAuth 2.0 -- the token source seam
58
+ "TokenSource",
59
+ "AsyncTokenSource",
60
+ "RefreshableTokenSource",
61
+ "AsyncRefreshableTokenSource",
62
+ # Managed OAuth 2.0 -- the provider's vocabulary
63
+ "OAuthToken",
64
+ "OAuthTokenRefreshable",
65
+ "OAuthProviderError",
66
+ "OAuthError",
67
+ "oauth_error_response",
68
+ "client_secret_basic",
69
+ "client_secret_post",
70
+ ]
@@ -0,0 +1,175 @@
1
+ """The absent credential, and the two ways an operation's ``security`` composes several schemes.
2
+
3
+ :data:`no_auth` is what an unconfigured credential becomes, and :func:`is_configured` tests identity
4
+ against that one instance -- never "applying it did not raise", so a *blank* credential is configured
5
+ and is sent. :class:`AllSchemes` and :class:`AnySchemes`, with their async twins, are OpenAPI's AND and
6
+ OR requirements. Neither withholds a request over a partial set: the SDK never refuses to send because
7
+ a credential is missing, the server decides.
8
+
9
+ All four composites carry ``invalidate``, which makes a composite itself revocable so nesting
10
+ propagates."""
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass
15
+ from typing import Final
16
+
17
+ from .models import AuthParams
18
+ from .protocols import AsyncAuthScheme, AuthScheme, RevocableAuthScheme
19
+
20
+
21
+ async def resolve_auth(scheme: AsyncAuthScheme) -> AuthParams:
22
+ """The async client's counterpart to a plain ``scheme.apply()``.
23
+
24
+ Narrowing is on :class:`AuthParams` rather than on awaitability, so the awaited branch keeps its
25
+ precise type instead of widening to ``Any``.
26
+
27
+ Args:
28
+ scheme: The scheme to apply, whether or not it needs I/O to produce its parameters.
29
+
30
+ Returns:
31
+ What the scheme contributes to this request."""
32
+ applied = scheme.apply()
33
+ return applied if isinstance(applied, AuthParams) else await applied
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class _NoAuth:
38
+ """The absent credential: a scheme that contributes nothing.
39
+
40
+ Private so that :func:`is_configured` can test identity against the one instance below; a second
41
+ instance would be indistinguishable from a configured scheme by that test."""
42
+
43
+ def apply(self) -> AuthParams:
44
+ return AuthParams()
45
+
46
+
47
+ no_auth: Final[AuthScheme] = _NoAuth()
48
+ """What the client writes when a credential is absent, and ``execute``'s default.
49
+
50
+ An endpoint whose operation declares no ``security`` omits ``auth=`` entirely and takes this. The SDK
51
+ never refuses to send a request because a credential is missing -- the server decides."""
52
+
53
+
54
+ def is_configured(scheme: AuthScheme | AsyncAuthScheme) -> bool:
55
+ """Whether a credential was supplied for ``scheme``.
56
+
57
+ Identity against the one null object, never "applying it did not raise". A *blank* credential is
58
+ configured and is sent: ``None`` is the only sentinel.
59
+
60
+ Args:
61
+ scheme: The scheme to test, a caller's own included.
62
+
63
+ Returns:
64
+ ``True`` unless this is the null object :data:`no_auth`."""
65
+ return scheme is not no_auth
66
+
67
+
68
+ def invalidate(scheme: AuthScheme | AsyncAuthScheme) -> None:
69
+ """Drop whatever ``scheme`` cached, if it caches anything at all.
70
+
71
+ A no-op for every static scheme, so on an API with no managed OAuth the 401 branch in ``execute``
72
+ costs one ``isinstance`` and nothing else.
73
+
74
+ Args:
75
+ scheme: The scheme a 401 was received against."""
76
+ if isinstance(scheme, RevocableAuthScheme):
77
+ scheme.invalidate()
78
+
79
+
80
+ class AllSchemes:
81
+ """AND-composition: every configured member applies.
82
+
83
+ An unconfigured member contributes nothing and drops out on its own; the request still goes out
84
+ with whatever *was* configured. This layer never withholds a request over a partial set."""
85
+
86
+ __slots__ = ("_schemes",)
87
+
88
+ def __init__(self, *schemes: AuthScheme) -> None:
89
+ self._schemes = schemes
90
+
91
+ def apply(self) -> AuthParams:
92
+ applied = [scheme.apply() for scheme in self._schemes]
93
+ return AuthParams(
94
+ headers=tuple(p for a in applied for p in a.headers),
95
+ query_params=tuple(p for a in applied for p in a.query_params),
96
+ cookies=tuple(p for a in applied for p in a.cookies),
97
+ )
98
+
99
+ def invalidate(self) -> None:
100
+ """Drop every member's cached credential, via the module-level :func:`invalidate`.
101
+
102
+ Having this makes a composite itself revocable, so nesting one inside another propagates."""
103
+ for scheme in self._schemes:
104
+ invalidate(scheme)
105
+
106
+
107
+ class AnySchemes:
108
+ """OR-composition: the first *configured* member applies.
109
+
110
+ Selection is on credential presence, never on "applying it did not fail" -- an unconfigured member
111
+ listed ahead of a configured one must not win and send the request unauthenticated. With nothing
112
+ configured the request goes out unauthenticated, as a single unconfigured scheme would."""
113
+
114
+ __slots__ = ("_schemes",)
115
+
116
+ def __init__(self, *schemes: AuthScheme) -> None:
117
+ self._schemes = schemes
118
+
119
+ def apply(self) -> AuthParams:
120
+ for scheme in self._schemes:
121
+ if is_configured(scheme):
122
+ return scheme.apply()
123
+ return AuthParams()
124
+
125
+ def invalidate(self) -> None:
126
+ """Drop every member's cached credential; see :meth:`AllSchemes.invalidate`.
127
+
128
+ Over-invalidates on purpose: which arm won the last :meth:`apply` is not tracked, and tracking
129
+ it would mean per-request state on an object every request shares."""
130
+ for scheme in self._schemes:
131
+ invalidate(scheme)
132
+
133
+
134
+ class AsyncAllSchemes:
135
+ """:class:`AllSchemes` for the asynchronous client: a member may await its credential."""
136
+
137
+ __slots__ = ("_schemes",)
138
+
139
+ def __init__(self, *schemes: AsyncAuthScheme) -> None:
140
+ self._schemes = schemes
141
+
142
+ async def apply(self) -> AuthParams:
143
+ applied = [await resolve_auth(scheme) for scheme in self._schemes]
144
+ return AuthParams(
145
+ headers=tuple(p for a in applied for p in a.headers),
146
+ query_params=tuple(p for a in applied for p in a.query_params),
147
+ cookies=tuple(p for a in applied for p in a.cookies),
148
+ )
149
+
150
+ def invalidate(self) -> None:
151
+ """See :meth:`AllSchemes.invalidate`. Not a coroutine: dropping a cache never awaits."""
152
+ for scheme in self._schemes:
153
+ invalidate(scheme)
154
+
155
+
156
+ class AsyncAnySchemes:
157
+ """:class:`AnySchemes` for the asynchronous client: the chosen member may await its credential.
158
+
159
+ Selection still happens without I/O, so only the one member that wins is ever resolved."""
160
+
161
+ __slots__ = ("_schemes",)
162
+
163
+ def __init__(self, *schemes: AsyncAuthScheme) -> None:
164
+ self._schemes = schemes
165
+
166
+ async def apply(self) -> AuthParams:
167
+ for scheme in self._schemes:
168
+ if is_configured(scheme):
169
+ return await resolve_auth(scheme)
170
+ return AuthParams()
171
+
172
+ def invalidate(self) -> None:
173
+ """See :meth:`AnySchemes.invalidate`, including why it over-invalidates."""
174
+ for scheme in self._schemes:
175
+ invalidate(scheme)
@@ -0,0 +1,217 @@
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 text_values
27
+ from ..decoding import ErrorMapper
28
+ from ..params import Param, param
29
+ from ..results import RawError
30
+ from ..transport import HttpResponse
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class AuthParams:
35
+ """What one scheme contributes to a request.
36
+
37
+ Held as :class:`Param` rather than plain strings, so a credential is validated, dumped and
38
+ encoded by the same code as an endpoint's own parameters."""
39
+
40
+ headers: tuple[Param[Any], ...] = ()
41
+ query_params: tuple[Param[Any], ...] = ()
42
+ cookies: tuple[Param[Any], ...] = ()
43
+
44
+ def request_headers(self) -> tuple[Param[Any], ...]:
45
+ """Every header this contributes, cookie pairs folded into one ``Cookie`` (RFC 6265).
46
+
47
+ Folding here rather than at each scheme is what lets two cookie-bearing schemes compose;
48
+ two ``Cookie`` parameters would see the second overwrite the first. This covers only what
49
+ *this request's* schemes contributed -- folding the jar across the API's, the endpoint's and
50
+ the caller's layers is ``_internal/headers.py``'s.
51
+
52
+ The jar is tested rather than the tuple, so cookies that all render to nothing contribute no
53
+ header at all instead of an empty one that would clobber a lower layer.
54
+
55
+ Returns:
56
+ The headers, plus one folded ``Cookie`` when any cookie was contributed."""
57
+ jar = "; ".join(f"{cookie.key}={value}" for cookie in self.cookies for value in text_values(cookie))
58
+ if not jar:
59
+ return self.headers
60
+
61
+ return *self.headers, param[str]("Cookie", jar)
62
+
63
+
64
+ ScopeT = TypeVar("ScopeT", bound=str, default=str)
65
+ """A grant's scope vocabulary, as its flow declares it.
66
+
67
+ Bound to ``str`` because RFC 6749 §3.3 puts scopes on the wire as space-delimited strings, and
68
+ defaulted to it so an unparametrized credential means what it always did: any scope the provider
69
+ accepts, which is also what a flow declaring no scopes emits."""
70
+
71
+
72
+ class OAuth2Credentials(BaseModel):
73
+ """The base every OAuth 2.0 grant's credentials share, carrying one thing: their repr.
74
+
75
+ Each grant's credentials are generic over :data:`ScopeT`, and pydantic names a parametrized model
76
+ after its parametrization -- so an unhelped repr prints the flow's whole scope vocabulary in place
77
+ of the class a caller wrote. This restores the latter.
78
+
79
+ ⚠️ **It declares no fields, and must not gain one.** Pydantic orders a base's fields ahead of a
80
+ subclass's, so a field here would move to the front of every credential's repr and ``model_dump``.
81
+ A grant's own fields stay in its own model, which is also where ``model_config``, its ``coerce``
82
+ and its validators stay."""
83
+
84
+ def __repr_name__(self) -> str:
85
+ """The class name without its parametrization.
86
+
87
+ Returns:
88
+ The unsubscripted class's name, or this class's own when it was never subscripted."""
89
+ origin = type(self).__pydantic_generic_metadata__["origin"]
90
+ return origin.__name__ if origin is not None else type(self).__name__
91
+
92
+
93
+ class OAuthToken(BaseModel):
94
+ """The token endpoint's success body (RFC 6749 §5.1), and what a token source returns.
95
+
96
+ ``expires_in`` is RECOMMENDED rather than required, so its absence means "no client-side
97
+ deadline". ``token_type`` is parsed because the RFC requires it and deliberately **not** used
98
+ when applying: echoing a wire keyword into ``Authorization`` would put unvalidated data there.
99
+
100
+ No ``refresh_token``: RFC 6749 §4.4.3 says the client-credentials grant SHOULD NOT be issued
101
+ one. A grant that may be returns :class:`OAuthTokenRefreshable` instead."""
102
+
103
+ model_config = ConfigDict(frozen=True)
104
+
105
+ access_token: str = Field(repr=False)
106
+ token_type: str
107
+ expires_in: int | None = None
108
+ scope: str | None = None
109
+
110
+
111
+ class OAuthTokenRefreshable(OAuthToken):
112
+ """The token endpoint's success body for a grant that may issue a refresh token.
113
+
114
+ RFC 6749 §4.1.4 and §4.3.3 both make ``refresh_token`` OPTIONAL. A subclass rather than an
115
+ optional field, so a source's declared return type says which tier it belongs to."""
116
+
117
+ refresh_token: str | None = Field(default=None, repr=False)
118
+
119
+
120
+ class OAuthProviderError(BaseModel):
121
+ """The token endpoint's error body (RFC 6749 §5.2).
122
+
123
+ Typed, unlike the rest of a failed token fetch, because §5.2 fixes these three members for every
124
+ provider."""
125
+
126
+ model_config = ConfigDict(frozen=True)
127
+
128
+ error: str
129
+ error_description: str | None = None
130
+ error_uri: str | None = None
131
+
132
+
133
+ OAuthError: TypeAlias = OAuthProviderError | RawError
134
+ """What a failed token fetch carries: the RFC 6749 §5.2 body, or the raw response without one."""
135
+
136
+
137
+ @dataclass(frozen=True, slots=True)
138
+ class OAuthErrorResponse:
139
+ """Error mapper for the token endpoint.
140
+
141
+ The only mapper here that selects a *schema*, and it can: RFC 6749 §5.2 fixes one error body for
142
+ every provider. A response that does not conform becomes a :class:`RawError` rather than raising
143
+ out of the mapper, because a failed fetch must surface the provider's answer, not a parse error."""
144
+
145
+ def map(self, response: HttpResponse) -> OAuthError:
146
+ try:
147
+ return OAuthProviderError.model_validate(response.json())
148
+ except ValueError:
149
+ return RawError(response)
150
+
151
+
152
+ oauth_error_response: Final[ErrorMapper[OAuthError]] = OAuthErrorResponse()
153
+
154
+
155
+ OAuthParams: TypeAlias = tuple[Param[Any], ...]
156
+ """One destination's worth of parameters: half a token request, or an authorization URL's query."""
157
+
158
+ CredentialsPlacement: TypeAlias = Callable[[str, str | None], tuple[OAuthParams, OAuthParams]]
159
+ """Where a client's own credentials travel on a token request: its headers, and its form fields.
160
+
161
+ Named after the specification extension it is read from, ``x-oauth2-credentials-placement``.
162
+ Grant-agnostic: RFC 6749 §2.3.1 authenticates the *client*, which every grant does identically."""
163
+
164
+
165
+ def scope_params(scopes: Sequence[str] | None) -> OAuthParams:
166
+ """The ``scope`` field, or nothing at all when no scopes were asked for.
167
+
168
+ RFC 6749 §3.3's space delimiter, written once, so a caller passes a list and never learns it.
169
+ Takes a ``Sequence`` so a credential's narrowed ``list[ScopeT]`` reaches it without a copy.
170
+
171
+ Args:
172
+ scopes: The scopes to request; absent or empty asks for the client's defaults.
173
+
174
+ Returns:
175
+ One ``scope`` parameter, or an empty tuple."""
176
+ if not scopes:
177
+ return ()
178
+ return (param[str]("scope", " ".join(scopes)),)
179
+
180
+
181
+ def client_secret_basic(client_id: str, client_secret: str | None) -> tuple[OAuthParams, OAuthParams]:
182
+ """RFC 6749 §2.3.1: the client authenticates with HTTP Basic on the token request.
183
+
184
+ Both halves are percent-encoded before base64, as §2.3.1 requires. A client with **no secret**
185
+ identifies itself with a ``client_id`` form field instead, which is §3.2.1's requirement exactly
186
+ then rather than a degraded fallback.
187
+
188
+ Args:
189
+ client_id: The client's identifier, as issued by the provider.
190
+ client_secret: Its secret, or ``None`` for a public client.
191
+
192
+ Returns:
193
+ The headers and the form fields the client's credentials contribute, in that order."""
194
+ if client_secret is None:
195
+ return (), (param[str]("client_id", client_id),)
196
+ raw = f"{quote(client_id, safe='')}:{quote(client_secret, safe='')}"
197
+ return (param[str]("Authorization", f"Basic {b64encode(raw.encode()).decode()}"),), ()
198
+
199
+
200
+ def client_secret_post(client_id: str, client_secret: str | None) -> tuple[OAuthParams, OAuthParams]:
201
+ """RFC 6749 §2.3.1's alternative: the credentials travel in the form body instead.
202
+
203
+ The empty header tuple is the invariant, not an oversight -- carrying the pair in both places
204
+ would authenticate the client twice.
205
+
206
+ Args:
207
+ client_id: The client's identifier, as issued by the provider.
208
+ client_secret: Its secret, omitted entirely when ``None`` (§3.2.1).
209
+
210
+ Returns:
211
+ An empty header tuple, and the form fields the client's credentials contribute."""
212
+ if client_secret is None:
213
+ return (), (param[str]("client_id", client_id),)
214
+ return (), (
215
+ param[str]("client_id", client_id),
216
+ param[str]("client_secret", client_secret),
217
+ )