python3-commons 0.22.6__tar.gz → 0.22.7__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.
- {python3_commons-0.22.6 → python3_commons-0.22.7}/PKG-INFO +1 -1
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/auth.py +44 -24
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons.egg-info/PKG-INFO +1 -1
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons.egg-info/SOURCES.txt +1 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons.egg-info/scm_file_list.json +45 -44
- python3_commons-0.22.7/src/python3_commons.egg-info/scm_version.json +8 -0
- python3_commons-0.22.7/tests/unit/test_auth.py +111 -0
- python3_commons-0.22.6/src/python3_commons.egg-info/scm_version.json +0 -8
- {python3_commons-0.22.6 → python3_commons-0.22.7}/.coveragerc +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/.devcontainer/Dockerfile +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/.devcontainer/devcontainer.json +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/.devcontainer/docker-compose.yml +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/.env_template +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/.github/workflows/checks.yml +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/.github/workflows/python-publish.yaml +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/.github/workflows/release-on-tag-push.yml +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/.gitignore +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/.pre-commit-config.yaml +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/.python-version +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/AUTHORS.rst +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/CHANGELOG.rst +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/LICENSE +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/README.md +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/README.rst +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/docs/Makefile +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/docs/_static/.gitignore +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/docs/authors.rst +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/docs/changelog.rst +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/docs/conf.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/docs/index.rst +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/docs/license.rst +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/pyproject.toml +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/setup.cfg +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/__init__.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/api_client.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/async_functools.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/audit.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/cache.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/conf.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/db/__init__.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/db/helpers.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/db/models/__init__.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/db/models/auth.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/db/models/common.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/db/models/rbac.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/db/models/users.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/exceptions.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/fs.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/generators.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/helpers.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/log/__init__.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/log/filters.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/log/formatters.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/object_storage.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/permissions.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/serializers/__init__.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/serializers/common.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/serializers/json.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/serializers/msgpack.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/serializers/msgspec.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/soap_client.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons.egg-info/dependency_links.txt +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons.egg-info/requires.txt +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons.egg-info/top_level.txt +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/tests/__init__.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/tests/integration/__init__.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/tests/integration/conftest.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/tests/integration/test_auth.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/tests/integration/test_cache.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/tests/integration/test_osc.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/tests/unit/__init__.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/tests/unit/conftest.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/tests/unit/db/__init__.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/tests/unit/db/test_async_session_manager.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/tests/unit/log/__init__.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/tests/unit/log/test_formatters.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/tests/unit/test_async_functools.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/tests/unit/test_audit.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/tests/unit/test_helpers.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/tests/unit/test_msgpack.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/tests/unit/test_msgspec.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.7}/uv.lock +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import threading
|
|
3
|
-
from collections.abc import
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from time import monotonic
|
|
4
5
|
from typing import Any, Self, TypeVar
|
|
5
6
|
|
|
6
7
|
from pydantic import HttpUrl
|
|
@@ -18,9 +19,8 @@ except ImportError as e:
|
|
|
18
19
|
import msgspec
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
_OIDC_SESSION_LOCK = threading.Lock()
|
|
22
|
+
|
|
23
|
+
DEFAULT_JWKS_CACHE_TTL = 300.0
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class TokenData(msgspec.Struct):
|
|
@@ -81,6 +81,7 @@ class OIDCClient:
|
|
|
81
81
|
timeout: float = 10.0,
|
|
82
82
|
verify_cert: bool = True,
|
|
83
83
|
connection_limit: int = 100,
|
|
84
|
+
jwks_cache_ttl: float = DEFAULT_JWKS_CACHE_TTL,
|
|
84
85
|
authority_internal_host: HttpUrl | None = None,
|
|
85
86
|
audit_name: str | None = None,
|
|
86
87
|
) -> None:
|
|
@@ -97,17 +98,22 @@ class OIDCClient:
|
|
|
97
98
|
self._timeout = timeout
|
|
98
99
|
self._verify_cert = verify_cert
|
|
99
100
|
|
|
100
|
-
self.
|
|
101
|
-
self.
|
|
101
|
+
self._session_lock = threading.Lock()
|
|
102
|
+
self._config: dict[str, Any] | None = None
|
|
103
|
+
self._config_lock = threading.Lock()
|
|
104
|
+
self._jwks: dict[str, Any] | None = None
|
|
105
|
+
self._jwks_lock = threading.Lock()
|
|
106
|
+
self._jwks_cache_ttl = jwks_cache_ttl
|
|
107
|
+
self._jwks_fetched_at: float | None = None
|
|
102
108
|
self._audit_name: str | None = audit_name
|
|
103
109
|
|
|
104
110
|
def _get_session(self) -> aiohttp.ClientSession:
|
|
105
|
-
if self._session:
|
|
106
|
-
return
|
|
111
|
+
if (session := self._session) and not session.closed:
|
|
112
|
+
return session
|
|
107
113
|
|
|
108
|
-
with
|
|
109
|
-
if self._session:
|
|
110
|
-
return
|
|
114
|
+
with self._session_lock:
|
|
115
|
+
if (session := self._session) and not session.closed:
|
|
116
|
+
return session
|
|
111
117
|
|
|
112
118
|
connector = aiohttp.TCPConnector(verify_ssl=self._verify_cert, limit=self._connection_limit)
|
|
113
119
|
timeout = aiohttp.ClientTimeout(total=self._timeout)
|
|
@@ -137,20 +143,20 @@ class OIDCClient:
|
|
|
137
143
|
) as response:
|
|
138
144
|
return await response.json()
|
|
139
145
|
|
|
140
|
-
async def get_config(self) ->
|
|
141
|
-
if self._config:
|
|
142
|
-
return
|
|
146
|
+
async def get_config(self) -> dict[str, Any]:
|
|
147
|
+
if config := self._config:
|
|
148
|
+
return config
|
|
143
149
|
|
|
144
|
-
with
|
|
145
|
-
if self._config:
|
|
146
|
-
return
|
|
150
|
+
with self._config_lock:
|
|
151
|
+
if config := self._config:
|
|
152
|
+
return config
|
|
147
153
|
|
|
148
154
|
config = await self._fetch_config()
|
|
149
155
|
self._config = config
|
|
150
156
|
|
|
151
157
|
return config
|
|
152
158
|
|
|
153
|
-
async def _fetch_jwks(self, jwks_uri: str) -> dict:
|
|
159
|
+
async def _fetch_jwks(self, jwks_uri: str) -> dict[str, Any]:
|
|
154
160
|
"""
|
|
155
161
|
Fetch the JSON Web Key Set (JWKS) for validating the token's signature.
|
|
156
162
|
"""
|
|
@@ -163,18 +169,32 @@ class OIDCClient:
|
|
|
163
169
|
async with api_client.request(self._get_session(), jwks_uri, '', audit_name=self._audit_name) as response:
|
|
164
170
|
return await response.json()
|
|
165
171
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
def _is_fresh_jwks(self) -> bool:
|
|
173
|
+
fetched_at = self._jwks_fetched_at
|
|
174
|
+
|
|
175
|
+
if self._jwks is None or fetched_at is None:
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
return (monotonic() - fetched_at) < self._jwks_cache_ttl
|
|
179
|
+
|
|
180
|
+
async def get_jwks(self, *, force_refresh: bool = False) -> dict[str, Any]:
|
|
181
|
+
if not force_refresh and (jwks := self._jwks) and self._is_fresh_jwks():
|
|
182
|
+
return jwks
|
|
183
|
+
|
|
184
|
+
fetched_at = self._jwks_fetched_at
|
|
185
|
+
|
|
186
|
+
with self._jwks_lock:
|
|
187
|
+
if (jwks := self._jwks) and self._jwks_fetched_at != fetched_at:
|
|
188
|
+
return jwks
|
|
169
189
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return self._jwks
|
|
190
|
+
if not force_refresh and jwks and self._is_fresh_jwks():
|
|
191
|
+
return jwks
|
|
173
192
|
|
|
174
193
|
oidc_config = await self.get_config()
|
|
175
194
|
|
|
176
195
|
jwks = await self._fetch_jwks(oidc_config['jwks_uri'])
|
|
177
196
|
self._jwks = jwks
|
|
197
|
+
self._jwks_fetched_at = monotonic()
|
|
178
198
|
|
|
179
199
|
return jwks
|
|
180
200
|
|
{python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons.egg-info/scm_file_list.json
RENAMED
|
@@ -1,75 +1,76 @@
|
|
|
1
1
|
{
|
|
2
2
|
"files": [
|
|
3
|
+
".gitignore",
|
|
4
|
+
".python-version",
|
|
5
|
+
"README.md",
|
|
6
|
+
"AUTHORS.rst",
|
|
7
|
+
"CHANGELOG.rst",
|
|
8
|
+
"pyproject.toml",
|
|
9
|
+
".env_template",
|
|
3
10
|
".coveragerc",
|
|
4
11
|
".pre-commit-config.yaml",
|
|
5
|
-
"README.md",
|
|
6
12
|
"uv.lock",
|
|
7
|
-
".python-version",
|
|
8
13
|
"LICENSE",
|
|
9
|
-
"pyproject.toml",
|
|
10
|
-
"CHANGELOG.rst",
|
|
11
|
-
".env_template",
|
|
12
14
|
"README.rst",
|
|
13
|
-
".
|
|
14
|
-
"AUTHORS.rst",
|
|
15
|
-
".devcontainer/Dockerfile",
|
|
16
|
-
".devcontainer/docker-compose.yml",
|
|
17
|
-
".devcontainer/devcontainer.json",
|
|
18
|
-
"docs/Makefile",
|
|
19
|
-
"docs/license.rst",
|
|
20
|
-
"docs/authors.rst",
|
|
21
|
-
"docs/index.rst",
|
|
22
|
-
"docs/changelog.rst",
|
|
23
|
-
"docs/conf.py",
|
|
24
|
-
"docs/_static/.gitignore",
|
|
25
|
-
"src/python3_commons/helpers.py",
|
|
15
|
+
"src/python3_commons/conf.py",
|
|
26
16
|
"src/python3_commons/soap_client.py",
|
|
27
|
-
"src/python3_commons/
|
|
28
|
-
"src/python3_commons/__init__.py",
|
|
29
|
-
"src/python3_commons/exceptions.py",
|
|
30
|
-
"src/python3_commons/object_storage.py",
|
|
17
|
+
"src/python3_commons/async_functools.py",
|
|
31
18
|
"src/python3_commons/permissions.py",
|
|
32
|
-
"src/python3_commons/auth.py",
|
|
33
|
-
"src/python3_commons/audit.py",
|
|
34
|
-
"src/python3_commons/cache.py",
|
|
35
19
|
"src/python3_commons/fs.py",
|
|
36
|
-
"src/python3_commons/
|
|
37
|
-
"src/python3_commons/
|
|
20
|
+
"src/python3_commons/api_client.py",
|
|
21
|
+
"src/python3_commons/audit.py",
|
|
22
|
+
"src/python3_commons/object_storage.py",
|
|
23
|
+
"src/python3_commons/exceptions.py",
|
|
24
|
+
"src/python3_commons/helpers.py",
|
|
38
25
|
"src/python3_commons/generators.py",
|
|
26
|
+
"src/python3_commons/cache.py",
|
|
27
|
+
"src/python3_commons/auth.py",
|
|
28
|
+
"src/python3_commons/__init__.py",
|
|
39
29
|
"src/python3_commons/db/helpers.py",
|
|
40
30
|
"src/python3_commons/db/__init__.py",
|
|
31
|
+
"src/python3_commons/db/models/common.py",
|
|
41
32
|
"src/python3_commons/db/models/rbac.py",
|
|
42
|
-
"src/python3_commons/db/models/__init__.py",
|
|
43
|
-
"src/python3_commons/db/models/users.py",
|
|
44
33
|
"src/python3_commons/db/models/auth.py",
|
|
45
|
-
"src/python3_commons/db/models/
|
|
46
|
-
"src/python3_commons/
|
|
34
|
+
"src/python3_commons/db/models/users.py",
|
|
35
|
+
"src/python3_commons/db/models/__init__.py",
|
|
47
36
|
"src/python3_commons/log/filters.py",
|
|
48
37
|
"src/python3_commons/log/formatters.py",
|
|
49
|
-
"src/python3_commons/
|
|
50
|
-
"src/python3_commons/serializers/msgpack.py",
|
|
51
|
-
"src/python3_commons/serializers/json.py",
|
|
38
|
+
"src/python3_commons/log/__init__.py",
|
|
52
39
|
"src/python3_commons/serializers/msgspec.py",
|
|
40
|
+
"src/python3_commons/serializers/json.py",
|
|
53
41
|
"src/python3_commons/serializers/common.py",
|
|
42
|
+
"src/python3_commons/serializers/msgpack.py",
|
|
43
|
+
"src/python3_commons/serializers/__init__.py",
|
|
44
|
+
".devcontainer/docker-compose.yml",
|
|
45
|
+
".devcontainer/Dockerfile",
|
|
46
|
+
".devcontainer/devcontainer.json",
|
|
47
|
+
".github/workflows/checks.yml",
|
|
48
|
+
".github/workflows/release-on-tag-push.yml",
|
|
49
|
+
".github/workflows/python-publish.yaml",
|
|
54
50
|
"tests/__init__.py",
|
|
55
|
-
"tests/integration/__init__.py",
|
|
56
|
-
"tests/integration/test_auth.py",
|
|
57
51
|
"tests/integration/conftest.py",
|
|
58
52
|
"tests/integration/test_osc.py",
|
|
59
53
|
"tests/integration/test_cache.py",
|
|
60
|
-
"tests/
|
|
61
|
-
"tests/
|
|
54
|
+
"tests/integration/test_auth.py",
|
|
55
|
+
"tests/integration/__init__.py",
|
|
62
56
|
"tests/unit/test_async_functools.py",
|
|
63
|
-
"tests/unit/test_msgspec.py",
|
|
64
57
|
"tests/unit/conftest.py",
|
|
58
|
+
"tests/unit/test_msgspec.py",
|
|
65
59
|
"tests/unit/test_audit.py",
|
|
60
|
+
"tests/unit/test_auth.py",
|
|
66
61
|
"tests/unit/test_msgpack.py",
|
|
67
|
-
"tests/unit/
|
|
62
|
+
"tests/unit/test_helpers.py",
|
|
63
|
+
"tests/unit/__init__.py",
|
|
68
64
|
"tests/unit/db/test_async_session_manager.py",
|
|
69
|
-
"tests/unit/
|
|
65
|
+
"tests/unit/db/__init__.py",
|
|
70
66
|
"tests/unit/log/test_formatters.py",
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
67
|
+
"tests/unit/log/__init__.py",
|
|
68
|
+
"docs/conf.py",
|
|
69
|
+
"docs/Makefile",
|
|
70
|
+
"docs/index.rst",
|
|
71
|
+
"docs/authors.rst",
|
|
72
|
+
"docs/changelog.rst",
|
|
73
|
+
"docs/license.rst",
|
|
74
|
+
"docs/_static/.gitignore"
|
|
74
75
|
]
|
|
75
76
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from pydantic import HttpUrl
|
|
6
|
+
|
|
7
|
+
from python3_commons.auth import OIDCClient
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from pytest_mock import MockerFixture
|
|
11
|
+
|
|
12
|
+
_CONFIG = {'jwks_uri': 'https://issuer.example/keys'}
|
|
13
|
+
_JWKS_V1 = {'keys': [{'kid': 'key-1'}]}
|
|
14
|
+
_JWKS_V2 = {'keys': [{'kid': 'key-2'}]}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def client() -> OIDCClient:
|
|
19
|
+
return OIDCClient(HttpUrl('https://issuer.example'), 'jean', jwks_cache_ttl=100.0)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.asyncio
|
|
23
|
+
async def test_get_jwks_serves_cached_set_within_ttl(client: OIDCClient, mocker: MockerFixture) -> None:
|
|
24
|
+
mocker.patch.object(client, '_fetch_config', mocker.AsyncMock(return_value=_CONFIG))
|
|
25
|
+
fetch_jwks = mocker.AsyncMock(return_value=_JWKS_V1)
|
|
26
|
+
mocker.patch.object(client, '_fetch_jwks', fetch_jwks)
|
|
27
|
+
mocker.patch('python3_commons.auth.monotonic', return_value=10.0)
|
|
28
|
+
|
|
29
|
+
first = await client.get_jwks()
|
|
30
|
+
second = await client.get_jwks()
|
|
31
|
+
|
|
32
|
+
assert first == _JWKS_V1
|
|
33
|
+
assert second == _JWKS_V1
|
|
34
|
+
assert fetch_jwks.await_count == 1
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.asyncio
|
|
38
|
+
async def test_get_jwks_refetches_after_ttl_expiry(client: OIDCClient, mocker: MockerFixture) -> None:
|
|
39
|
+
"""A rotated signing key is picked up once the cached set goes stale."""
|
|
40
|
+
mocker.patch.object(client, '_fetch_config', mocker.AsyncMock(return_value=_CONFIG))
|
|
41
|
+
fetch_jwks = mocker.AsyncMock(side_effect=[_JWKS_V1, _JWKS_V2])
|
|
42
|
+
mocker.patch.object(client, '_fetch_jwks', fetch_jwks)
|
|
43
|
+
clock = mocker.patch('python3_commons.auth.monotonic', return_value=0.0)
|
|
44
|
+
|
|
45
|
+
first = await client.get_jwks()
|
|
46
|
+
clock.return_value = 100.0
|
|
47
|
+
second = await client.get_jwks()
|
|
48
|
+
|
|
49
|
+
assert first == _JWKS_V1
|
|
50
|
+
assert second == _JWKS_V2
|
|
51
|
+
assert fetch_jwks.await_count == 2
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.asyncio
|
|
55
|
+
async def test_get_jwks_force_refresh_bypasses_fresh_cache(client: OIDCClient, mocker: MockerFixture) -> None:
|
|
56
|
+
mocker.patch.object(client, '_fetch_config', mocker.AsyncMock(return_value=_CONFIG))
|
|
57
|
+
fetch_jwks = mocker.AsyncMock(side_effect=[_JWKS_V1, _JWKS_V2])
|
|
58
|
+
mocker.patch.object(client, '_fetch_jwks', fetch_jwks)
|
|
59
|
+
mocker.patch('python3_commons.auth.monotonic', return_value=0.0)
|
|
60
|
+
|
|
61
|
+
first = await client.get_jwks()
|
|
62
|
+
second = await client.get_jwks(force_refresh=True)
|
|
63
|
+
|
|
64
|
+
assert first == _JWKS_V1
|
|
65
|
+
assert second == _JWKS_V2
|
|
66
|
+
assert fetch_jwks.await_count == 2
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.asyncio
|
|
70
|
+
async def test_get_jwks_coalesces_concurrent_refreshes(client: OIDCClient, mocker: MockerFixture) -> None:
|
|
71
|
+
"""Concurrent first-time callers share a single fetch and never deadlock on the lock."""
|
|
72
|
+
mocker.patch.object(client, '_fetch_config', mocker.AsyncMock(return_value=_CONFIG))
|
|
73
|
+
mocker.patch('python3_commons.auth.monotonic', return_value=0.0)
|
|
74
|
+
|
|
75
|
+
started = asyncio.Event()
|
|
76
|
+
release = asyncio.Event()
|
|
77
|
+
|
|
78
|
+
async def slow_fetch(_uri: str) -> dict:
|
|
79
|
+
started.set()
|
|
80
|
+
await release.wait()
|
|
81
|
+
|
|
82
|
+
return _JWKS_V1
|
|
83
|
+
|
|
84
|
+
fetch_jwks = mocker.AsyncMock(side_effect=slow_fetch)
|
|
85
|
+
mocker.patch.object(client, '_fetch_jwks', fetch_jwks)
|
|
86
|
+
|
|
87
|
+
first = asyncio.create_task(client.get_jwks())
|
|
88
|
+
await started.wait()
|
|
89
|
+
second = asyncio.create_task(client.get_jwks())
|
|
90
|
+
await asyncio.sleep(0)
|
|
91
|
+
release.set()
|
|
92
|
+
|
|
93
|
+
results = await asyncio.gather(first, second)
|
|
94
|
+
|
|
95
|
+
assert results == [_JWKS_V1, _JWKS_V1]
|
|
96
|
+
assert fetch_jwks.await_count == 1
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@pytest.mark.asyncio
|
|
100
|
+
async def test_get_session_recreates_a_closed_session(client: OIDCClient) -> None:
|
|
101
|
+
"""A cache refresh after the context manager closed the session must get a live one."""
|
|
102
|
+
async with client:
|
|
103
|
+
first = client._get_session() # noqa: SLF001
|
|
104
|
+
|
|
105
|
+
assert first.closed
|
|
106
|
+
|
|
107
|
+
async with client:
|
|
108
|
+
second = client._get_session() # noqa: SLF001
|
|
109
|
+
|
|
110
|
+
assert not second.closed
|
|
111
|
+
assert second is not first
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/serializers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/serializers/msgpack.py
RENAMED
|
File without changes
|
{python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons/serializers/msgspec.py
RENAMED
|
File without changes
|
|
File without changes
|
{python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{python3_commons-0.22.6 → python3_commons-0.22.7}/src/python3_commons.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python3_commons-0.22.6 → python3_commons-0.22.7}/tests/unit/db/test_async_session_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|