python-daynest 0.1.2__tar.gz → 0.1.4__tar.gz
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.
- {python_daynest-0.1.2 → python_daynest-0.1.4}/PKG-INFO +1 -1
- {python_daynest-0.1.2 → python_daynest-0.1.4}/pyproject.toml +1 -1
- {python_daynest-0.1.2 → python_daynest-0.1.4}/src/daynest/client.py +17 -1
- {python_daynest-0.1.2 → python_daynest-0.1.4}/tests/test_client.py +30 -0
- {python_daynest-0.1.2 → python_daynest-0.1.4}/.gitignore +0 -0
- {python_daynest-0.1.2 → python_daynest-0.1.4}/src/daynest/__init__.py +0 -0
- {python_daynest-0.1.2 → python_daynest-0.1.4}/src/daynest/exceptions.py +0 -0
- {python_daynest-0.1.2 → python_daynest-0.1.4}/src/daynest/models.py +0 -0
- {python_daynest-0.1.2 → python_daynest-0.1.4}/tests/__init__.py +0 -0
- {python_daynest-0.1.2 → python_daynest-0.1.4}/tests/test_package.py +0 -0
- {python_daynest-0.1.2 → python_daynest-0.1.4}/uv.lock +0 -0
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
+
import inspect
|
|
6
7
|
import time
|
|
7
|
-
from collections.abc import Callable, Mapping
|
|
8
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
8
9
|
from datetime import date
|
|
9
10
|
from typing import Any, TypeVar
|
|
10
11
|
from urllib.parse import urljoin
|
|
@@ -56,6 +57,7 @@ class DaynestClient:
|
|
|
56
57
|
client_id: str | None = None,
|
|
57
58
|
client_secret: str | None = None,
|
|
58
59
|
token_url: str | None = None,
|
|
60
|
+
access_token_getter: Callable[[], str | None | Awaitable[str | None]] | None = None,
|
|
59
61
|
) -> None:
|
|
60
62
|
if base_url is not None and not base_url.strip():
|
|
61
63
|
msg = "A base URL is required to initialize DaynestClient"
|
|
@@ -65,6 +67,7 @@ class DaynestClient:
|
|
|
65
67
|
self._client_id = client_id
|
|
66
68
|
self._client_secret = client_secret
|
|
67
69
|
self._token_url = token_url
|
|
70
|
+
self._access_token_getter = access_token_getter
|
|
68
71
|
self._cached_token: str | None = None
|
|
69
72
|
self._token_expires_at: float = 0.0
|
|
70
73
|
self._session = session
|
|
@@ -93,6 +96,17 @@ class DaynestClient:
|
|
|
93
96
|
"""Return whether the client has OAuth client credentials configured."""
|
|
94
97
|
return bool(self._client_id and self._client_secret and self._token_url)
|
|
95
98
|
|
|
99
|
+
async def _get_external_access_token(self) -> str | None:
|
|
100
|
+
"""Return an externally managed OAuth access token, if configured."""
|
|
101
|
+
if self._access_token_getter is None:
|
|
102
|
+
return None
|
|
103
|
+
token = self._access_token_getter()
|
|
104
|
+
if inspect.isawaitable(token):
|
|
105
|
+
token = await token
|
|
106
|
+
if not token:
|
|
107
|
+
return None
|
|
108
|
+
return token
|
|
109
|
+
|
|
96
110
|
async def _fetch_oauth_token(self) -> str:
|
|
97
111
|
"""Exchange client credentials for an access token."""
|
|
98
112
|
session = self._session_or_raise()
|
|
@@ -136,6 +150,8 @@ class DaynestClient:
|
|
|
136
150
|
|
|
137
151
|
async def _get_auth_headers(self) -> dict[str, str]:
|
|
138
152
|
"""Return auth headers for an API request."""
|
|
153
|
+
if external_token := await self._get_external_access_token():
|
|
154
|
+
return {"Authorization": f"Bearer {external_token}"}
|
|
139
155
|
if self.has_oauth_credentials:
|
|
140
156
|
if not self._cached_token or time.monotonic() >= self._token_expires_at:
|
|
141
157
|
await self._fetch_oauth_token()
|
|
@@ -298,6 +298,36 @@ class TestDaynestClientRequests:
|
|
|
298
298
|
|
|
299
299
|
assert await client._get_auth_headers() == {"Authorization": "Bearer access-token"}
|
|
300
300
|
|
|
301
|
+
async def test_external_access_token_getter_is_used_before_other_auth_modes(self) -> None:
|
|
302
|
+
response = _make_mock_response(200, {"access_token": "ignored-token", "expires_in": "300"})
|
|
303
|
+
session = MagicMock(spec=aiohttp.ClientSession)
|
|
304
|
+
session.post = MagicMock(return_value=response)
|
|
305
|
+
client = DaynestClient(
|
|
306
|
+
base_url="https://api.example",
|
|
307
|
+
integration_key="legacy-key",
|
|
308
|
+
client_id="integration-client",
|
|
309
|
+
client_secret="client-secret",
|
|
310
|
+
token_url="https://auth.example/token",
|
|
311
|
+
access_token_getter=lambda: "oidc-access-token",
|
|
312
|
+
session=session,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
assert await client._get_auth_headers() == {"Authorization": "Bearer oidc-access-token"}
|
|
316
|
+
session.post.assert_not_called()
|
|
317
|
+
|
|
318
|
+
async def test_external_access_token_getter_supports_async_callback(self) -> None:
|
|
319
|
+
async def _token() -> str:
|
|
320
|
+
return "async-token"
|
|
321
|
+
|
|
322
|
+
session = MagicMock(spec=aiohttp.ClientSession)
|
|
323
|
+
client = DaynestClient(
|
|
324
|
+
base_url="https://api.example",
|
|
325
|
+
access_token_getter=_token,
|
|
326
|
+
session=session,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
assert await client._get_auth_headers() == {"Authorization": "Bearer async-token"}
|
|
330
|
+
|
|
301
331
|
async def test_client_error_raises_communication_error(self) -> None:
|
|
302
332
|
session = MagicMock(spec=aiohttp.ClientSession)
|
|
303
333
|
mock_ctx = MagicMock()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|