python3-commons 0.9.17__tar.gz → 0.9.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.
Potentially problematic release.
This version of python3-commons might be problematic. Click here for more details.
- {python3_commons-0.9.17 → python3_commons-0.9.19}/.github/workflows/python-publish.yaml +3 -3
- {python3_commons-0.9.17 → python3_commons-0.9.19}/.github/workflows/release-on-tag-push.yml +3 -3
- {python3_commons-0.9.17 → python3_commons-0.9.19}/.pre-commit-config.yaml +2 -2
- {python3_commons-0.9.17 → python3_commons-0.9.19}/PKG-INFO +4 -4
- {python3_commons-0.9.17 → python3_commons-0.9.19}/pyproject.toml +70 -12
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/api_client.py +34 -14
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/auth.py +22 -23
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/cache.py +6 -5
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/db/__init__.py +3 -3
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/db/helpers.py +5 -7
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/fs.py +2 -2
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/helpers.py +3 -2
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/object_storage.py +19 -19
- python3_commons-0.9.19/src/python3_commons/serializers/common.py +8 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/serializers/json.py +2 -4
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/serializers/msgpack.py +14 -12
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/serializers/msgspec.py +26 -17
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons.egg-info/PKG-INFO +4 -4
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons.egg-info/SOURCES.txt +1 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons.egg-info/requires.txt +3 -3
- {python3_commons-0.9.17 → python3_commons-0.9.19}/tests/conftest.py +11 -9
- {python3_commons-0.9.17 → python3_commons-0.9.19}/tests/test_msgpack.py +2 -2
- {python3_commons-0.9.17 → python3_commons-0.9.19}/uv.lock +132 -122
- {python3_commons-0.9.17 → python3_commons-0.9.19}/.coveragerc +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/.gitignore +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/.python-version +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/AUTHORS.rst +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/CHANGELOG.rst +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/LICENSE +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/README.md +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/README.rst +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/docs/Makefile +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/docs/_static/.gitignore +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/docs/authors.rst +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/docs/changelog.rst +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/docs/conf.py +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/docs/index.rst +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/docs/license.rst +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/setup.cfg +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/__init__.py +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/audit.py +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/conf.py +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/db/models/__init__.py +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/db/models/auth.py +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/db/models/common.py +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/db/models/rbac.py +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/log/__init__.py +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/log/filters.py +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/log/formatters.py +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/permissions.py +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons/serializers/__init__.py +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons.egg-info/dependency_links.txt +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/src/python3_commons.egg-info/top_level.txt +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/tests/test_audit.py +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/tests/test_cache.py +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/tests/test_helpers.py +0 -0
- {python3_commons-0.9.17 → python3_commons-0.9.19}/tests/test_msgspec.py +0 -0
|
@@ -21,16 +21,16 @@ jobs:
|
|
|
21
21
|
runs-on: ubuntu-latest
|
|
22
22
|
|
|
23
23
|
steps:
|
|
24
|
-
- uses: actions/checkout@
|
|
24
|
+
- uses: actions/checkout@v5
|
|
25
25
|
|
|
26
26
|
- name: Install uv
|
|
27
|
-
uses: astral-sh/setup-uv@
|
|
27
|
+
uses: astral-sh/setup-uv@v6
|
|
28
28
|
with:
|
|
29
29
|
enable-cache: true
|
|
30
30
|
cache-dependency-glob: "uv.lock"
|
|
31
31
|
|
|
32
32
|
- name: "Set up Python"
|
|
33
|
-
uses: actions/setup-python@
|
|
33
|
+
uses: actions/setup-python@v6
|
|
34
34
|
with:
|
|
35
35
|
python-version-file: "pyproject.toml"
|
|
36
36
|
|
|
@@ -26,16 +26,16 @@ jobs:
|
|
|
26
26
|
runs-on: ubuntu-latest
|
|
27
27
|
|
|
28
28
|
steps:
|
|
29
|
-
- uses: actions/checkout@
|
|
29
|
+
- uses: actions/checkout@v5
|
|
30
30
|
|
|
31
31
|
- name: Install uv
|
|
32
|
-
uses: astral-sh/setup-uv@
|
|
32
|
+
uses: astral-sh/setup-uv@v6
|
|
33
33
|
with:
|
|
34
34
|
enable-cache: true
|
|
35
35
|
cache-dependency-glob: "uv.lock"
|
|
36
36
|
|
|
37
37
|
- name: "Set up Python"
|
|
38
|
-
uses: actions/setup-python@
|
|
38
|
+
uses: actions/setup-python@v6
|
|
39
39
|
with:
|
|
40
40
|
python-version-file: "pyproject.toml"
|
|
41
41
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
repos:
|
|
2
2
|
- repo: https://github.com/astral-sh/uv-pre-commit
|
|
3
|
-
rev: 0.8.
|
|
3
|
+
rev: 0.8.20
|
|
4
4
|
hooks:
|
|
5
5
|
- id: uv-lock
|
|
6
6
|
# - id: uv-export
|
|
7
7
|
|
|
8
8
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
9
|
-
rev: v0.13.
|
|
9
|
+
rev: v0.13.1
|
|
10
10
|
hooks:
|
|
11
11
|
# Run the linter.
|
|
12
12
|
- id: ruff-check
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python3-commons
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.19
|
|
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
|
|
@@ -17,15 +17,15 @@ Requires-Dist: aiohttp[speedups]~=3.12.15
|
|
|
17
17
|
Requires-Dist: asyncpg~=0.30.0
|
|
18
18
|
Requires-Dist: fastapi-users-db-sqlalchemy~=7.0.0
|
|
19
19
|
Requires-Dist: fastapi-users[sqlalchemy]~=14.0.1
|
|
20
|
-
Requires-Dist: lxml~=6.0.
|
|
20
|
+
Requires-Dist: lxml~=6.0.2
|
|
21
21
|
Requires-Dist: msgpack~=1.1.1
|
|
22
22
|
Requires-Dist: msgspec~=0.19.0
|
|
23
|
-
Requires-Dist: pydantic[email]~=2.11.
|
|
23
|
+
Requires-Dist: pydantic[email]~=2.11.9
|
|
24
24
|
Requires-Dist: pydantic-settings~=2.10.1
|
|
25
25
|
Requires-Dist: python-jose==3.5.0
|
|
26
26
|
Requires-Dist: SQLAlchemy[asyncio]~=2.0.43
|
|
27
27
|
Requires-Dist: valkey[libvalkey]~=6.1.1
|
|
28
|
-
Requires-Dist: zeep~=4.3.
|
|
28
|
+
Requires-Dist: zeep~=4.3.2
|
|
29
29
|
Dynamic: license-file
|
|
30
30
|
|
|
31
31
|
Re-usable Python3 code
|
|
@@ -17,22 +17,21 @@ classifiers = [
|
|
|
17
17
|
]
|
|
18
18
|
keywords = []
|
|
19
19
|
requires-python = "==3.13.*"
|
|
20
|
-
|
|
21
20
|
dependencies = [
|
|
22
21
|
"aiobotocore~=2.24.2",
|
|
23
22
|
"aiohttp[speedups]~=3.12.15",
|
|
24
23
|
"asyncpg~=0.30.0",
|
|
25
24
|
"fastapi-users-db-sqlalchemy~=7.0.0",
|
|
26
25
|
"fastapi-users[sqlalchemy]~=14.0.1",
|
|
27
|
-
"lxml~=6.0.
|
|
26
|
+
"lxml~=6.0.2",
|
|
28
27
|
"msgpack~=1.1.1",
|
|
29
28
|
"msgspec~=0.19.0",
|
|
30
|
-
"pydantic[email]~=2.11.
|
|
29
|
+
"pydantic[email]~=2.11.9",
|
|
31
30
|
"pydantic-settings~=2.10.1",
|
|
32
31
|
"python-jose==3.5.0",
|
|
33
32
|
"SQLAlchemy[asyncio]~=2.0.43",
|
|
34
33
|
"valkey[libvalkey]~=6.1.1",
|
|
35
|
-
"zeep~=4.3.
|
|
34
|
+
"zeep~=4.3.2"
|
|
36
35
|
]
|
|
37
36
|
|
|
38
37
|
[dependency-groups]
|
|
@@ -51,7 +50,7 @@ testing = [
|
|
|
51
50
|
"pytest",
|
|
52
51
|
"pytest-asyncio",
|
|
53
52
|
"pytest-cov",
|
|
54
|
-
"pytest-mock"
|
|
53
|
+
"pytest-mock",
|
|
55
54
|
]
|
|
56
55
|
|
|
57
56
|
[project.urls]
|
|
@@ -67,13 +66,6 @@ exclude = ["tests"]
|
|
|
67
66
|
[tool.bdist_wheel]
|
|
68
67
|
universal = true
|
|
69
68
|
|
|
70
|
-
[tool.pytest.ini_options]
|
|
71
|
-
addopts = [
|
|
72
|
-
"--verbose"
|
|
73
|
-
]
|
|
74
|
-
norecursedirs = ["dist", "build", ".tox"]
|
|
75
|
-
testpaths = ["tests"]
|
|
76
|
-
|
|
77
69
|
[tool.ruff]
|
|
78
70
|
line-length = 120
|
|
79
71
|
indent-width = 4
|
|
@@ -82,8 +74,66 @@ target-version = "py313"
|
|
|
82
74
|
[tool.ruff.analyze]
|
|
83
75
|
detect-string-imports = true
|
|
84
76
|
|
|
77
|
+
[tool.ruff.lint]
|
|
78
|
+
select = [
|
|
79
|
+
"FAST",
|
|
80
|
+
"YTT",
|
|
81
|
+
# "ANN",
|
|
82
|
+
"I",
|
|
83
|
+
"E",
|
|
84
|
+
"W",
|
|
85
|
+
"PYI",
|
|
86
|
+
|
|
87
|
+
"A",
|
|
88
|
+
# "ARG",
|
|
89
|
+
"ASYNC",
|
|
90
|
+
# "B",
|
|
91
|
+
"C4",
|
|
92
|
+
# "DTZ",
|
|
93
|
+
"EM",
|
|
94
|
+
"EXE",
|
|
95
|
+
"F",
|
|
96
|
+
"FA",
|
|
97
|
+
"FBT",
|
|
98
|
+
"FLY",
|
|
99
|
+
"FURB",
|
|
100
|
+
# "G",
|
|
101
|
+
"ICN",
|
|
102
|
+
# "INP",
|
|
103
|
+
"ISC",
|
|
104
|
+
"LOG",
|
|
105
|
+
"N",
|
|
106
|
+
"NPY",
|
|
107
|
+
"PD",
|
|
108
|
+
"PERF",
|
|
109
|
+
"PIE",
|
|
110
|
+
"PLC",
|
|
111
|
+
"PLE",
|
|
112
|
+
# "PLR",
|
|
113
|
+
"PLW",
|
|
114
|
+
"PT",
|
|
115
|
+
"PTH",
|
|
116
|
+
"Q",
|
|
117
|
+
# "RET",
|
|
118
|
+
"RSE",
|
|
119
|
+
# "RUF",
|
|
120
|
+
# "S",
|
|
121
|
+
"SIM",
|
|
122
|
+
"SLF",
|
|
123
|
+
"SLOT",
|
|
124
|
+
"T20",
|
|
125
|
+
"TC",
|
|
126
|
+
"TID",
|
|
127
|
+
# "TRY",
|
|
128
|
+
"UP",
|
|
129
|
+
]
|
|
130
|
+
ignore = [
|
|
131
|
+
"ASYNC109",
|
|
132
|
+
]
|
|
133
|
+
|
|
85
134
|
[tool.ruff.lint.flake8-quotes]
|
|
86
135
|
docstring-quotes = "double"
|
|
136
|
+
inline-quotes = "single"
|
|
87
137
|
|
|
88
138
|
[tool.ruff.format]
|
|
89
139
|
exclude = ["*.pyi"]
|
|
@@ -95,3 +145,11 @@ venvPath = "."
|
|
|
95
145
|
venv = ".venv"
|
|
96
146
|
reportMatchNotExhaustive = "error"
|
|
97
147
|
reportUnnecessaryComparison = "error"
|
|
148
|
+
|
|
149
|
+
[tool.pytest.ini_options]
|
|
150
|
+
pythonpath = ["src"]
|
|
151
|
+
asyncio_mode = "strict"
|
|
152
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
153
|
+
addopts = "--cov biab_legal_mate --cov-fail-under=20 --cov-report term-missing --verbose"
|
|
154
|
+
norecursedirs = ["dist", "build", ".tox"]
|
|
155
|
+
testpaths = ["tests"]
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import errno
|
|
1
2
|
import logging
|
|
3
|
+
from collections.abc import AsyncGenerator, Mapping, Sequence
|
|
2
4
|
from contextlib import asynccontextmanager
|
|
3
5
|
from datetime import UTC, datetime
|
|
4
6
|
from enum import Enum
|
|
5
7
|
from http import HTTPStatus
|
|
6
8
|
from json import dumps
|
|
7
|
-
from typing import
|
|
9
|
+
from typing import Literal
|
|
8
10
|
from uuid import uuid4
|
|
9
11
|
|
|
10
12
|
from aiohttp import ClientResponse, ClientSession, ClientTimeout, client_exceptions
|
|
@@ -52,8 +54,8 @@ async def request(
|
|
|
52
54
|
date_path = now.strftime('%Y/%m/%d')
|
|
53
55
|
timestamp = now.strftime('%H%M%S_%f')
|
|
54
56
|
request_id = str(uuid4())[-12:]
|
|
55
|
-
uri_path = uri
|
|
56
|
-
uri_path = uri_path
|
|
57
|
+
uri_path = uri.removesuffix('/')
|
|
58
|
+
uri_path = uri_path.removeprefix('/')
|
|
57
59
|
url = f'{u[:-1] if (u := str(base_url)).endswith("/") else u}{uri}'
|
|
58
60
|
|
|
59
61
|
if audit_name:
|
|
@@ -90,15 +92,25 @@ async def request(
|
|
|
90
92
|
else:
|
|
91
93
|
match response.status:
|
|
92
94
|
case HTTPStatus.UNAUTHORIZED:
|
|
93
|
-
|
|
95
|
+
msg = 'Unauthorized'
|
|
96
|
+
|
|
97
|
+
raise PermissionError(msg)
|
|
94
98
|
case HTTPStatus.FORBIDDEN:
|
|
95
|
-
|
|
99
|
+
msg = 'Forbidden'
|
|
100
|
+
|
|
101
|
+
raise PermissionError(msg)
|
|
96
102
|
case HTTPStatus.NOT_FOUND:
|
|
97
|
-
|
|
103
|
+
msg = 'Not found'
|
|
104
|
+
|
|
105
|
+
raise LookupError(msg)
|
|
98
106
|
case HTTPStatus.BAD_REQUEST:
|
|
99
|
-
|
|
107
|
+
msg = 'Bad request'
|
|
108
|
+
|
|
109
|
+
raise ValueError(msg)
|
|
100
110
|
case HTTPStatus.TOO_MANY_REQUESTS:
|
|
101
|
-
|
|
111
|
+
msg = 'Too many requests'
|
|
112
|
+
|
|
113
|
+
raise InterruptedError(msg)
|
|
102
114
|
case _:
|
|
103
115
|
response.raise_for_status()
|
|
104
116
|
else:
|
|
@@ -116,13 +128,21 @@ async def request(
|
|
|
116
128
|
|
|
117
129
|
yield response
|
|
118
130
|
except client_exceptions.ClientConnectorError as e:
|
|
119
|
-
|
|
131
|
+
msg = 'Cient connection error'
|
|
132
|
+
|
|
133
|
+
raise ConnectionRefusedError(msg) from e
|
|
120
134
|
except client_exceptions.ClientOSError as e:
|
|
121
|
-
if e.errno ==
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
raise ConnectionResetError(
|
|
135
|
+
if e.errno == errno.EPIPE:
|
|
136
|
+
msg = 'Broken pipe'
|
|
137
|
+
|
|
138
|
+
raise ConnectionResetError(msg) from e
|
|
139
|
+
elif e.errno == errno.ECONNRESET:
|
|
140
|
+
msg = 'Connection reset by peer'
|
|
141
|
+
|
|
142
|
+
raise ConnectionResetError(msg) from e
|
|
125
143
|
|
|
126
144
|
raise
|
|
127
145
|
except client_exceptions.ServerDisconnectedError as e:
|
|
128
|
-
|
|
146
|
+
msg = 'Server disconnected'
|
|
147
|
+
|
|
148
|
+
raise ConnectionResetError(msg) from e
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from collections.abc import Callable, Coroutine, MutableMapping, Sequence
|
|
2
3
|
from http import HTTPStatus
|
|
3
|
-
from typing import Annotated, Any,
|
|
4
|
+
from typing import Annotated, Any, TypeVar
|
|
4
5
|
|
|
5
6
|
import aiohttp
|
|
6
7
|
import msgspec
|
|
@@ -21,10 +22,7 @@ class TokenData(msgspec.Struct):
|
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
T = TypeVar('T', bound=TokenData)
|
|
24
|
-
|
|
25
25
|
OIDC_CONFIG_URL = f'{oidc_settings.authority_url}/.well-known/openid-configuration'
|
|
26
|
-
_JWKS: dict | None = None
|
|
27
|
-
|
|
28
26
|
bearer_security = HTTPBearer(auto_error=oidc_settings.enabled)
|
|
29
27
|
|
|
30
28
|
|
|
@@ -32,51 +30,52 @@ async def fetch_openid_config() -> dict:
|
|
|
32
30
|
"""
|
|
33
31
|
Fetch the OpenID configuration (including JWKS URI) from OIDC authority.
|
|
34
32
|
"""
|
|
35
|
-
async with aiohttp.ClientSession() as session:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
33
|
+
async with aiohttp.ClientSession() as session, session.get(OIDC_CONFIG_URL) as response:
|
|
34
|
+
if response.status != HTTPStatus.OK:
|
|
35
|
+
raise HTTPException(
|
|
36
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail='Failed to fetch OpenID configuration'
|
|
37
|
+
)
|
|
41
38
|
|
|
42
|
-
|
|
39
|
+
return await response.json()
|
|
43
40
|
|
|
44
41
|
|
|
45
42
|
async def fetch_jwks(jwks_uri: str) -> dict:
|
|
46
43
|
"""
|
|
47
44
|
Fetch the JSON Web Key Set (JWKS) for validating the token's signature.
|
|
48
45
|
"""
|
|
49
|
-
async with aiohttp.ClientSession() as session:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail='Failed to fetch JWKS')
|
|
46
|
+
async with aiohttp.ClientSession() as session, session.get(jwks_uri) as response:
|
|
47
|
+
if response.status != HTTPStatus.OK:
|
|
48
|
+
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail='Failed to fetch JWKS')
|
|
53
49
|
|
|
54
|
-
|
|
50
|
+
return await response.json()
|
|
55
51
|
|
|
56
52
|
|
|
57
|
-
def get_token_verifier
|
|
53
|
+
def get_token_verifier[T](
|
|
54
|
+
token_cls: type[T],
|
|
55
|
+
jwks: MutableMapping,
|
|
56
|
+
) -> Callable[[HTTPAuthorizationCredentials], Coroutine[Any, Any, T | None]]:
|
|
58
57
|
async def get_verified_token(
|
|
59
58
|
authorization: Annotated[HTTPAuthorizationCredentials, Depends(bearer_security)],
|
|
60
59
|
) -> T | None:
|
|
61
60
|
"""
|
|
62
61
|
Verify the JWT access token using OIDC authority JWKS.
|
|
63
62
|
"""
|
|
64
|
-
global _JWKS
|
|
65
|
-
|
|
66
63
|
if not oidc_settings.enabled:
|
|
67
64
|
return None
|
|
68
65
|
|
|
69
66
|
token = authorization.credentials
|
|
70
67
|
|
|
71
68
|
try:
|
|
72
|
-
if not
|
|
69
|
+
if not jwks:
|
|
73
70
|
openid_config = await fetch_openid_config()
|
|
74
|
-
|
|
71
|
+
_jwks = await fetch_jwks(openid_config['jwks_uri'])
|
|
72
|
+
jwks.clear()
|
|
73
|
+
jwks.update(_jwks)
|
|
75
74
|
|
|
76
75
|
if oidc_settings.client_id:
|
|
77
|
-
payload = jwt.decode(token,
|
|
76
|
+
payload = jwt.decode(token, jwks, algorithms=['RS256'], audience=oidc_settings.client_id)
|
|
78
77
|
else:
|
|
79
|
-
payload = jwt.decode(token,
|
|
78
|
+
payload = jwt.decode(token, jwks, algorithms=['RS256'])
|
|
80
79
|
|
|
81
80
|
token_data = token_cls(**payload)
|
|
82
81
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import socket
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
3
4
|
from platform import platform
|
|
4
|
-
from typing import Any
|
|
5
|
+
from typing import Any
|
|
5
6
|
|
|
6
7
|
import valkey
|
|
7
8
|
from pydantic import RedisDsn
|
|
@@ -34,7 +35,7 @@ class AsyncValkeyClient(metaclass=SingletonMeta):
|
|
|
34
35
|
|
|
35
36
|
@staticmethod
|
|
36
37
|
def _get_keepalive_options():
|
|
37
|
-
if platform
|
|
38
|
+
if platform in {'linux', 'darwin'}:
|
|
38
39
|
return {socket.TCP_KEEPIDLE: 10, socket.TCP_KEEPINTVL: 5, socket.TCP_KEEPCNT: 5}
|
|
39
40
|
else:
|
|
40
41
|
return {}
|
|
@@ -88,7 +89,7 @@ async def delete(*names: str | bytes | memoryview):
|
|
|
88
89
|
await get_valkey_client().delete(*names)
|
|
89
90
|
|
|
90
91
|
|
|
91
|
-
async def store_bytes(name: str, data: bytes, ttl: int = None, if_not_set: bool = False):
|
|
92
|
+
async def store_bytes(name: str, data: bytes, ttl: int = None, *, if_not_set: bool = False):
|
|
92
93
|
r = get_valkey_client()
|
|
93
94
|
|
|
94
95
|
return await r.set(name, data, ex=ttl, nx=if_not_set)
|
|
@@ -100,8 +101,8 @@ async def get_bytes(name: str) -> bytes | None:
|
|
|
100
101
|
return await r.get(name)
|
|
101
102
|
|
|
102
103
|
|
|
103
|
-
async def store(name: str, obj: Any, ttl: int = None, if_not_set: bool = False):
|
|
104
|
-
return await store_bytes(name, serialize_msgpack_native(obj), ttl, if_not_set)
|
|
104
|
+
async def store(name: str, obj: Any, ttl: int = None, *, if_not_set: bool = False):
|
|
105
|
+
return await store_bytes(name, serialize_msgpack_native(obj), ttl, if_not_set=if_not_set)
|
|
105
106
|
|
|
106
107
|
|
|
107
108
|
async def get(name: str, default=None, data_type: Any = None) -> Any:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import logging
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import AsyncGenerator, Callable, Mapping
|
|
4
4
|
|
|
5
5
|
from sqlalchemy import MetaData
|
|
6
6
|
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_engine_from_config
|
|
@@ -63,8 +63,8 @@ class AsyncSessionManager:
|
|
|
63
63
|
|
|
64
64
|
return session_maker
|
|
65
65
|
|
|
66
|
-
def get_async_session(self, name: str) -> Callable[[], AsyncGenerator[AsyncSession
|
|
67
|
-
async def get_session() -> AsyncGenerator[AsyncSession
|
|
66
|
+
def get_async_session(self, name: str) -> Callable[[], AsyncGenerator[AsyncSession]]:
|
|
67
|
+
async def get_session() -> AsyncGenerator[AsyncSession]:
|
|
68
68
|
session_maker = self.get_session_maker(name)
|
|
69
69
|
|
|
70
70
|
async with session_maker() as session:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from
|
|
2
|
+
from collections.abc import Mapping
|
|
3
3
|
|
|
4
4
|
import sqlalchemy as sa
|
|
5
5
|
from sqlalchemy import asc, desc, func
|
|
@@ -26,11 +26,12 @@ def get_query(
|
|
|
26
26
|
for order_by_col in order_by.split(','):
|
|
27
27
|
if order_by_col.startswith('-'):
|
|
28
28
|
direction = desc
|
|
29
|
-
|
|
29
|
+
order_by_col_clean = order_by_col[1:]
|
|
30
30
|
else:
|
|
31
31
|
direction = asc
|
|
32
|
+
order_by_col_clean = order_by_col
|
|
32
33
|
|
|
33
|
-
order_by_cols[
|
|
34
|
+
order_by_cols[order_by_col_clean] = direction
|
|
34
35
|
|
|
35
36
|
order_by_clauses = tuple(
|
|
36
37
|
direction(columns[order_by_col][0]) for order_by_col, direction in order_by_cols.items()
|
|
@@ -54,9 +55,6 @@ def get_query(
|
|
|
54
55
|
else:
|
|
55
56
|
where_parts = None
|
|
56
57
|
|
|
57
|
-
if where_parts
|
|
58
|
-
where_clause = sa.and_(*where_parts)
|
|
59
|
-
else:
|
|
60
|
-
where_clause = None
|
|
58
|
+
where_clause = sa.and_(*where_parts) if where_parts else None
|
|
61
59
|
|
|
62
60
|
return where_clause, order_by_clauses
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
1
2
|
from pathlib import Path
|
|
2
|
-
from typing import Generator
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
def iter_files(root: Path, recursive: bool = True) -> Generator[Path
|
|
5
|
+
def iter_files(root: Path, *, recursive: bool = True) -> Generator[Path]:
|
|
6
6
|
for item in root.iterdir():
|
|
7
7
|
if item.is_file():
|
|
8
8
|
yield item
|
|
@@ -6,11 +6,12 @@ import threading
|
|
|
6
6
|
import time
|
|
7
7
|
from abc import ABCMeta
|
|
8
8
|
from collections import defaultdict
|
|
9
|
+
from collections.abc import Mapping, Sequence
|
|
9
10
|
from datetime import date, datetime, timedelta
|
|
10
11
|
from decimal import ROUND_HALF_UP, Decimal
|
|
11
12
|
from http.cookies import BaseCookie
|
|
12
13
|
from json import dumps
|
|
13
|
-
from typing import Literal
|
|
14
|
+
from typing import Literal
|
|
14
15
|
from urllib.parse import urlencode
|
|
15
16
|
|
|
16
17
|
from python3_commons.serializers.json import CustomJSONEncoder
|
|
@@ -34,7 +35,7 @@ class SingletonMeta(ABCMeta):
|
|
|
34
35
|
try:
|
|
35
36
|
return cls.__instances[cls]
|
|
36
37
|
except KeyError:
|
|
37
|
-
instance = super(
|
|
38
|
+
instance = super().__call__(*args, **kwargs)
|
|
38
39
|
cls.__instances[cls] = instance
|
|
39
40
|
|
|
40
41
|
return instance
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import io
|
|
4
3
|
import logging
|
|
5
4
|
from contextlib import asynccontextmanager
|
|
6
|
-
from
|
|
7
|
-
from typing import TYPE_CHECKING, AsyncGenerator, Iterable, Mapping, Sequence
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
8
6
|
|
|
9
7
|
import aiobotocore.session
|
|
10
|
-
from aiobotocore.response import StreamingBody
|
|
11
8
|
from botocore.config import Config
|
|
12
9
|
|
|
13
10
|
if TYPE_CHECKING:
|
|
11
|
+
import io
|
|
12
|
+
from collections.abc import AsyncGenerator, Iterable, Mapping, Sequence
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
from aiobotocore.response import StreamingBody
|
|
14
16
|
from types_aiobotocore_s3.client import S3Client
|
|
15
17
|
|
|
16
18
|
from python3_commons.conf import S3Settings, s3_settings
|
|
@@ -41,14 +43,13 @@ class ObjectStorage(metaclass=SingletonMeta):
|
|
|
41
43
|
self._config = config
|
|
42
44
|
|
|
43
45
|
@asynccontextmanager
|
|
44
|
-
async def get_client(self) -> AsyncGenerator[S3Client
|
|
46
|
+
async def get_client(self) -> AsyncGenerator[S3Client]:
|
|
45
47
|
async with self._session.create_client('s3', **self._config) as client:
|
|
46
48
|
yield client
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
def get_absolute_path(path: str) -> str:
|
|
50
|
-
|
|
51
|
-
path = path[1:]
|
|
52
|
+
path = path.removeprefix('/')
|
|
52
53
|
|
|
53
54
|
if bucket_root := s3_settings.s3_bucket_root:
|
|
54
55
|
path = f'{bucket_root[:1] if bucket_root.startswith("/") else bucket_root}/{path}'
|
|
@@ -102,7 +103,7 @@ async def get_object(bucket_name: str, path: str) -> bytes:
|
|
|
102
103
|
return body
|
|
103
104
|
|
|
104
105
|
|
|
105
|
-
async def list_objects(bucket_name: str, prefix: str, recursive: bool = True) -> AsyncGenerator[Mapping
|
|
106
|
+
async def list_objects(bucket_name: str, prefix: str, *, recursive: bool = True) -> AsyncGenerator[Mapping]:
|
|
106
107
|
storage = ObjectStorage(s3_settings)
|
|
107
108
|
|
|
108
109
|
async with storage.get_client() as s3_client:
|
|
@@ -117,9 +118,9 @@ async def list_objects(bucket_name: str, prefix: str, recursive: bool = True) ->
|
|
|
117
118
|
|
|
118
119
|
|
|
119
120
|
async def get_object_streams(
|
|
120
|
-
bucket_name: str, path: str, recursive: bool = True
|
|
121
|
-
) -> AsyncGenerator[tuple[str, datetime, StreamingBody]
|
|
122
|
-
async for obj in list_objects(bucket_name, path, recursive):
|
|
121
|
+
bucket_name: str, path: str, *, recursive: bool = True
|
|
122
|
+
) -> AsyncGenerator[tuple[str, datetime, StreamingBody]]:
|
|
123
|
+
async for obj in list_objects(bucket_name, path, recursive=recursive):
|
|
123
124
|
object_name = obj['Key']
|
|
124
125
|
last_modified = obj['LastModified']
|
|
125
126
|
|
|
@@ -128,9 +129,9 @@ async def get_object_streams(
|
|
|
128
129
|
|
|
129
130
|
|
|
130
131
|
async def get_objects(
|
|
131
|
-
bucket_name: str, path: str, recursive: bool = True
|
|
132
|
-
) -> AsyncGenerator[tuple[str, datetime, bytes]
|
|
133
|
-
async for object_name, last_modified, stream in get_object_streams(bucket_name, path, recursive):
|
|
132
|
+
bucket_name: str, path: str, *, recursive: bool = True
|
|
133
|
+
) -> AsyncGenerator[tuple[str, datetime, bytes]]:
|
|
134
|
+
async for object_name, last_modified, stream in get_object_streams(bucket_name, path, recursive=recursive):
|
|
134
135
|
data = await stream.read()
|
|
135
136
|
|
|
136
137
|
yield object_name, last_modified, data
|
|
@@ -155,13 +156,12 @@ async def remove_objects(
|
|
|
155
156
|
storage = ObjectStorage(s3_settings)
|
|
156
157
|
|
|
157
158
|
async with storage.get_client() as s3_client:
|
|
158
|
-
objects_to_delete = []
|
|
159
|
-
|
|
160
159
|
if prefix:
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
objects_to_delete = tuple(
|
|
161
|
+
{'Key': obj['Key']} async for obj in list_objects(bucket_name, prefix, recursive=True)
|
|
162
|
+
)
|
|
163
163
|
elif object_names:
|
|
164
|
-
objects_to_delete =
|
|
164
|
+
objects_to_delete = tuple({'Key': name} for name in object_names)
|
|
165
165
|
else:
|
|
166
166
|
return None
|
|
167
167
|
|
|
@@ -10,11 +10,9 @@ from typing import Any
|
|
|
10
10
|
class CustomJSONEncoder(json.JSONEncoder):
|
|
11
11
|
def default(self, o) -> Any:
|
|
12
12
|
try:
|
|
13
|
-
return super(
|
|
13
|
+
return super().default(o)
|
|
14
14
|
except TypeError:
|
|
15
|
-
if isinstance(o, datetime):
|
|
16
|
-
return o.isoformat()
|
|
17
|
-
elif isinstance(o, date):
|
|
15
|
+
if isinstance(o, (datetime, date)):
|
|
18
16
|
return o.isoformat()
|
|
19
17
|
elif isinstance(o, bytes):
|
|
20
18
|
return base64.b64encode(o).decode('ascii')
|