okta-client-python 0.1.0__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 (56) hide show
  1. okta_client/__init__.py +20 -0
  2. okta_client/authfoundation/__init__.py +197 -0
  3. okta_client/authfoundation/authentication.py +247 -0
  4. okta_client/authfoundation/coalesced_result.py +113 -0
  5. okta_client/authfoundation/codable.py +72 -0
  6. okta_client/authfoundation/expires.py +49 -0
  7. okta_client/authfoundation/key_provider.py +130 -0
  8. okta_client/authfoundation/networking/__init__.py +56 -0
  9. okta_client/authfoundation/networking/body.py +46 -0
  10. okta_client/authfoundation/networking/client.py +200 -0
  11. okta_client/authfoundation/networking/types.py +293 -0
  12. okta_client/authfoundation/oauth2/__init__.py +104 -0
  13. okta_client/authfoundation/oauth2/claims.py +44 -0
  14. okta_client/authfoundation/oauth2/client.py +402 -0
  15. okta_client/authfoundation/oauth2/client_authorization.py +172 -0
  16. okta_client/authfoundation/oauth2/config.py +298 -0
  17. okta_client/authfoundation/oauth2/errors.py +32 -0
  18. okta_client/authfoundation/oauth2/jwt_bearer_claims.py +59 -0
  19. okta_client/authfoundation/oauth2/jwt_bearer_utils.py +30 -0
  20. okta_client/authfoundation/oauth2/jwt_context.py +52 -0
  21. okta_client/authfoundation/oauth2/jwt_token.py +214 -0
  22. okta_client/authfoundation/oauth2/models.py +198 -0
  23. okta_client/authfoundation/oauth2/parameters.py +36 -0
  24. okta_client/authfoundation/oauth2/refresh_token.py +165 -0
  25. okta_client/authfoundation/oauth2/request_protocols.py +174 -0
  26. okta_client/authfoundation/oauth2/requests/__init__.py +37 -0
  27. okta_client/authfoundation/oauth2/requests/introspect.py +50 -0
  28. okta_client/authfoundation/oauth2/requests/jwks.py +44 -0
  29. okta_client/authfoundation/oauth2/requests/oauth_authorization_server.py +44 -0
  30. okta_client/authfoundation/oauth2/requests/openid_configuration.py +47 -0
  31. okta_client/authfoundation/oauth2/requests/revoke.py +54 -0
  32. okta_client/authfoundation/oauth2/requests/user_info.py +37 -0
  33. okta_client/authfoundation/oauth2/utils.py +25 -0
  34. okta_client/authfoundation/oauth2/validation_protocols.py +33 -0
  35. okta_client/authfoundation/oauth2/validator_registry.py +64 -0
  36. okta_client/authfoundation/oauth2/validators/token_hash.py +37 -0
  37. okta_client/authfoundation/oauth2/validators/token_validator.py +26 -0
  38. okta_client/authfoundation/time_coordinator.py +57 -0
  39. okta_client/authfoundation/token.py +201 -0
  40. okta_client/authfoundation/user_agent.py +80 -0
  41. okta_client/authfoundation/utils.py +63 -0
  42. okta_client/browser_signin/__init__.py +11 -0
  43. okta_client/directauth/__init__.py +11 -0
  44. okta_client/oauth2auth/__init__.py +63 -0
  45. okta_client/oauth2auth/authorization_code.py +594 -0
  46. okta_client/oauth2auth/cross_app.py +626 -0
  47. okta_client/oauth2auth/jwt_bearer.py +182 -0
  48. okta_client/oauth2auth/resource_owner.py +159 -0
  49. okta_client/oauth2auth/token_exchange.py +380 -0
  50. okta_client/oauth2auth/utils.py +87 -0
  51. okta_client/py.typed +0 -0
  52. okta_client_python-0.1.0.dist-info/METADATA +936 -0
  53. okta_client_python-0.1.0.dist-info/RECORD +56 -0
  54. okta_client_python-0.1.0.dist-info/WHEEL +5 -0
  55. okta_client_python-0.1.0.dist-info/licenses/LICENSE +171 -0
  56. okta_client_python-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,20 @@
