python3-commons 0.17.4__tar.gz → 0.17.5__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.
Files changed (74) hide show
  1. {python3_commons-0.17.4/src/python3_commons.egg-info → python3_commons-0.17.5}/PKG-INFO +1 -1
  2. python3_commons-0.17.5/src/python3_commons/auth.py +178 -0
  3. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/conf.py +6 -5
  4. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/object_storage.py +4 -4
  5. {python3_commons-0.17.4 → python3_commons-0.17.5/src/python3_commons.egg-info}/PKG-INFO +1 -1
  6. python3_commons-0.17.4/src/python3_commons/auth.py +0 -79
  7. {python3_commons-0.17.4 → python3_commons-0.17.5}/.coveragerc +0 -0
  8. {python3_commons-0.17.4 → python3_commons-0.17.5}/.devcontainer/Dockerfile +0 -0
  9. {python3_commons-0.17.4 → python3_commons-0.17.5}/.devcontainer/devcontainer.json +0 -0
  10. {python3_commons-0.17.4 → python3_commons-0.17.5}/.devcontainer/docker-compose.yml +0 -0
  11. {python3_commons-0.17.4 → python3_commons-0.17.5}/.env_template +0 -0
  12. {python3_commons-0.17.4 → python3_commons-0.17.5}/.github/workflows/checks.yml +0 -0
  13. {python3_commons-0.17.4 → python3_commons-0.17.5}/.github/workflows/python-publish.yaml +0 -0
  14. {python3_commons-0.17.4 → python3_commons-0.17.5}/.github/workflows/release-on-tag-push.yml +0 -0
  15. {python3_commons-0.17.4 → python3_commons-0.17.5}/.gitignore +0 -0
  16. {python3_commons-0.17.4 → python3_commons-0.17.5}/.pre-commit-config.yaml +0 -0
  17. {python3_commons-0.17.4 → python3_commons-0.17.5}/.python-version +0 -0
  18. {python3_commons-0.17.4 → python3_commons-0.17.5}/AUTHORS.rst +0 -0
  19. {python3_commons-0.17.4 → python3_commons-0.17.5}/CHANGELOG.rst +0 -0
  20. {python3_commons-0.17.4 → python3_commons-0.17.5}/LICENSE +0 -0
  21. {python3_commons-0.17.4 → python3_commons-0.17.5}/README.md +0 -0
  22. {python3_commons-0.17.4 → python3_commons-0.17.5}/README.rst +0 -0
  23. {python3_commons-0.17.4 → python3_commons-0.17.5}/docs/Makefile +0 -0
  24. {python3_commons-0.17.4 → python3_commons-0.17.5}/docs/_static/.gitignore +0 -0
  25. {python3_commons-0.17.4 → python3_commons-0.17.5}/docs/authors.rst +0 -0
  26. {python3_commons-0.17.4 → python3_commons-0.17.5}/docs/changelog.rst +0 -0
  27. {python3_commons-0.17.4 → python3_commons-0.17.5}/docs/conf.py +0 -0
  28. {python3_commons-0.17.4 → python3_commons-0.17.5}/docs/index.rst +0 -0
  29. {python3_commons-0.17.4 → python3_commons-0.17.5}/docs/license.rst +0 -0
  30. {python3_commons-0.17.4 → python3_commons-0.17.5}/pyproject.toml +0 -0
  31. {python3_commons-0.17.4 → python3_commons-0.17.5}/setup.cfg +0 -0
  32. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/__init__.py +0 -0
  33. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/api_client.py +0 -0
  34. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/async_functools.py +0 -0
  35. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/audit.py +0 -0
  36. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/cache.py +0 -0
  37. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/db/__init__.py +0 -0
  38. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/db/helpers.py +0 -0
  39. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/db/models/__init__.py +0 -0
  40. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/db/models/auth.py +0 -0
  41. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/db/models/common.py +0 -0
  42. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/db/models/rbac.py +0 -0
  43. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/db/models/users.py +0 -0
  44. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/exceptions.py +0 -0
  45. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/fs.py +0 -0
  46. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/generators.py +0 -0
  47. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/helpers.py +0 -0
  48. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/log/__init__.py +0 -0
  49. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/log/filters.py +0 -0
  50. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/log/formatters.py +0 -0
  51. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/permissions.py +0 -0
  52. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/serializers/__init__.py +0 -0
  53. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/serializers/common.py +0 -0
  54. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/serializers/json.py +0 -0
  55. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/serializers/msgpack.py +0 -0
  56. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons/serializers/msgspec.py +0 -0
  57. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons.egg-info/SOURCES.txt +0 -0
  58. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons.egg-info/dependency_links.txt +0 -0
  59. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons.egg-info/requires.txt +0 -0
  60. {python3_commons-0.17.4 → python3_commons-0.17.5}/src/python3_commons.egg-info/top_level.txt +0 -0
  61. {python3_commons-0.17.4 → python3_commons-0.17.5}/tests/__init__.py +0 -0
  62. {python3_commons-0.17.4 → python3_commons-0.17.5}/tests/integration/__init__.py +0 -0
  63. {python3_commons-0.17.4 → python3_commons-0.17.5}/tests/integration/test_cache.py +0 -0
  64. {python3_commons-0.17.4 → python3_commons-0.17.5}/tests/integration/test_osc.py +0 -0
  65. {python3_commons-0.17.4 → python3_commons-0.17.5}/tests/unit/__init__.py +0 -0
  66. {python3_commons-0.17.4 → python3_commons-0.17.5}/tests/unit/conftest.py +0 -0
  67. {python3_commons-0.17.4 → python3_commons-0.17.5}/tests/unit/log/__init__.py +0 -0
  68. {python3_commons-0.17.4 → python3_commons-0.17.5}/tests/unit/log/test_formatters.py +0 -0
  69. {python3_commons-0.17.4 → python3_commons-0.17.5}/tests/unit/test_async_functools.py +0 -0
  70. {python3_commons-0.17.4 → python3_commons-0.17.5}/tests/unit/test_audit.py +0 -0
  71. {python3_commons-0.17.4 → python3_commons-0.17.5}/tests/unit/test_helpers.py +0 -0
  72. {python3_commons-0.17.4 → python3_commons-0.17.5}/tests/unit/test_msgpack.py +0 -0
  73. {python3_commons-0.17.4 → python3_commons-0.17.5}/tests/unit/test_msgspec.py +0 -0
  74. {python3_commons-0.17.4 → python3_commons-0.17.5}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python3-commons
