python3-commons 0.9.18__py3-none-any.whl → 0.9.20__py3-none-any.whl

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.

@@ -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
python3_commons/audit.py CHANGED
@@ -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:
python3_commons/auth.py CHANGED
@@ -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
python3_commons/cache.py CHANGED
@@ -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
python3_commons/fs.py CHANGED
@@ -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
@@ -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, Mapping, Sequence
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(SingletonMeta, cls).__call__(*args, **kwargs)
38
+ instance = super().__call__(*args, **kwargs)
38
39
  cls.__instances[cls] = instance
39
40
 
40
41
  return instance
@@ -66,9 +67,9 @@ def tries(times):
66
67
  # noinspection PyBroadException
67
68
  try:
68
69
  return await f(*args, **kwargs)
69
- except Exception as exc:
70
+ except Exception:
70
71
  if _time >= times:
71
- raise exc
72
+ raise
72
73
 
73
74
  return wrapper
74
75
 
@@ -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 datetime import datetime
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, None]:
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
- if path.startswith('/'):
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}'
@@ -66,14 +67,13 @@ async def put_object(bucket_name: str, path: str, data: io.BytesIO, length: int,
66
67
  await s3_client.put_object(Bucket=bucket_name, Key=path, Body=data, ContentLength=length)
67
68
 
68
69
  logger.debug(f'Stored object into object storage: {bucket_name}:{path}')
69
-
70
- return f's3://{bucket_name}/{path}'
71
-
72
70
  except Exception as e:
73
- logger.error(f'Failed to put object to object storage: {bucket_name}:{path}', exc_info=e)
71
+ logger.exception(f'Failed to put object to object storage: {bucket_name}:{path}', exc_info=e)
74
72
 
75
73
  raise
76
74
 
75
+ return f's3://{bucket_name}/{path}'
76
+
77
77
 
78
78
  @asynccontextmanager
79
79
  async def get_object_stream(bucket_name: str, path: str) -> AsyncGenerator[StreamingBody]:
@@ -88,7 +88,7 @@ async def get_object_stream(bucket_name: str, path: str) -> AsyncGenerator[Strea
88
88
  async with response['Body'] as stream:
89
89
  yield stream
90
90
  except Exception as e:
91
- logger.debug(f'Failed getting object from object storage: {bucket_name}:{path}', exc_info=e)
91
+ logger.exception(f'Failed getting object from object storage: {bucket_name}:{path}', exc_info=e)
92
92
 
93
93
  raise
94
94
 
@@ -102,7 +102,7 @@ async def get_object(bucket_name: str, path: str) -> bytes:
102
102
  return body
103
103
 
104
104
 
105
- async def list_objects(bucket_name: str, prefix: str, recursive: bool = True) -> AsyncGenerator[Mapping, None]:
105
+ async def list_objects(bucket_name: str, prefix: str, *, recursive: bool = True) -> AsyncGenerator[Mapping]:
106
106
  storage = ObjectStorage(s3_settings)
107
107
 
108
108
  async with storage.get_client() as s3_client:
@@ -117,9 +117,9 @@ async def list_objects(bucket_name: str, prefix: str, recursive: bool = True) ->
117
117
 
118
118
 
119
119
  async def get_object_streams(
120
- bucket_name: str, path: str, recursive: bool = True
121
- ) -> AsyncGenerator[tuple[str, datetime, StreamingBody], None]:
122
- async for obj in list_objects(bucket_name, path, recursive):
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=recursive):
123
123
  object_name = obj['Key']
124
124
  last_modified = obj['LastModified']
125
125
 
@@ -128,9 +128,9 @@ async def get_object_streams(
128
128
 
129
129
 
130
130
  async def get_objects(
131
- bucket_name: str, path: str, recursive: bool = True
132
- ) -> AsyncGenerator[tuple[str, datetime, bytes], None]:
133
- async for object_name, last_modified, stream in get_object_streams(bucket_name, path, recursive):
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=recursive):
134
134
  data = await stream.read()
135
135
 
136
136
  yield object_name, last_modified, data
@@ -144,7 +144,7 @@ async def remove_object(bucket_name: str, object_name: str):
144
144
  await s3_client.delete_object(Bucket=bucket_name, Key=object_name)
145
145
  logger.debug(f'Removed object from object storage: {bucket_name}:{object_name}')
146
146
  except Exception as e:
147
- logger.error(f'Failed to remove object from object storage: {bucket_name}:{object_name}', exc_info=e)
147
+ logger.exception(f'Failed to remove object from object storage: {bucket_name}:{object_name}', exc_info=e)
148
148
 
149
149
  raise
150
150
 
@@ -155,13 +155,12 @@ async def remove_objects(
155
155
  storage = ObjectStorage(s3_settings)
156
156
 
157
157
  async with storage.get_client() as s3_client:
158
- objects_to_delete = []
159
-
160
158
  if prefix:
161
- async for obj in list_objects(bucket_name, prefix, recursive=True):
162
- objects_to_delete.append({'Key': obj['Key']})
159
+ objects_to_delete = tuple(
160
+ {'Key': obj['Key']} async for obj in list_objects(bucket_name, prefix, recursive=True)
161
+ )
163
162
  elif object_names:
164
- objects_to_delete = [{'Key': name} for name in object_names]
163
+ objects_to_delete = tuple({'Key': name} for name in object_names)
165
164
  else:
166
165
  return None
167
166
 
@@ -182,9 +181,9 @@ async def remove_objects(
182
181
  errors.extend(response['Errors'])
183
182
 
184
183
  logger.debug(f'Removed {len(objects_to_delete)} objects from object storage: {bucket_name}')
185
-
186
- return errors if errors else None
187
184
  except Exception as e:
188
- logger.error(f'Failed to remove objects from object storage: {bucket_name}', exc_info=e)
185
+ logger.exception(f'Failed to remove objects from object storage: {bucket_name}', exc_info=e)
189
186
 
190
187
  raise
188
+
189
+ return errors if errors else None
@@ -0,0 +1,8 @@
1
+ from enum import IntEnum
2
+
3
+
4
+ class ExtendedType(IntEnum):
5
+ DECIMAL = 1
6
+ DATETIME = 2
7
+ DATE = 3
8
+ DATACLASS = 4
@@ -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(CustomJSONEncoder, self).default(o)
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')
@@ -7,6 +7,7 @@ from decimal import Decimal
7
7
  import msgpack
8
8
  from msgpack import ExtType
9
9
 
10
+ from python3_commons.serializers.common import ExtendedType
10
11
  from python3_commons.serializers.json import CustomJSONEncoder
11
12
 
12
13
  logger = logging.getLogger(__name__)
@@ -14,26 +15,27 @@ logger = logging.getLogger(__name__)
14
15
 
15
16
  def msgpack_encoder(obj):
16
17
  if isinstance(obj, Decimal):
17
- return ExtType(1, str(obj).encode())
18
+ return ExtType(ExtendedType.DECIMAL, str(obj).encode())
18
19
  elif isinstance(obj, datetime):
19
- return ExtType(2, obj.isoformat().encode())
20
+ return ExtType(ExtendedType.DATETIME, obj.isoformat().encode())
20
21
  elif isinstance(obj, date):
21
- return ExtType(3, obj.isoformat().encode())
22
+ return ExtType(ExtendedType.DATE, obj.isoformat().encode())
22
23
  elif dataclasses.is_dataclass(obj):
23
- return ExtType(4, json.dumps(dataclasses.asdict(obj), cls=CustomJSONEncoder).encode())
24
+ return ExtType(ExtendedType.DATACLASS, json.dumps(dataclasses.asdict(obj), cls=CustomJSONEncoder).encode())
24
25
 
25
26
  return f'no encoder for {obj}'
26
27
 
27
28
 
28
29
  def msgpack_decoder(code, data):
29
- if code == 1:
30
- return Decimal(data.decode())
31
- elif code == 2:
32
- return datetime.fromisoformat(data.decode())
33
- elif code == 3:
34
- return date.fromisoformat(data.decode())
35
- elif code == 4:
36
- return json.loads(data)
30
+ match code:
31
+ case ExtendedType.DECIMAL:
32
+ return Decimal(data.decode())
33
+ case ExtendedType.DATETIME:
34
+ return datetime.fromisoformat(data.decode())
35
+ case ExtendedType.DATE:
36
+ return date.fromisoformat(data.decode())
37
+ case ExtendedType.DATACLASS:
38
+ return json.loads(data)
37
39
 
38
40
  return f'no decoder for type {code}'
39
41
 
@@ -4,12 +4,13 @@ import logging
4
4
  import struct
5
5
  from datetime import date, datetime
6
6
  from decimal import Decimal
7
- from typing import Any, Type, TypeVar
7
+ from typing import Any, TypeVar
8
8
 
9
9
  from msgspec import msgpack
10
10
  from msgspec.msgpack import Ext, encode
11
11
  from pydantic import BaseModel
12
12
 
13
+ from python3_commons.serializers.common import ExtendedType
13
14
  from python3_commons.serializers.json import CustomJSONEncoder
14
15
 
15
16
  logger = logging.getLogger(__name__)
@@ -18,28 +19,36 @@ T = TypeVar('T')
18
19
 
19
20
  def enc_hook(obj: Any) -> Any:
20
21
  if isinstance(obj, Decimal):
21
- return Ext(1, struct.pack('b', str(obj).encode()))
22
+ return Ext(ExtendedType.DECIMAL, struct.pack('b', str(obj).encode()))
22
23
  elif isinstance(obj, datetime):
23
- return Ext(2, struct.pack('b', obj.isoformat().encode()))
24
+ return Ext(ExtendedType.DATETIME, struct.pack('b', obj.isoformat().encode()))
24
25
  elif isinstance(obj, date):
25
- return Ext(3, struct.pack('b', obj.isoformat().encode()))
26
+ return Ext(ExtendedType.DATE, struct.pack('b', obj.isoformat().encode()))
26
27
  elif dataclasses.is_dataclass(obj):
27
- return Ext(4, struct.pack('b', json.dumps(dataclasses.asdict(obj), cls=CustomJSONEncoder).encode()))
28
+ return Ext(
29
+ ExtendedType.DATACLASS,
30
+ struct.pack('b', json.dumps(dataclasses.asdict(obj), cls=CustomJSONEncoder).encode()),
31
+ )
28
32
 
29
- raise NotImplementedError(f'Objects of type {type(obj)} are not supported')
33
+ msg = f'Objects of type {type(obj)} are not supported'
34
+
35
+ raise NotImplementedError(msg)
30
36
 
31
37
 
32
38
  def ext_hook(code: int, data: memoryview) -> Any:
33
- if code == 1:
34
- return Decimal(data.tobytes().decode())
35
- elif code == 2:
36
- return datetime.fromisoformat(data.tobytes().decode())
37
- elif code == 3:
38
- return date.fromisoformat(data.tobytes().decode())
39
- elif code == 4:
40
- return json.loads(data.tobytes())
39
+ match code:
40
+ case ExtendedType.DECIMAL:
41
+ return Decimal(data.tobytes().decode())
42
+ case ExtendedType.DATETIME:
43
+ return datetime.fromisoformat(data.tobytes().decode())
44
+ case ExtendedType.DATE:
45
+ return date.fromisoformat(data.tobytes().decode())
46
+ case ExtendedType.DATACLASS:
47
+ return json.loads(data.tobytes())
48
+ case _:
49
+ msg = f'Extension type code {code} is not supported'
41
50
 
42
- raise NotImplementedError(f'Extension type code {code} is not supported')
51
+ raise NotImplementedError(msg)
43
52
 
44
53
 
45
54
  MSGPACK_ENCODER = msgpack.Encoder(enc_hook=enc_hook)
@@ -56,7 +65,7 @@ def serialize_msgpack_native(data: Any) -> bytes:
56
65
  return result
57
66
 
58
67
 
59
- def deserialize_msgpack_native(data: bytes, data_type: Type[T] | None = None) -> T | Any:
68
+ def deserialize_msgpack_native[T](data: bytes, data_type: type[T] | None = None) -> T | Any:
60
69
  if data_type:
61
70
  if issubclass(data_type, BaseModel):
62
71
  decoded = MSGPACK_DECODER_NATIVE.decode(data)
@@ -78,7 +87,7 @@ def serialize_msgpack(data: Any) -> bytes:
78
87
  return result
79
88
 
80
89
 
81
- def deserialize_msgpack(data: bytes, data_type: Type[T] | None = None) -> T | Any:
90
+ def deserialize_msgpack[T](data: bytes, data_type: type[T] | None = None) -> T | Any:
82
91
  if data_type:
83
92
  if issubclass(data_type, BaseModel):
84
93
  decoded = MSGPACK_DECODER.decode(data)
@@ -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
@@ -0,0 +1,30 @@
1
+ python3_commons/__init__.py,sha256=0KgaYU46H_IMKn-BuasoRN3C4Hi45KlkHHoPbU9cwiA,189
2
+ python3_commons/api_client.py,sha256=0PE8iYW5zq7n89veC6afSkwj_fzD_nldBc5wdXyg5jo,5266
3
+ python3_commons/audit.py,sha256=kfUaSiNAg9NJuUhnBXqplNiyuPQuDFM9NcD-HZV2qd4,6077
4
+ python3_commons/auth.py,sha256=fWfoh5665F-YnGTgG9wDAosYIM9vFoTsQXEcCyzD72M,2951
5
+ python3_commons/cache.py,sha256=Hib86F1k1SviF-a2RyoTxRrFi0XV2KaWOfBi5TlOu-g,7697
6
+ python3_commons/conf.py,sha256=GOvR1oYss6AKnNlyj9JbT0mSrfQFrnzmgm0IiGVahho,2428
7
+ python3_commons/fs.py,sha256=dn8ZcwsQf9xcAEg6neoxLN6IzJbWpprfm8wV8S55BL0,337
8
+ python3_commons/helpers.py,sha256=utcorGvXvZLsC4H_H8WGwzeIMgCOCHac2jbKpVCeixA,3928
9
+ python3_commons/object_storage.py,sha256=pvA-gJehnbIJmqu8ebT6oSqW-dR-uZl3E1AV86ffUpY,6568
10
+ python3_commons/permissions.py,sha256=bhjTp-tq-oaTGFMHNnSBlcVX5XQCTL0nWcu6SdPEAB4,1555
11
+ python3_commons/db/__init__.py,sha256=PZTIC0RPzYzZ2Fh4HeyKtYIHaUPB7A92yeOK--PGxnw,2942
12
+ python3_commons/db/helpers.py,sha256=n56yYCE0fvzvU7nL1936NfZhbaQmvfumzRsGimBlNV4,1776
13
+ python3_commons/db/models/__init__.py,sha256=zjZCf0DNDkqmPZ49quJ6KZohtKH87viI_ijDG3E0PVE,554
14
+ python3_commons/db/models/auth.py,sha256=NMHirujigpaRR0Bhhe2gzy8Q8PPABuaA-D8ZY7aaqeE,1177
15
+ python3_commons/db/models/common.py,sha256=nRLQVi7Y0SsXo3qMIwQX6GuDO9kHnlma4O_mYXQVtHQ,1512
16
+ python3_commons/db/models/rbac.py,sha256=BIB7nJXQkCes0XA-fg-oCHP6YU0_rXIm29O73j4pNUg,3160
17
+ python3_commons/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ python3_commons/log/filters.py,sha256=fuyjXZAUm-i2MNrxvFYag8F8Rr27x8W8MdV3ke6miSs,175
19
+ python3_commons/log/formatters.py,sha256=p2AtZD4Axp3Em0e9gWzW8U_yOR5entD7xn7Edvc-IuM,719
20
+ python3_commons/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ python3_commons/serializers/common.py,sha256=VkA7C6wODvHk0QBXVX_x2JieDstihx3U__UFbTYf654,120
22
+ python3_commons/serializers/json.py,sha256=Dae0gouk9jiUOkmh8z1AcTIHD52OgBn2aXK9JLKMMms,718
23
+ python3_commons/serializers/msgpack.py,sha256=AwzBSUdbdq8yYdGzmEsiWw0bnL9XRQFa1Vh-nt2s56k,1499
24
+ python3_commons/serializers/msgspec.py,sha256=yp3LvsxDUTIv__3AaLed5ddZ0rHm7BYqmrrehVAJb14,3000
25
+ python3_commons-0.9.20.dist-info/licenses/AUTHORS.rst,sha256=3R9JnfjfjH5RoPWOeqKFJgxVShSSfzQPIrEr1nxIo9Q,90
26
+ python3_commons-0.9.20.dist-info/licenses/LICENSE,sha256=xxILuojHm4fKQOrMHPSslbyy6WuKAN2RiG74HbrYfzM,34575
27
+ python3_commons-0.9.20.dist-info/METADATA,sha256=BfW_2kWlG3QkN3fmzHlmXrkNjHZGLM_WiXujw6BTTjc,1134
28
+ python3_commons-0.9.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
29
+ python3_commons-0.9.20.dist-info/top_level.txt,sha256=lJI6sCBf68eUHzupCnn2dzG10lH3jJKTWM_hrN1cQ7M,16
30
+ python3_commons-0.9.20.dist-info/RECORD,,
@@ -1,29 +0,0 @@
1
- python3_commons/__init__.py,sha256=0KgaYU46H_IMKn-BuasoRN3C4Hi45KlkHHoPbU9cwiA,189
2
- python3_commons/api_client.py,sha256=vIWt6QIbHw_x-1yKhXyJEN3Aj22KMsZwJXMsdWR7oaQ,4970
3
- python3_commons/audit.py,sha256=p4KRKt0ogkHhJSulg6j5GU-JKBBE903H2c0nuW16GtM,6083
4
- python3_commons/auth.py,sha256=fINE7zeq-oaEk2lwkdP1KOhfCpcIBaC8P9UzXQI37J0,2922
5
- python3_commons/cache.py,sha256=lf27LTD4Z9Iqi5GaK8jH8UC0cL9sHH8wicZ88YDp6Mg,7725
6
- python3_commons/conf.py,sha256=GOvR1oYss6AKnNlyj9JbT0mSrfQFrnzmgm0IiGVahho,2428
7
- python3_commons/fs.py,sha256=wfLjybXndwLqNlOxTpm_HRJnuTcC4wbrHEOaEeCo9Wc,337
8
- python3_commons/helpers.py,sha256=9m9Q_ImXzOsKmt7ObLUhd11vnGfwUH_KO4IwLaTMrFA,3930
9
- python3_commons/object_storage.py,sha256=6jESNzI__r9wJdTWJQRXhpbdmC2QrZVUoZyvetfQNwY,6544
10
- python3_commons/permissions.py,sha256=bhjTp-tq-oaTGFMHNnSBlcVX5XQCTL0nWcu6SdPEAB4,1555
11
- python3_commons/db/__init__.py,sha256=5nArsGm17e-pelpOwAeBKy2n_Py20XqklZsNgkcJ-DQ,2947
12
- python3_commons/db/helpers.py,sha256=PY0h08aLiGx-J54wmP3GHPCgGCcLd60rayAUnR3aWdI,1742
13
- python3_commons/db/models/__init__.py,sha256=zjZCf0DNDkqmPZ49quJ6KZohtKH87viI_ijDG3E0PVE,554
14
- python3_commons/db/models/auth.py,sha256=NMHirujigpaRR0Bhhe2gzy8Q8PPABuaA-D8ZY7aaqeE,1177
15
- python3_commons/db/models/common.py,sha256=nRLQVi7Y0SsXo3qMIwQX6GuDO9kHnlma4O_mYXQVtHQ,1512
16
- python3_commons/db/models/rbac.py,sha256=BIB7nJXQkCes0XA-fg-oCHP6YU0_rXIm29O73j4pNUg,3160
17
- python3_commons/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- python3_commons/log/filters.py,sha256=fuyjXZAUm-i2MNrxvFYag8F8Rr27x8W8MdV3ke6miSs,175
19
- python3_commons/log/formatters.py,sha256=p2AtZD4Axp3Em0e9gWzW8U_yOR5entD7xn7Edvc-IuM,719
20
- python3_commons/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- python3_commons/serializers/json.py,sha256=91UaXLGKGj0yPyrnuMeNrkG2GuPUgcgAsmIokUgEwpU,808
22
- python3_commons/serializers/msgpack.py,sha256=WrvaPE187shSK8zkH4UHHMimEZNMv9RaDSwsBE2HlCw,1269
23
- python3_commons/serializers/msgspec.py,sha256=0AliXlEl5sewi0UENjI8St5ZScXE5DNRERKzqWKy2Ps,2674
24
- python3_commons-0.9.18.dist-info/licenses/AUTHORS.rst,sha256=3R9JnfjfjH5RoPWOeqKFJgxVShSSfzQPIrEr1nxIo9Q,90
25
- python3_commons-0.9.18.dist-info/licenses/LICENSE,sha256=xxILuojHm4fKQOrMHPSslbyy6WuKAN2RiG74HbrYfzM,34575
26
- python3_commons-0.9.18.dist-info/METADATA,sha256=H1oSSmkAcQc8WxKeBi7A8JNIs12sFpxHa-exEMlyoWo,1134
27
- python3_commons-0.9.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
- python3_commons-0.9.18.dist-info/top_level.txt,sha256=lJI6sCBf68eUHzupCnn2dzG10lH3jJKTWM_hrN1cQ7M,16
29
- python3_commons-0.9.18.dist-info/RECORD,,