1
+ # The Okta software accompanied by this notice is provided pursuant to the following terms:
2
+ # Copyright © 2026-Present, Okta, Inc.
3
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
4
+ # License.
5
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
6
+ # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
7
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8
+ # See the License for the specific language governing permissions and limitations under the License.
9
+ # coding: utf-8
10
+
11
+ """Okta Client SDK (Python)."""
12
+
13
+ from importlib.metadata import PackageNotFoundError, version
14
+
15
+ try:
16
+ __version__: str = version("okta-client-python")
17
+ except PackageNotFoundError: # pragma: no cover - not installed
18
+ __version__ = "0.0.0-dev"
19
+
20
+ __all__ = ["__version__", "authfoundation", "browser_signin", "directauth", "oauth2auth"]
@@ -0,0 +1,197 @@
1
+ # The Okta software accompanied by this notice is provided pursuant to the following terms:
2
+ # Copyright © 2026-Present, Okta, Inc.
3
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
4
+ # License.
5
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
6
+ # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
7
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8
+ # See the License for the specific language governing permissions and limitations under the License.
9
+ # coding: utf-8
10
+
11
+ """Core networking, OAuth2 primitives, token models, and storage interfaces."""
12
+
13
+ from .authentication import (
14
+ AuthenticationContext,
15
+ AuthenticationFlow,
16
+ AuthenticationListener,
17
+ AuthenticationState,
18
+ BaseAuthenticationFlow,
19
+ PKCEData,
20
+ StandardAuthenticationContext,
21
+ generate_pkce,
22
+ )
23
+ from .coalesced_result import CoalescedResult
24
+ from .codable import Codable
25
+ from .expires import Expires
26
+ from .key_provider import (
27
+ DefaultKeyProvider,
28
+ KeyProvider,
29
+ LocalKeyProvider,
30
+ get_key_provider,
31
+ set_key_provider,
32
+ )
33
+ from .networking import (
34
+ APIAuthorization,
35
+ APIClient,
36
+ APIClientConfiguration,
37
+ APIClientListener,
38
+ APIContentType,
39
+ APIParsingContext,
40
+ APIRateLimit,
41
+ APIRequest,
42
+ APIRequestBody,
43
+ APIRequestBodyMixin,
44
+ APIRequestMethod,
45
+ APIResponse,
46
+ APIRetry,
47
+ BaseAPIRequest,
48
+ DefaultNetworkInterface,
49
+ HTTPRequest,
50
+ ListenerCollection,
51
+ NetworkInterface,
52
+ RawResponse,
53
+ RequestValue,
54
+ RequestValueConvertible,
55
+ )
56
+ from .oauth2 import (
57
+ JWK,
58
+ JWKS,
59
+ JWT,
60
+ ClientAssertionAuthorization,
61
+ ClientAuthorization,
62
+ ClientIdAuthorization,
63
+ ClientSecretAuthorization,
64
+ ConfigurationError,
65
+ ConfigurationFileNotFoundError,
66
+ ConfigurationParseError,
67
+ DefaultTokenHashValidator,
68
+ DefaultTokenValidator,
69
+ HasClaims,
70
+ IdTokenClaim,
71
+ IDTokenValidatorContext,
72
+ InvalidConfigurationError,
73
+ JWTBearerClaims,
74
+ JWTType,
75
+ JWTUsageContext,
76
+ JWTValidationContext,
77
+ OAuth2APIRequest,
78
+ OAuth2APIRequestCategory,
79
+ OAuth2ClientConfiguration,
80
+ OAuth2Error,
81
+ OAuth2TokenRequest,
82
+ OAuth2TokenRequestDefaults,
83
+ OpenIdConfiguration,
84
+ ProvidesOAuth2Parameters,
85
+ TokenHashValidator,
86
+ TokenInfo,
87
+ TokenValidator,
88
+ UserInfo,
89
+ get_access_token_validator,
90
+ get_device_secret_validator,
91
+ get_token_validator,
92
+ set_access_token_validator,
93
+ set_device_secret_validator,
94
+ set_token_validator,
95
+ )
96
+ from .oauth2.client import OAuth2Client, OAuth2ClientListener
97
+ from .oauth2.refresh_token import RefreshTokenFlow, RefreshTokenRequest
98
+ from .time_coordinator import (
99
+ DefaultTimeCoordinator,
100
+ TimeCoordinator,
101
+ get_time_coordinator,
102
+ set_time_coordinator,
103
+ )
104
+ from .token import (
105
+ GrantedTokenType,
106
+ Token,
107
+ TokenContext,
108
+ )
109
+
110
+ __all__ = [
111
+ "JWK",
112
+ "JWKS",
113
+ "JWT",
114
+ "APIAuthorization",
115
+ "APIClient",
116
+ "APIClientConfiguration",
117
+ "APIClientListener",
118
+ "APIContentType",
119
+ "APIParsingContext",
120
+ "APIRateLimit",
121
+ "APIRequest",
122
+ "APIRequestBody",
123
+ "APIRequestBodyMixin",
124
+ "APIRequestMethod",
125
+ "APIResponse",
126
+ "APIRetry",
127
+ "AuthenticationContext",
128
+ "AuthenticationFlow",
129
+ "AuthenticationListener",
130
+ "AuthenticationState",
131
+ "BaseAPIRequest",
132
+ "BaseAuthenticationFlow",
133
+ "ClientAssertionAuthorization",
134
+ "ClientAuthorization",
135
+ "ClientIdAuthorization",
136
+ "ClientSecretAuthorization",
137
+ "CoalescedResult",
138
+ "Codable",
139
+ "ConfigurationError",
140
+ "ConfigurationFileNotFoundError",
141
+ "ConfigurationParseError",
142
+ "DefaultKeyProvider",
143
+ "DefaultNetworkInterface",
144
+ "DefaultTimeCoordinator",
145
+ "DefaultTokenHashValidator",
146
+ "DefaultTokenValidator",
147
+ "Expires",
148
+ "GrantedTokenType",
149
+ "HTTPRequest",
150
+ "HasClaims",
151
+ "IDTokenValidatorContext",
152
+ "IdTokenClaim",
153
+ "InvalidConfigurationError",
154
+ "JWTBearerClaims",
155
+ "JWTType",
156
+ "JWTUsageContext",
157
+ "JWTValidationContext",
158
+ "KeyProvider",
159
+ "ListenerCollection",
160
+ "LocalKeyProvider",
161
+ "NetworkInterface",
162
+ "OAuth2APIRequest",
163
+ "OAuth2APIRequestCategory",
164
+ "OAuth2Client",
165
+ "OAuth2ClientConfiguration",
166
+ "OAuth2ClientListener",
167
+ "OAuth2Error",
168
+ "OAuth2TokenRequest",
169
+ "OAuth2TokenRequestDefaults",
170
+ "OpenIdConfiguration",
171
+ "PKCEData",
172
+ "ProvidesOAuth2Parameters",
173
+ "RawResponse",
174
+ "RefreshTokenFlow",
175
+ "RefreshTokenRequest",
176
+ "RequestValue",
177
+ "RequestValueConvertible",
178
+ "StandardAuthenticationContext",
179
+ "TimeCoordinator",
180
+ "Token",
181
+ "TokenContext",
182
+ "TokenHashValidator",
183
+ "TokenInfo",
184
+ "TokenValidator",
185
+ "UserInfo",
186
+ "generate_pkce",
187
+ "get_access_token_validator",
188
+ "get_device_secret_validator",
189
+ "get_key_provider",
190
+ "get_time_coordinator",
191
+ "get_token_validator",
192
+ "set_access_token_validator",
193
+ "set_device_secret_validator",
194
+ "set_key_provider",
195
+ "set_time_coordinator",
196
+ "set_token_validator",
197
+ ]
@@ -0,0 +1,247 @@
1
+ # The Okta software accompanied by this notice is provided pursuant to the following terms:
2
+ # Copyright © 2026-Present, Okta, Inc.
3
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
4
+ # License.
5
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
6
+ # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
7
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8
+ # See the License for the specific language governing permissions and limitations under the License.
9
+ # coding: utf-8
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import hashlib
15
+ import os
16
+ from collections.abc import Mapping
17
+ from dataclasses import dataclass
18
+ from enum import Enum
19
+ from typing import Any, Generic, Protocol, TypeVar, cast, runtime_checkable
20
+
21
+ from .networking import ListenerCollection, RequestValue
22
+ from .oauth2.parameters import OAuth2APIRequestCategory
23
+ from .utils import base64url_encode
24
+
25
+ ContextT = TypeVar("ContextT", bound="AuthenticationContext")
26
+
27
+
28
+ class AuthenticationState(str, Enum):
29
+ """Lifecycle states for authentication flows."""
30
+ IDLE = "idle"
31
+ AUTHENTICATING = "authenticating"
32
+ COMPLETED = "completed"
33
+ FAILED = "failed"
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class PKCEData:
38
+ """PKCE values used by authorization code flows."""
39
+ code_verifier: str
40
+ code_challenge: str
41
+ code_challenge_method: str = "S256"
42
+
43
+
44
+ def generate_pkce() -> PKCEData:
45
+ """Generate a PKCE verifier/challenge pair using S256.
46
+
47
+ - ``code_verifier``: 32 random bytes, base64url-encoded (no padding).
48
+ - ``code_challenge``: SHA-256 hash of the verifier, base64url-encoded (no padding).
49
+ """
50
+ code_verifier = base64url_encode(os.urandom(32))
51
+ code_challenge = base64url_encode(hashlib.sha256(code_verifier.encode("ascii")).digest())
52
+ return PKCEData(
53
+ code_verifier=code_verifier,
54
+ code_challenge=code_challenge,
55
+ code_challenge_method="S256",
56
+ )
57
+
58
+
59
+ @runtime_checkable
60
+ class AuthenticationContext(Protocol):
61
+ """Protocol for per-session authentication context."""
62
+ @property
63
+ def acr_values(self) -> list[str] | None:
64
+ """ACR values requested for the session."""
65
+ ...
66
+
67
+ @property
68
+ def persist_values(self) -> Mapping[str, str] | None:
69
+ """Values to persist into token context, if any."""
70
+ ...
71
+
72
+ @property
73
+ def additional_parameters(self) -> Mapping[str, RequestValue] | None:
74
+ """Additional parameters for the session."""
75
+ ...
76
+
77
+ def parameters(self, category: OAuth2APIRequestCategory) -> Mapping[str, RequestValue] | None:
78
+ """Return OAuth2 parameters to include for the given request category."""
79
+ ...
80
+
81
+
82
+ @runtime_checkable
83
+ class AuthenticationListener(Protocol):
84
+ """Listener for authentication flow lifecycle events."""
85
+ def authentication_started(self, flow: AuthenticationFlow[Any]) -> None:
86
+ """Called when a flow starts authenticating."""
87
+ ...
88
+
89
+ def authentication_updated(self, flow: AuthenticationFlow[Any], context: AuthenticationContext) -> None:
90
+ """Called when a flow updates its context."""
91
+ ...
92
+
93
+ def authentication_completed(self, flow: AuthenticationFlow[Any], result: Any) -> None:
94
+ """Called when a flow completes successfully."""
95
+ ...
96
+
97
+ def authentication_failed(self, flow: AuthenticationFlow[Any], error: Exception) -> None:
98
+ """Called when a flow fails with an error."""
99
+ ...
100
+
101
+
102
+ @dataclass(frozen=True)
103
+ class StandardAuthenticationContext(AuthenticationContext):
104
+ """Default authentication context for OAuth2 flows."""
105
+
106
+ _acr_values: list[str] | None = None
107
+ _persist_values: Mapping[str, str] | None = None
108
+ _additional_parameters: Mapping[str, RequestValue] | None = None
109
+
110
+ @property
111
+ def acr_values(self) -> list[str] | None:
112
+ """ACR values requested for the session."""
113
+ return self._acr_values
114
+
115
+ @property
116
+ def persist_values(self) -> Mapping[str, str] | None:
117
+ """Values to persist into token context, if any."""
118
+ return self._persist_values
119
+
120
+ @property
121
+ def additional_parameters(self) -> Mapping[str, RequestValue] | None:
122
+ """Additional parameters for the session."""
123
+ return self._additional_parameters
124
+
125
+ def parameters(self, category: OAuth2APIRequestCategory) -> Mapping[str, RequestValue] | None:
126
+ """Return OAuth2 parameters contributed by this context."""
127
+ result: dict[str, RequestValue] = dict(self._additional_parameters or {})
128
+ if category == OAuth2APIRequestCategory.AUTHORIZATION and self._acr_values:
129
+ result["acr_values"] = " ".join(self._acr_values)
130
+ return result or None
131
+
132
+
133
+ @runtime_checkable
134
+ class AuthenticationFlow(Protocol, Generic[ContextT]):
135
+ """Protocol for authentication flows with start/resume/reset semantics."""
136
+ @property
137
+ def context(self) -> ContextT | None:
138
+ """Current session context, if any."""
139
+ ...
140
+
141
+ @property
142
+ def state(self) -> AuthenticationState:
143
+ """Current flow state."""
144
+ ...
145
+
146
+ @property
147
+ def is_authenticating(self) -> bool:
148
+ """Return True when authentication is in progress."""
149
+ ...
150
+
151
+ @property
152
+ def additional_parameters(self) -> Mapping[str, RequestValue] | None:
153
+ """Flow-level parameters applied to all requests."""
154
+ ...
155
+
156
+ @property
157
+ def listeners(self) -> ListenerCollection[AuthenticationListener]:
158
+ """Listener collection for flow events."""
159
+ ...
160
+
161
+ def reset(self) -> None:
162
+ """Reset flow state and context."""
163
+ ...
164
+
165
+ async def start(self, *args: Any, context: ContextT | None = None, **kwargs: Any) -> Any:
166
+ """Start authentication with optional context."""
167
+ ...
168
+
169
+ async def resume(self, *args: Any, context: ContextT, **kwargs: Any) -> Any:
170
+ """Resume authentication with additional context."""
171
+ ...
172
+
173
+
174
+ class BaseAuthenticationFlow(Generic[ContextT]):
175
+ """Base class implementing common flow state and listener behavior."""
176
+ def __init__(self, additional_parameters: Mapping[str, RequestValue] | None = None) -> None:
177
+ """Initialize the flow with optional additional parameters."""
178
+ self._context: ContextT | None = None
179
+ self._state = AuthenticationState.IDLE
180
+ self._additional_parameters = additional_parameters
181
+ self._lock = asyncio.Lock()
182
+ self._listeners: ListenerCollection[AuthenticationListener] = ListenerCollection()
183
+
184
+ @property
185
+ def context(self) -> ContextT | None:
186
+ """Return the current context."""
187
+ return self._context
188
+
189
+ @property
190
+ def state(self) -> AuthenticationState:
191
+ """Return the current flow state."""
192
+ return self._state
193
+
194
+ @property
195
+ def is_authenticating(self) -> bool:
196
+ """Return True when the flow is authenticating."""
197
+ return self._state == AuthenticationState.AUTHENTICATING
198
+
199
+ @property
200
+ def additional_parameters(self) -> Mapping[str, RequestValue] | None:
201
+ """Return flow-level additional parameters."""
202
+ return self._additional_parameters
203
+
204
+ @property
205
+ def listeners(self) -> ListenerCollection[AuthenticationListener]:
206
+ """Return the listener collection."""
207
+ return self._listeners
208
+
209
+ def reset(self) -> None:
210
+ """Reset state and clear the current context."""
211
+ self._context = None
212
+ self._state = AuthenticationState.IDLE
213
+
214
+ async def _begin(self, context: ContextT | None) -> None:
215
+ """Begin a new authentication session and notify listeners."""
216
+ async with self._lock:
217
+ if self._state == AuthenticationState.AUTHENTICATING:
218
+ raise RuntimeError("Authentication already in progress")
219
+ self._context = context
220
+ self._state = AuthenticationState.AUTHENTICATING
221
+ flow = cast(AuthenticationFlow[Any], self)
222
+ for listener in self._listeners:
223
+ listener.authentication_started(flow)
224
+ if context is not None:
225
+ for listener in self._listeners:
226
+ listener.authentication_updated(flow, context)
227
+
228
+ def _update_context(self, context: ContextT) -> None:
229
+ """Update the current context and notify listeners."""
230
+ self._context = context
231
+ flow = cast(AuthenticationFlow[Any], self)
232
+ for listener in self._listeners:
233
+ listener.authentication_updated(flow, context)
234
+
235
+ def _complete(self, result: Any) -> None:
236
+ """Mark flow as completed and notify listeners."""
237
+ self._state = AuthenticationState.COMPLETED
238
+ flow = cast(AuthenticationFlow[Any], self)
239
+ for listener in self._listeners:
240
+ listener.authentication_completed(flow, result)
241
+
242
+ def _fail(self, error: Exception) -> None:
243
+ """Mark flow as failed and notify listeners."""
244
+ self._state = AuthenticationState.FAILED
245
+ flow = cast(AuthenticationFlow[Any], self)
246
+ for listener in self._listeners:
247
+ listener.authentication_failed(flow, error)
@@ -0,0 +1,113 @@
1
+ # The Okta software accompanied by this notice is provided pursuant to the following terms:
2
+ # Copyright © 2026-Present, Okta, Inc.
3
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
4
+ # License.
5
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
6
+ # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
7
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8
+ # See the License for the specific language governing permissions and limitations under the License.
9
+ # coding: utf-8
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import time
15
+ from collections.abc import Awaitable, Callable
16
+ from typing import Generic, TypeVar
17
+
18
+ T = TypeVar("T")
19
+
20
+
21
+ class CoalescedResult(Generic[T]):
22
+ """Coalesce concurrent async operations so only one in-flight task runs.
23
+
24
+ - Caches the last successful result.
25
+ - Errors are propagated to all awaiting callers and are not cached.
26
+ """
27
+
28
+ def __init__(self, *, ttl: float | None = None, time_provider: Callable[[], float] | None = None) -> None:
29
+ self._lock = asyncio.Lock()
30
+ self._active = False
31
+ self._value: T | None = None
32
+ self._fetched_at: float | None = None
33
+ self._ttl = ttl
34
+ self._time_provider = time_provider or time.time
35
+ self._waiters: list[asyncio.Future[T]] = []
36
+
37
+ @property
38
+ def is_active(self) -> bool:
39
+ return self._active
40
+
41
+ @property
42
+ def value(self) -> T | None:
43
+ return self._value
44
+
45
+ async def perform(
46
+ self,
47
+ operation: Callable[[], Awaitable[T]],
48
+ *,
49
+ reset: bool = False,
50
+ ) -> T:
51
+ """Perform the operation or await the in-flight result.
52
+
53
+ Args:
54
+ operation: Async callable that produces the result.
55
+ reset: If True, clears the cached value before proceeding.
56
+ """
57
+ future: asyncio.Future[T] | None = None
58
+ async with self._lock:
59
+ if reset:
60
+ self._value = None
61
+ self._fetched_at = None
62
+ if self._active:
63
+ loop = asyncio.get_running_loop()
64
+ future = loop.create_future()
65
+ self._waiters.append(future)
66
+ else:
67
+ if self._is_cache_valid():
68
+ return self._value # type: ignore[return-value]
69
+ self._active = True
70
+
71
+ if future is not None:
72
+ return await future
73
+
74
+ try:
75
+ result = await operation()
76
+ except Exception as exc:
77
+ await self._finish(error=exc)
78
+ raise
79
+ else:
80
+ await self._finish(result=result)
81
+ return result
82
+
83
+ async def _finish(self, result: T | None = None, error: Exception | None = None) -> None:
84
+ async with self._lock:
85
+ if error is None:
86
+ if self._ttl is not None and self._ttl <= 0:
87
+ self._value = None
88
+ self._fetched_at = None
89
+ else:
90
+ self._value = result
91
+ self._fetched_at = self._time_provider()
92
+ self._active = False
93
+ waiters = self._waiters
94
+ self._waiters = []
95
+
96
+ for waiter in waiters:
97
+ if waiter.done():
98
+ continue
99
+ if error is None:
100
+ waiter.set_result(result) # type: ignore[arg-type]
101
+ else:
102
+ waiter.set_exception(error)
103
+
104
+ def _is_cache_valid(self) -> bool:
105
+ if self._value is None:
106
+ return False
107
+ if self._ttl is None:
108
+ return True
109
+ if self._ttl <= 0:
110
+ return False
111
+ if self._fetched_at is None:
112
+ return False
113
+ return (self._time_provider() - self._fetched_at) < self._ttl
@@ -0,0 +1,72 @@
1
+ # The Okta software accompanied by this notice is provided pursuant to the following terms:
2
+ # Copyright © 2026-Present, Okta, Inc.
3
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
4
+ # License.
5
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
6
+ # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
7
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8
+ # See the License for the specific language governing permissions and limitations under the License.
9
+ # coding: utf-8
10
+
11
+ """Serialization mixin for frozen dataclasses.
12
+
13
+ Provides a lightweight ``Codable`` protocol (inspired by Swift's ``Codable``)
14
+ that any :func:`~dataclasses.dataclass` can adopt to gain ``to_dict()`` /
15
+ ``from_dict()`` round-trip serialization without third-party dependencies.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import dataclasses
21
+ from collections.abc import Mapping
22
+ from enum import Enum
23
+ from typing import Any
24
+
25
+
26
+ class Codable:
27
+ """Mixin for frozen dataclasses that need dictionary (de)serialization.
28
+
29
+ Provides :meth:`to_dict` for serialization using :func:`dataclasses.asdict`
30
+ (with automatic :class:`~enum.Enum` → value conversion) and a
31
+ :meth:`from_dict` classmethod that subclasses override to reconstruct
32
+ nested types.
33
+
34
+ Usage::
35
+
36
+ @dataclass(frozen=True)
37
+ class MyModel(Codable):
38
+ name: str
39
+ kind: SomeEnum
40
+
41
+ @classmethod
42
+ def from_dict(cls, data: Mapping[str, Any]) -> "MyModel":
43
+ return cls(
44
+ name=data["name"],
45
+ kind=SomeEnum(data["kind"]),
46
+ )
47
+ """
48
+
49
+ def to_dict(self) -> dict[str, Any]:
50
+ """Serialize to a plain dict suitable for JSON encoding.
51
+
52
+ Nested dataclasses are recursively converted to dicts.
53
+ :class:`~enum.Enum` members are reduced to their ``.value``.
54
+ """
55
+ return dataclasses.asdict(self, dict_factory=_enum_aware_dict_factory)
56
+
57
+ @classmethod
58
+ def from_dict(cls, data: Mapping[str, Any]) -> Codable:
59
+ """Reconstruct an instance from a plain dict.
60
+
61
+ Subclasses **must** override this to handle nested dataclass and enum
62
+ fields that require type-aware reconstruction.
63
+ """
64
+ raise NotImplementedError(f"{cls.__name__} must implement from_dict()")
65
+
66
+
67
+ def _enum_aware_dict_factory(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
68
+ """Dict factory for :func:`dataclasses.asdict` that converts enum values."""
69
+ return {
70
+ key: value.value if isinstance(value, Enum) else value
71
+ for key, value in pairs
72
+ }
@@ -0,0 +1,49 @@
1
+ # The Okta software accompanied by this notice is provided pursuant to the following terms:
2
+ # Copyright © 2026-Present, Okta, Inc.
3
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
4
+ # License.
5
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
6
+ # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
7
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8
+ # See the License for the specific language governing permissions and limitations under the License.
9
+ # coding: utf-8
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Protocol, runtime_checkable
14
+
15
+ from .time_coordinator import get_time_coordinator
16
+
17
+
18
+ @runtime_checkable
19
+ class Expires(Protocol):
20
+ """Protocol for objects that can expire based on issued time."""
21
+ @property
22
+ def expires_in(self) -> float:
23
+ """Return the lifetime in seconds."""
24
+ ...
25
+
26
+ @property
27
+ def issued_at(self) -> float | None:
28
+ """Return the issue time in seconds since epoch, if known."""
29
+ ...
30
+
31
+ @property
32
+ def expires_at(self) -> float | None:
33
+ """Return the calculated expiration time, if possible."""
34
+ if self.issued_at is None:
35
+ return None
36
+ return float(self.issued_at) + float(self.expires_in)
37
+
38
+ @property
39
+ def is_expired(self) -> bool:
40
+ """Return True when the object is expired."""
41
+ expires_at = self.expires_at
42
+ if expires_at is None:
43
+ return False
44
+ return get_time_coordinator().now() >= expires_at
45
+
46
+ @property
47
+ def is_valid(self) -> bool:
48
+ """Return True when the object is not expired."""
49
+ return not self.is_expired