python-daynest 0.1.2__tar.gz → 0.1.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-daynest
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Python client library for the Daynest API
5
5
  License: MIT
6
6
  Requires-Python: >=3.12
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-daynest"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "Python client library for the Daynest API"
5
5
  requires-python = ">=3.12"
6
6
  license = { text = "MIT" }
@@ -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