python3-commons 0.8.18__py3-none-any.whl → 0.8.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.
@@ -1,13 +1,13 @@
1
1
  import logging
2
2
  from contextlib import asynccontextmanager
3
- from datetime import datetime, UTC
3
+ from datetime import UTC, datetime
4
4
  from enum import Enum
5
5
  from http import HTTPStatus
6
6
  from json import dumps
7
7
  from typing import AsyncGenerator, Literal, Mapping, Sequence
8
8
  from uuid import uuid4
9
9
 
10
- from aiohttp import ClientResponse, ClientSession, client_exceptions, ClientTimeout
10
+ from aiohttp import ClientResponse, ClientSession, ClientTimeout, client_exceptions
11
11
  from pydantic import HttpUrl
12
12
 
13
13
  from python3_commons import audit
@@ -15,16 +15,11 @@ from python3_commons.conf import s3_settings
15
15
  from python3_commons.helpers import request_to_curl
16
16
  from python3_commons.serializers.json import CustomJSONEncoder
17
17
 
18
-
19
18
  logger = logging.getLogger(__name__)
20
19
 
21
20
 
22
21
  async def _store_response_for_audit(
23
- response: ClientResponse,
24
- audit_name: str,
25
- uri_path: str,
26
- method: str,
27
- request_id: str
22
+ response: ClientResponse, audit_name: str, uri_path: str, method: str, request_id: str
28
23
  ):
29
24
  response_text = await response.text()
30
25
 
