python3-commons 0.8.18__tar.gz → 0.8.19__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 (59) hide show
  1. {python3_commons-0.8.18 → python3_commons-0.8.19}/.github/workflows/python-publish.yaml +14 -6
  2. python3_commons-0.8.19/.pre-commit-config.yaml +35 -0
  3. python3_commons-0.8.19/.python-version +1 -0
  4. {python3_commons-0.8.18 → python3_commons-0.8.19}/PKG-INFO +3 -5
  5. {python3_commons-0.8.18 → python3_commons-0.8.19}/pyproject.toml +42 -18
  6. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/api_client.py +7 -12
  7. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/audit.py +6 -5
  8. python3_commons-0.8.19/src/python3_commons/auth.py +82 -0
  9. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/cache.py +13 -14
  10. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/conf.py +10 -1
  11. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/db/__init__.py +1 -1
  12. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/db/helpers.py +13 -19
  13. python3_commons-0.8.19/src/python3_commons/db/models/__init__.py +2 -0
  14. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/db/models/auth.py +1 -3
  15. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/db/models/common.py +3 -11
  16. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/db/models/rbac.py +5 -15
  17. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/helpers.py +8 -7
  18. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/logging/formatters.py +0 -1
  19. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/object_storage.py +14 -12
  20. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/permissions.py +4 -4
  21. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/serializers/json.py +1 -1
  22. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/serializers/msgpack.py +1 -1
  23. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/serializers/msgspec.py +2 -2
  24. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons.egg-info/PKG-INFO +3 -5
  25. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons.egg-info/SOURCES.txt +4 -4
  26. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons.egg-info/requires.txt +1 -4
  27. python3_commons-0.8.19/uv.lock +1492 -0
  28. python3_commons-0.8.18/requirements.txt +0 -13
  29. python3_commons-0.8.18/requirements_dev.txt +0 -4
  30. python3_commons-0.8.18/requirements_test.txt +0 -3
  31. python3_commons-0.8.18/setup.py +0 -13
  32. python3_commons-0.8.18/src/python3_commons/db/models/__init__.py +0 -4
  33. {python3_commons-0.8.18 → python3_commons-0.8.19}/.coveragerc +0 -0
  34. {python3_commons-0.8.18 → python3_commons-0.8.19}/.gitignore +0 -0
  35. {python3_commons-0.8.18 → python3_commons-0.8.19}/AUTHORS.rst +0 -0
  36. {python3_commons-0.8.18 → python3_commons-0.8.19}/CHANGELOG.rst +0 -0
  37. {python3_commons-0.8.18 → python3_commons-0.8.19}/LICENSE +0 -0
  38. {python3_commons-0.8.18 → python3_commons-0.8.19}/README.md +0 -0
  39. {python3_commons-0.8.18 → python3_commons-0.8.19}/README.rst +0 -0
  40. {python3_commons-0.8.18 → python3_commons-0.8.19}/docs/Makefile +0 -0
  41. {python3_commons-0.8.18 → python3_commons-0.8.19}/docs/_static/.gitignore +0 -0
  42. {python3_commons-0.8.18 → python3_commons-0.8.19}/docs/authors.rst +0 -0
  43. {python3_commons-0.8.18 → python3_commons-0.8.19}/docs/changelog.rst +0 -0
  44. {python3_commons-0.8.18 → python3_commons-0.8.19}/docs/conf.py +0 -0
  45. {python3_commons-0.8.18 → python3_commons-0.8.19}/docs/index.rst +0 -0
  46. {python3_commons-0.8.18 → python3_commons-0.8.19}/docs/license.rst +0 -0
  47. {python3_commons-0.8.18 → python3_commons-0.8.19}/setup.cfg +0 -0
  48. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/__init__.py +0 -0
  49. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/fs.py +0 -0
  50. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/logging/__init__.py +0 -0
  51. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/logging/filters.py +0 -0
  52. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons/serializers/__init__.py +0 -0
  53. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons.egg-info/dependency_links.txt +0 -0
  54. {python3_commons-0.8.18 → python3_commons-0.8.19}/src/python3_commons.egg-info/top_level.txt +0 -0
  55. {python3_commons-0.8.18 → python3_commons-0.8.19}/tests/conftest.py +0 -0
  56. {python3_commons-0.8.18 → python3_commons-0.8.19}/tests/test_audit.py +0 -0
  57. {python3_commons-0.8.18 → python3_commons-0.8.19}/tests/test_helpers.py +0 -0
  58. {python3_commons-0.8.18 → python3_commons-0.8.19}/tests/test_msgpack.py +0 -0
  59. {python3_commons-0.8.18 → python3_commons-0.8.19}/tests/test_msgspec.py +0 -0