3
- Version: 0.17.4
3
+ Version: 0.17.5
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
@@ -0,0 +1,178 @@
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
+ try:
9
+ import aiohttp
10
+ except ImportError as e:
11
+ msg = 'Install python3-commons[authn] to use this feature'
12
+ raise RuntimeError(msg) from e
13
+
14
+ import msgspec
15
+
16
+ from python3_commons.conf import oidc_settings
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class TokenData(msgspec.Struct):
22
+ exp: int
23
+ iat: int
24
+ iss: str
25
+ sub: str
26
+ aud: str | Sequence[str] | None = None
27
+ email: str | None = None
28
+ name: str | None = None
29
+ preferred_username: str | None = None
30
+ realm_access: dict[str, Sequence[str]] | None = None
31
+ resource_access: dict[str, dict[str, Sequence[str]]] | None = None
32
+
33
+ @property
34
+ def roles(self) -> list[str]:
35
+ roles_list = []
36
+
37
+ if self.realm_access:
38
+ roles_list.extend(self.realm_access.get('roles', []))
39
+
40
+ if self.resource_access:
41
+ for client in self.resource_access.values():
42
+ roles_list.extend(client.get('roles', []))
43
+
44
+ return list(set(roles_list))
45
+
46
+
47
+ T = TypeVar('T', bound=TokenData)
48
+ OIDC_CONFIG_URL = (
49
+ f'{oidc_settings.authority_internal_url or oidc_settings.authority_url}/.well-known/openid-configuration'
50
+ )
51
+
52
+
53
+ class OIDCTokenResponse(msgspec.Struct):
54
+ access_token: str
55
+ token_type: str
56
+ expires_in: int
57
+ refresh_token: str
58
+ scope: str
59
+ id_token: str
60
+
61
+
62
+ async def fetch_openid_config() -> dict:
63
+ """
64
+ Fetch the OpenID configuration (including JWKS URI) from OIDC authority.
65
+ """
66
+ async with aiohttp.ClientSession() as session, session.get(OIDC_CONFIG_URL) as response:
67
+ if response.status != HTTPStatus.OK:
68
+ _msg = 'Failed to fetch OpenID configuration'
69
+
70
+ raise RuntimeError(_msg)
71
+
72
+ return await response.json()
73
+
74
+
75
+ async def fetch_jwks(jwks_uri: str) -> dict:
76
+ """
77
+ Fetch the JSON Web Key Set (JWKS) for validating the token's signature.
78
+ """
79
+ if oidc_settings.authority_internal_url:
80
+ jwks_uri = jwks_uri.replace(str(oidc_settings.authority_url), str(oidc_settings.authority_internal_url))
81
+
82
+ async with aiohttp.ClientSession() as session, session.get(jwks_uri) as response:
83
+ if response.status != HTTPStatus.OK:
84
+ _msg = 'Failed to fetch JWKS'
85
+
86
+ raise RuntimeError(_msg)
87
+
88
+ return await response.json()
89
+
90
+
91
+ class OIDCError(Exception):
92
+ pass
93
+
94
+
95
+ class OIDCAuthError(OIDCError):
96
+ pass
97
+
98
+
99
+ class OIDCClient:
100
+ def __init__(
101
+ self,
102
+ token_url: str,
103
+ client_id: str,
104
+ client_secret: str | None = None,
105
+ *,
106
+ timeout: float = 10.0,
107
+ session: aiohttp.ClientSession | None = None,
108
+ ) -> None:
109
+ self._token_url = token_url
110
+ self._client_id = client_id
111
+ self._client_secret = client_secret
112
+ self._timeout = aiohttp.ClientTimeout(total=timeout)
113
+ self._session = session
114
+ self._owns_session = session is None
115
+
116
+ async def __aenter__(self) -> Self:
117
+ if self._session is None:
118
+ self._session = aiohttp.ClientSession(timeout=self._timeout)
119
+ return self
120
+
121
+ async def __aexit__(self, *_: object) -> None:
122
+ if self._owns_session and self._session:
123
+ await self._session.close()
124
+
125
+ async def fetch_token(
126
+ self,
127
+ *,
128
+ username: str,
129
+ password: str,
130
+ scope: str = 'openid profile email',
131
+ ) -> OIDCTokenResponse:
132
+ if self._session is None:
133
+ msg = 'ClientSession not initialized'
134
+
135
+ raise RuntimeError(msg)
136
+
137
+ data = {
138
+ 'grant_type': 'password',
139
+ 'username': username,
140
+ 'password': password,
141
+ 'client_id': self._client_id,
142
+ 'scope': scope,
143
+ }
144
+
145
+ if self._client_secret:
146
+ data['client_secret'] = self._client_secret
147
+
148
+ try:
149
+ async with self._session.post(
150
+ self._token_url,
151
+ data=data,
152
+ headers={'Content-Type': 'application/x-www-form-urlencoded'},
153
+ ) as resp:
154
+ payload = await resp.json(content_type=None)
155
+
156
+ except TimeoutError as e:
157
+ msg = 'OIDC request timed out'
158
+
159
+ raise OIDCError(msg) from e
160
+ except aiohttp.ClientError as e:
161
+ msg = 'OIDC transport error'
162
+
163
+ raise OIDCError(msg) from e
164
+
165
+ if not resp.ok:
166
+ error = payload.get('error')
167
+ description = payload.get('error_description')
168
+
169
+ if error in {'invalid_grant', 'invalid_client'}:
170
+ msg = f'{error}: {description}'
171
+
172
+ raise OIDCAuthError(msg)
173
+
174
+ msg = f'{error}: {description}'
175
+
176
+ raise OIDCError(msg)
177
+
178
+ return payload
@@ -21,6 +21,7 @@ class OIDCSettings(BaseSettings):
21
21
  authority_url: HttpUrl | None = None