@@ -36,7 +31,7 @@ async def _store_response_for_audit(
36
31
  await audit.write_audit_data(
37
32
  s3_settings,
38
33
  f'{date_path}/{audit_name}/{uri_path}/{method}_{timestamp}_{request_id}_response.txt',
39
- response_text.encode('utf-8')
34
+ response_text.encode('utf-8'),
40
35
  )
41
36
 
42
37
 
@@ -51,7 +46,7 @@ async def request(
51
46
  json: Mapping | Sequence | str | None = None,
52
47
  data: bytes | None = None,
53
48
  timeout: ClientTimeout | Enum | None = None,
54
- audit_name: str | None = None
49
+ audit_name: str | None = None,
55
50
  ) -> AsyncGenerator[ClientResponse]:
56
51
  now = datetime.now(tz=UTC)
57
52
  date_path = now.strftime('%Y/%m/%d')
@@ -59,7 +54,7 @@ async def request(
59
54
  request_id = str(uuid4())[-12:]
60
55
  uri_path = uri[:-1] if uri.endswith('/') else uri
61
56
  uri_path = uri_path[1:] if uri_path.startswith('/') else uri_path
62
- url = f'{u[:-1] if (u := str(base_url)).endswith('/') else u}{uri}'
57
+ url = f'{u[:-1] if (u := str(base_url)).endswith("/") else u}{uri}'
63
58
 
64
59
  if audit_name:
65
60
  curl_request = None
@@ -74,7 +69,7 @@ async def request(
74
69
  await audit.write_audit_data(
75
70
  s3_settings,
76
71
  f'{date_path}/{audit_name}/{uri_path}/{method}_{timestamp}_{request_id}_request.txt',
77
- curl_request.encode('utf-8')
72
+ curl_request.encode('utf-8'),
78
73
  )
79
74
  client_method = getattr(client, method)
80
75
 
python3_commons/audit.py CHANGED
@@ -4,7 +4,7 @@ import logging
4
4
  import tarfile
5
5
  from bz2 import BZ2Compressor
6
6
  from collections import deque
7
- from datetime import datetime, timedelta, UTC
7
+ from datetime import UTC, datetime, timedelta
8
8
  from typing import Generator, Iterable
9
9
  from uuid import uuid4
10
10
 
@@ -54,7 +54,7 @@ class GeneratedStream(io.BytesIO):
54
54
  unread_data_size = len(buf) - pos
55
55
 
56
56
  if unread_data_size > 0:
57
- buf[:unread_data_size] = buf[pos:pos+unread_data_size]
57
+ buf[:unread_data_size] = buf[pos : pos + unread_data_size]
58
58
 
59
59
  del buf
60
60
 
@@ -67,8 +67,9 @@ class GeneratedStream(io.BytesIO):
67
67
  return True
68
68
 
69
69
 
70
- def generate_archive(objects: Iterable[tuple[str, datetime, bytes]],
71
- chunk_size: int = 4096) -> Generator[bytes, None, None]:
70
+ def generate_archive(
71
+ objects: Iterable[tuple[str, datetime, bytes]], chunk_size: int = 4096
72
+ ) -> Generator[bytes, None, None]:
72
73
  buffer = deque()
73
74
 
74
75
  with tarfile.open(fileobj=buffer, mode='w') as archive:
@@ -154,7 +155,7 @@ async def archive_audit_data(root_path: str = 'audit'):
154
155
  archive_stream = GeneratedStream(bzip2_generator)
155
156
 
156
157
  archive_path = object_storage.get_absolute_path(f'audit/.archive/{year}_{month:02}_{day:02}.tar.bz2')
157
- object_storage.put_object(bucket_name, archive_path, archive_stream, -1, part_size=5*1024*1024)
158
+ object_storage.put_object(bucket_name, archive_path, archive_stream, -1, part_size=5 * 1024 * 1024)
158
159
 
159
160
  if errors := object_storage.remove_objects(bucket_name, date_path):
160
161
  for error in errors:
@@ -0,0 +1,82 @@
1
+ import logging
2
+ from http import HTTPStatus
3
+ from typing import Annotated
4
+
5
+ import aiohttp
6
+ from fastapi import Depends, HTTPException
7
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
8
+ from jose import JWTError, jwt
9
+ from pydantic import BaseModel
10
+
11
+ from python3_commons.conf import oidc_settings
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class TokenData(BaseModel):
17
+ sub: str
18
+ aud: str
19
+ exp: int
20
+ iss: str
21
+
22
+
23
+ OIDC_CONFIG_URL = f'{oidc_settings.authority_url}/.well-known/openid-configuration'
24
+ _JWKS: dict | None = None
25
+
26
+ bearer_security = HTTPBearer(auto_error=oidc_settings.enabled)
27
+
28
+
29
+ async def fetch_openid_config() -> dict:
30
+ """
31
+ Fetch the OpenID configuration (including JWKS URI) from OIDC authority.
32
+ """
33
+ async with aiohttp.ClientSession() as session:
34
+ async with session.get(OIDC_CONFIG_URL) as response:
35
+ if response.status != HTTPStatus.OK:
36
+ raise HTTPException(
37
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail='Failed to fetch OpenID configuration'
38
+ )
39
+
40
+ return await response.json()
41
+
42
+
43
+ async def fetch_jwks(jwks_uri: str) -> dict:
44
+ """
45
+ Fetch the JSON Web Key Set (JWKS) for validating the token's signature.
46
+ """
47
+ async with aiohttp.ClientSession() as session:
48
+ async with session.get(jwks_uri) as response:
49
+ if response.status != HTTPStatus.OK:
50
+ raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail='Failed to fetch JWKS')
51
+
52
+ return await response.json()
53
+
54
+
55
+ async def get_verified_token(
56
+ authorization: Annotated[HTTPAuthorizationCredentials, Depends(bearer_security)],
57
+ ) -> TokenData | None:
58
+ """
59
+ Verify the JWT access token using OIDC authority JWKS.
60
+ """
61
+ global _JWKS
62
+
63
+ if not oidc_settings.enabled:
64
+ return None
65
+
66
+ token = authorization.credentials
67
+
68
+ try:
69
+ if not _JWKS:
70
+ openid_config = await fetch_openid_config()
71
+ _JWKS = await fetch_jwks(openid_config['jwks_uri'])
72
+
73
+ if oidc_settings.client_id:
74
+ payload = jwt.decode(token, _JWKS, algorithms=['RS256'], audience=oidc_settings.client_id)
75
+ else:
76
+ payload = jwt.decode(token, _JWKS, algorithms=['RS256'])
77
+
78
+ token_data = TokenData(**payload)
79
+
80
+ return token_data
81
+ except JWTError as e:
82
+ raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail=f'Token is invalid: {str(e)}')
python3_commons/cache.py CHANGED
@@ -5,7 +5,7 @@ from typing import Any, Mapping, Sequence
5
5
 
6
6
  import valkey
7
7
  from pydantic import RedisDsn
8
- from valkey.asyncio import Valkey, StrictValkey, ConnectionPool, Sentinel
8
+ from valkey.asyncio import ConnectionPool, Sentinel, StrictValkey, Valkey
9
9
  from valkey.asyncio.retry import Retry
10
10
  from valkey.backoff import FullJitterBackoff
11
11
  from valkey.typing import ResponseT
@@ -13,7 +13,10 @@ from valkey.typing import ResponseT
13
13
  from python3_commons.conf import valkey_settings
14
14
  from python3_commons.helpers import SingletonMeta
15
15
  from python3_commons.serializers.msgspec import (
16
- serialize_msgpack, deserialize_msgpack, deserialize_msgpack_native, serialize_msgpack_native
16
+ deserialize_msgpack,
17
+ deserialize_msgpack_native,
18
+ serialize_msgpack,
19
+ serialize_msgpack_native,
17
20
  )
18
21
 
19
22
  logger = logging.getLogger(__name__)
@@ -32,11 +35,7 @@ class AsyncValkeyClient(metaclass=SingletonMeta):
32
35
  @staticmethod
33
36
  def _get_keepalive_options():
34
37
  if platform == 'linux' or platform == 'darwin':
35
- return {
36
- socket.TCP_KEEPIDLE: 10,
37
- socket.TCP_KEEPINTVL: 5,
38
- socket.TCP_KEEPCNT: 5
39
- }
38
+ return {socket.TCP_KEEPIDLE: 10, socket.TCP_KEEPINTVL: 5, socket.TCP_KEEPCNT: 5}
40
39
  else:
41
40
  return {}
42
41
 
@@ -46,7 +45,7 @@ class AsyncValkeyClient(metaclass=SingletonMeta):
46
45
  socket_connect_timeout=10,
47
46
  socket_timeout=60,
48
47
  password=dsn.password,
49
- sentinel_kwargs={'password': dsn.password}
48
+ sentinel_kwargs={'password': dsn.password},
50
49
  )
