-
Notifications
You must be signed in to change notification settings - Fork 5
feat(platform): support external token providers and simplify caching #513
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
c2f0b1b
e9cf3aa
f74f48d
7e3fac5
f2296b9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,89 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| """Authenticated API wrapper and configuration. | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| This module defines the thin API subclass and configuration that lift | ||||||||||||||||||||||||||||||||||||||||||||||
| ``token_provider`` to a first-class attribute. Kept separate from ``_client`` | ||||||||||||||||||||||||||||||||||||||||||||||
| so that resource modules can import these types directly without circular | ||||||||||||||||||||||||||||||||||||||||||||||
| dependencies. | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| from collections.abc import Callable | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| from aignx.codegen.api.public_api import PublicApi | ||||||||||||||||||||||||||||||||||||||||||||||
| from aignx.codegen.api_client import ApiClient | ||||||||||||||||||||||||||||||||||||||||||||||
| from aignx.codegen.configuration import AuthSettings, Configuration | ||||||||||||||||||||||||||||||||||||||||||||||
| from loguru import logger | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| class _OAuth2TokenProviderConfiguration(Configuration): | ||||||||||||||||||||||||||||||||||||||||||||||
| """Overwrites the original Configuration to call a function to obtain a bearer token. | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| The base class does not support callbacks. This is necessary for integrations where | ||||||||||||||||||||||||||||||||||||||||||||||
| access tokens may expire or need to be refreshed or rotated automatically. | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def __init__( | ||||||||||||||||||||||||||||||||||||||||||||||
| self, host: str, ssl_ca_cert: str | None = None, token_provider: Callable[[], str] | None = None | ||||||||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||
| super().__init__(host=host, ssl_ca_cert=ssl_ca_cert) | ||||||||||||||||||||||||||||||||||||||||||||||
| self.token_provider = token_provider | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def auth_settings(self) -> AuthSettings: | ||||||||||||||||||||||||||||||||||||||||||||||
| token = self.token_provider() if self.token_provider else None | ||||||||||||||||||||||||||||||||||||||||||||||
| if not token: | ||||||||||||||||||||||||||||||||||||||||||||||
| if self.token_provider is not None: | ||||||||||||||||||||||||||||||||||||||||||||||
| logger.warning( | ||||||||||||||||||||||||||||||||||||||||||||||
| "token_provider returned an empty or None token; " | ||||||||||||||||||||||||||||||||||||||||||||||
| "request will proceed without an Authorization header" | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| return {} | ||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||
| "OAuth2AuthorizationCodeBearer": { | ||||||||||||||||||||||||||||||||||||||||||||||
| "type": "oauth2", | ||||||||||||||||||||||||||||||||||||||||||||||
| "in": "header", | ||||||||||||||||||||||||||||||||||||||||||||||
| "key": "Authorization", | ||||||||||||||||||||||||||||||||||||||||||||||
| "value": f"Bearer {token}", | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+39
to
+44
|
||||||||||||||||||||||||||||||||||||||||||||||
| return { | |
| "OAuth2AuthorizationCodeBearer": { | |
| "type": "oauth2", | |
| "in": "header", | |
| "key": "Authorization", | |
| "value": f"Bearer {token}", | |
| # Normalize token to avoid double 'Bearer ' prefixes if the provider | |
| # already returns a value starting with 'Bearer '. | |
| token_str = str(token).strip() | |
| bearer_value: str | |
| if token_str.lower().startswith("bearer "): | |
| bearer_value = token_str | |
| else: | |
| bearer_value = f"Bearer {token_str}" | |
| return { | |
| "OAuth2AuthorizationCodeBearer": { | |
| "type": "oauth2", | |
| "in": "header", | |
| "key": "Authorization", | |
| "value": bearer_value, |
akunft marked this conversation as resolved.
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,9 +4,7 @@ | |||||||||||
| from urllib.request import getproxies | ||||||||||||
|
|
||||||||||||
| import semver | ||||||||||||
| from aignx.codegen.api.public_api import PublicApi | ||||||||||||
| from aignx.codegen.api_client import ApiClient | ||||||||||||
| from aignx.codegen.configuration import AuthSettings, Configuration | ||||||||||||
| from aignx.codegen.exceptions import NotFoundException, ServiceException | ||||||||||||
| from aignx.codegen.models import ApplicationReadResponse as Application | ||||||||||||
| from aignx.codegen.models import MeReadResponse as Me | ||||||||||||
|
|
@@ -22,6 +20,7 @@ | |||||||||||
| from urllib3.exceptions import IncompleteRead, PoolError, ProtocolError, ProxyError | ||||||||||||
| from urllib3.exceptions import TimeoutError as Urllib3TimeoutError | ||||||||||||
|
|
||||||||||||
| from aignostics.platform._api import _AuthenticatedApi, _OAuth2TokenProviderConfiguration | ||||||||||||
| from aignostics.platform._authentication import get_token | ||||||||||||
| from aignostics.platform._operation_cache import cached_operation | ||||||||||||
| from aignostics.platform.resources.applications import Applications, Versions | ||||||||||||
|
|
@@ -30,6 +29,10 @@ | |||||||||||
|
|
||||||||||||
| from ._settings import settings | ||||||||||||
|
|
||||||||||||
| # Safety bound for the external token-provider cache. In normal usage callers | ||||||||||||
| # reuse a single provider reference, so this limit should never be reached. | ||||||||||||
| _MAX_EXTERNAL_CLIENTS = 16 | ||||||||||||
|
|
||||||||||||
| RETRYABLE_EXCEPTIONS = ( | ||||||||||||
| ServiceException, | ||||||||||||
| Urllib3TimeoutError, | ||||||||||||
|
|
@@ -59,34 +62,6 @@ def _log_retry_attempt(retry_state: RetryCallState) -> None: | |||||||||||
| ) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| class _OAuth2TokenProviderConfiguration(Configuration): | ||||||||||||
| """ | ||||||||||||
| Overwrites the original Configuration to call a function to obtain a refresh token. | ||||||||||||
|
|
||||||||||||
| The base class does not support callbacks. This is necessary for integrations where | ||||||||||||
| tokens may expire or need to be refreshed automatically. | ||||||||||||
| """ | ||||||||||||
|
|
||||||||||||
| def __init__( | ||||||||||||
| self, host: str, ssl_ca_cert: str | None = None, token_provider: Callable[[], str] | None = None | ||||||||||||
| ) -> None: | ||||||||||||
sentry[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
| super().__init__(host=host, ssl_ca_cert=ssl_ca_cert) | ||||||||||||
| self.token_provider = token_provider | ||||||||||||
|
|
||||||||||||
| def auth_settings(self) -> AuthSettings: | ||||||||||||
| token = self.token_provider() if self.token_provider else None | ||||||||||||
| if not token: | ||||||||||||
| return {} | ||||||||||||
| return { | ||||||||||||
| "OAuth2AuthorizationCodeBearer": { | ||||||||||||
| "type": "oauth2", | ||||||||||||
| "in": "header", | ||||||||||||
| "key": "Authorization", | ||||||||||||
| "value": f"Bearer {token}", | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| class Client: | ||||||||||||
| """Main client for interacting with the Aignostics Platform API. | ||||||||||||
|
|
||||||||||||
|
|
@@ -96,25 +71,31 @@ class Client: | |||||||||||
| - Caches operation results for specific operations. | ||||||||||||
| """ | ||||||||||||
|
|
||||||||||||
| _api_client_cached: ClassVar[PublicApi | None] = None | ||||||||||||
| _api_client_uncached: ClassVar[PublicApi | None] = None | ||||||||||||
| _api_client_cached: ClassVar[_AuthenticatedApi | None] = None | ||||||||||||
| _api_client_uncached: ClassVar[_AuthenticatedApi | None] = None | ||||||||||||
| _api_client_external: ClassVar[dict[Callable[[], str], _AuthenticatedApi]] = {} | ||||||||||||
|
|
||||||||||||
| _api: _AuthenticatedApi | ||||||||||||
| applications: Applications | ||||||||||||
| versions: Versions | ||||||||||||
| runs: Runs | ||||||||||||
|
|
||||||||||||
| def __init__(self, cache_token: bool = True) -> None: | ||||||||||||
| def __init__(self, cache_token: bool = True, token_provider: Callable[[], str] | None = None) -> None: | ||||||||||||
| """Initializes a client instance with authenticated API access. | ||||||||||||
|
|
||||||||||||
| Args: | ||||||||||||
| cache_token (bool): If True, caches the authentication token. | ||||||||||||
| Defaults to True. | ||||||||||||
| cache_token: If True, caches the authentication token. Defaults to True. | ||||||||||||
| Ignored when ``token_provider`` is supplied. | ||||||||||||
olivermeyer marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
| token_provider: Optional external token provider callable. When provided, | ||||||||||||
| bypasses internal OAuth authentication entirely. The callable must | ||||||||||||
| return a valid bearer token string. When set, ``cache_token`` has no | ||||||||||||
| effect because the external provider manages its own token lifecycle. | ||||||||||||
|
||||||||||||
| return a valid bearer token string. When set, ``cache_token`` has no | |
| effect because the external provider manages its own token lifecycle. | |
| return a raw access token string (without the ``Bearer `` prefix). | |
| When set, ``cache_token`` has no effect because the external provider | |
| manages its own token lifecycle. |
Uh oh!
There was an error while loading. Please reload this page.