python3-commons 0.18.22__tar.gz → 0.20.0__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.18.22 → python3_commons-0.20.0}/.env_template +6 -0
- {python3_commons-0.18.22/src/python3_commons.egg-info → python3_commons-0.20.0}/PKG-INFO +2 -2
- {python3_commons-0.18.22 → python3_commons-0.20.0}/pyproject.toml +1 -1
- python3_commons-0.20.0/src/python3_commons/auth.py +258 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0/src/python3_commons.egg-info}/PKG-INFO +2 -2
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons.egg-info/SOURCES.txt +2 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons.egg-info/requires.txt +1 -1
- python3_commons-0.20.0/tests/integration/conftest.py +29 -0
- python3_commons-0.20.0/tests/integration/test_auth.py +29 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/uv.lock +19 -18
- python3_commons-0.18.22/src/python3_commons/auth.py +0 -198
- {python3_commons-0.18.22 → python3_commons-0.20.0}/.coveragerc +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/.devcontainer/Dockerfile +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/.devcontainer/devcontainer.json +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/.devcontainer/docker-compose.yml +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/.github/workflows/checks.yml +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/.github/workflows/python-publish.yaml +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/.github/workflows/release-on-tag-push.yml +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/.gitignore +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/.pre-commit-config.yaml +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/.python-version +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/AUTHORS.rst +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/CHANGELOG.rst +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/LICENSE +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/README.md +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/README.rst +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/docs/Makefile +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/docs/_static/.gitignore +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/docs/authors.rst +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/docs/changelog.rst +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/docs/conf.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/docs/index.rst +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/docs/license.rst +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/setup.cfg +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/__init__.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/api_client.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/async_functools.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/audit.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/cache.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/conf.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/db/__init__.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/db/helpers.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/db/models/__init__.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/db/models/auth.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/db/models/common.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/db/models/rbac.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/db/models/users.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/exceptions.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/fs.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/generators.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/helpers.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/log/__init__.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/log/filters.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/log/formatters.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/object_storage.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/permissions.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/serializers/__init__.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/serializers/common.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/serializers/json.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/serializers/msgpack.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/serializers/msgspec.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/soap_client.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons.egg-info/dependency_links.txt +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons.egg-info/top_level.txt +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/tests/__init__.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/tests/integration/__init__.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/tests/integration/test_cache.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/tests/integration/test_osc.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/tests/unit/__init__.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/tests/unit/conftest.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/tests/unit/log/__init__.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/tests/unit/log/test_formatters.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/tests/unit/test_async_functools.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/tests/unit/test_audit.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/tests/unit/test_helpers.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/tests/unit/test_msgpack.py +0 -0
- {python3_commons-0.18.22 → python3_commons-0.20.0}/tests/unit/test_msgspec.py +0 -0
|
@@ -17,3 +17,9 @@ DB_DSN=postgresql+asyncpg://dev:dev@localhost:5432/fastapi-project-template
|
|
|
17
17
|
API_AUTH_ENABLED=false
|
|
18
18
|
OIDC_AUTHORITY_URL=https://oidc.provider.com/auth
|
|
19
19
|
OIDC_CLIENT_ID=12345-54321
|
|
20
|
+
|
|
21
|
+
TEST_OIDC_AUTHORITY_URL=https://oidc.provider.com/auth
|
|
22
|
+
TEST_OIDC_CLIENT_ID=12345-54321
|
|
23
|
+
TEST_OIDC_CLIENT_SECRET=
|
|
24
|
+
TEST_OIDC_USERNAME=testuser
|
|
25
|
+
TEST_OIDC_PASSWORD=
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python3-commons
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.0
|
|
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
|
|
@@ -36,7 +36,7 @@ Provides-Extra: cache
|
|
|
36
36
|
Requires-Dist: valkey[libvalkey]~=6.1.1; extra == "cache"
|
|
37
37
|
Provides-Extra: database
|
|
38
38
|
Requires-Dist: asyncpg~=0.31.0; extra == "database"
|
|
39
|
-
Requires-Dist: SQLAlchemy[asyncio]~=2.0.
|
|
39
|
+
Requires-Dist: SQLAlchemy[asyncio]~=2.0.50; extra == "database"
|
|
40
40
|
Provides-Extra: object-storage
|
|
41
41
|
Requires-Dist: aiobotocore~=3.7.0; extra == "object-storage"
|
|
42
42
|
Requires-Dist: object-storage-client==0.0.30; extra == "object-storage"
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from http import HTTPStatus
|
|
5
|
+
from typing import Any, Self, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import HttpUrl
|
|
8
|
+
|
|
9
|
+
from python3_commons.helpers import replace_origin
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import aiohttp
|
|
13
|
+
except ImportError as e:
|
|
14
|
+
msg = 'Install python3-commons[authn] to use this feature'
|
|
15
|
+
raise RuntimeError(msg) from e
|
|
16
|
+
|
|
17
|
+
import msgspec
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
_OIDC_LOCK = threading.Lock()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TokenData(msgspec.Struct):
|
|
24
|
+
exp: int
|
|
25
|
+
iat: int
|
|
26
|
+
iss: str
|
|
27
|
+
sub: str
|
|
28
|
+
aud: str | Sequence[str] | None = None
|
|
29
|
+
email: str | None = None
|
|
30
|
+
name: str | None = None
|
|
31
|
+
preferred_username: str | None = None
|
|
32
|
+
realm_access: dict[str, Sequence[str]] | None = None
|
|
33
|
+
resource_access: dict[str, dict[str, Sequence[str]]] | None = None
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def roles(self) -> list[str]:
|
|
37
|
+
roles_list = []
|
|
38
|
+
|
|
39
|
+
if self.realm_access:
|
|
40
|
+
roles_list.extend(self.realm_access.get('roles', []))
|
|
41
|
+
|
|
42
|
+
if self.resource_access:
|
|
43
|
+
for client in self.resource_access.values():
|
|
44
|
+
roles_list.extend(client.get('roles', []))
|
|
45
|
+
|
|
46
|
+
return list(set(roles_list))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
T = TypeVar('T', bound=TokenData)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class OIDCTokenResponse(msgspec.Struct):
|
|
53
|
+
access_token: str
|
|
54
|
+
token_type: str
|
|
55
|
+
expires_in: int
|
|
56
|
+
refresh_token: str
|
|
57
|
+
scope: str
|
|
58
|
+
id_token: str
|
|
59
|
+
error: str | None = None
|
|
60
|
+
error_description: str | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class OIDCError(Exception):
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class OIDCAuthError(OIDCError):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# TODO: use api_client
|
|
72
|
+
class OIDCClient:
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
authority_url: HttpUrl,
|
|
76
|
+
client_id: str,
|
|
77
|
+
client_secret: str | None = None,
|
|
78
|
+
*,
|
|
79
|
+
timeout: float = 10.0,
|
|
80
|
+
verify_ssl: bool = True,
|
|
81
|
+
connection_limit: int = 100,
|
|
82
|
+
authority_internal_host: HttpUrl | None = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
if authority_internal_host:
|
|
85
|
+
authority_url = replace_origin(authority_url, authority_internal_host)
|
|
86
|
+
|
|
87
|
+
self._authority_url = authority_url
|
|
88
|
+
self._authority_internal_host = authority_internal_host
|
|
89
|
+
self._client_id = client_id
|
|
90
|
+
self._client_secret = client_secret
|
|
91
|
+
|
|
92
|
+
self._connection_limit = connection_limit
|
|
93
|
+
self._session: aiohttp.ClientSession | None = None
|
|
94
|
+
self._timeout = timeout
|
|
95
|
+
self._verify_ssl = verify_ssl
|
|
96
|
+
|
|
97
|
+
self._config: Mapping[str, Any] | None = None
|
|
98
|
+
self._jwks: Mapping[str, Any] | None = None
|
|
99
|
+
|
|
100
|
+
def get_session(self) -> aiohttp.ClientSession:
|
|
101
|
+
if self._session:
|
|
102
|
+
return self._session
|
|
103
|
+
|
|
104
|
+
with _OIDC_LOCK:
|
|
105
|
+
if self._session:
|
|
106
|
+
return self._session
|
|
107
|
+
|
|
108
|
+
connector = aiohttp.TCPConnector(verify_ssl=self._verify_ssl, limit=self._connection_limit)
|
|
109
|
+
timeout = aiohttp.ClientTimeout(total=self._timeout)
|
|
110
|
+
session = aiohttp.ClientSession(connector=connector, timeout=timeout)
|
|
111
|
+
self._session = session
|
|
112
|
+
|
|
113
|
+
return session
|
|
114
|
+
|
|
115
|
+
async def __aenter__(self) -> Self:
|
|
116
|
+
self.get_session()
|
|
117
|
+
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
async def __aexit__(self, *_: object) -> None:
|
|
121
|
+
if self._session:
|
|
122
|
+
await self._session.close()
|
|
123
|
+
|
|
124
|
+
async def _fetch_config(self) -> dict:
|
|
125
|
+
"""
|
|
126
|
+
Fetch the OpenID configuration (including JWKS URI) from OIDC authority.
|
|
127
|
+
"""
|
|
128
|
+
if self._session is None:
|
|
129
|
+
msg = 'ClientSession not initialized'
|
|
130
|
+
raise RuntimeError(msg)
|
|
131
|
+
|
|
132
|
+
oidc_config_url = f'{self._authority_url}/.well-known/openid-configuration'
|
|
133
|
+
|
|
134
|
+
logger.debug('Fetching OpenID configuration from: %s', oidc_config_url)
|
|
135
|
+
|
|
136
|
+
async with self._session.get(oidc_config_url) as response:
|
|
137
|
+
if response.status != HTTPStatus.OK:
|
|
138
|
+
_msg = 'Failed to fetch OpenID configuration'
|
|
139
|
+
|
|
140
|
+
raise RuntimeError(_msg)
|
|
141
|
+
|
|
142
|
+
return await response.json()
|
|
143
|
+
|
|
144
|
+
async def get_config(self) -> Mapping[str, Any]:
|
|
145
|
+
if self._config:
|
|
146
|
+
return self._config
|
|
147
|
+
|
|
148
|
+
with _OIDC_LOCK:
|
|
149
|
+
if self._config:
|
|
150
|
+
return self._config
|
|
151
|
+
|
|
152
|
+
config = await self._fetch_config()
|
|
153
|
+
self._config = config
|
|
154
|
+
|
|
155
|
+
return config
|
|
156
|
+
|
|
157
|
+
async def _fetch_jwks(self, jwks_uri: str) -> dict:
|
|
158
|
+
"""
|
|
159
|
+
Fetch the JSON Web Key Set (JWKS) for validating the token's signature.
|
|
160
|
+
"""
|
|
161
|
+
if self._session is None:
|
|
162
|
+
msg = 'ClientSession not initialized'
|
|
163
|
+
raise RuntimeError(msg)
|
|
164
|
+
|
|
165
|
+
if authority_internal_host := self._authority_internal_host:
|
|
166
|
+
logger.debug('Received jwks_uri: %s', jwks_uri)
|
|
167
|
+
logger.debug('Replacing OIDC authority host with: %s', authority_internal_host)
|
|
168
|
+
jwks_uri = str(replace_origin(HttpUrl(jwks_uri), authority_internal_host))
|
|
169
|
+
logger.debug('Modified jwks_uri: %s', jwks_uri)
|
|
170
|
+
|
|
171
|
+
async with self._session.get(jwks_uri) as response:
|
|
172
|
+
if response.status != HTTPStatus.OK:
|
|
173
|
+
_msg = 'Failed to fetch JWKS'
|
|
174
|
+
|
|
175
|
+
raise RuntimeError(_msg)
|
|
176
|
+
|
|
177
|
+
return await response.json()
|
|
178
|
+
|
|
179
|
+
async def get_jwks(self) -> Mapping[str, Any]:
|
|
180
|
+
if self._jwks:
|
|
181
|
+
return self._jwks
|
|
182
|
+
|
|
183
|
+
with _OIDC_LOCK:
|
|
184
|
+
if self._jwks:
|
|
185
|
+
return self._jwks
|
|
186
|
+
|
|
187
|
+
oidc_config = await self.get_config()
|
|
188
|
+
|
|
189
|
+
jwks = await self._fetch_jwks(oidc_config['jwks_uri'])
|
|
190
|
+
self._jwks = jwks
|
|
191
|
+
|
|
192
|
+
return jwks
|
|
193
|
+
|
|
194
|
+
async def fetch_token(
|
|
195
|
+
self,
|
|
196
|
+
*,
|
|
197
|
+
username: str,
|
|
198
|
+
password: str,
|
|
199
|
+
scope: str = 'openid profile email',
|
|
200
|
+
) -> OIDCTokenResponse:
|
|
201
|
+
if self._session is None:
|
|
202
|
+
msg = 'ClientSession not initialized'
|
|
203
|
+
raise RuntimeError(msg)
|
|
204
|
+
|
|
205
|
+
data = {
|
|
206
|
+
'grant_type': 'password',
|
|
207
|
+
'username': username,
|
|
208
|
+
'password': password,
|
|
209
|
+
'client_id': self._client_id,
|
|
210
|
+
'scope': scope,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if self._client_secret:
|
|
214
|
+
data['client_secret'] = self._client_secret
|
|
215
|
+
|
|
216
|
+
openid_config = await self.get_config()
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
async with self._session.post(
|
|
220
|
+
openid_config['token_endpoint'],
|
|
221
|
+
data=data,
|
|
222
|
+
headers={'Content-Type': 'application/x-www-form-urlencoded'},
|
|
223
|
+
) as response:
|
|
224
|
+
payload = await response.read()
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
body = msgspec.json.decode(payload)
|
|
228
|
+
except Exception as e:
|
|
229
|
+
msg = f'Non-JSON response from OIDC provider: {payload[:300]!r}'
|
|
230
|
+
raise OIDCError(msg) from e
|
|
231
|
+
|
|
232
|
+
if response.status >= 400:
|
|
233
|
+
error = None
|
|
234
|
+
description = None
|
|
235
|
+
|
|
236
|
+
if isinstance(body, dict):
|
|
237
|
+
error = body.get('error') or body.get('code')
|
|
238
|
+
description = body.get('error_description') or body.get('message') or ''
|
|
239
|
+
|
|
240
|
+
error = error or f'http_{response.status}'
|
|
241
|
+
|
|
242
|
+
if error in {'invalid_grant', 'invalid_client'}:
|
|
243
|
+
msg = f'{error}: {description}'
|
|
244
|
+
raise OIDCAuthError(msg)
|
|
245
|
+
|
|
246
|
+
msg = f'{error}: {description}'
|
|
247
|
+
raise OIDCError(msg)
|
|
248
|
+
|
|
249
|
+
decoder = msgspec.json.Decoder(OIDCTokenResponse)
|
|
250
|
+
|
|
251
|
+
return decoder.decode(payload)
|
|
252
|
+
|
|
253
|
+
except TimeoutError as e:
|
|
254
|
+
msg = 'OIDC request timed out'
|
|
255
|
+
raise OIDCError(msg) from e
|
|
256
|
+
except aiohttp.ClientError as e:
|
|
257
|
+
msg = 'OIDC transport error'
|
|
258
|
+
raise OIDCError(msg) from e
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python3-commons
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.0
|
|
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
|
|
@@ -36,7 +36,7 @@ Provides-Extra: cache
|
|
|
36
36
|
Requires-Dist: valkey[libvalkey]~=6.1.1; extra == "cache"
|
|
37
37
|
Provides-Extra: database
|
|
38
38
|
Requires-Dist: asyncpg~=0.31.0; extra == "database"
|
|
39
|
-
Requires-Dist: SQLAlchemy[asyncio]~=2.0.
|
|
39
|
+
Requires-Dist: SQLAlchemy[asyncio]~=2.0.50; extra == "database"
|
|
40
40
|
Provides-Extra: object-storage
|
|
41
41
|
Requires-Dist: aiobotocore~=3.7.0; extra == "object-storage"
|
|
42
42
|
Requires-Dist: object-storage-client==0.0.30; extra == "object-storage"
|
|
@@ -59,6 +59,8 @@ src/python3_commons/serializers/msgpack.py
|
|
|
59
59
|
src/python3_commons/serializers/msgspec.py
|
|
60
60
|
tests/__init__.py
|
|
61
61
|
tests/integration/__init__.py
|
|
62
|
+
tests/integration/conftest.py
|
|
63
|
+
tests/integration/test_auth.py
|
|
62
64
|
tests/integration/test_cache.py
|
|
63
65
|
tests/integration/test_osc.py
|
|
64
66
|
tests/unit/__init__.py
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from os import getenv
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pydantic import HttpUrl
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.fixture(scope='session')
|
|
8
|
+
def authority_url() -> HttpUrl:
|
|
9
|
+
return HttpUrl(getenv('TEST_OIDC_AUTHORITY_URL', ''))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture(scope='session')
|
|
13
|
+
def client_id() -> str:
|
|
14
|
+
return getenv('TEST_OIDC_CLIENT_ID', '')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture(scope='session')
|
|
18
|
+
def client_secret() -> str:
|
|
19
|
+
return getenv('TEST_OIDC_CLIENT_SECRET', '')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture(scope='session')
|
|
23
|
+
def oidc_username() -> str:
|
|
24
|
+
return getenv('TEST_OIDC_USERNAME', '')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture(scope='session')
|
|
28
|
+
def oidc_password() -> str:
|
|
29
|
+
return getenv('TEST_OIDC_PASSWORD', '')
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from pydantic import HttpUrl
|
|
7
|
+
|
|
8
|
+
from python3_commons.auth import OIDCClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.asyncio
|
|
12
|
+
async def test_get_token_cognito(
|
|
13
|
+
authority_url: HttpUrl, client_id: str, client_secret: str, oidc_username: str, oidc_password: str
|
|
14
|
+
) -> None:
|
|
15
|
+
async with (
|
|
16
|
+
OIDCClient(
|
|
17
|
+
authority_url=authority_url,
|
|
18
|
+
client_id=client_id,
|
|
19
|
+
client_secret=client_secret,
|
|
20
|
+
verify_ssl=False,
|
|
21
|
+
timeout=10,
|
|
22
|
+
) as client,
|
|
23
|
+
):
|
|
24
|
+
token = await client.fetch_token(
|
|
25
|
+
username=oidc_username,
|
|
26
|
+
password=oidc_password,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
assert token.access_token
|
|
@@ -1171,7 +1171,7 @@ requires-dist = [
|
|
|
1171
1171
|
{ name = "python3-commons", extras = ["database"], marker = "extra == 'authz'" },
|
|
1172
1172
|
{ name = "python3-commons", extras = ["object-storage"], marker = "extra == 'audit'" },
|
|
1173
1173
|
{ name = "requests", marker = "extra == 'soap-client'", specifier = "~=2.34.2" },
|
|
1174
|
-
{ name = "sqlalchemy", extras = ["asyncio"], marker = "extra == 'database'", specifier = "~=2.0.
|
|
1174
|
+
{ name = "sqlalchemy", extras = ["asyncio"], marker = "extra == 'database'", specifier = "~=2.0.50" },
|
|
1175
1175
|
{ name = "valkey", extras = ["libvalkey"], marker = "extra == 'cache'", specifier = "~=6.1.1" },
|
|
1176
1176
|
{ name = "zeep", extras = ["async"], marker = "extra == 'audit'", specifier = "~=4.3.2" },
|
|
1177
1177
|
{ name = "zeep", extras = ["async"], marker = "extra == 'soap-client'", specifier = "~=4.3.2" },
|
|
@@ -1331,28 +1331,29 @@ wheels = [
|
|
|
1331
1331
|
|
|
1332
1332
|
[[package]]
|
|
1333
1333
|
name = "sqlalchemy"
|
|
1334
|
-
version = "2.0.
|
|
1334
|
+
version = "2.0.50"
|
|
1335
1335
|
source = { registry = "https://pypi.org/simple" }
|
|
1336
1336
|
dependencies = [
|
|
1337
1337
|
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
|
1338
1338
|
{ name = "typing-extensions" },
|
|
1339
1339
|
]
|
|
1340
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
1341
|
-
wheels = [
|
|
1342
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1343
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1344
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1345
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1346
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1347
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1348
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1349
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1350
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1351
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1352
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1353
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1354
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1355
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1340
|
+
sdist = { url = "https://files.pythonhosted.org/packages/57/da/6fbf010c8ebb347679d0d100b22fe9ba5e13fd04046c5df7280d2f0bf706/sqlalchemy-2.0.50.tar.gz", hash = "sha256:af5607d11ef90fd6a5c0549fe0045dce1663d427426bcfb506dcb5346a85a3b9", size = 9907424, upload-time = "2026-05-24T19:20:04.018Z" }
|
|
1341
|
+
wheels = [
|
|
1342
|
+
{ url = "https://files.pythonhosted.org/packages/df/32/10ac51b4be7cdecd7e93d069251c86dfbf70b7adbd7c67b48ccea6c49e1c/sqlalchemy-2.0.50-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c966932507a4d7d0a37314927dbfcd89720e3f37d2a1e3352e7ae7939fa8e8a0", size = 2158519, upload-time = "2026-05-24T19:27:56.472Z" },
|
|
1343
|
+
{ url = "https://files.pythonhosted.org/packages/5a/76/e703d2f7681d7d66c4c891af3f07c7ccf4c76ad7f18351de035b5eda007a/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:faffef4bcc20a1892e65e155293d99d60855bbbc79250ab712819cfd56a8e6bb", size = 3282063, upload-time = "2026-05-24T20:09:38.57Z" },
|
|
1344
|
+
{ url = "https://files.pythonhosted.org/packages/31/26/ef168b184a25701f9995e8fb7e503fafd7a99c1c77cda1bc1a26ea2ed486/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c206aec519a2e7bd08abbfb33436e325fd22c632d9c21a9047e376ce241646e", size = 3287069, upload-time = "2026-05-24T20:17:21.942Z" },
|
|
1345
|
+
{ url = "https://files.pythonhosted.org/packages/c2/15/765acc2bc693bccc43ca4a95d5b69750da8aaf6db1b5c616536e087f8920/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bef4ac756363227ef6402a75fee025a4bc690f92328e825868939b3b3a446a6d", size = 3230453, upload-time = "2026-05-24T20:09:40.398Z" },
|
|
1346
|
+
{ url = "https://files.pythonhosted.org/packages/63/61/08e03c3adbf5db0087a0b6816746fec8f3032fb2f7fc899a9bb9b2a48ce4/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96fbee6b19c19cd1556c8bf9419447cf2ec149ffcab7ab64348c23e54ef8547f", size = 3252413, upload-time = "2026-05-24T20:17:24.067Z" },
|
|
1347
|
+
{ url = "https://files.pythonhosted.org/packages/03/0c/370a1f2db38436c615e10134c8a37de3688e74084792380695f3f5083860/sqlalchemy-2.0.50-cp314-cp314-win32.whl", hash = "sha256:8f00e3eb43ba30eb1b238ee03a8a62309486d1321eda3328bb611e0340033ad8", size = 2120063, upload-time = "2026-05-24T19:50:11.08Z" },
|
|
1348
|
+
{ url = "https://files.pythonhosted.org/packages/7f/a0/fe92bb9817863bc13ba093bda931979a26cc2ca69f8e8f26d07add3d7c6f/sqlalchemy-2.0.50-cp314-cp314-win_amd64.whl", hash = "sha256:15708c613cd5005b7dffe1f66ee6a63ee8f5e46799f71c70ebad74178c676a39", size = 2145830, upload-time = "2026-05-24T19:50:12.452Z" },
|
|
1349
|
+
{ url = "https://files.pythonhosted.org/packages/cc/ff/e5640a98a0b2f491eb8fde10fb6c773621a2e44340de231fafcc9370f4a9/sqlalchemy-2.0.50-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3699dac4be410e97049a1658e9480da9cde956594aa0f3aebc60b88f21c5ba70", size = 2178435, upload-time = "2026-05-24T19:42:58.889Z" },
|
|
1350
|
+
{ url = "https://files.pythonhosted.org/packages/b7/85/337116e186f1236375b5fb70c21cfac98e8e8ab0d3a47be838dc47a59e08/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f96233858e3df43932ac11589e22520da6e8aeb624b03fedfeebb0e8ea213086", size = 3566059, upload-time = "2026-05-24T20:01:20.848Z" },
|
|
1351
|
+
{ url = "https://files.pythonhosted.org/packages/96/34/bb0e190e161c3c2c24314a65add57218be14a4a9486886b7f5047c1ff7c8/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4e70c46fad30c3bcc6a4708bc0130a3173e11a5b25f0ea4a9d8911b450f1f52", size = 3535366, upload-time = "2026-05-24T20:03:56.768Z" },
|
|
1352
|
+
{ url = "https://files.pythonhosted.org/packages/df/5a/a7f759f97e4fd499c5d4e4488c760d5a7fbecf3028b465a04274fcd52384/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1918a3cf564d16d95bca7301005f41ab2ad50b07cd3b9da50d3ed986db148d6a", size = 3474879, upload-time = "2026-05-24T20:01:23.058Z" },
|
|
1353
|
+
{ url = "https://files.pythonhosted.org/packages/9d/d9/2907ea38eb60687d297bf9c39e5ee58053c87b57fe8a9cae97090cecbf10/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b00098cdbdbd38c7be3d568b0c9c3122b8c0ec62b911b57cd5e6e0254d60a76d", size = 3486117, upload-time = "2026-05-24T20:03:59.052Z" },
|
|
1354
|
+
{ url = "https://files.pythonhosted.org/packages/f2/e3/5aa06f167559f8c0bdae487e297d23ba548150ab016a3418265d617a4985/sqlalchemy-2.0.50-cp314-cp314t-win32.whl", hash = "sha256:1fbd55a969d7ac44a98e3dec75016074f809fa08f871585ace58dde110d1bf3e", size = 2150823, upload-time = "2026-05-24T20:08:58.644Z" },
|
|
1355
|
+
{ url = "https://files.pythonhosted.org/packages/65/9b/112fb8f977582d7489d036e409e3723948bcf5320b3ac465f3c481bbe8f9/sqlalchemy-2.0.50-cp314-cp314t-win_amd64.whl", hash = "sha256:c5c3cdb753a9004183e1ccb634b41611654c989e61bc68617ce878e46d6f1e51", size = 2185794, upload-time = "2026-05-24T20:09:00.319Z" },
|
|
1356
|
+
{ url = "https://files.pythonhosted.org/packages/d0/10/f7220e9b784d295d241c86ed99aeb537f92afcd469a64861f2717e9bb077/sqlalchemy-2.0.50-py3-none-any.whl", hash = "sha256:92064363517a3ff8212b5a93b8c62876579d8dfd1ca5b561335f30152d884fa9", size = 1943861, upload-time = "2026-05-24T19:59:01.119Z" },
|
|
1356
1357
|
]
|
|
1357
1358
|
|
|
1358
1359
|
[package.optional-dependencies]
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
from collections.abc import Sequence
|
|
5
|
-
from http import HTTPStatus
|
|
6
|
-
from typing import Self, TypeVar
|
|
7
|
-
|
|
8
|
-
from pydantic import HttpUrl
|
|
9
|
-
|
|
10
|
-
from python3_commons.helpers import replace_origin
|
|
11
|
-
|
|
12
|
-
try:
|
|
13
|
-
import aiohttp
|
|
14
|
-
except ImportError as e:
|
|
15
|
-
msg = 'Install python3-commons[authn] to use this feature'
|
|
16
|
-
raise RuntimeError(msg) from e
|
|
17
|
-
|
|
18
|
-
import msgspec
|
|
19
|
-
|
|
20
|
-
from python3_commons.conf import oidc_settings
|
|
21
|
-
|
|
22
|
-
logger = logging.getLogger(__name__)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class TokenData(msgspec.Struct):
|
|
26
|
-
exp: int
|
|
27
|
-
iat: int
|
|
28
|
-
iss: str
|
|
29
|
-
sub: str
|
|
30
|
-
aud: str | Sequence[str] | None = None
|
|
31
|
-
email: str | None = None
|
|
32
|
-
name: str | None = None
|
|
33
|
-
preferred_username: str | None = None
|
|
34
|
-
realm_access: dict[str, Sequence[str]] | None = None
|
|
35
|
-
resource_access: dict[str, dict[str, Sequence[str]]] | None = None
|
|
36
|
-
|
|
37
|
-
@property
|
|
38
|
-
def roles(self) -> list[str]:
|
|
39
|
-
roles_list = []
|
|
40
|
-
|
|
41
|
-
if self.realm_access:
|
|
42
|
-
roles_list.extend(self.realm_access.get('roles', []))
|
|
43
|
-
|
|
44
|
-
if self.resource_access:
|
|
45
|
-
for client in self.resource_access.values():
|
|
46
|
-
roles_list.extend(client.get('roles', []))
|
|
47
|
-
|
|
48
|
-
return list(set(roles_list))
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
T = TypeVar('T', bound=TokenData)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class OIDCTokenResponse(msgspec.Struct):
|
|
55
|
-
access_token: str
|
|
56
|
-
token_type: str
|
|
57
|
-
expires_in: int
|
|
58
|
-
refresh_token: str
|
|
59
|
-
scope: str
|
|
60
|
-
id_token: str
|
|
61
|
-
error: str | None = None
|
|
62
|
-
error_description: str | None = None
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
async def fetch_openid_config() -> dict:
|
|
66
|
-
"""
|
|
67
|
-
Fetch the OpenID configuration (including JWKS URI) from OIDC authority.
|
|
68
|
-
"""
|
|
69
|
-
if (authority_url := oidc_settings.authority_url) is None:
|
|
70
|
-
msg = 'OIDC authority URL is required'
|
|
71
|
-
raise ValueError(msg)
|
|
72
|
-
|
|
73
|
-
if oidc_settings.authority_internal_host:
|
|
74
|
-
authority_url = replace_origin(authority_url, oidc_settings.authority_internal_host)
|
|
75
|
-
|
|
76
|
-
oidc_config_url = f'{authority_url}/.well-known/openid-configuration'
|
|
77
|
-
|
|
78
|
-
logger.debug('Fetching OpenID configuration from: %s', oidc_config_url)
|
|
79
|
-
|
|
80
|
-
async with aiohttp.ClientSession() as session, session.get(oidc_config_url) as response:
|
|
81
|
-
if response.status != HTTPStatus.OK:
|
|
82
|
-
_msg = 'Failed to fetch OpenID configuration'
|
|
83
|
-
|
|
84
|
-
raise RuntimeError(_msg)
|
|
85
|
-
|
|
86
|
-
return await response.json()
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
async def fetch_jwks(jwks_uri: str) -> dict:
|
|
90
|
-
"""
|
|
91
|
-
Fetch the JSON Web Key Set (JWKS) for validating the token's signature.
|
|
92
|
-
"""
|
|
93
|
-
if authority_internal_host := oidc_settings.authority_internal_host:
|
|
94
|
-
logger.debug('Received jwks_uri: %s', jwks_uri)
|
|
95
|
-
logger.debug('Replacing OIDC authority host with: %s', authority_internal_host)
|
|
96
|
-
jwks_uri = str(replace_origin(HttpUrl(jwks_uri), authority_internal_host))
|
|
97
|
-
logger.debug('Modified jwks_uri: %s', jwks_uri)
|
|
98
|
-
|
|
99
|
-
async with aiohttp.ClientSession() as session, session.get(jwks_uri) as response:
|
|
100
|
-
if response.status != HTTPStatus.OK:
|
|
101
|
-
_msg = 'Failed to fetch JWKS'
|
|
102
|
-
|
|
103
|
-
raise RuntimeError(_msg)
|
|
104
|
-
|
|
105
|
-
return await response.json()
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
class OIDCError(Exception):
|
|
109
|
-
pass
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
class OIDCAuthError(OIDCError):
|
|
113
|
-
pass
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
# TODO: use api_client
|
|
117
|
-
class OIDCClient:
|
|
118
|
-
def __init__(
|
|
119
|
-
self,
|
|
120
|
-
authority_url: str,
|
|
121
|
-
client_id: str,
|
|
122
|
-
client_secret: str | None = None,
|
|
123
|
-
*,
|
|
124
|
-
timeout: float = 10.0,
|
|
125
|
-
session: aiohttp.ClientSession | None = None,
|
|
126
|
-
) -> None:
|
|
127
|
-
self._token_url = f'{authority_url}/protocol/openid-connect/token' # TODO: get it from openid-configuration
|
|
128
|
-
self._client_id = client_id
|
|
129
|
-
self._client_secret = client_secret
|
|
130
|
-
self._timeout = aiohttp.ClientTimeout(total=timeout)
|
|
131
|
-
self._session = session
|
|
132
|
-
self._owns_session = session is None
|
|
133
|
-
|
|
134
|
-
async def __aenter__(self) -> Self:
|
|
135
|
-
if self._session is None:
|
|
136
|
-
self._session = aiohttp.ClientSession(timeout=self._timeout)
|
|
137
|
-
return self
|
|
138
|
-
|
|
139
|
-
async def __aexit__(self, *_: object) -> None:
|
|
140
|
-
if self._owns_session and self._session:
|
|
141
|
-
await self._session.close()
|
|
142
|
-
|
|
143
|
-
async def fetch_token(
|
|
144
|
-
self,
|
|
145
|
-
*,
|
|
146
|
-
username: str,
|
|
147
|
-
password: str,
|
|
148
|
-
scope: str = 'openid profile email',
|
|
149
|
-
) -> OIDCTokenResponse:
|
|
150
|
-
if self._session is None:
|
|
151
|
-
msg = 'ClientSession not initialized'
|
|
152
|
-
|
|
153
|
-
raise RuntimeError(msg)
|
|
154
|
-
|
|
155
|
-
data = {
|
|
156
|
-
'grant_type': 'password',
|
|
157
|
-
'username': username,
|
|
158
|
-
'password': password,
|
|
159
|
-
'client_id': self._client_id,
|
|
160
|
-
'scope': scope,
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if self._client_secret:
|
|
164
|
-
data['client_secret'] = self._client_secret
|
|
165
|
-
|
|
166
|
-
try:
|
|
167
|
-
async with self._session.post(
|
|
168
|
-
self._token_url,
|
|
169
|
-
data=data,
|
|
170
|
-
headers={'Content-Type': 'application/x-www-form-urlencoded'},
|
|
171
|
-
) as resp:
|
|
172
|
-
payload = await resp.read()
|
|
173
|
-
decoder = msgspec.json.Decoder(type=OIDCTokenResponse)
|
|
174
|
-
token = decoder.decode(payload)
|
|
175
|
-
|
|
176
|
-
except TimeoutError as e:
|
|
177
|
-
msg = 'OIDC request timed out'
|
|
178
|
-
|
|
179
|
-
raise OIDCError(msg) from e
|
|
180
|
-
except aiohttp.ClientError as e:
|
|
181
|
-
msg = 'OIDC transport error'
|
|
182
|
-
|
|
183
|
-
raise OIDCError(msg) from e
|
|
184
|
-
|
|
185
|
-
if not resp.ok:
|
|
186
|
-
error = token.error
|
|
187
|
-
description = token.error_description
|
|
188
|
-
|
|
189
|
-
if error in {'invalid_grant', 'invalid_client'}:
|
|
190
|
-
msg = f'{error}: {description}'
|
|
191
|
-
|
|
192
|
-
raise OIDCAuthError(msg)
|
|
193
|
-
|
|
194
|
-
msg = f'{error}: {description}'
|
|
195
|
-
|
|
196
|
-
raise OIDCError(msg)
|
|
197
|
-
|
|
198
|
-
return token
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python3_commons-0.18.22 → python3_commons-0.20.0}/.github/workflows/release-on-tag-push.yml
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/db/models/__init__.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/serializers/__init__.py
RENAMED
|
File without changes
|
{python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/serializers/common.py
RENAMED
|
File without changes
|
|
File without changes
|
{python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/serializers/msgpack.py
RENAMED
|
File without changes
|
{python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons/serializers/msgspec.py
RENAMED
|
File without changes
|
|
File without changes
|
{python3_commons-0.18.22 → python3_commons-0.20.0}/src/python3_commons.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{python3_commons-0.18.22 → python3_commons-0.20.0}/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
|