python3-commons 0.9.18__tar.gz → 0.9.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.
Potentially problematic release.
This version of python3-commons might be problematic. Click here for more details.
- {python3_commons-0.9.18 → python3_commons-0.9.20}/.github/workflows/python-publish.yaml +3 -3
- {python3_commons-0.9.18 → python3_commons-0.9.20}/.github/workflows/release-on-tag-push.yml +3 -3
- {python3_commons-0.9.18 → python3_commons-0.9.20}/.pre-commit-config.yaml +2 -2
- {python3_commons-0.9.18 → python3_commons-0.9.20}/PKG-INFO +4 -4
- {python3_commons-0.9.18 → python3_commons-0.9.20}/pyproject.toml +73 -12
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/api_client.py +34 -14
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/audit.py +2 -2
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/auth.py +27 -28
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/cache.py +28 -27
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/db/__init__.py +6 -6
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/db/helpers.py +5 -7
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/fs.py +2 -2
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/helpers.py +5 -4
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/object_storage.py +27 -28
- python3_commons-0.9.20/src/python3_commons/serializers/common.py +8 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/serializers/json.py +2 -4
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/serializers/msgpack.py +14 -12
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/serializers/msgspec.py +26 -17
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons.egg-info/PKG-INFO +4 -4
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons.egg-info/SOURCES.txt +2 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons.egg-info/requires.txt +3 -3
- python3_commons-0.9.20/tests/__init__.py +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/tests/conftest.py +11 -9
- {python3_commons-0.9.18 → python3_commons-0.9.20}/tests/test_msgpack.py +2 -2
- {python3_commons-0.9.18 → python3_commons-0.9.20}/uv.lock +132 -122
- {python3_commons-0.9.18 → python3_commons-0.9.20}/.coveragerc +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/.gitignore +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/.python-version +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/AUTHORS.rst +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/CHANGELOG.rst +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/LICENSE +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/README.md +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/README.rst +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/docs/Makefile +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/docs/_static/.gitignore +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/docs/authors.rst +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/docs/changelog.rst +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/docs/conf.py +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/docs/index.rst +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/docs/license.rst +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/setup.cfg +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/__init__.py +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/conf.py +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/db/models/__init__.py +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/db/models/auth.py +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/db/models/common.py +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/db/models/rbac.py +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/log/__init__.py +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/log/filters.py +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/log/formatters.py +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/permissions.py +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/serializers/__init__.py +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons.egg-info/dependency_links.txt +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons.egg-info/top_level.txt +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/tests/test_audit.py +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/tests/test_cache.py +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/tests/test_helpers.py +0 -0
- {python3_commons-0.9.18 → python3_commons-0.9.20}/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.20
|
|
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,69 @@ 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
|
+
|
|
134
|
+
[tool.ruff.lint.per-file-ignores]
|
|
135
|
+
"tests/*.py" = ["S101"]
|
|
136
|
+
|
|
85
137
|
[tool.ruff.lint.flake8-quotes]
|
|
86
138
|
docstring-quotes = "double"
|
|
139
|
+
inline-quotes = "single"
|
|
87
140
|
|
|
88
141
|
[tool.ruff.format]
|
|
89
142
|
exclude = ["*.pyi"]
|
|
@@ -95,3 +148,11 @@ venvPath = "."
|
|
|
95
148
|
venv = ".venv"
|
|
96
149
|
reportMatchNotExhaustive = "error"
|
|
97
150
|
reportUnnecessaryComparison = "error"
|
|
151
|
+
|
|
152
|
+
[tool.pytest.ini_options]
|
|
153
|
+
pythonpath = ["src"]
|
|
154
|
+
asyncio_mode = "strict"
|
|
155
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
156
|
+
addopts = "--cov biab_legal_mate --cov-fail-under=20 --cov-report term-missing --verbose"
|
|
157
|
+
norecursedirs = ["dist", "build", ".tox"]
|
|
158
|
+
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
|
|
@@ -143,8 +143,8 @@ async def write_audit_data(settings: S3Settings, key: str, data: bytes):
|
|
|
143
143
|
absolute_path = object_storage.get_absolute_path(f'audit/{key}')
|
|
144
144
|
|
|
145
145
|
await object_storage.put_object(settings.s3_bucket, absolute_path, io.BytesIO(data), len(data))
|
|
146
|
-
except Exception
|
|
147
|
-
logger.
|
|
146
|
+
except Exception:
|
|
147
|
+
logger.exception('Failed storing object in storage.')
|
|
148
148
|
else:
|
|
149
149
|
logger.debug(f'Stored object in storage: {key}')
|
|
150
150
|
else:
|
|
@@ -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,58 +30,59 @@ 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
|
-
|
|
83
|
-
|
|
84
|
-
except jwt.ExpiredSignatureError:
|
|
85
|
-
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail='Token has expired')
|
|
81
|
+
except jwt.ExpiredSignatureError as e:
|
|
82
|
+
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail='Token has expired') from e
|
|
86
83
|
except JWTError as e:
|
|
87
|
-
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=f'Token is invalid: {str(e)}')
|
|
84
|
+
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=f'Token is invalid: {str(e)}') from e
|
|
85
|
+
|
|
86
|
+
return token_data
|
|
88
87
|
|
|
89
88
|
return get_verified_token
|
|
@@ -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:
|
|
@@ -130,8 +131,8 @@ async def store_sequence(name: str, data: Sequence, ttl: int = None):
|
|
|
130
131
|
|
|
131
132
|
if ttl:
|
|
132
133
|
await r.expire(name, ttl)
|
|
133
|
-
except valkey.exceptions.ConnectionError
|
|
134
|
-
logger.
|
|
134
|
+
except valkey.exceptions.ConnectionError:
|
|
135
|
+
logger.exception('Failed to store sequence in cache.')
|
|
135
136
|
|
|
136
137
|
|
|
137
138
|
async def get_sequence(name: str, _type: type = list) -> Sequence:
|
|
@@ -150,8 +151,8 @@ async def store_dict(name: str, data: Mapping, ttl: int = None):
|
|
|
150
151
|
|
|
151
152
|
if ttl:
|
|
152
153
|
await r.expire(name, ttl)
|
|
153
|
-
except valkey.exceptions.ConnectionError
|
|
154
|
-
logger.
|
|
154
|
+
except valkey.exceptions.ConnectionError:
|
|
155
|
+
logger.exception('Failed to store dict in cache.')
|
|
155
156
|
|
|
156
157
|
|
|
157
158
|
async def get_dict(name: str, value_data_type=None) -> dict | None:
|
|
@@ -174,8 +175,8 @@ async def set_dict(name: str, mapping: dict, ttl: int = None):
|
|
|
174
175
|
|
|
175
176
|
if ttl:
|
|
176
177
|
await r.expire(name, ttl)
|
|
177
|
-
except valkey.exceptions.ConnectionError
|
|
178
|
-
logger.
|
|
178
|
+
except valkey.exceptions.ConnectionError:
|
|
179
|
+
logger.exception('Failed to set dict in cache.')
|
|
179
180
|
|
|
180
181
|
|
|
181
182
|
async def get_dict_item(name: str, key: str, data_type=None, default=None):
|
|
@@ -184,28 +185,28 @@ async def get_dict_item(name: str, key: str, data_type=None, default=None):
|
|
|
184
185
|
|
|
185
186
|
if data := await r.hget(name, key):
|
|
186
187
|
return deserialize_msgpack_native(data, data_type)
|
|
188
|
+
except valkey.exceptions.ConnectionError:
|
|
189
|
+
logger.exception('Failed to get dict item from cache.')
|
|
187
190
|
|
|
188
|
-
return
|
|
189
|
-
except valkey.exceptions.ConnectionError as e:
|
|
190
|
-
logger.error(f'Failed to get dict item from cache: {e}')
|
|
191
|
+
return None
|
|
191
192
|
|
|
192
|
-
return
|
|
193
|
+
return default
|
|
193
194
|
|
|
194
195
|
|
|
195
196
|
async def set_dict_item(name: str, key: str, obj: Any):
|
|
196
197
|
try:
|
|
197
198
|
r = get_valkey_client()
|
|
198
199
|
await r.hset(name, key, serialize_msgpack_native(obj))
|
|
199
|
-
except valkey.exceptions.ConnectionError
|
|
200
|
-
logger.
|
|
200
|
+
except valkey.exceptions.ConnectionError:
|
|
201
|
+
logger.exception('Failed to set dict item in cache.')
|
|
201
202
|
|
|
202
203
|
|
|
203
204
|
async def delete_dict_item(name: str, *keys):
|
|
204
205
|
try:
|
|
205
206
|
r = get_valkey_client()
|
|
206
207
|
await r.hdel(name, *keys)
|
|
207
|
-
except valkey.exceptions.ConnectionError
|
|
208
|
-
logger.
|
|
208
|
+
except valkey.exceptions.ConnectionError:
|
|
209
|
+
logger.exception('Failed to delete dict item from cache.')
|
|
209
210
|
|
|
210
211
|
|
|
211
212
|
async def store_set(name: str, value: set, ttl: int = None):
|
|
@@ -215,8 +216,8 @@ async def store_set(name: str, value: set, ttl: int = None):
|
|
|
215
216
|
|
|
216
217
|
if ttl:
|
|
217
218
|
await r.expire(name, ttl)
|
|
218
|
-
except valkey.exceptions.ConnectionError
|
|
219
|
-
logger.
|
|
219
|
+
except valkey.exceptions.ConnectionError:
|
|
220
|
+
logger.exception('Failed to store set in cache.')
|
|
220
221
|
|
|
221
222
|
|
|
222
223
|
async def has_set_item(name: str, value: str) -> bool:
|
|
@@ -224,8 +225,8 @@ async def has_set_item(name: str, value: str) -> bool:
|
|
|
224
225
|
r = get_valkey_client()
|
|
225
226
|
|
|
226
227
|
return await r.sismember(name, serialize_msgpack_native(value)) == 1
|
|
227
|
-
except valkey.exceptions.ConnectionError
|
|
228
|
-
logger.
|
|
228
|
+
except valkey.exceptions.ConnectionError:
|
|
229
|
+
logger.exception('Failed to check if set has item in cache.')
|
|
229
230
|
|
|
230
231
|
return False
|
|
231
232
|
|
|
@@ -234,8 +235,8 @@ async def add_set_item(name: str, *values: str):
|
|
|
234
235
|
try:
|
|
235
236
|
r = get_valkey_client()
|
|
236
237
|
await r.sadd(name, *map(serialize_msgpack_native, values))
|
|
237
|
-
except valkey.exceptions.ConnectionError
|
|
238
|
-
logger.
|
|
238
|
+
except valkey.exceptions.ConnectionError:
|
|
239
|
+
logger.exception('Failed to add set item into cache.')
|
|
239
240
|
|
|
240
241
|
|
|
241
242
|
async def delete_set_item(name: str, value: str):
|
|
@@ -249,8 +250,8 @@ async def get_set_members(name: str) -> set[str] | None:
|
|
|
249
250
|
smembers = await r.smembers(name)
|
|
250
251
|
|
|
251
252
|
return set(map(deserialize_msgpack_native, smembers))
|
|
252
|
-
except valkey.exceptions.ConnectionError
|
|
253
|
-
logger.
|
|
253
|
+
except valkey.exceptions.ConnectionError:
|
|
254
|
+
logger.exception('Failed to get set members from cache.')
|
|
254
255
|
|
|
255
256
|
return None
|
|
256
257
|
|
|
@@ -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
|
|
@@ -24,7 +24,7 @@ class AsyncSessionManager:
|
|
|
24
24
|
try:
|
|
25
25
|
return self.db_settings[name]
|
|
26
26
|
except KeyError:
|
|
27
|
-
logger.
|
|
27
|
+
logger.exception(f'Missing database settings: {name}')
|
|
28
28
|
|
|
29
29
|
raise
|
|
30
30
|
|
|
@@ -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:
|
|
@@ -83,7 +83,7 @@ async def is_healthy(engine: AsyncEngine) -> bool:
|
|
|
83
83
|
result = await conn.execute('SELECT 1;')
|
|
84
84
|
|
|
85
85
|
return result.scalar() == 1
|
|
86
|
-
except Exception
|
|
87
|
-
logger.
|
|
86
|
+
except Exception:
|
|
87
|
+
logger.exception('Database connection is not healthy.')
|
|
88
88
|
|
|
89
89
|
return False
|
|
@@ -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
|