Skip to content

Commit de2c81d

Browse files
committed
fixup! feat(platform): support external token providers and simplify caching
1 parent b728640 commit de2c81d

File tree

5 files changed

+45
-49
lines changed

5 files changed

+45
-49
lines changed

src/aignostics/platform/_api.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515

1616

1717
class _OAuth2TokenProviderConfiguration(Configuration):
18-
"""Overwrites the original Configuration to call a function to obtain a refresh token.
18+
"""Overwrites the original Configuration to call a function to obtain a bearer token.
1919
2020
The base class does not support callbacks. This is necessary for integrations where
21-
tokens may expire or need to be refreshed automatically.
21+
access tokens may expire or need to be refreshed or rotated automatically.
2222
"""
2323

2424
def __init__(
@@ -58,3 +58,32 @@ class _AuthenticatedApi(PublicApi):
5858
def __init__(self, api_client: ApiClient, token_provider: Callable[[], str] | None = None) -> None:
5959
super().__init__(api_client)
6060
self.token_provider = token_provider
61+
62+
63+
class _AuthenticatedResource:
64+
"""Base for platform resource classes that require an authenticated API client.
65+
66+
Validates at construction time that the provided API object is a genuine
67+
:class:`_AuthenticatedApi` instance, ensuring ``token_provider`` is available
68+
for per-user cache key isolation in ``@cached_operation``.
69+
"""
70+
71+
_api: _AuthenticatedApi
72+
73+
def __init__(self, api: _AuthenticatedApi) -> None:
74+
"""Initialize with an authenticated API client.
75+
76+
Args:
77+
api: The configured API client providing ``token_provider``.
78+
79+
Raises:
80+
TypeError: If *api* is not an :class:`_AuthenticatedApi` instance.
81+
"""
82+
if not isinstance(api, _AuthenticatedApi): # runtime guard for untyped callers
83+
msg = ( # type: ignore[unreachable]
84+
f"{type(self).__name__} requires _AuthenticatedApi, "
85+
f"got {type(api).__name__!r}. "
86+
"Use Client to obtain a correctly configured instance."
87+
)
88+
raise TypeError(msg)
89+
self._api = api

src/aignostics/platform/_client.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import os
2-
import weakref
32
from collections.abc import Callable
43
from typing import ClassVar
54
from urllib.request import getproxies
@@ -70,9 +69,7 @@ class Client:
7069

7170
_api_client_cached: ClassVar[_AuthenticatedApi | None] = None
7271
_api_client_uncached: ClassVar[_AuthenticatedApi | None] = None
73-
_api_client_external: ClassVar[weakref.WeakKeyDictionary[Callable[[], str], _AuthenticatedApi]] = (
74-
weakref.WeakKeyDictionary()
75-
)
72+
_api_client_external: ClassVar[dict[Callable[[], str], _AuthenticatedApi]] = {}
7673

7774
_api: _AuthenticatedApi
7875
applications: Applications
@@ -253,7 +250,8 @@ def get_api_client(cache_token: bool = True, token_provider: Callable[[], str] |
253250
254251
API client instances are shared across all Client instances for efficient connection reuse.
255252
Three pools are maintained: cached-token, uncached-token, and external-provider (keyed by
256-
provider identity via a WeakKeyDictionary — entries are evicted when the provider is GC'd).
253+
the provider callable — callers should reuse a stable ``token_provider`` reference for
254+
connection reuse).
257255
258256
Args:
259257
cache_token: If True, caches the authentication token. Defaults to True.

src/aignostics/platform/resources/applications.py

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from urllib3.exceptions import IncompleteRead, PoolError, ProtocolError, ProxyError
2626
from urllib3.exceptions import TimeoutError as Urllib3TimeoutError
2727

28-
from aignostics.platform._api import _AuthenticatedApi
28+
from aignostics.platform._api import _AuthenticatedApi, _AuthenticatedResource
2929
from aignostics.platform._operation_cache import cached_operation
3030
from aignostics.platform._settings import settings
3131
from aignostics.platform.resources.utils import paginate
@@ -60,24 +60,12 @@ def _log_retry_attempt(retry_state: RetryCallState) -> None:
6060
)
6161

6262

63-
class Versions:
63+
class Versions(_AuthenticatedResource):
6464
"""Resource class for managing application versions.
6565
6666
Provides operations to list and retrieve application versions.
6767
"""
6868

69-
def __init__(self, api: _AuthenticatedApi) -> None:
70-
"""Initializes the Versions resource with the API platform.
71-
72-
Args:
73-
api (_AuthenticatedApi): The configured API platform.
74-
"""
75-
self._api = api
76-
# No runtime hasattr check for token_provider: MyPy strict mode (enforced in CI)
77-
# already rejects non-_AuthenticatedApi arguments at static-analysis time.
78-
# A silent getattr fallback is intentionally avoided — it would disable per-user
79-
# cache key isolation in @cached_operation, causing cross-user cache leakage.
80-
8169
def list(self, application: Application | str, nocache: bool = False) -> builtins.list[VersionTuple]:
8270
"""Find all versions for a specific application.
8371
@@ -228,7 +216,7 @@ def latest(self, application: Application | str, nocache: bool = False) -> Versi
228216
return sorted_versions[0] if sorted_versions else None
229217

