python3-commons 0.8.18__tar.gz → 0.8.20__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.8.18 → python3_commons-0.8.20}/.github/workflows/python-publish.yaml +14 -6
- python3_commons-0.8.20/.pre-commit-config.yaml +35 -0
- python3_commons-0.8.20/.python-version +1 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/PKG-INFO +4 -6
- {python3_commons-0.8.18 → python3_commons-0.8.20}/pyproject.toml +43 -19
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons/api_client.py +7 -12
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons/audit.py +6 -5
- python3_commons-0.8.20/src/python3_commons/auth.py +82 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons/cache.py +13 -14
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons/conf.py +10 -1
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons/db/__init__.py +1 -1
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons/db/helpers.py +13 -19
- python3_commons-0.8.20/src/python3_commons/db/models/__init__.py +2 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons/db/models/auth.py +1 -3
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons/db/models/common.py +3 -11
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons/db/models/rbac.py +5 -15
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons/helpers.py +8 -7
- {python3_commons-0.8.18/src/python3_commons/logging → python3_commons-0.8.20/src/python3_commons/log}/formatters.py +0 -1
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons/object_storage.py +14 -12
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons/permissions.py +4 -4
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons/serializers/json.py +1 -1
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons/serializers/msgpack.py +1 -1
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons/serializers/msgspec.py +2 -2
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons.egg-info/PKG-INFO +4 -6
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons.egg-info/SOURCES.txt +7 -7
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons.egg-info/requires.txt +1 -4
- python3_commons-0.8.20/uv.lock +1492 -0
- python3_commons-0.8.18/requirements.txt +0 -13
- python3_commons-0.8.18/requirements_dev.txt +0 -4
- python3_commons-0.8.18/requirements_test.txt +0 -3
- python3_commons-0.8.18/setup.py +0 -13
- python3_commons-0.8.18/src/python3_commons/db/models/__init__.py +0 -4
- {python3_commons-0.8.18 → python3_commons-0.8.20}/.coveragerc +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/.gitignore +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/AUTHORS.rst +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/CHANGELOG.rst +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/LICENSE +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/README.md +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/README.rst +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/docs/Makefile +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/docs/_static/.gitignore +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/docs/authors.rst +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/docs/changelog.rst +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/docs/conf.py +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/docs/index.rst +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/docs/license.rst +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/setup.cfg +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons/__init__.py +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons/fs.py +0 -0
- {python3_commons-0.8.18/src/python3_commons/logging → python3_commons-0.8.20/src/python3_commons/log}/__init__.py +0 -0
- {python3_commons-0.8.18/src/python3_commons/logging → python3_commons-0.8.20/src/python3_commons/log}/filters.py +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons/serializers/__init__.py +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons.egg-info/dependency_links.txt +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/src/python3_commons.egg-info/top_level.txt +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/tests/conftest.py +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/tests/test_audit.py +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/tests/test_helpers.py +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/tests/test_msgpack.py +0 -0
- {python3_commons-0.8.18 → python3_commons-0.8.20}/tests/test_msgspec.py +0 -0
@@ -22,16 +22,24 @@ jobs:
|
|
22
22
|
|
23
23
|
steps:
|
24
24
|
- uses: actions/checkout@v4
|
25
|
-
|
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:
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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,14 +1,14 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: python3-commons
|
3
|
-
Version: 0.8.
|
3
|
+
Version: 0.8.20
|
4
4
|
Summary: Re-usable Python3 code
|
5
5
|
Author-email: Oleg Korsak <kamikaze.is.waiting.you@gmail.com>
|
6
|
-
License:
|
6
|
+
License-Expression: GPL-3.0
|
7
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:
|
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,21 +1,23 @@
|
|
1
1
|
[build-system]
|
2
|
-
requires = ["setuptools", "wheel", "
|
2
|
+
requires = ["setuptools", "wheel", "setuptools_scm"]
|
3
3
|
build-backend = "setuptools.build_meta"
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "python3-commons"
|
7
|
-
|
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"}
|
11
11
|
]
|
12
|
-
license =
|
12
|
+
license = "GPL-3.0"
|
13
13
|
readme = {file = "README.rst", content-type = "text/x-rst"}
|
14
14
|
classifiers = [
|
15
15
|
"Development Status :: 4 - Beta",
|
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
|
-
[
|
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.
|
58
|
-
|
59
|
-
|
60
|
-
|
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.
|
67
|
-
|
68
|
-
package = "python3_commons"
|
80
|
+
[tool.ruff.analyze]
|
81
|
+
detect-string-imports = true
|
69
82
|
|
70
|
-
[tool.
|
71
|
-
|
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
|
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,
|
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
|
-
|
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(
|
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
|
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(
|
71
|
-
|
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
|
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
|
-
|
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
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
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
|
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
|
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(
|
12
|
-
|
13
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
@@ -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):
|