51
50
 
52
51
  ka_options = self._get_keepalive_options()
@@ -60,7 +59,7 @@ class AsyncValkeyClient(metaclass=SingletonMeta):
60
59
  retry_on_timeout=True,
61
60
  retry=Retry(FullJitterBackoff(cap=5, base=1), 5),
62
61
  socket_keepalive=True,
63
- socket_keepalive_options=ka_options
62
+ socket_keepalive_options=ka_options,
64
63
  )
65
64
 
66
65
  def _initialize_standard_pool(self, dsn: RedisDsn):
@@ -76,11 +75,11 @@ def get_valkey_client() -> Valkey:
76
75
 
77
76
 
78
77
  async def scan(
79
- cursor: int = 0,
80
- match: bytes | str | memoryview | None = None,
81
- count: int | None = None,
82
- _type: str | None = None,
83
- **kwargs,
78
+ cursor: int = 0,
79
+ match: bytes | str | memoryview | None = None,
80
+ count: int | None = None,
81
+ _type: str | None = None,
82
+ **kwargs,
84
83
  ) -> ResponseT:
85
84
  return await get_valkey_client().scan(cursor, match, count, _type, **kwargs)
86
85
 
python3_commons/conf.py CHANGED
@@ -1,4 +1,4 @@
1
- from pydantic import SecretStr, PostgresDsn, Field, RedisDsn
1
+ from pydantic import Field, HttpUrl, PostgresDsn, RedisDsn, SecretStr
2
2
  from pydantic_settings import BaseSettings, SettingsConfigDict
3
3
 
4
4
 
@@ -8,6 +8,14 @@ class CommonSettings(BaseSettings):
8
8
  logging_formatter: str = 'default'
9
9
 
10
10
 
11
+ class OIDCSettings(BaseSettings):
12
+ model_config = SettingsConfigDict(env_prefix='OIDC_')
13
+
14
+ enabled: bool = True
15
+ authority_url: HttpUrl | None = None
16
+ client_id: str | None = None
17
+
18
+
11
19
  class ValkeySettings(BaseSettings):
12
20
  model_config = SettingsConfigDict(env_prefix='VALKEY_')
13
21
 
@@ -38,6 +46,7 @@ class S3Settings(BaseSettings):
38
46
 
39
47
 
40
48
  settings = CommonSettings()
49
+ oidc_settings = OIDCSettings()
41
50
  valkey_settings = ValkeySettings()
42
51
  db_settings = DBSettings()
43
52
  s3_settings = S3Settings()
@@ -3,7 +3,7 @@ import logging
3
3
  from typing import AsyncGenerator, Callable, Mapping
4
4
 
5
5
  from sqlalchemy import MetaData
6
- from sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine, async_engine_from_config
6
+ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_engine_from_config
7
7
  from sqlalchemy.ext.asyncio.session import async_sessionmaker
8
8
  from sqlalchemy.orm import declarative_base
9
9
 
@@ -2,22 +2,22 @@ import logging
2
2
  from typing import Mapping
3
3
 
4
4
  import sqlalchemy as sa
5
- from sqlalchemy import desc, asc, func
5
+ from sqlalchemy import asc, desc, func
6
6
  from sqlalchemy.sql.elements import BooleanClauseList, UnaryExpression
7
7
 
8
8
  logger = logging.getLogger(__name__)
9
9
 
10
10
 
11
- def get_query(search: Mapping[str, str] | None = None,
12
- order_by: str | None = None,
13
- columns: Mapping | None = None) -> tuple[BooleanClauseList, UnaryExpression]:
11
+ def get_query(
12
+ search: Mapping[str, str] | None = None, order_by: str | None = None, columns: Mapping | None = None
13
+ ) -> tuple[BooleanClauseList, UnaryExpression]:
14
14
  """
15
- :columns:
16
- Param name ->
17
- 0: Model column
18
- 1: case-insensitive if True
19
- 2: cast value to type
20
- 3: exact match if True, LIKE %value% if False
15
+ :columns:
16
+ Param name ->
17
+ 0: Model column
18
+ 1: case-insensitive if True
19
+ 2: cast value to type
20
+ 3: exact match if True, LIKE %value% if False
21
21
  """
22
22
 
23
23
  order_by_cols = {}
