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.
- python3_commons/api_client.py +7 -12
- python3_commons/audit.py +6 -5
- python3_commons/auth.py +82 -0
- python3_commons/cache.py +13 -14
- python3_commons/conf.py +10 -1
- python3_commons/db/__init__.py +1 -1
- python3_commons/db/helpers.py +13 -19
- python3_commons/db/models/__init__.py +1 -3
- python3_commons/db/models/auth.py +1 -3
- python3_commons/db/models/common.py +3 -11
- python3_commons/db/models/rbac.py +5 -15
- python3_commons/helpers.py +8 -7
- python3_commons/{logging → log}/formatters.py +0 -1
- python3_commons/object_storage.py +14 -12
- python3_commons/permissions.py +4 -4
- python3_commons/serializers/json.py +1 -1
- python3_commons/serializers/msgpack.py +1 -1
- python3_commons/serializers/msgspec.py +2 -2
- {python3_commons-0.8.18.dist-info → python3_commons-0.8.20.dist-info}/METADATA +4 -6
- python3_commons-0.8.20.dist-info/RECORD +29 -0
- {python3_commons-0.8.18.dist-info → python3_commons-0.8.20.dist-info}/WHEEL +1 -1
- python3_commons-0.8.18.dist-info/RECORD +0 -28
- /python3_commons/{logging → log}/__init__.py +0 -0
- /python3_commons/{logging → log}/filters.py +0 -0
- {python3_commons-0.8.18.dist-info → python3_commons-0.8.20.dist-info}/licenses/AUTHORS.rst +0 -0
- {python3_commons-0.8.18.dist-info → python3_commons-0.8.20.dist-info}/licenses/LICENSE +0 -0
- {python3_commons-0.8.18.dist-info → python3_commons-0.8.20.dist-info}/top_level.txt +0 -0
python3_commons/api_client.py
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
import logging
|
2
2
|
from contextlib import asynccontextmanager
|
3
|
-
from datetime import
|
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,
|
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
|
-
|
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(
|
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
|
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(
|
71
|
-
|
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:
|
python3_commons/auth.py
ADDED
@@ -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
|
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
|
-
|
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
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
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()
|
python3_commons/db/__init__.py
CHANGED
@@ -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
|
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
|
|
python3_commons/db/helpers.py
CHANGED
@@ -2,22 +2,22 @@ import logging
|
|
2
2
|
from typing import Mapping
|
3
3
|
|
4
4
|
import sqlalchemy as sa
|
5
|
-
from sqlalchemy import
|
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(
|
12
|
-
|
13
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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):
|
python3_commons/helpers.py
CHANGED
@@ -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
|
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
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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)}'
|
@@ -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
|
9
|
+
from minio.deleteobjects import DeleteError, DeleteObject
|
10
10
|
|
11
|
-
from python3_commons.conf import
|
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(
|
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(
|
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(
|
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(
|
92
|
-
|
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(
|
110
|
-
|
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),
|
116
|
-
|
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)
|
python3_commons/permissions.py
CHANGED
@@ -2,10 +2,10 @@ import logging
|
|
2
2
|
from uuid import UUID
|
3
3
|
|
4
4
|
import sqlalchemy as sa
|
5
|
-
from sqlalchemy import
|
5
|
+
from sqlalchemy import and_, exists, func
|
6
6
|
from sqlalchemy.ext.asyncio import AsyncSession
|
7
7
|
|
8
|
-
from python3_commons.db.models import
|
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
|
)
|
@@ -3,11 +3,11 @@ import json
|
|
3
3
|
import logging
|
4
4
|
import struct
|
5
5
|
from _decimal import Decimal
|
6
|
-
from datetime import
|
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
|
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.
|
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:
|
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:
|
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,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
|
File without changes
|
File without changes
|
File without changes
|