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.

Files changed (58) hide show
  1. {python3_commons-0.9.18 → python3_commons-0.9.20}/.github/workflows/python-publish.yaml +3 -3
  2. {python3_commons-0.9.18 → python3_commons-0.9.20}/.github/workflows/release-on-tag-push.yml +3 -3
  3. {python3_commons-0.9.18 → python3_commons-0.9.20}/.pre-commit-config.yaml +2 -2
  4. {python3_commons-0.9.18 → python3_commons-0.9.20}/PKG-INFO +4 -4
  5. {python3_commons-0.9.18 → python3_commons-0.9.20}/pyproject.toml +73 -12
  6. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/api_client.py +34 -14
  7. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/audit.py +2 -2
  8. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/auth.py +27 -28
  9. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/cache.py +28 -27
  10. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/db/__init__.py +6 -6
  11. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/db/helpers.py +5 -7
  12. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/fs.py +2 -2
  13. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/helpers.py +5 -4
  14. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/object_storage.py +27 -28
  15. python3_commons-0.9.20/src/python3_commons/serializers/common.py +8 -0
  16. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/serializers/json.py +2 -4
  17. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/serializers/msgpack.py +14 -12
  18. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/serializers/msgspec.py +26 -17
  19. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons.egg-info/PKG-INFO +4 -4
  20. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons.egg-info/SOURCES.txt +2 -0
  21. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons.egg-info/requires.txt +3 -3
  22. python3_commons-0.9.20/tests/__init__.py +0 -0
  23. {python3_commons-0.9.18 → python3_commons-0.9.20}/tests/conftest.py +11 -9
  24. {python3_commons-0.9.18 → python3_commons-0.9.20}/tests/test_msgpack.py +2 -2
  25. {python3_commons-0.9.18 → python3_commons-0.9.20}/uv.lock +132 -122
  26. {python3_commons-0.9.18 → python3_commons-0.9.20}/.coveragerc +0 -0
  27. {python3_commons-0.9.18 → python3_commons-0.9.20}/.gitignore +0 -0
  28. {python3_commons-0.9.18 → python3_commons-0.9.20}/.python-version +0 -0
  29. {python3_commons-0.9.18 → python3_commons-0.9.20}/AUTHORS.rst +0 -0
  30. {python3_commons-0.9.18 → python3_commons-0.9.20}/CHANGELOG.rst +0 -0
  31. {python3_commons-0.9.18 → python3_commons-0.9.20}/LICENSE +0 -0
  32. {python3_commons-0.9.18 → python3_commons-0.9.20}/README.md +0 -0
  33. {python3_commons-0.9.18 → python3_commons-0.9.20}/README.rst +0 -0
  34. {python3_commons-0.9.18 → python3_commons-0.9.20}/docs/Makefile +0 -0
  35. {python3_commons-0.9.18 → python3_commons-0.9.20}/docs/_static/.gitignore +0 -0
  36. {python3_commons-0.9.18 → python3_commons-0.9.20}/docs/authors.rst +0 -0
  37. {python3_commons-0.9.18 → python3_commons-0.9.20}/docs/changelog.rst +0 -0
  38. {python3_commons-0.9.18 → python3_commons-0.9.20}/docs/conf.py +0 -0
  39. {python3_commons-0.9.18 → python3_commons-0.9.20}/docs/index.rst +0 -0
  40. {python3_commons-0.9.18 → python3_commons-0.9.20}/docs/license.rst +0 -0
  41. {python3_commons-0.9.18 → python3_commons-0.9.20}/setup.cfg +0 -0
  42. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/__init__.py +0 -0
  43. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/conf.py +0 -0
  44. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/db/models/__init__.py +0 -0
  45. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/db/models/auth.py +0 -0
  46. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/db/models/common.py +0 -0
  47. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/db/models/rbac.py +0 -0
  48. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/log/__init__.py +0 -0
  49. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/log/filters.py +0 -0
  50. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/log/formatters.py +0 -0
  51. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/permissions.py +0 -0
  52. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons/serializers/__init__.py +0 -0
  53. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons.egg-info/dependency_links.txt +0 -0
  54. {python3_commons-0.9.18 → python3_commons-0.9.20}/src/python3_commons.egg-info/top_level.txt +0 -0
  55. {python3_commons-0.9.18 → python3_commons-0.9.20}/tests/test_audit.py +0 -0
  56. {python3_commons-0.9.18 → python3_commons-0.9.20}/tests/test_cache.py +0 -0
  57. {python3_commons-0.9.18 → python3_commons-0.9.20}/tests/test_helpers.py +0 -0
  58. {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@v4
24
+ - uses: actions/checkout@v5
25
25
 
26
26
  - name: Install uv
27
- uses: astral-sh/setup-uv@v5
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@v5
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@v4
29
+ - uses: actions/checkout@v5
30
30
 
31
31
  - name: Install uv
32
- uses: astral-sh/setup-uv@v5
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@v5
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.16
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.0
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.18
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.1
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.7
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.1
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.1",
26
+ "lxml~=6.0.2",
28
27
  "msgpack~=1.1.1",
29
28
  "msgspec~=0.19.0",
30
- "pydantic[email]~=2.11.7",
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.1"
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 AsyncGenerator, Literal, Mapping, Sequence
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[:-1] if uri.endswith('/') else uri
56
- uri_path = uri_path[1:] if uri_path.startswith('/') else 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
- raise PermissionError('Unauthorized')
95
+ msg = 'Unauthorized'
96
+
97
+ raise PermissionError(msg)
94
98
  case HTTPStatus.FORBIDDEN:
95
- raise PermissionError('Forbidden')
99
+ msg = 'Forbidden'
100
+
101
+ raise PermissionError(msg)
96
102
  case HTTPStatus.NOT_FOUND:
97
- raise LookupError('Not found')
103
+ msg = 'Not found'
104
+
105
+ raise LookupError(msg)
98
106
  case HTTPStatus.BAD_REQUEST:
99
- raise ValueError('Bad request')
107
+ msg = 'Bad request'
108
+
109
+ raise ValueError(msg)
100
110
  case HTTPStatus.TOO_MANY_REQUESTS:
101
- raise InterruptedError('Too many requests')
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
- raise ConnectionRefusedError('Cient connection error') from e
131
+ msg = 'Cient connection error'
132
+
133
+ raise ConnectionRefusedError(msg) from e
120
134
  except client_exceptions.ClientOSError as e:
121
- if e.errno == 32:
122
- raise ConnectionResetError('Broken pipe') from e
123
- elif e.errno == 104:
124
- raise ConnectionResetError('Connection reset by peer') from e
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
- raise ConnectionResetError('Server disconnected') from e
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 as e:
147
- logger.error(f'Failed storing object in storage: {e}')
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, Callable, Coroutine, Sequence, Type, TypeVar
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
- async with session.get(OIDC_CONFIG_URL) as response:
37
- if response.status != HTTPStatus.OK:
38
- raise HTTPException(
39
- status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail='Failed to fetch OpenID configuration'
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
- return await response.json()
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
- async with session.get(jwks_uri) as response:
51
- if response.status != HTTPStatus.OK:
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
- return await response.json()
50
+ return await response.json()
55
51
 
56
52
 
57
- def get_token_verifier(token_cls: Type[T]) -> Callable[[HTTPAuthorizationCredentials], Coroutine[Any, Any, T | None]]:
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 _JWKS:
69
+ if not jwks:
73
70
  openid_config = await fetch_openid_config()
74
- _JWKS = await fetch_jwks(openid_config['jwks_uri'])
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, _JWKS, algorithms=['RS256'], audience=oidc_settings.client_id)
76
+ payload = jwt.decode(token, jwks, algorithms=['RS256'], audience=oidc_settings.client_id)
78
77
  else:
79
- payload = jwt.decode(token, _JWKS, algorithms=['RS256'])
78
+ payload = jwt.decode(token, jwks, algorithms=['RS256'])
80
79
 
81
80
  token_data = token_cls(**payload)
82
-
83
- return token_data
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, Mapping, Sequence
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 == 'linux' or platform == 'darwin':
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 as e:
134
- logger.error(f'Failed to store sequence in cache: {e}')
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 as e:
154
- logger.error(f'Failed to store dict in cache: {e}')
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 as e:
178
- logger.error(f'Failed to set dict in cache: {e}')
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 default
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 None
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 as e:
200
- logger.error(f'Failed to set dict item in cache: {e}')
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 as e:
208
- logger.error(f'Failed to delete dict item from cache: {e}')
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 as e:
219
- logger.error(f'Failed to store set in cache: {e}')
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 as e:
228
- logger.error(f'Failed to check if set has item in cache: {e}')
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 as e:
238
- logger.error(f'Failed to add set item into cache: {e}')
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 as e:
253
- logger.error(f'Failed to get set members from cache: {e}')
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 typing import AsyncGenerator, Callable, Mapping
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.error(f'Missing database settings: {name}')
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, None]]:
67
- async def get_session() -> AsyncGenerator[AsyncSession, None]:
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 as e:
87
- logger.error(f'Database connection is not healthy: {e}')
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 typing import Mapping
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
- order_by_col = order_by_col[1:]
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[order_by_col] = direction
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, None, None]:
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