@@ -41,21 +41,15 @@ def get_query(search: Mapping[str, str] | None = None,
41
41
  if search:
42
42
  where_parts = [
43
43
  *(
44
- (func.upper(columns[k][0])
45
- if columns[k][1]
46
- else columns[k][0]
47
- ) == columns[k][2](v)
44
+ (func.upper(columns[k][0]) if columns[k][1] else columns[k][0]) == columns[k][2](v)
48
45
  for k, v in search.items()
49
46
  if columns[k][3]
50
47
  ),
51
48
  *(
52
- (func.upper(columns[k][0])
53
- if columns[k][1]
54
- else columns[k][0]
55
- ).like(f'%{v.upper()}%')
49
+ (func.upper(columns[k][0]) if columns[k][1] else columns[k][0]).like(f'%{v.upper()}%')
56
50
  for k, v in search.items()
57
51
  if not columns[k][3]
58
- )
52
+ ),
59
53
  ]
60
54
  else:
61
55
  where_parts = None
@@ -1,4 +1,2 @@
1
1
  from python3_commons.db.models.auth import ApiKey, User, UserGroup
2
- from python3_commons.db.models.rbac import (
3
- RBACRole, RBACPermission, RBACRolePermission, RBACUserRole, RBACApiKeyRole
4
- )
2
+ from python3_commons.db.models.rbac import RBACApiKeyRole, RBACPermission, RBACRole, RBACRolePermission, RBACUserRole
@@ -2,9 +2,7 @@ import uuid
2
2
 
3
3
  from fastapi_users_db_sqlalchemy import GUID, SQLAlchemyBaseUserTableUUID
4
4
  from pydantic import AwareDatetime
5
- from sqlalchemy import (
6
- String, BIGINT, ForeignKey, DateTime
7
- )
5
+ from sqlalchemy import BIGINT, DateTime, ForeignKey, String
8
6
  from sqlalchemy.orm import Mapped, mapped_column
9
7
 
10
8
  from python3_commons.db import Base
@@ -1,7 +1,5 @@
1
1
  from pydantic import AwareDatetime
2
- from sqlalchemy import (
3
- DateTime, BIGINT
4
- )
2
+ from sqlalchemy import BIGINT, DateTime
5
3
  from sqlalchemy.dialects.postgresql import UUID
6
4
  from sqlalchemy.ext.compiler import compiles
7
5
  from sqlalchemy.orm import Mapped, mapped_column
@@ -28,10 +26,7 @@ def use_identity(element, compiler, **kw):
28
26
  class BaseDBModel:
29
27
  id: Mapped[int] = mapped_column(BIGINT, primary_key=True, sort_order=-3)
30
28
  created_at: Mapped[AwareDatetime] = mapped_column(
31
- DateTime(timezone=True),
32
- nullable=False,
33
- server_default=UTCNow(),
34
- sort_order=-2
29
+ DateTime(timezone=True), nullable=False, server_default=UTCNow(), sort_order=-2
35
30
  )
36
31
  updated_at: Mapped[AwareDatetime] = mapped_column(DateTime(timezone=True), onupdate=UTCNow(), sort_order=-1)
37
32
 
@@ -39,9 +34,6 @@ class BaseDBModel:
39
34
  class BaseDBUUIDModel:
40
35
  uid: Mapped[UUID] = mapped_column(UUID, primary_key=True, sort_order=-3)
41
36
  created_at: Mapped[AwareDatetime] = mapped_column(
42
- DateTime(timezone=True),
43
- nullable=False,
44
- server_default=UTCNow(),
45
- sort_order=-2
37
+ DateTime(timezone=True), nullable=False, server_default=UTCNow(), sort_order=-2
46
38
  )
47
39
  updated_at: Mapped[AwareDatetime | None] = mapped_column(DateTime(timezone=True), onupdate=UTCNow(), sort_order=-1)
@@ -2,9 +2,7 @@ import uuid
2
2
 
3
3
  from fastapi_users_db_sqlalchemy import GUID
4
4
  from pydantic import AwareDatetime
5
- from sqlalchemy import (
6
- String, DateTime, ForeignKey, PrimaryKeyConstraint, CheckConstraint
7
- )
5
+ from sqlalchemy import CheckConstraint, DateTime, ForeignKey, PrimaryKeyConstraint, String
8
6
  from sqlalchemy.dialects.postgresql import UUID
9
7
  from sqlalchemy.orm import Mapped, mapped_column
10
8
 
@@ -24,9 +22,7 @@ class RBACPermission(Base):
24
22
  uid: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True)
25
23
  name: Mapped[str] = mapped_column(String, unique=True, nullable=False)
26
24
 
27
- __table_args__ = (
28
- CheckConstraint("name ~ '^[a-z0-9_.]+$'", name='check_rbac_permissions_name'),
29
- )
25
+ __table_args__ = (CheckConstraint("name ~ '^[a-z0-9_.]+$'", name='check_rbac_permissions_name'),)
30
26
 
31
27
 
32
28
  class RBACRolePermission(Base):
@@ -43,9 +39,7 @@ class RBACRolePermission(Base):
43
39
  index=True,
44
40
  )
45
41
 
46
- __table_args__ = (
47
- PrimaryKeyConstraint('role_uid', 'permission_uid', name='pk_rbac_role_permissions'),
48
- )
42
+ __table_args__ = (PrimaryKeyConstraint('role_uid', 'permission_uid', name='pk_rbac_role_permissions'),)
49
43
 