230218

231-
class Applications:
219+
class Applications(_AuthenticatedResource):
232220
"""Resource class for managing applications.
233221
234222
Provides operations to list applications and access version resources.
@@ -240,11 +228,7 @@ def __init__(self, api: _AuthenticatedApi) -> None:
240228
Args:
241229
api (_AuthenticatedApi): The configured API platform.
242230
"""
243-
self._api = api
244-
# No runtime hasattr check for token_provider: MyPy strict mode (enforced in CI)
245-
# already rejects non-_AuthenticatedApi arguments at static-analysis time.
246-
# A silent getattr fallback is intentionally avoided — it would disable per-user
247-
# cache key isolation in @cached_operation, causing cross-user cache leakage.
231+
super().__init__(api)
248232
self.versions: Versions = Versions(self._api)
249233

250234
def details(self, application_id: str, nocache: bool = False) -> Application:

src/aignostics/platform/resources/runs.py

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
from urllib3.exceptions import IncompleteRead, PoolError, ProtocolError, ProxyError
4949
from urllib3.exceptions import TimeoutError as Urllib3TimeoutError
5050

51-
from aignostics.platform._api import _AuthenticatedApi
51+
from aignostics.platform._api import _AuthenticatedApi, _AuthenticatedResource
5252
from aignostics.platform._operation_cache import cached_operation, operation_cache_clear
5353
from aignostics.platform._sdk_metadata import (
5454
build_item_sdk_metadata,
@@ -105,7 +105,7 @@ class DownloadTimeoutError(RuntimeError):
105105
"""Exception raised when the download operation exceeds its timeout."""
106106

107107

108-
class Run:
108+
class Run(_AuthenticatedResource):
109109
"""Represents a single application run.
110110
111111
Provides operations to check status, retrieve results, and download artifacts.
@@ -118,11 +118,7 @@ def __init__(self, api: _AuthenticatedApi, run_id: str) -> None:
118118
api (_AuthenticatedApi): The configured API client.
119119
run_id (str): The ID of the application run.
120120
"""
121-
self._api = api
122-
# No runtime hasattr check for token_provider: MyPy strict mode (enforced in CI)
123-
# already rejects non-_AuthenticatedApi arguments at static-analysis time.
124-
# A silent getattr fallback is intentionally avoided — it would disable per-user
125-
# cache key isolation in @cached_operation, causing cross-user cache leakage.
121+
super().__init__(api)
126122
self.run_id = run_id
127123

128124
@classmethod
@@ -502,24 +498,12 @@ def __str__(self) -> str:
502498
)
503499

504500

505-
class Runs:
501+
class Runs(_AuthenticatedResource):
506502
"""Resource class for managing application runs.
507503
508504
Provides operations to submit, find, and retrieve runs.
509505
"""
510506

511-
def __init__(self, api: _AuthenticatedApi) -> None:
512-
"""Initializes the Runs resource with the API client.
513-
514-
Args:
515-
api (_AuthenticatedApi): The configured API client.
516-
"""
517-
self._api = api
518-
# No runtime hasattr check for token_provider: MyPy strict mode (enforced in CI)
519-
# already rejects non-_AuthenticatedApi arguments at static-analysis time.
520-
# A silent getattr fallback is intentionally avoided — it would disable per-user
521-
# cache key isolation in @cached_operation, causing cross-user cache leakage.
522-
523507
def __call__(self, run_id: str) -> Run:
524508
"""Retrieves an Run instance for an existing run.
525509

tests/aignostics/platform/conftest.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import pytest
77

8+
from aignostics.platform._api import _AuthenticatedApi
89
from aignostics.platform._client import Client
910
from aignostics.platform._operation_cache import _operation_cache
1011
from aignostics.platform._service import Service
@@ -49,9 +50,9 @@ def mock_api_client() -> MagicMock:
4950
"""Provide a mock API client.
5051
5152
Returns:
52-
MagicMock: A mock of the PublicApi client.
53+
MagicMock: A mock of the _AuthenticatedApi client.
5354
"""
54-
return MagicMock()
55+
return MagicMock(spec=_AuthenticatedApi)
5556

5657

5758
@pytest.fixture(autouse=True)

0 commit comments

Comments
 (0)