python3-commons 0.22.6__tar.gz → 0.22.8__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.8}/PKG-INFO +1 -1
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/auth.py +50 -30
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/db/__init__.py +1 -1
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons.egg-info/PKG-INFO +1 -1
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons.egg-info/SOURCES.txt +1 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons.egg-info/scm_file_list.json +45 -44
- python3_commons-0.22.8/src/python3_commons.egg-info/scm_version.json +8 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/tests/unit/db/test_async_session_manager.py +3 -3
- python3_commons-0.22.8/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.8}/.coveragerc +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/.devcontainer/Dockerfile +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/.devcontainer/devcontainer.json +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/.devcontainer/docker-compose.yml +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/.env_template +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/.github/workflows/checks.yml +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/.github/workflows/python-publish.yaml +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/.github/workflows/release-on-tag-push.yml +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/.gitignore +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/.pre-commit-config.yaml +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/.python-version +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/AUTHORS.rst +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/CHANGELOG.rst +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/LICENSE +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/README.md +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/README.rst +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/docs/Makefile +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/docs/_static/.gitignore +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/docs/authors.rst +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/docs/changelog.rst +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/docs/conf.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/docs/index.rst +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/docs/license.rst +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/pyproject.toml +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/setup.cfg +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/__init__.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/api_client.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/async_functools.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/audit.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/cache.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/conf.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/db/helpers.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/db/models/__init__.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/db/models/auth.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/db/models/common.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/db/models/rbac.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/db/models/users.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/exceptions.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/fs.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/generators.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/helpers.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/log/__init__.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/log/filters.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/log/formatters.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/object_storage.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/permissions.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/serializers/__init__.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/serializers/common.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/serializers/json.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/serializers/msgpack.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/serializers/msgspec.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/soap_client.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons.egg-info/dependency_links.txt +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons.egg-info/requires.txt +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons.egg-info/top_level.txt +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/tests/__init__.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/tests/integration/__init__.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/tests/integration/conftest.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/tests/integration/test_auth.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/tests/integration/test_cache.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/tests/integration/test_osc.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/tests/unit/__init__.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/tests/unit/conftest.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/tests/unit/db/__init__.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/tests/unit/log/__init__.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/tests/unit/log/test_formatters.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/tests/unit/test_async_functools.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/tests/unit/test_audit.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/tests/unit/test_helpers.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/tests/unit/test_msgpack.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/tests/unit/test_msgspec.py +0 -0
- {python3_commons-0.22.6 → python3_commons-0.22.8}/uv.lock +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import logging
|
|
2
|
-
import
|
|
3
|
-
from
|
|
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 = asyncio.Lock()
|
|
102
|
+
self._config: dict[str, Any] | None = None
|
|
103
|
+
self._config_lock = asyncio.Lock()
|
|
104
|
+
self._jwks: dict[str, Any] | None = None
|
|
105
|
+
self._jwks_lock = asyncio.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
|
-
def _get_session(self) -> aiohttp.ClientSession:
|
|
105
|
-
if self._session:
|
|
106
|
-
return
|
|
110
|
+
async def _get_session(self) -> aiohttp.ClientSession:
|
|
111
|
+
if (session := self._session) and not session.closed:
|
|
112
|
+
return session
|
|
107
113
|
|
|
108
|
-
with
|
|
109
|
-
if self._session:
|
|
110
|
-
return
|
|
114
|
+
async 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)
|
|
@@ -117,7 +123,7 @@ class OIDCClient:
|
|
|
117
123
|
return session
|
|
118
124
|
|
|
119
125
|
async def __aenter__(self) -> Self:
|
|
120
|
-
self._get_session()
|
|
126
|
+
await self._get_session()
|
|
121
127
|
|
|
122
128
|
return self
|
|
123
129
|
|
|
@@ -130,27 +136,27 @@ class OIDCClient:
|
|
|
130
136
|
Fetch the OpenID configuration (including JWKS URI) from OIDC authority.
|
|
131
137
|
"""
|
|
132
138
|
async with api_client.request(
|
|
133
|
-
self._get_session(),
|
|
139
|
+
await self._get_session(),
|
|
134
140
|
str(self._authority_url),
|
|
135
141
|
'/.well-known/openid-configuration',
|
|
136
142
|
audit_name=self._audit_name,
|
|
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
|
+
async 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
|
"""
|
|
@@ -160,21 +166,35 @@ class OIDCClient:
|
|
|
160
166
|
jwks_uri = str(replace_origin(HttpUrl(jwks_uri), authority_internal_host))
|
|
161
167
|
logger.debug('Modified jwks_uri: %s', jwks_uri)
|
|
162
168
|
|
|
163
|
-
async with api_client.request(self._get_session(), jwks_uri, '', audit_name=self._audit_name) as response:
|
|
169
|
+
async with api_client.request(await 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
|
+
async 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
|
|
|
@@ -200,7 +220,7 @@ class OIDCClient:
|
|
|
200
220
|
|
|
201
221
|
try:
|
|
202
222
|
async with api_client.request(
|
|
203
|
-
self._get_session(),
|
|
223
|
+
await self._get_session(),
|
|
204
224
|
openid_config['token_endpoint'],
|
|
205
225
|
'',
|
|
206
226
|
method='post',
|
|
@@ -185,7 +185,7 @@ class AsyncSessionManager:
|
|
|
185
185
|
try:
|
|
186
186
|
yield session
|
|
187
187
|
except Exception:
|
|
188
|
-
logger.exception('Error
|
|
188
|
+
logger.exception('Error occurred while db session %r was open; rolling back', name)
|
|
189
189
|
await session.rollback()
|
|
190
190
|
|
|
191
191
|
raise
|
{python3_commons-0.22.6 → python3_commons-0.22.8}/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
|
}
|
{python3_commons-0.22.6 → python3_commons-0.22.8}/tests/unit/db/test_async_session_manager.py
RENAMED
|
@@ -27,12 +27,12 @@ async def test_async_session_manager_timeout(mocker, caplog):
|
|
|
27
27
|
get_session_ctx_func = manager.get_session_context('default')
|
|
28
28
|
|
|
29
29
|
# We want to verify that TimeoutError from the driver is caught,
|
|
30
|
-
# logged as "
|
|
30
|
+
# logged as "Error occurred while db session ... was open", rolled back, and re-raised.
|
|
31
31
|
with pytest.raises(TimeoutError):
|
|
32
32
|
async with get_session_ctx_func() as session:
|
|
33
33
|
await session.execute('SELECT 1')
|
|
34
34
|
|
|
35
|
-
assert '
|
|
35
|
+
assert 'Error occurred while db session' in caplog.text
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
@pytest.mark.asyncio
|
|
@@ -72,4 +72,4 @@ async def test_async_session_manager_logging(mocker, caplog):
|
|
|
72
72
|
async with get_session_ctx_func() as session:
|
|
73
73
|
await session.execute('SELECT 1')
|
|
74
74
|
|
|
75
|
-
assert '
|
|
75
|
+
assert 'Error occurred while db session' in caplog.text
|
|
@@ -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 = await client._get_session() # noqa: SLF001
|
|
104
|
+
|
|
105
|
+
assert first.closed
|
|
106
|
+
|
|
107
|
+
async with client:
|
|
108
|
+
second = await 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
|
{python3_commons-0.22.6 → python3_commons-0.22.8}/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.8}/src/python3_commons/serializers/msgpack.py
RENAMED
|
File without changes
|
{python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons/serializers/msgspec.py
RENAMED
|
File without changes
|
|
File without changes
|
{python3_commons-0.22.6 → python3_commons-0.22.8}/src/python3_commons.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{python3_commons-0.22.6 → python3_commons-0.22.8}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|