50
44
 
51
45
  class RBACUserRole(Base):
@@ -64,9 +58,7 @@ class RBACUserRole(Base):
64
58
  starts_at: Mapped[AwareDatetime] = mapped_column(DateTime(timezone=True), nullable=False)
65
59
  expires_at: Mapped[AwareDatetime | None] = mapped_column(DateTime(timezone=True))
66
60
 
67
- __table_args__ = (
68
- PrimaryKeyConstraint('user_id', 'role_uid', name='pk_rbac_user_roles'),
69
- )
61
+ __table_args__ = (PrimaryKeyConstraint('user_id', 'role_uid', name='pk_rbac_user_roles'),)
70
62
 
71
63
 
72
64
  class RBACApiKeyRole(Base):
@@ -85,9 +77,7 @@ class RBACApiKeyRole(Base):
85
77
  starts_at: Mapped[AwareDatetime] = mapped_column(DateTime(timezone=True), nullable=False)
86
78
  expires_at: Mapped[AwareDatetime | None] = mapped_column(DateTime(timezone=True))
87
79
 
88
- __table_args__ = (
89
- PrimaryKeyConstraint('api_key_uid', 'role_uid', name='pk_rbac_api_key_roles'),
90
- )
80
+ __table_args__ = (PrimaryKeyConstraint('api_key_uid', 'role_uid', name='pk_rbac_api_key_roles'),)
91
81
 
92
82
 
93
83
  # class RBACRoleRelation(Base):
@@ -2,7 +2,7 @@ import logging
2
2
  import shlex
3
3
  import threading
4
4
  from datetime import date, datetime, timedelta
5
- from decimal import Decimal, ROUND_HALF_UP
5
+ from decimal import ROUND_HALF_UP, Decimal
6
6
  from json import dumps
7
7
  from typing import Literal, Mapping, Sequence
8
8
  from urllib.parse import urlencode
@@ -16,6 +16,7 @@ class SingletonMeta(type):
16
16
  """
17
17
  A metaclass that creates a Singleton base class when called.
18
18
  """
19
+
19
20
  _instances = {}
20
21
  _lock = threading.Lock()
21
22
 
@@ -76,12 +77,12 @@ def round_decimal(value: Decimal, decimal_places=2, rounding_mode=ROUND_HALF_UP)
76
77
 
77
78
 
78
79
  def request_to_curl(
79
- url: str,
80
- query: Mapping | None = None,
81
- method: Literal['get', 'post', 'put', 'patch', 'options', 'head', 'delete'] = 'get',
82
- headers: Mapping | None = None,
83
- json: Mapping | Sequence | str | None = None,
84
- data: bytes | None = None
80
+ url: str,
81
+ query: Mapping | None = None,
82
+ method: Literal['get', 'post', 'put', 'patch', 'options', 'head', 'delete'] = 'get',
83
+ headers: Mapping | None = None,
84
+ json: Mapping | Sequence | str | None = None,
85
+ data: bytes | None = None,
85
86
  ) -> str:
86
87
  if query:
87
88
  url = f'{url}?{urlencode(query)}'
@@ -5,7 +5,6 @@ from contextvars import ContextVar
5
5
 
6
6
  from python3_commons.serializers.json import CustomJSONEncoder
7
7
 
8
-
9
8
  correlation_id: ContextVar[str | None] = ContextVar('correlation_id', default=None)
10
9
 
11
10
 
@@ -6,9 +6,9 @@ from typing import Generator, Iterable
6
6
 
7
7
  from minio import Minio
8
8
  from minio.datatypes import Object
9
- from minio.deleteobjects import DeleteObject, DeleteError
9
+ from minio.deleteobjects import DeleteError, DeleteObject
10
10
 
11
- from python3_commons.conf import s3_settings, S3Settings
11
+ from python3_commons.conf import S3Settings, s3_settings
12
12
  from python3_commons.helpers import SingletonMeta
13
13
 
14
14
  logger = logging.getLogger(__name__)
@@ -25,7 +25,7 @@ class ObjectStorage(metaclass=SingletonMeta):
25
25
  access_key=settings.s3_access_key_id.get_secret_value(),
26
26
  secret_key=settings.s3_secret_access_key.get_secret_value(),
27
27
  secure=settings.s3_secure,
28
- cert_check=settings.s3_cert_verify
28
+ cert_check=settings.s3_cert_verify,
29
29
  )
30
30
 
31
31
  def get_client(self) -> Minio:
@@ -37,7 +37,7 @@ def get_absolute_path(path: str) -> str:
37
37
  path = path[1:]
38
38
 
39
39
  if bucket_root := s3_settings.s3_bucket_root:
40
- path = f'{bucket_root[:1] if bucket_root.startswith('/') else bucket_root}/{path}'
40
+ path = f'{bucket_root[:1] if bucket_root.startswith("/") else bucket_root}/{path}'
41
41
 