@@ -22,16 +22,24 @@ jobs:
22
22
 
23
23
  steps:
24
24
  - uses: actions/checkout@v4
25
- - name: Set up Python
25
+
26
+ - name: Install uv
27
+ uses: astral-sh/setup-uv@v5
28
+ with:
29
+ enable-cache: true
30
+ cache-dependency-glob: "uv.lock"
31
+
32
+ - name: "Set up Python"
26
33
  uses: actions/setup-python@v5
27
34
  with:
28
- python-version: '3.13'
29
- - name: Install dependencies
30
- run: |
31
- python -m pip install --upgrade pip
32
- python -m pip install build
35
+ python-version-file: "pyproject.toml"
36
+
37
+ - name: Install build package
38
+ run: uv pip install --system build setuptools_scm
39
+
33
40
  - name: Build package
34
41
  run: python -m build
42
+
35
43
  - name: Publish package
36
44
  uses: pypa/gh-action-pypi-publish@release/v1
37
45
  with:
@@ -0,0 +1,35 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/uv-pre-commit
3
+ rev: 0.7.3
4
+ hooks:
5
+ - id: uv-lock
6
+ - id: uv-export
7
+
8
+ - repo: https://github.com/astral-sh/ruff-pre-commit
9
+ rev: v0.11.10
10
+ hooks:
11
+ # Run the linter.
12
+ - id: ruff-check
13
+ args: [ --fix ]
14
+ files: ^(migrations/|src/|tests/).*
15
+ types_or: [ python, pyi ]
16
+ # Run the import sort.
17
+ - id: ruff-check
18
+ args:
19
+ - --select
20
+ - I
21
+ - --fix
22
+ files: ^(migrations/|src/|tests/).*
23
+ types_or: [ python, pyi ]
24
+ # Run the formatter.
25
+ - id: ruff-format
26
+ files: ^(migrations/|src/|tests/).*
27
+ types_or: [ python, pyi ]
28
+
29
+ # - repo: local
30
+ # hooks:
31
+ # - id: pyright
32
+ # name: Run pyright via uvx
33
+ # entry: uvx pyright ./src ./tests
34
+ # language: system
35
+ # pass_filenames: false
@@ -0,0 +1 @@
1
+ 3.13
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python3-commons
3
- Version: 0.8.18
3
+ Version: 0.8.19
4
4
  Summary: Re-usable Python3 code
5
5
  Author-email: Oleg Korsak <kamikaze.is.waiting.you@gmail.com>
6
6
  License: gpl-3
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://github.com/kamikaze/python3-commons
8
8
  Project-URL: Documentation, https://github.com/kamikaze/python3-commons/wiki
9
9
  Classifier: Development Status :: 4 - Beta
10
10
  Classifier: Programming Language :: Python
11
- Requires-Python: >=3.13
11
+ Requires-Python: ==3.13.*
12
12
  Description-Content-Type: text/x-rst
13
13
  License-File: LICENSE
14
14
  License-File: AUTHORS.rst
@@ -22,12 +22,10 @@ Requires-Dist: msgpack~=1.1.0
22
22
  Requires-Dist: msgspec~=0.19.0
23
23
  Requires-Dist: pydantic[email]~=2.11.3
24
24
  Requires-Dist: pydantic-settings~=2.9.1
25
+ Requires-Dist: python-jose==3.4.0
25
26
  Requires-Dist: SQLAlchemy[asyncio]~=2.0.40
26
27
  Requires-Dist: valkey[libvalkey]~=6.1.0
27
28
  Requires-Dist: zeep~=4.3.1
28
- Provides-Extra: testing
29
- Requires-Dist: pytest; extra == "testing"
30
- Requires-Dist: pytest-cov; extra == "testing"
31
29
  Dynamic: license-file
