python3-commons 0.17.3__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.3/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.3 → python3_commons-0.17.5}/src/python3_commons/conf.py +1 -1
  4. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/object_storage.py +0 -3
  5. {python3_commons-0.17.3 → python3_commons-0.17.5/src/python3_commons.egg-info}/PKG-INFO +1 -1
  6. python3_commons-0.17.3/src/python3_commons/auth.py +0 -79
  7. {python3_commons-0.17.3 → python3_commons-0.17.5}/.coveragerc +0 -0
  8. {python3_commons-0.17.3 → python3_commons-0.17.5}/.devcontainer/Dockerfile +0 -0
  9. {python3_commons-0.17.3 → python3_commons-0.17.5}/.devcontainer/devcontainer.json +0 -0
  10. {python3_commons-0.17.3 → python3_commons-0.17.5}/.devcontainer/docker-compose.yml +0 -0
  11. {python3_commons-0.17.3 → python3_commons-0.17.5}/.env_template +0 -0
  12. {python3_commons-0.17.3 → python3_commons-0.17.5}/.github/workflows/checks.yml +0 -0
  13. {python3_commons-0.17.3 → python3_commons-0.17.5}/.github/workflows/python-publish.yaml +0 -0
  14. {python3_commons-0.17.3 → python3_commons-0.17.5}/.github/workflows/release-on-tag-push.yml +0 -0
  15. {python3_commons-0.17.3 → python3_commons-0.17.5}/.gitignore +0 -0
  16. {python3_commons-0.17.3 → python3_commons-0.17.5}/.pre-commit-config.yaml +0 -0
  17. {python3_commons-0.17.3 → python3_commons-0.17.5}/.python-version +0 -0
  18. {python3_commons-0.17.3 → python3_commons-0.17.5}/AUTHORS.rst +0 -0
  19. {python3_commons-0.17.3 → python3_commons-0.17.5}/CHANGELOG.rst +0 -0
  20. {python3_commons-0.17.3 → python3_commons-0.17.5}/LICENSE +0 -0
  21. {python3_commons-0.17.3 → python3_commons-0.17.5}/README.md +0 -0
  22. {python3_commons-0.17.3 → python3_commons-0.17.5}/README.rst +0 -0
  23. {python3_commons-0.17.3 → python3_commons-0.17.5}/docs/Makefile +0 -0
  24. {python3_commons-0.17.3 → python3_commons-0.17.5}/docs/_static/.gitignore +0 -0
  25. {python3_commons-0.17.3 → python3_commons-0.17.5}/docs/authors.rst +0 -0
  26. {python3_commons-0.17.3 → python3_commons-0.17.5}/docs/changelog.rst +0 -0
  27. {python3_commons-0.17.3 → python3_commons-0.17.5}/docs/conf.py +0 -0
  28. {python3_commons-0.17.3 → python3_commons-0.17.5}/docs/index.rst +0 -0
  29. {python3_commons-0.17.3 → python3_commons-0.17.5}/docs/license.rst +0 -0
  30. {python3_commons-0.17.3 → python3_commons-0.17.5}/pyproject.toml +0 -0
  31. {python3_commons-0.17.3 → python3_commons-0.17.5}/setup.cfg +0 -0
  32. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/__init__.py +0 -0
  33. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/api_client.py +0 -0
  34. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/async_functools.py +0 -0
  35. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/audit.py +0 -0
  36. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/cache.py +0 -0
  37. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/db/__init__.py +0 -0
  38. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/db/helpers.py +0 -0
  39. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/db/models/__init__.py +0 -0
  40. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/db/models/auth.py +0 -0
  41. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/db/models/common.py +0 -0
  42. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/db/models/rbac.py +0 -0
  43. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/db/models/users.py +0 -0
  44. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/exceptions.py +0 -0
  45. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/fs.py +0 -0
  46. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/generators.py +0 -0
  47. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/helpers.py +0 -0
  48. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/log/__init__.py +0 -0
  49. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/log/filters.py +0 -0
  50. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/log/formatters.py +0 -0
  51. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/permissions.py +0 -0
  52. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/serializers/__init__.py +0 -0
  53. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/serializers/common.py +0 -0
  54. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/serializers/json.py +0 -0
  55. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/serializers/msgpack.py +0 -0
  56. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons/serializers/msgspec.py +0 -0
  57. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons.egg-info/SOURCES.txt +0 -0
  58. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons.egg-info/dependency_links.txt +0 -0
  59. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons.egg-info/requires.txt +0 -0
  60. {python3_commons-0.17.3 → python3_commons-0.17.5}/src/python3_commons.egg-info/top_level.txt +0 -0
  61. {python3_commons-0.17.3 → python3_commons-0.17.5}/tests/__init__.py +0 -0
  62. {python3_commons-0.17.3 → python3_commons-0.17.5}/tests/integration/__init__.py +0 -0
  63. {python3_commons-0.17.3 → python3_commons-0.17.5}/tests/integration/test_cache.py +0 -0
  64. {python3_commons-0.17.3 → python3_commons-0.17.5}/tests/integration/test_osc.py +0 -0
  65. {python3_commons-0.17.3 → python3_commons-0.17.5}/tests/unit/__init__.py +0 -0
  66. {python3_commons-0.17.3 → python3_commons-0.17.5}/tests/unit/conftest.py +0 -0
  67. {python3_commons-0.17.3 → python3_commons-0.17.5}/tests/unit/log/__init__.py +0 -0
  68. {python3_commons-0.17.3 → python3_commons-0.17.5}/tests/unit/log/test_formatters.py +0 -0
  69. {python3_commons-0.17.3 → python3_commons-0.17.5}/tests/unit/test_async_functools.py +0 -0
  70. {python3_commons-0.17.3 → python3_commons-0.17.5}/tests/unit/test_audit.py +0 -0
  71. {python3_commons-0.17.3 → python3_commons-0.17.5}/tests/unit/test_helpers.py +0 -0
  72. {python3_commons-0.17.3 → python3_commons-0.17.5}/tests/unit/test_msgpack.py +0 -0
  73. {python3_commons-0.17.3 → python3_commons-0.17.5}/tests/unit/test_msgspec.py +0 -0
  74. {python3_commons-0.17.3 → 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.3
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',
@@ -82,7 +83,6 @@ class S3Settings(BaseSettings):
82
83
  aws_access_key_id: SecretStr | None = None
83
84
  aws_secret_access_key: SecretStr | None = None
84
85
 
85
- s3_endpoint_url: str | None = None
86
86
  s3_addressing_style: Literal['path', 'virtual'] = 'virtual'
87
87
  s3_secure: bool = True
88
88
  s3_bucket: str | None = None
@@ -50,9 +50,6 @@ class ObjectStorage(metaclass=SingletonMeta):
50
50
  'config': Config(s3={'addressing_style': settings.s3_addressing_style}, signature_version='s3v4'),
51
51
  }
52
52
 
53
- if s3_endpoint_url := settings.s3_endpoint_url:
54
- config['endpoint_url'] = s3_endpoint_url
55
-
56
53
  if aws_access_key_id := settings.aws_access_key_id:
57
54
  config['aws_access_key_id'] = aws_access_key_id.get_secret_value()
58
55
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python3-commons
3
- Version: 0.17.3
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()