42
42
  return path
43
43
 
@@ -50,7 +50,7 @@ def put_object(bucket_name: str, path: str, data: io.BytesIO, length: int, part_
50
50
 
51
51
  return result.location
52
52
  else:
53
- logger.warning(f'No S3 client available, skipping object put')
53
+ logger.warning('No S3 client available, skipping object put')
54
54
 
55
55
 
56
56
  @contextmanager
@@ -70,7 +70,7 @@ def get_object_stream(bucket_name: str, path: str):
70
70
  response.close()
71
71
  response.release_conn()
72
72
  else:
73
- logger.warning(f'No S3 client available, skipping object put')
73
+ logger.warning('No S3 client available, skipping object put')
74
74
 
75
75
 
76
76
  def get_object(bucket_name: str, path: str) -> bytes:
@@ -88,8 +88,9 @@ def list_objects(bucket_name: str, prefix: str, recursive: bool = True) -> Gener
88
88
  yield from s3_client.list_objects(bucket_name, prefix=prefix, recursive=recursive)
89
89
 
90
90
 
91
- def get_objects(bucket_name: str, path: str,
92
- recursive: bool = True) -> Generator[tuple[str, datetime, bytes], None, None]:
91
+ def get_objects(
92
+ bucket_name: str, path: str, recursive: bool = True
93
+ ) -> Generator[tuple[str, datetime, bytes], None, None]:
93
94
  for obj in list_objects(bucket_name, path, recursive):
94
95
  object_name = obj.object_name
95
96
 
@@ -106,14 +107,15 @@ def remove_object(bucket_name: str, object_name: str):
106
107
  s3_client.remove_object(bucket_name, object_name)
107
108
 
108
109
 
109
- def remove_objects(bucket_name: str, prefix: str = None,
110
- object_names: Iterable[str] = None) -> Iterable[DeleteError] | None:
110
+ def remove_objects(
111
+ bucket_name: str, prefix: str = None, object_names: Iterable[str] = None
112
+ ) -> Iterable[DeleteError] | None:
111
113
  s3_client = ObjectStorage(s3_settings).get_client()
112
114
 
113
115
  if prefix:
114
116
  delete_object_list = map(
115
- lambda obj: DeleteObject(obj.object_name), s3_client.list_objects(bucket_name, prefix=prefix,
116
- recursive=True)
117
+ lambda obj: DeleteObject(obj.object_name),
118
+ s3_client.list_objects(bucket_name, prefix=prefix, recursive=True),
117
119
  )
118
120
  elif object_names:
119
121
  delete_object_list = map(DeleteObject, object_names)
@@ -2,10 +2,10 @@ import logging
2
2
  from uuid import UUID
3
3
 
4
4
  import sqlalchemy as sa
5
- from sqlalchemy import func, exists, and_
5
+ from sqlalchemy import and_, exists, func
6
6
  from sqlalchemy.ext.asyncio import AsyncSession
7
7
 
8
- from python3_commons.db.models import RBACPermission, RBACApiKeyRole, RBACRolePermission, RBACUserRole
8
+ from python3_commons.db.models import RBACApiKeyRole, RBACPermission, RBACRolePermission, RBACUserRole
9
9
 
10
10
  logger = logging.getLogger(__name__)
11
11
 
@@ -18,7 +18,7 @@ async def has_api_key_permission(session: AsyncSession, api_key_uid: UUID, permi
18
18
  (RBACApiKeyRole.expires_at.is_(None) | (RBACApiKeyRole.expires_at > func.now())),
19
19
  RBACApiKeyRole.role_uid == RBACRolePermission.role_uid,
20
20
  RBACRolePermission.permission_uid == RBACPermission.uid,
21
- RBACPermission.name == permission
21
+ RBACPermission.name == permission,
22
22
  )
23
23
  )
24
24
  )
@@ -37,7 +37,7 @@ async def has_user_permission(session: AsyncSession, user_id: UUID, permission:
37
37
  (RBACUserRole.expires_at.is_(None) | (RBACUserRole.expires_at > func.now())),
38
38
  RBACUserRole.role_uid == RBACRolePermission.role_uid,
39
39
  RBACRolePermission.permission_uid == RBACPermission.uid,
40
- RBACPermission.name == permission
40
+ RBACPermission.name == permission,
41
41
  )
42
42
  )
43
43
  )
@@ -1,7 +1,7 @@
1
1
  import base64
2
2
  import dataclasses
3
3
  import json
4
- from datetime import datetime, date
4
+ from datetime import date, datetime
5
5
  from decimal import Decimal
6
6
  from socket import socket
7
7
  from typing import Any
@@ -1,7 +1,7 @@
1
1
  import dataclasses
2
2
  import json
3
3
  import logging
4
- from datetime import datetime, date
4
+ from datetime import date, datetime
5
5
  from decimal import Decimal
6
6
 
7
7
  import msgpack
@@ -3,11 +3,11 @@ import json
3
3
  import logging
4
4
  import struct
5
5
  from _decimal import Decimal