22
22
  authority_internal_url: HttpUrl | None = None
23
23
  client_id: str | None = None
24
+ client_secret: SecretStr = SecretStr('')
24
25
  redirect_uri: str | None = None
25
26
  scope: StringSeq = (
26
27
  'openid',
@@ -58,11 +59,11 @@ class DBSettings(BaseSettings):
58
59
  @model_validator(mode='after')
59
60
  def build_dsn_if_missing(self) -> DBSettings:
60
61
  if self.dsn is None and all(
61
- (
62
- self.user,
63
- self.password,
64
- self.name,
65
- )
62
+ (
63
+ self.user,
64
+ self.password,
65
+ self.name,
66
+ )
66
67
  ):
67
68
  self.dsn = PostgresDsn.build(
68
69
  scheme=self.scheme,
@@ -140,7 +140,7 @@ async def list_objects(bucket_name: str, prefix: str, *, recursive: bool = True)
140
140
 
141
141
 
142
142
  async def get_object_streams(
143
- bucket_name: str, path: str, *, recursive: bool = True
143
+ bucket_name: str, path: str, *, recursive: bool = True
144
144
  ) -> AsyncGenerator[tuple[str, datetime, StreamingBody]]:
145
145
  async for obj in list_objects(bucket_name, path, recursive=recursive):
146
146
  object_name = obj['Key']
@@ -151,7 +151,7 @@ async def get_object_streams(
151
151
 
152
152
 
153
153
  async def get_objects(
154
- bucket_name: str, path: str, *, recursive: bool = True
154
+ bucket_name: str, path: str, *, recursive: bool = True
155
155
  ) -> AsyncGenerator[tuple[str, datetime, bytes]]:
156
156
  async for object_name, last_modified, stream in get_object_streams(bucket_name, path, recursive=recursive):
157
157
  data = await stream.read()
@@ -173,7 +173,7 @@ async def remove_object(bucket_name: str, object_name: str) -> None:
173
173
 
174
174
 
175
175
  async def remove_objects(
176
- bucket_name: str, prefix: str | None = None, object_names: Iterable[str] | None = None
176
+ bucket_name: str, prefix: str | None = None, object_names: Iterable[str] | None = None
177
177
  ) -> Sequence[Mapping] | None:
178
178
  storage = ObjectStorage(s3_settings)
179
179
 
@@ -196,7 +196,7 @@ async def remove_objects(
196
196
  chunk_size = 1000
197
197
 
198
198
  for i in range(0, len(objects_to_delete), chunk_size):
199
- chunk = objects_to_delete[i: i + chunk_size]
199
+ chunk = objects_to_delete[i : i + chunk_size]
200
200
 
201
201
  response = await s3_client.delete_objects(Bucket=bucket_name, Delete={'Objects': chunk})
202
202
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python3-commons
3
- Version: 0.17.4
3
+ Version: 0.17.5
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
@@ -1,79 +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 TypeVar
7
-
8
- try:
9
- import aiohttp
10
- except ImportError as e:
11
- msg = 'Install python3-commons[authn] to use this feature'
12
- raise RuntimeError(msg) from e
13
-
14
- import msgspec
15
-
16
- from python3_commons.conf import oidc_settings
17
-
18
- logger = logging.getLogger(__name__)
19
-
20
-
21
- class TokenData(msgspec.Struct):
22
- exp: int
23
- iat: int
24
- iss: str
25
- sub: str
26
- aud: str | Sequence[str] | None = None
27
- email: str | None = None
28
- name: str | None = None
29
- preferred_username: str | None = None
30
- realm_access: dict[str, Sequence[str]] | None = None
31
- resource_access: dict[str, dict[str, Sequence[str]]] | None = None
32
-
33
- @property
34
- def roles(self) -> list[str]:
35
- roles_list = []
36
-
37
- if self.realm_access:
38
- roles_list.extend(self.realm_access.get('roles', []))
39
-
40
- if self.resource_access:
41
- for client in self.resource_access.values():
42
- roles_list.extend(client.get('roles', []))
43
-
44
- return list(set(roles_list))
45
-
46
-
47
- T = TypeVar('T', bound=TokenData)
48
- OIDC_CONFIG_URL = (
49
- f'{oidc_settings.authority_internal_url or oidc_settings.authority_url}/.well-known/openid-configuration'
50
- )
51
-
52
-
53
- async def fetch_openid_config() -> dict:
54
- """
55
- Fetch the OpenID configuration (including JWKS URI) from OIDC authority.
56
- """
57
- async with aiohttp.ClientSession() as session, session.get(OIDC_CONFIG_URL) as response:
58
- if response.status != HTTPStatus.OK:
59
- _msg = 'Failed to fetch OpenID configuration'
60
-
61
- raise RuntimeError(_msg)
62
-
63
- return await response.json()
64
-
65
-
66
- async def fetch_jwks(jwks_uri: str) -> dict:
67
- """
68
- Fetch the JSON Web Key Set (JWKS) for validating the token's signature.
69
- """
70
- if oidc_settings.authority_internal_url:
71
- jwks_uri = jwks_uri.replace(str(oidc_settings.authority_url), str(oidc_settings.authority_internal_url))
72
-
73
- async with aiohttp.ClientSession() as session, session.get(jwks_uri) as response:
74
- if response.status != HTTPStatus.OK:
75
- _msg = 'Failed to fetch JWKS'
76
-
77
- raise RuntimeError(_msg)
78
-
79
- return await response.json()