python3-commons 0.22.6__py3-none-any.whl → 0.22.7__py3-none-any.whl

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/auth.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import logging
2
2
  import threading
3
- from collections.abc import Mapping, Sequence
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
- _OIDC_CONFIG_LOCK = threading.Lock()
22
- _OIDC_JWKS_LOCK = threading.Lock()
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._config: Mapping[str, Any] | None = None
101
- self._jwks: Mapping[str, Any] | None = None
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 self._session
111
+ if (session := self._session) and not session.closed:
112
+ return session
107
113
 
108
- with _OIDC_SESSION_LOCK:
109
- if self._session:
110
- return self._session
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) -> Mapping[str, Any]:
141
- if self._config:
142
- return self._config
146
+ async def get_config(self) -> dict[str, Any]:
147
+ if config := self._config:
148
+ return config
143
149
 
144
- with _OIDC_CONFIG_LOCK:
145
- if self._config:
146
- return self._config
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
- async def get_jwks(self) -> Mapping[str, Any]:
167
- if self._jwks:
168
- return self._jwks
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
- with _OIDC_JWKS_LOCK:
171
- if self._jwks:
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python3-commons
3
- Version: 0.22.6
3
+ Version: 0.22.7
4
4
  Summary: Re-usable Python3 code
5
5
  Author-email: Oleg Korsak <kamikaze.is.waiting.you@gmail.com>
6
6
  License-Expression: GPL-3.0
@@ -2,7 +2,7 @@ python3_commons/__init__.py,sha256=0KgaYU46H_IMKn-BuasoRN3C4Hi45KlkHHoPbU9cwiA,1
2
2
  python3_commons/api_client.py,sha256=SThSzLhZJxVsKP8n1xDMCczA7SBFbxGFgrhKy9k-9_g,5532
3
3
  python3_commons/async_functools.py,sha256=A2HvwFzZHxOWTp4IQM5UiBY2yg1S_0U1CWra5BWK0gk,9101
4
4
  python3_commons/audit.py,sha256=uGoCwenDJ0Gdwbr_VNOZm5scT8luxW1weprJbbMoHo0,2608
5
- python3_commons/auth.py,sha256=veZBrCT5xn5IRi2vP_LJikkv4oWvq-9ALypxKpPQGlo,7401
5
+ python3_commons/auth.py,sha256=9gRdXe54O9e86DS8XgXlX9QDCqrhPIZLJaa4AC9RR_0,8239
6
6
  python3_commons/cache.py,sha256=lowiXJqFgFy1Yg86wi9IhuoNqIUGP6nc5eNibmf0dfY,8018
7
7
  python3_commons/conf.py,sha256=1HYtNkNBQc5IIABzhH3kz7GnLeF7rj5aNcOoW8xbEa4,3101
8
8
  python3_commons/exceptions.py,sha256=EGjHZVBnsM6CeBfPMqhL0IPMKjDJ_2-Z-aSPXwq91LE,36
@@ -27,11 +27,11 @@ python3_commons/serializers/common.py,sha256=VkA7C6wODvHk0QBXVX_x2JieDstihx3U__U
27
27
  python3_commons/serializers/json.py,sha256=UPkC3ps13x2C_NxwVV-K7Ewp4VjkVHSSUkJVw5k7Wiw,712
28
28
  python3_commons/serializers/msgpack.py,sha256=zESFBX34GsZ8rDu6Zk5V6CLT6P0mPilU0r04Ka6TblI,1474
29
29
  python3_commons/serializers/msgspec.py,sha256=upy5CBmK66-8hYnK5bAM_sZvZY5CAqZmzCw9GIF346I,2988
30
- python3_commons-0.22.6.dist-info/licenses/AUTHORS.rst,sha256=3R9JnfjfjH5RoPWOeqKFJgxVShSSfzQPIrEr1nxIo9Q,90
31
- python3_commons-0.22.6.dist-info/licenses/LICENSE,sha256=xxILuojHm4fKQOrMHPSslbyy6WuKAN2RiG74HbrYfzM,34575
32
- python3_commons-0.22.6.dist-info/METADATA,sha256=bkTFbeGhsCPx_RGutoUS8bG-bQIn7Vx2BEUO2lIom3A,9472
33
- python3_commons-0.22.6.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
34
- python3_commons-0.22.6.dist-info/scm_file_list.json,sha256=Hjo_NQrKMIL3RWUjLVGGDP57Rok5PvQpVvbIVYHC2Nw,2518
35
- python3_commons-0.22.6.dist-info/scm_version.json,sha256=bcBml9-5V4SzNVw8Ro8CgsqejWvP-uNAqCzpJKsPFKQ,161
36
- python3_commons-0.22.6.dist-info/top_level.txt,sha256=lJI6sCBf68eUHzupCnn2dzG10lH3jJKTWM_hrN1cQ7M,16
37
- python3_commons-0.22.6.dist-info/RECORD,,
30
+ python3_commons-0.22.7.dist-info/licenses/AUTHORS.rst,sha256=3R9JnfjfjH5RoPWOeqKFJgxVShSSfzQPIrEr1nxIo9Q,90
31
+ python3_commons-0.22.7.dist-info/licenses/LICENSE,sha256=xxILuojHm4fKQOrMHPSslbyy6WuKAN2RiG74HbrYfzM,34575
32
+ python3_commons-0.22.7.dist-info/METADATA,sha256=QHKCW7uvvkYZHybS_GrgR3t1kSKr3Zdp4oIqN1UuLgs,9472
33
+ python3_commons-0.22.7.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
34
+ python3_commons-0.22.7.dist-info/scm_file_list.json,sha256=_VHernCbTfExHtROxsobMxaHoIS1TJJ6S-8WTB3Aqvs,2549
35
+ python3_commons-0.22.7.dist-info/scm_version.json,sha256=Flp3XnyTjvM7r12ci_e2s_UtGH9o8RT-MNUA8D2WUWA,161
36
+ python3_commons-0.22.7.dist-info/top_level.txt,sha256=lJI6sCBf68eUHzupCnn2dzG10lH3jJKTWM_hrN1cQ7M,16
37
+ python3_commons-0.22.7.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.1)
2
+ Generator: setuptools (83.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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
- ".gitignore",
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/api_client.py",
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/conf.py",
37
- "src/python3_commons/async_functools.py",
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/common.py",
46
- "src/python3_commons/log/__init__.py",
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/serializers/__init__.py",
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/unit/__init__.py",
61
- "tests/unit/test_helpers.py",
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/db/__init__.py",
62
+ "tests/unit/test_helpers.py",
63
+ "tests/unit/__init__.py",
68
64
  "tests/unit/db/test_async_session_manager.py",
69
- "tests/unit/log/__init__.py",
65
+ "tests/unit/db/__init__.py",
70
66
  "tests/unit/log/test_formatters.py",
71
- ".github/workflows/release-on-tag-push.yml",
72
- ".github/workflows/python-publish.yaml",
73
- ".github/workflows/checks.yml"
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,8 @@
1
+ {
2
+ "tag": "0.22.7",
3
+ "distance": 0,
4
+ "node": "g5f178ff649355a3292548e00f3c32fdbfcb16d27",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-15"
8
+ }
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.22.6",
3
- "distance": 0,
4
- "node": "gd66dd259ee583bc3a586ae4be41f9f159169f450",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-06-26"
8
- }