6
- from datetime import datetime, date
6
+ from datetime import date, datetime
7
7
  from typing import Any
8
8
 
9
9
  from msgspec import msgpack
10
- from msgspec.msgpack import encode, Ext
10
+ from msgspec.msgpack import Ext, encode
11
11
 
12
12
  from python3_commons.serializers.json import CustomJSONEncoder
13
13
 
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python3-commons
3
- Version: 0.8.18
3
+ Version: 0.8.20
4
4
  Summary: Re-usable Python3 code
5
5
  Author-email: Oleg Korsak <kamikaze.is.waiting.you@gmail.com>
6
- License: gpl-3
6
+ License-Expression: GPL-3.0
7
7
  Project-URL: Homepage, https://github.com/kamikaze/python3-commons
8
8
  Project-URL: Documentation, https://github.com/kamikaze/python3-commons/wiki
9
9
  Classifier: Development Status :: 4 - Beta
10
10
  Classifier: Programming Language :: Python
11
- Requires-Python: >=3.13
11
+ Requires-Python: ==3.13.*
12
12
  Description-Content-Type: text/x-rst
13
13
  License-File: LICENSE
14
14
  License-File: AUTHORS.rst
@@ -22,12 +22,10 @@ Requires-Dist: msgpack~=1.1.0
22
22
  Requires-Dist: msgspec~=0.19.0
23
23
  Requires-Dist: pydantic[email]~=2.11.3
24
24
  Requires-Dist: pydantic-settings~=2.9.1
25
+ Requires-Dist: python-jose==3.4.0
25
26
  Requires-Dist: SQLAlchemy[asyncio]~=2.0.40
26
27
  Requires-Dist: valkey[libvalkey]~=6.1.0
27
28
  Requires-Dist: zeep~=4.3.1
28
- Provides-Extra: testing
29
- Requires-Dist: pytest; extra == "testing"
30
- Requires-Dist: pytest-cov; extra == "testing"
31
29
  Dynamic: license-file
32
30
 
33
31
  Re-usable Python3 code