32
30
 
33
31
  Re-usable Python3 code
@@ -1,10 +1,10 @@
1
1
  [build-system]
2
- requires = ["setuptools", "wheel", "pyscaffold>=3.2a0,<3.3a0"]
2
+ requires = ["setuptools", "wheel", "setuptools_scm"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python3-commons"
7
- version = "0.8.18"
7
+ dynamic = ["version"]
8
8
  description = "Re-usable Python3 code"
9
9
  authors = [
10
10
  {name = "Oleg Korsak", email = "kamikaze.is.waiting.you@gmail.com"}
@@ -16,6 +16,8 @@ classifiers = [
16
16
  "Programming Language :: Python"
17
17
  ]
18
18
  keywords = []
19
+ requires-python = "==3.13.*"
20
+
19
21
  dependencies = [
20
22
  "aiohttp[speedups]~=3.11.16",
21
23
  "asyncpg~=0.30.0",
@@ -27,16 +29,27 @@ dependencies = [
27
29
  "msgspec~=0.19.0",
28
30
  "pydantic[email]~=2.11.3",
29
31
  "pydantic-settings~=2.9.1",
32
+ "python-jose==3.4.0",
30
33
  "SQLAlchemy[asyncio]~=2.0.40",
31
34
  "valkey[libvalkey]~=6.1.0",
32
35
  "zeep~=4.3.1"
33
36
  ]
34
- requires-python = ">=3.13"
35
37
 
36
- [project.optional-dependencies]
38
+ [dependency-groups]
39
+ dev = [
40
+ "build",
41
+ "pip==25.1.1",
42
+ "pre-commit==4.2.0",
43
+ "pyright==1.1.400",
44
+ "ruff==0.11.10",
45
+ "setuptools==80.8.0",
46
+ "setuptools_scm==8.3.1",
47
+ "wheel==0.45.1",
48
+ ]
37
49
  testing = [
38
50
  "pytest",
39
- "pytest-cov"
51
+ "pytest-cov",
52
+ "pytest-mock"
40
53
  ]
41
54
 
42
55
  [project.urls]
@@ -47,6 +60,11 @@ Documentation = "https://github.com/kamikaze/python3-commons/wiki"
47
60
  where = ["src"]
48
61
  exclude = ["tests"]
49
62
 
63
+ [tool.setuptools_scm]
64
+
65
+ [tool.bdist_wheel]
66
+ universal = true
67
+
50
68
  [tool.pytest.ini_options]
51
69
  addopts = [
52
70
  "--verbose"
@@ -54,18 +72,24 @@ addopts = [
54
72
  norecursedirs = ["dist", "build", ".tox"]
55
73
  testpaths = ["tests"]
56
74
 
57
- [tool.flake8]
58
- exclude = [
59
- ".tox",
60
- "build",
61
- "dist",
62
- ".eggs",
63
- "docs/conf.py"
64
- ]
75
+ [tool.ruff]
76
+ line-length = 120
77
+ indent-width = 4
78
+ target-version = "py313"
65
79
 
66
- [tool.pyscaffold]
67
- version = "3.2.3"
68
- package = "python3_commons"
80
+ [tool.ruff.analyze]
81
+ detect-string-imports = true
69
82
 
70
- [tool.bdist_wheel]
71
- universal = true
83
+ [tool.ruff.lint.flake8-quotes]
84
+ docstring-quotes = "double"
85
+
86
+ [tool.ruff.format]
87
+ exclude = ["*.pyi"]
88
+ indent-style = "space"
89
+ quote-style = "single"
90
+
91
+ [tool.pyright]
92
+ venvPath = "."
93
+ venv = ".venv"
94
+ reportMatchNotExhaustive = "error"
95
+ reportUnnecessaryComparison = "error"
@@ -1,13 +1,13 @@
1
1
  import logging
2
2
  from contextlib import asynccontextmanager
3
- from datetime import datetime, UTC
3
+ from datetime import UTC, datetime
4
4
  from enum import Enum
5
5
  from http import HTTPStatus
6
6
  from json import dumps
7
7
  from typing import AsyncGenerator, Literal, Mapping, Sequence
8
8
  from uuid import uuid4
9
9
 
10
- from aiohttp import ClientResponse, ClientSession, client_exceptions, ClientTimeout
10
+ from aiohttp import ClientResponse, ClientSession, ClientTimeout, client_exceptions
11
11
  from pydantic import HttpUrl
12
12
 
13
13
  from python3_commons import audit
@@ -15,16 +15,11 @@ from python3_commons.conf import s3_settings
15
15
  from python3_commons.helpers import request_to_curl
16
16
  from python3_commons.serializers.json import CustomJSONEncoder
17
17
 
18
-
19
18
  logger = logging.getLogger(__name__)
20
19
 
21
20
 
22
21
  async def _store_response_for_audit(
23
- response: ClientResponse,
24
- audit_name: str,
25
- uri_path: str,
26
- method: str,
27
- request_id: str
22
+ response: ClientResponse, audit_name: str, uri_path: str, method: str, request_id: str
28
23
  ):
29
24
  response_text = await response.text()
30
25
 
@@ -36,7 +31,7 @@ async def _store_response_for_audit(
36
31
  await audit.write_audit_data(
37
32
  s3_settings,
38
33
  f'{date_path}/{audit_name}/{uri_path}/{method}_{timestamp}_{request_id}_response.txt',
39
- response_text.encode('utf-8')
34
+ response_text.encode('utf-8'),
40
35
  )
41
36
 
42
37
 
@@ -51,7 +46,7 @@ async def request(
51
46
  json: Mapping | Sequence | str | None = None,
52
47
  data: bytes | None = None,
53
48
  timeout: ClientTimeout | Enum | None = None,
54
- audit_name: str | None = None
49
+ audit_name: str | None = None,
55
50
  ) -> AsyncGenerator[ClientResponse]:
56
51
  now = datetime.now(tz=UTC)
57
52
  date_path = now.strftime('%Y/%m/%d')
@@ -59,7 +54,7 @@ async def request(
59
54
  request_id = str(uuid4())[-12:]
60
55
  uri_path = uri[:-1] if uri.endswith('/') else uri
61
56
  uri_path = uri_path[1:] if uri_path.startswith('/') else uri_path
62
- url = f'{u[:-1] if (u := str(base_url)).endswith('/') else u}{uri}'
57
+ url = f'{u[:-1] if (u := str(base_url)).endswith("/") else u}{uri}'
63
58
 
64
59
  if audit_name:
65
60
  curl_request = None
@@ -74,7 +69,7 @@ async def request(
74
69
  await audit.write_audit_data(
75
70
  s3_settings,
76
71
  f'{date_path}/{audit_name}/{uri_path}/{method}_{timestamp}_{request_id}_request.txt',
77
- curl_request.encode('utf-8')
72
+ curl_request.encode('utf-8'),
78
73
  )
79
74
  client_method = getattr(client, method)
80
75
 
@@ -4,7 +4,7 @@ import logging
4
4
  import tarfile
5
5
  from bz2 import BZ2Compressor
6
6
  from collections import deque
7
- from datetime import datetime, timedelta, UTC
7
+ from datetime import UTC, datetime, timedelta
8
8
  from typing import Generator, Iterable
9
9
  from uuid import uuid4
10
10
 
@@ -54,7 +54,7 @@ class GeneratedStream(io.BytesIO):
54
54
  unread_data_size = len(buf) - pos
55
55
 
56
56
  if unread_data_size > 0:
57
- buf[:unread_data_size] = buf[pos:pos+unread_data_size]
57
+ buf[:unread_data_size] = buf[pos : pos + unread_data_size]
58
58
 
59
59
  del buf
60
60
 
@@ -67,8 +67,9 @@ class GeneratedStream(io.BytesIO):
67
67
  return True
68
68
 
69
69
 
70
- def generate_archive(objects: Iterable[tuple[str, datetime, bytes]],
71
- chunk_size: int = 4096) -> Generator[bytes, None, None]:
70
+ def generate_archive(
71
+ objects: Iterable[tuple[str, datetime, bytes]], chunk_size: int = 4096
72
+ ) -> Generator[bytes, None, None]:
72
73
  buffer = deque()
73
74
 
74
75
  with tarfile.open(fileobj=buffer, mode='w') as archive:
@@ -154,7 +155,7 @@ async def archive_audit_data(root_path: str = 'audit'):
154
155
  archive_stream = GeneratedStream(bzip2_generator)
155
156
 
156
157
  archive_path = object_storage.get_absolute_path(f'audit/.archive/{year}_{month:02}_{day:02}.tar.bz2')
157
- object_storage.put_object(bucket_name, archive_path, archive_stream, -1, part_size=5*1024*1024)
158
+ object_storage.put_object(bucket_name, archive_path, archive_stream, -1, part_size=5 * 1024 * 1024)
158
159
 
159
160
  if errors := object_storage.remove_objects(bucket_name, date_path):
160
161
  for error in errors:
@@ -0,0 +1,82 @@
1
+ import logging
2
+ from http import HTTPStatus
3
+ from typing import Annotated
4
+
5
+ import aiohttp
6
+ from fastapi import Depends, HTTPException
7
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
8
+ from jose import JWTError, jwt
9
+ from pydantic import BaseModel
10
+
11
+ from python3_commons.conf import oidc_settings
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class TokenData(BaseModel):
17
+ sub: str
18
+ aud: str
19
+ exp: int
20
+ iss: str
21
+
22
+
23
+ OIDC_CONFIG_URL = f'{oidc_settings.authority_url}/.well-known/openid-configuration'
24
+ _JWKS: dict | None = None
25
+
26
+ bearer_security = HTTPBearer(auto_error=oidc_settings.enabled)
27
+
28
+
29
+ async def fetch_openid_config() -> dict:
30
+ """
31
+ Fetch the OpenID configuration (including JWKS URI) from OIDC authority.
32
+ """
33
+ async with aiohttp.ClientSession() as session:
34
+ async with session.get(OIDC_CONFIG_URL) as response:
35
+ if response.status != HTTPStatus.OK:
36
+ raise HTTPException(
37
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail='Failed to fetch OpenID configuration'
38
+ )
39
+
40
+ return await response.json()
41
+
42
+
43
+ async def fetch_jwks(jwks_uri: str) -> dict:
44
+ """
45
+ Fetch the JSON Web Key Set (JWKS) for validating the token's signature.
46
+ """
47
+ async with aiohttp.ClientSession() as session:
48
+ async with session.get(jwks_uri) as response:
49
+ if response.status != HTTPStatus.OK:
50
+ raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail='Failed to fetch JWKS')
51
+
52
+ return await response.json()
53
+
54
+
55
+ async def get_verified_token(
56
+ authorization: Annotated[HTTPAuthorizationCredentials, Depends(bearer_security)],
57
+ ) -> TokenData | None:
58
+ """
59
+ Verify the JWT access token using OIDC authority JWKS.
60
+ """
61
+ global _JWKS
62
+
63
+ if not oidc_settings.enabled:
64
+ return None
65
+
66
+ token = authorization.credentials
67
+
68
+ try:
69
+ if not _JWKS:
70
+ openid_config = await fetch_openid_config()
71
+ _JWKS = await fetch_jwks(openid_config['jwks_uri'])
72
+
73
+ if oidc_settings.client_id:
74
+ payload = jwt.decode(token, _JWKS, algorithms=['RS256'], audience=oidc_settings.client_id)
75
+ else:
76
+ payload = jwt.decode(token, _JWKS, algorithms=['RS256'])
77
+
78
+ token_data = TokenData(**payload)
79
+
80
+ return token_data
81
+ except JWTError as e:
82
+ raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail=f'Token is invalid: {str(e)}')
@@ -5,7 +5,7 @@ from typing import Any, Mapping, Sequence
5
5
 
6
6
  import valkey
7
7
  from pydantic import RedisDsn
8
- from valkey.asyncio import Valkey, StrictValkey, ConnectionPool, Sentinel
8
+ from valkey.asyncio import ConnectionPool, Sentinel, StrictValkey, Valkey
9
9
  from valkey.asyncio.retry import Retry
10
10
  from valkey.backoff import FullJitterBackoff
11
11
  from valkey.typing import ResponseT
@@ -13,7 +13,10 @@ from valkey.typing import ResponseT
13
13
  from python3_commons.conf import valkey_settings
14
14
  from python3_commons.helpers import SingletonMeta
15
15
  from python3_commons.serializers.msgspec import (
16
- serialize_msgpack, deserialize_msgpack, deserialize_msgpack_native, serialize_msgpack_native
16
+ deserialize_msgpack,
17
+ deserialize_msgpack_native,
18
+ serialize_msgpack,
19
+ serialize_msgpack_native,
17
20
  )
18
21
 
19
22
  logger = logging.getLogger(__name__)
@@ -32,11 +35,7 @@ class AsyncValkeyClient(metaclass=SingletonMeta):
32
35
  @staticmethod
33
36
  def _get_keepalive_options():
34
37
  if platform == 'linux' or platform == 'darwin':
35
- return {
36
- socket.TCP_KEEPIDLE: 10,
37
- socket.TCP_KEEPINTVL: 5,
38
- socket.TCP_KEEPCNT: 5
39
- }
38
+ return {socket.TCP_KEEPIDLE: 10, socket.TCP_KEEPINTVL: 5, socket.TCP_KEEPCNT: 5}
40
39
  else:
41
40
  return {}
42
41
 
@@ -46,7 +45,7 @@ class AsyncValkeyClient(metaclass=SingletonMeta):
46
45
  socket_connect_timeout=10,
47
46
  socket_timeout=60,
48
47
  password=dsn.password,
49
- sentinel_kwargs={'password': dsn.password}
48
+ sentinel_kwargs={'password': dsn.password},
50
49
  )
51
50
 
52
51
  ka_options = self._get_keepalive_options()
@@ -60,7 +59,7 @@ class AsyncValkeyClient(metaclass=SingletonMeta):
60
59
  retry_on_timeout=True,
61
60
  retry=Retry(FullJitterBackoff(cap=5, base=1), 5),
62
61
  socket_keepalive=True,
63
- socket_keepalive_options=ka_options
62
+ socket_keepalive_options=ka_options,
64
63
  )
65
64
 
66
65
  def _initialize_standard_pool(self, dsn: RedisDsn):
@@ -76,11 +75,11 @@ def get_valkey_client() -> Valkey:
76
75
 
77
76
 
78
77
  async def scan(
79
- cursor: int = 0,
80
- match: bytes | str | memoryview | None = None,
81
- count: int | None = None,
82
- _type: str | None = None,
83
- **kwargs,
78
+ cursor: int = 0,
79
+ match: bytes | str | memoryview | None = None,
80
+ count: int | None = None,
81
+ _type: str | None = None,
82
+ **kwargs,
84
83
  ) -> ResponseT:
85
84
  return await get_valkey_client().scan(cursor, match, count, _type, **kwargs)
86
85
 
@@ -1,4 +1,4 @@
1
- from pydantic import SecretStr, PostgresDsn, Field, RedisDsn
1
+ from pydantic import Field, HttpUrl, PostgresDsn, RedisDsn, SecretStr
2
2
  from pydantic_settings import BaseSettings, SettingsConfigDict
3
3
 
4
4
 
@@ -8,6 +8,14 @@ class CommonSettings(BaseSettings):
8
8
  logging_formatter: str = 'default'
9
9
 
10
10
 
11
+ class OIDCSettings(BaseSettings):
12
+ model_config = SettingsConfigDict(env_prefix='OIDC_')
13
+
14
+ enabled: bool = True
15
+ authority_url: HttpUrl | None = None
16
+ client_id: str | None = None
17
+
18
+
11
19
  class ValkeySettings(BaseSettings):
12
20
  model_config = SettingsConfigDict(env_prefix='VALKEY_')
13
21
 
@@ -38,6 +46,7 @@ class S3Settings(BaseSettings):
38
46
 
39
47
 
40
48
  settings = CommonSettings()
49
+ oidc_settings = OIDCSettings()
41
50
  valkey_settings = ValkeySettings()
42
51
  db_settings = DBSettings()
43
52
  s3_settings = S3Settings()
@@ -3,7 +3,7 @@ import logging
3
3
  from typing import AsyncGenerator, Callable, Mapping
4
4
 
5
5
  from sqlalchemy import MetaData
6
- from sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine, async_engine_from_config
6
+ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_engine_from_config
7
7
  from sqlalchemy.ext.asyncio.session import async_sessionmaker
8
8
  from sqlalchemy.orm import declarative_base
9
9
 
@@ -2,22 +2,22 @@ import logging
2
2
  from typing import Mapping
3
3
 
4
4
  import sqlalchemy as sa
5
- from sqlalchemy import desc, asc, func
5
+ from sqlalchemy import asc, desc, func
6
6
  from sqlalchemy.sql.elements import BooleanClauseList, UnaryExpression
7
7
 
8
8
  logger = logging.getLogger(__name__)
9
9
 
10
10
 
11
- def get_query(search: Mapping[str, str] | None = None,
12
- order_by: str | None = None,
13
- columns: Mapping | None = None) -> tuple[BooleanClauseList, UnaryExpression]:
11
+ def get_query(
12
+ search: Mapping[str, str] | None = None, order_by: str | None = None, columns: Mapping | None = None
13
+ ) -> tuple[BooleanClauseList, UnaryExpression]:
14
14
  """
15
- :columns:
16
- Param name ->
17
- 0: Model column
18
- 1: case-insensitive if True
19
- 2: cast value to type
20
- 3: exact match if True, LIKE %value% if False
15
+ :columns:
16
+ Param name ->
17
+ 0: Model column
18
+ 1: case-insensitive if True
19
+ 2: cast value to type
20
+ 3: exact match if True, LIKE %value% if False
21
21
  """
22
22
 
23
23
  order_by_cols = {}
@@ -41,21 +41,15 @@ def get_query(search: Mapping[str, str] | None = None,
41
41
  if search:
42
42
  where_parts = [
43
43
  *(
44
- (func.upper(columns[k][0])
45
- if columns[k][1]
46
- else columns[k][0]
47
- ) == columns[k][2](v)
44
+ (func.upper(columns[k][0]) if columns[k][1] else columns[k][0]) == columns[k][2](v)
48
45
  for k, v in search.items()
49
46
  if columns[k][3]
50
47
  ),
51
48
  *(
52
- (func.upper(columns[k][0])
53
- if columns[k][1]
54
- else columns[k][0]
55
- ).like(f'%{v.upper()}%')
49
+ (func.upper(columns[k][0]) if columns[k][1] else columns[k][0]).like(f'%{v.upper()}%')
56
50
  for k, v in search.items()
57
51
  if not columns[k][3]
58
- )
52
+ ),
59
53
  ]
60
54
  else:
61
55
  where_parts = None
@@ -0,0 +1,2 @@
1
+ from python3_commons.db.models.auth import ApiKey, User, UserGroup
2
+ from python3_commons.db.models.rbac import RBACApiKeyRole, RBACPermission, RBACRole, RBACRolePermission, RBACUserRole
@@ -2,9 +2,7 @@ import uuid
2
2
 
3
3
  from fastapi_users_db_sqlalchemy import GUID, SQLAlchemyBaseUserTableUUID
4
4
  from pydantic import AwareDatetime
5
- from sqlalchemy import (
6
- String, BIGINT, ForeignKey, DateTime
7
- )
5
+ from sqlalchemy import BIGINT, DateTime, ForeignKey, String
8
6
  from sqlalchemy.orm import Mapped, mapped_column
9
7
 
10
8
  from python3_commons.db import Base
@@ -1,7 +1,5 @@
1
1
  from pydantic import AwareDatetime
2
- from sqlalchemy import (
3
- DateTime, BIGINT
4
- )
2
+ from sqlalchemy import BIGINT, DateTime
5
3
  from sqlalchemy.dialects.postgresql import UUID
6
4
  from sqlalchemy.ext.compiler import compiles
7
5
  from sqlalchemy.orm import Mapped, mapped_column
@@ -28,10 +26,7 @@ def use_identity(element, compiler, **kw):
28
26
  class BaseDBModel:
29
27
  id: Mapped[int] = mapped_column(BIGINT, primary_key=True, sort_order=-3)
30
28
  created_at: Mapped[AwareDatetime] = mapped_column(
31
- DateTime(timezone=True),
32
- nullable=False,
33
- server_default=UTCNow(),
34
- sort_order=-2
29
+ DateTime(timezone=True), nullable=False, server_default=UTCNow(), sort_order=-2
35
30
  )
36
31
  updated_at: Mapped[AwareDatetime] = mapped_column(DateTime(timezone=True), onupdate=UTCNow(), sort_order=-1)
37
32
 
@@ -39,9 +34,6 @@ class BaseDBModel:
39
34
  class BaseDBUUIDModel:
40
35
  uid: Mapped[UUID] = mapped_column(UUID, primary_key=True, sort_order=-3)
41
36
  created_at: Mapped[AwareDatetime] = mapped_column(
42
- DateTime(timezone=True),
43
- nullable=False,
44
- server_default=UTCNow(),
45
- sort_order=-2
37
+ DateTime(timezone=True), nullable=False, server_default=UTCNow(), sort_order=-2
46
38
  )
47
39
  updated_at: Mapped[AwareDatetime | None] = mapped_column(DateTime(timezone=True), onupdate=UTCNow(), sort_order=-1)
@@ -2,9 +2,7 @@ import uuid
2
2
 
3
3
  from fastapi_users_db_sqlalchemy import GUID
4
4
  from pydantic import AwareDatetime
5
- from sqlalchemy import (
6
- String, DateTime, ForeignKey, PrimaryKeyConstraint, CheckConstraint
7
- )
5
+ from sqlalchemy import CheckConstraint, DateTime, ForeignKey, PrimaryKeyConstraint, String
8
6
  from sqlalchemy.dialects.postgresql import UUID
9
7
  from sqlalchemy.orm import Mapped, mapped_column
10
8
 
@@ -24,9 +22,7 @@ class RBACPermission(Base):
24
22
  uid: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True)
25
23
  name: Mapped[str] = mapped_column(String, unique=True, nullable=False)
26
24
 
27
- __table_args__ = (
28
- CheckConstraint("name ~ '^[a-z0-9_.]+$'", name='check_rbac_permissions_name'),
29
- )
25
+ __table_args__ = (CheckConstraint("name ~ '^[a-z0-9_.]+$'", name='check_rbac_permissions_name'),)
30
26
 
31
27
 
32
28
  class RBACRolePermission(Base):
@@ -43,9 +39,7 @@ class RBACRolePermission(Base):
43
39
  index=True,
44
40
  )
45
41
 
46
- __table_args__ = (
47
- PrimaryKeyConstraint('role_uid', 'permission_uid', name='pk_rbac_role_permissions'),
48
- )
42
+ __table_args__ = (PrimaryKeyConstraint('role_uid', 'permission_uid', name='pk_rbac_role_permissions'),)
49
43
 
50
44
 
51
45
  class RBACUserRole(Base):
@@ -64,9 +58,7 @@ class RBACUserRole(Base):
64
58
  starts_at: Mapped[AwareDatetime] = mapped_column(DateTime(timezone=True), nullable=False)
65
59
  expires_at: Mapped[AwareDatetime | None] = mapped_column(DateTime(timezone=True))
66
60
 
67
- __table_args__ = (
68
- PrimaryKeyConstraint('user_id', 'role_uid', name='pk_rbac_user_roles'),
69
- )
61
+ __table_args__ = (PrimaryKeyConstraint('user_id', 'role_uid', name='pk_rbac_user_roles'),)
70
62
 
71
63
 
72
64
  class RBACApiKeyRole(Base):
@@ -85,9 +77,7 @@ class RBACApiKeyRole(Base):
85
77
  starts_at: Mapped[AwareDatetime] = mapped_column(DateTime(timezone=True), nullable=False)
86
78
  expires_at: Mapped[AwareDatetime | None] = mapped_column(DateTime(timezone=True))
87
79
 
88
- __table_args__ = (
89
- PrimaryKeyConstraint('api_key_uid', 'role_uid', name='pk_rbac_api_key_roles'),
90
- )
80
+ __table_args__ = (PrimaryKeyConstraint('api_key_uid', 'role_uid', name='pk_rbac_api_key_roles'),)
91
81
 
92
82
 
93
83
  # class RBACRoleRelation(Base):