@@ -0,0 +1,29 @@
1
+ python3_commons/__init__.py,sha256=0KgaYU46H_IMKn-BuasoRN3C4Hi45KlkHHoPbU9cwiA,189
2
+ python3_commons/api_client.py,sha256=LT7_YmnYVHK2ucKxIhUJCZrmxgfy-lfOxx08-R0WvW0,4505
3
+ python3_commons/audit.py,sha256=osx2ywZXf-V0zOkrhlNgSyzCBvojXQwSYBQ4-ze1xiM,6249
4
+ python3_commons/auth.py,sha256=X8mo7L0dhy1EwzHjOkjBAPvHCCqeTS6yhkcVfVi89cU,2459
5
+ python3_commons/cache.py,sha256=lf27LTD4Z9Iqi5GaK8jH8UC0cL9sHH8wicZ88YDp6Mg,7725
6
+ python3_commons/conf.py,sha256=1VebgIPy195dX4mHDSsDRt-8rE1WrloxKGR-dP0PzBw,1466
7
+ python3_commons/fs.py,sha256=wfLjybXndwLqNlOxTpm_HRJnuTcC4wbrHEOaEeCo9Wc,337
8
+ python3_commons/helpers.py,sha256=ygnTv3KYoiibOFIi99-g8EXaETKHLt5i3jvykGrv6aE,3079
9
+ python3_commons/object_storage.py,sha256=nQsXca0zzzeSY35qhnjE6pLfkLuxn7jDul0-hw0jizE,3985
10
+ python3_commons/permissions.py,sha256=bhjTp-tq-oaTGFMHNnSBlcVX5XQCTL0nWcu6SdPEAB4,1555
11
+ python3_commons/db/__init__.py,sha256=ONlvuAYEagLeSdU9oX02JBpECvWdqNx0OoR9BMCvAIQ,2741
12
+ python3_commons/db/helpers.py,sha256=PY0h08aLiGx-J54wmP3GHPCgGCcLd60rayAUnR3aWdI,1742
13
+ python3_commons/db/models/__init__.py,sha256=Utr5AJf1FwcrxNtdesgjq92WMK4zpK4VL_8z1JEkJw0,185
14
+ python3_commons/db/models/auth.py,sha256=dmyD3BX7LVBgKiepPN-bxlY6J3PhcmUfVdQwhNR45fU,1187
15
+ python3_commons/db/models/common.py,sha256=8JUDUBJLf59xLNhF57zZBgcgrmvcWAYXGYO4P-LKzHY,1512
16
+ python3_commons/db/models/rbac.py,sha256=7NNTUbS8whuPUHpm4oba_UWDdNiJlHrm8HBO7oGtk64,3185
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=5SS_wQXcrThPmCWjI7k1a_cHWLz46Jzn3pit89SYJGY,2038
24
+ python3_commons-0.8.20.dist-info/licenses/AUTHORS.rst,sha256=3R9JnfjfjH5RoPWOeqKFJgxVShSSfzQPIrEr1nxIo9Q,90
25
+ python3_commons-0.8.20.dist-info/licenses/LICENSE,sha256=xxILuojHm4fKQOrMHPSslbyy6WuKAN2RiG74HbrYfzM,34575
26
+ python3_commons-0.8.20.dist-info/METADATA,sha256=PkY1GX8xkVeuqa3awQM1i-tIeUN9FtkB4MZ30pk_Vqk,1127
27
+ python3_commons-0.8.20.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
28
+ python3_commons-0.8.20.dist-info/top_level.txt,sha256=lJI6sCBf68eUHzupCnn2dzG10lH3jJKTWM_hrN1cQ7M,16
29
+ python3_commons-0.8.20.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.7.1)
2
+ Generator: setuptools (80.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,28 +0,0 @@
1
- python3_commons/__init__.py,sha256=0KgaYU46H_IMKn-BuasoRN3C4Hi45KlkHHoPbU9cwiA,189
2
- python3_commons/api_client.py,sha256=zj6zTF-DhUMu4bjj9VWLi63mVODG_SB47Q5qlmG-Sxg,4539
3
- python3_commons/audit.py,sha256=DMQ-nrWSs0qilD7wkz_8PV4jXcee75O8FgAm2YIuOiY,6256
4
- python3_commons/cache.py,sha256=PIzEcJSblDZJ9L4-9N3kI4fIG-vImwScXQ9gb0jk_L8,7792
5
- python3_commons/conf.py,sha256=Vu6eZ8uHiKFHe3OLdtImrUZe5JH4KH7HULwF5M4kY8U,1232
6
- python3_commons/fs.py,sha256=wfLjybXndwLqNlOxTpm_HRJnuTcC4wbrHEOaEeCo9Wc,337
7
- python3_commons/helpers.py,sha256=OmuCF7UeJ6oe-rH1Y4ZYVW_uPQ973lCLsikj01wmNqs,3101
8
- python3_commons/object_storage.py,sha256=Bte49twIJ4l6VAzIqK6tLx7DFYwijErUmmhWkrZpSJE,4074
9
- python3_commons/permissions.py,sha256=V6kT7gBQ0fZRDQwreM9es90BvvpB3gUofuqnFvx2g-o,1553
10
- python3_commons/db/__init__.py,sha256=_SKKKf4uW4xYdSkL6Getow9ERGd_RYBlaM4xKEQKos8,2741
11
- python3_commons/db/helpers.py,sha256=0z-pXAxQg0KqrqfoUcRooRHNrrKi-g2mSO6euVMscpE,1891
12
- python3_commons/db/models/__init__.py,sha256=AeeTLUqdqQrhCTi14ACWO0ccyVyWFppZVtCeWShSvR0,193
13
- python3_commons/db/models/auth.py,sha256=ctHiz_5x17TdXac7a3SjfuZ0-nEgnxK7RwKOY8qflZc,1195
14
- python3_commons/db/models/common.py,sha256=G_6nTxholINO8Fr1uur36K5EYmrUCGsE2U6jTxjpoIo,1568
15
- python3_commons/db/models/rbac.py,sha256=atgCfAWgb8BJYKd7wuzV1HTDhasCL7448iPRNLBAjlw,3249
16
- python3_commons/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- python3_commons/logging/filters.py,sha256=fuyjXZAUm-i2MNrxvFYag8F8Rr27x8W8MdV3ke6miSs,175
18
- python3_commons/logging/formatters.py,sha256=UXmmh1yd5Kc2dpvSHn6uCWLDWE2LMjlYAaH8cg3siV4,720
19
- python3_commons/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- python3_commons/serializers/json.py,sha256=P288wWz9ic38QWEMrpp_uwKPYkQiOgvE1cI4WZn6ZCg,808
21
- python3_commons/serializers/msgpack.py,sha256=tzIGGyDL3UpZnnouCtnxuYDx6InKM_C3PP1N4PN8wd4,1269
22
- python3_commons/serializers/msgspec.py,sha256=FuZVqOLJb0-lEKrs7dtjhwEbHIpfMUk5yu1hD64zRdc,2038
23
- python3_commons-0.8.18.dist-info/licenses/AUTHORS.rst,sha256=3R9JnfjfjH5RoPWOeqKFJgxVShSSfzQPIrEr1nxIo9Q,90
24
- python3_commons-0.8.18.dist-info/licenses/LICENSE,sha256=xxILuojHm4fKQOrMHPSslbyy6WuKAN2RiG74HbrYfzM,34575
25
- python3_commons-0.8.18.dist-info/METADATA,sha256=JnFj1oxFjh5cQ49ndtN6EI3Znc4UttU8Fq1LSVN1ebc,1190
26
- python3_commons-0.8.18.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
27
- python3_commons-0.8.18.dist-info/top_level.txt,sha256=lJI6sCBf68eUHzupCnn2dzG10lH3jJKTWM_hrN1cQ7M,16
28
- python3_commons-0.8.18.dist-info/RECORD,,
File without changes
File without changes