python3-commons 0.6.20__tar.gz → 0.7.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python3_commons-0.6.20 → python3_commons-0.7.1}/PKG-INFO +4 -1
- {python3_commons-0.6.20 → python3_commons-0.7.1}/pyproject.toml +4 -1
- {python3_commons-0.6.20 → python3_commons-0.7.1}/requirements.txt +3 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons/conf.py +6 -1
- python3_commons-0.7.1/src/python3_commons/db/__init__.py +60 -0
- python3_commons-0.7.1/src/python3_commons/db/helpers.py +61 -0
- python3_commons-0.7.1/src/python3_commons/db/models/__init__.py +4 -0
- python3_commons-0.7.1/src/python3_commons/db/models/auth.py +43 -0
- python3_commons-0.7.1/src/python3_commons/db/models/common.py +37 -0
- python3_commons-0.7.1/src/python3_commons/db/models/rbac.py +101 -0
- python3_commons-0.7.1/src/python3_commons/permissions.py +29 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons.egg-info/PKG-INFO +4 -1
- {python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons.egg-info/SOURCES.txt +7 -1
- {python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons.egg-info/requires.txt +3 -0
- python3_commons-0.6.20/src/python3_commons/db.py +0 -28
- {python3_commons-0.6.20 → python3_commons-0.7.1}/.coveragerc +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/.github/workflows/python-publish.yaml +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/.gitignore +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/AUTHORS.rst +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/CHANGELOG.rst +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/LICENSE +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/README.md +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/README.rst +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/docs/Makefile +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/docs/_static/.gitignore +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/docs/authors.rst +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/docs/changelog.rst +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/docs/conf.py +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/docs/index.rst +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/docs/license.rst +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/requirements_dev.txt +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/requirements_test.txt +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/setup.cfg +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/setup.py +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons/__init__.py +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons/api_client.py +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons/audit.py +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons/fs.py +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons/helpers.py +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons/logging/__init__.py +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons/logging/filters.py +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons/logging/formatters.py +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons/object_storage.py +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons/serializers/__init__.py +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons/serializers/json.py +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons/serializers/msgpack.py +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons/serializers/msgspec.py +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons.egg-info/dependency_links.txt +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons.egg-info/top_level.txt +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/tests/conftest.py +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/tests/test_audit.py +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/tests/test_helpers.py +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/tests/test_msgpack.py +0 -0
- {python3_commons-0.6.20 → python3_commons-0.7.1}/tests/test_msgspec.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: python3-commons
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.7.1
|
4
4
|
Summary: Re-usable Python3 code
|
5
5
|
Author-email: Oleg Korsak <kamikaze.is.waiting.you@gmail.com>
|
6
6
|
License: gpl-3
|
@@ -14,12 +14,15 @@ License-File: LICENSE
|
|
14
14
|
License-File: AUTHORS.rst
|
15
15
|
Requires-Dist: aiohttp[speedups]~=3.11.13
|
16
16
|
Requires-Dist: asyncpg~=0.30.0
|
17
|
+
Requires-Dist: fastapi-users-db-sqlalchemy~=7.0.0
|
18
|
+
Requires-Dist: fastapi-users[sqlalchemy]~=14.0.1
|
17
19
|
Requires-Dist: lxml~=5.3.1
|
18
20
|
Requires-Dist: minio~=7.2.15
|
19
21
|
Requires-Dist: msgpack~=1.1.0
|
20
22
|
Requires-Dist: msgspec~=0.19.0
|
21
23
|
Requires-Dist: pydantic[email]~=2.10.6
|
22
24
|
Requires-Dist: pydantic-settings~=2.8.1
|
25
|
+
Requires-Dist: SQLAlchemy[asyncio]~=2.0.38
|
23
26
|
Requires-Dist: zeep~=4.3.1
|
24
27
|
Provides-Extra: testing
|
25
28
|
Requires-Dist: pytest; extra == "testing"
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "python3-commons"
|
7
|
-
version = "0.
|
7
|
+
version = "0.7.1"
|
8
8
|
description = "Re-usable Python3 code"
|
9
9
|
authors = [
|
10
10
|
{name = "Oleg Korsak", email = "kamikaze.is.waiting.you@gmail.com"}
|
@@ -19,12 +19,15 @@ keywords = []
|
|
19
19
|
dependencies = [
|
20
20
|
"aiohttp[speedups]~=3.11.13",
|
21
21
|
"asyncpg~=0.30.0",
|
22
|
+
"fastapi-users-db-sqlalchemy~=7.0.0",
|
23
|
+
"fastapi-users[sqlalchemy]~=14.0.1",
|
22
24
|
"lxml~=5.3.1",
|
23
25
|
"minio~=7.2.15",
|
24
26
|
"msgpack~=1.1.0",
|
25
27
|
"msgspec~=0.19.0",
|
26
28
|
"pydantic[email]~=2.10.6",
|
27
29
|
"pydantic-settings~=2.8.1",
|
30
|
+
"SQLAlchemy[asyncio]~=2.0.38",
|
28
31
|
"zeep~=4.3.1"
|
29
32
|
# Replace msgspec Git dependency for PyPI compatibility, consider releasing
|
30
33
|
# a vendorized or fixed version to your environment
|
@@ -1,9 +1,12 @@
|
|
1
1
|
aiohttp[speedups]~=3.11.13
|
2
2
|
asyncpg~=0.30.0
|
3
|
+
fastapi-users-db-sqlalchemy~=7.0.0
|
4
|
+
fastapi-users[sqlalchemy]~=14.0.1
|
3
5
|
lxml~=5.3.1
|
4
6
|
minio~=7.2.15
|
5
7
|
msgpack~=1.1.0
|
6
8
|
msgspec~=0.19.0
|
7
9
|
pydantic[email]~=2.10.6
|
8
10
|
pydantic-settings~=2.8.1
|
11
|
+
SQLAlchemy[asyncio]~=2.0.38
|
9
12
|
zeep~=4.3.1
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from pydantic import SecretStr
|
1
|
+
from pydantic import SecretStr, PostgresDsn
|
2
2
|
from pydantic_settings import BaseSettings
|
3
3
|
|
4
4
|
|
@@ -8,6 +8,10 @@ class CommonSettings(BaseSettings):
|
|
8
8
|
logging_formatter: str = 'default'
|
9
9
|
|
10
10
|
|
11
|
+
class DBSettings(BaseSettings):
|
12
|
+
db_dsn: PostgresDsn | None = None
|
13
|
+
|
14
|
+
|
11
15
|
class S3Settings(BaseSettings):
|
12
16
|
s3_endpoint_url: str | None = None
|
13
17
|
s3_region_name: str | None = None
|
@@ -20,4 +24,5 @@ class S3Settings(BaseSettings):
|
|
20
24
|
|
21
25
|
|
22
26
|
settings = CommonSettings()
|
27
|
+
db_settings = DBSettings()
|
23
28
|
s3_settings = S3Settings()
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import asyncio
|
2
|
+
import contextlib
|
3
|
+
import logging
|
4
|
+
from typing import AsyncGenerator
|
5
|
+
|
6
|
+
from asyncpg import CannotConnectNowError
|
7
|
+
from pydantic import PostgresDsn
|
8
|
+
from sqlalchemy import MetaData
|
9
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
10
|
+
from sqlalchemy.ext.asyncio.session import async_sessionmaker
|
11
|
+
from sqlalchemy.orm import declarative_base
|
12
|
+
|
13
|
+
from python3_commons.conf import db_settings
|
14
|
+
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
metadata = MetaData()
|
18
|
+
Base = declarative_base(metadata=metadata)
|
19
|
+
engine = create_async_engine(
|
20
|
+
str(db_settings.db_dsn),
|
21
|
+
# echo=True,
|
22
|
+
pool_size=20,
|
23
|
+
max_overflow=0,
|
24
|
+
pool_timeout=30,
|
25
|
+
pool_recycle=1800, # 30 minutes
|
26
|
+
)
|
27
|
+
async_session_maker = async_sessionmaker(engine, expire_on_commit=False)
|
28
|
+
|
29
|
+
|
30
|
+
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
|
31
|
+
async with async_session_maker() as session:
|
32
|
+
yield session
|
33
|
+
|
34
|
+
|
35
|
+
get_async_session_context = contextlib.asynccontextmanager(get_async_session)
|
36
|
+
|
37
|
+
|
38
|
+
async def is_healthy(pg) -> bool:
|
39
|
+
return await pg.fetchval('SELECT 1 FROM alembic_version;') == 1
|
40
|
+
|
41
|
+
|
42
|
+
async def connect_to_db(database, dsn: PostgresDsn):
|
43
|
+
logger.info('Waiting for services')
|
44
|
+
logger.debug(f'DB_DSN: {dsn}')
|
45
|
+
timeout = 0.001
|
46
|
+
total_timeout = 0
|
47
|
+
|
48
|
+
for i in range(15):
|
49
|
+
try:
|
50
|
+
await database.connect()
|
51
|
+
except (ConnectionRefusedError, CannotConnectNowError):
|
52
|
+
timeout *= 2
|
53
|
+
await asyncio.sleep(timeout)
|
54
|
+
total_timeout += timeout
|
55
|
+
else:
|
56
|
+
break
|
57
|
+
else:
|
58
|
+
msg = f'Unable to connect database for {int(total_timeout)}s'
|
59
|
+
logger.error(msg)
|
60
|
+
raise ConnectionRefusedError(msg)
|
@@ -0,0 +1,61 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import Mapping
|
3
|
+
|
4
|
+
import sqlalchemy as sa
|
5
|
+
from sqlalchemy import desc, asc, func
|
6
|
+
from sqlalchemy.sql.elements import BooleanClauseList, UnaryExpression
|
7
|
+
|
8
|
+
logger = logging.getLogger(__name__)
|
9
|
+
|
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]:
|
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
|
21
|
+
"""
|
22
|
+
|
23
|
+
if order_by:
|
24
|
+
if order_by.startswith('-'):
|
25
|
+
direction = desc
|
26
|
+
order_by = order_by[1:]
|
27
|
+
else:
|
28
|
+
direction = asc
|
29
|
+
|
30
|
+
order_by_clause = direction(columns[order_by][0])
|
31
|
+
else:
|
32
|
+
order_by_clause = None
|
33
|
+
|
34
|
+
if search:
|
35
|
+
where_parts = [
|
36
|
+
*(
|
37
|
+
(func.upper(columns[k][0])
|
38
|
+
if columns[k][1]
|
39
|
+
else columns[k][0]
|
40
|
+
) == columns[k][2](v)
|
41
|
+
for k, v in search.items()
|
42
|
+
if columns[k][3]
|
43
|
+
),
|
44
|
+
*(
|
45
|
+
(func.upper(columns[k][0])
|
46
|
+
if columns[k][1]
|
47
|
+
else columns[k][0]
|
48
|
+
).like(f'%{v.upper()}%')
|
49
|
+
for k, v in search.items()
|
50
|
+
if not columns[k][3]
|
51
|
+
)
|
52
|
+
]
|
53
|
+
else:
|
54
|
+
where_parts = None
|
55
|
+
|
56
|
+
if where_parts:
|
57
|
+
where_clause = sa.and_(*where_parts)
|
58
|
+
else:
|
59
|
+
where_clause = None
|
60
|
+
|
61
|
+
return where_clause, order_by_clause
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import uuid
|
2
|
+
|
3
|
+
from fastapi_users_db_sqlalchemy import GUID, SQLAlchemyBaseUserTableUUID
|
4
|
+
from pydantic import AwareDatetime
|
5
|
+
from sqlalchemy import (
|
6
|
+
String, BIGINT, ForeignKey, DateTime
|
7
|
+
)
|
8
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
9
|
+
|
10
|
+
from python3_commons.db import Base
|
11
|
+
from python3_commons.db.models.common import BaseDBModel, BaseDBUUIDModel
|
12
|
+
|
13
|
+
|
14
|
+
class UserGroup(BaseDBModel, Base):
|
15
|
+
__tablename__ = 'user_groups'
|
16
|
+
|
17
|
+
name: Mapped[str] = mapped_column(String, nullable=False)
|
18
|
+
|
19
|
+
|
20
|
+
class User(SQLAlchemyBaseUserTableUUID, Base):
|
21
|
+
__tablename__ = 'users'
|
22
|
+
|
23
|
+
username: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False)
|
24
|
+
group_id: Mapped[int | None] = mapped_column(BIGINT, ForeignKey('user_groups.id'))
|
25
|
+
role_id: Mapped[uuid.UUID | None] = mapped_column(
|
26
|
+
GUID,
|
27
|
+
ForeignKey('permissions.id', name='fk_api_key_permission_api_key', ondelete='RESTRICT'),
|
28
|
+
nullable=False,
|
29
|
+
index=True,
|
30
|
+
)
|
31
|
+
|
32
|
+
|
33
|
+
class ApiKey(BaseDBUUIDModel, Base):
|
34
|
+
__tablename__ = 'api_keys'
|
35
|
+
|
36
|
+
user_id: Mapped[uuid.UUID | None] = mapped_column(
|
37
|
+
GUID,
|
38
|
+
ForeignKey('users.id', name='fk_api_key_user', ondelete='RESTRICT'),
|
39
|
+
index=True,
|
40
|
+
)
|
41
|
+
partner_name: Mapped[str] = mapped_column(String, unique=True)
|
42
|
+
key: Mapped[str] = mapped_column(String, unique=True)
|
43
|
+
expires_at: Mapped[AwareDatetime] = mapped_column(DateTime(timezone=True))
|
@@ -0,0 +1,37 @@
|
|
1
|
+
from pydantic import AwareDatetime
|
2
|
+
from sqlalchemy import (
|
3
|
+
DateTime, BIGINT
|
4
|
+
)
|
5
|
+
from sqlalchemy.dialects.postgresql import UUID
|
6
|
+
from sqlalchemy.ext.compiler import compiles
|
7
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
8
|
+
from sqlalchemy.sql import expression
|
9
|
+
from sqlalchemy.sql.ddl import CreateColumn
|
10
|
+
|
11
|
+
|
12
|
+
class UTCNow(expression.FunctionElement):
|
13
|
+
type = DateTime(timezone=True)
|
14
|
+
|
15
|
+
|
16
|
+
@compiles(UTCNow, 'postgresql')
|
17
|
+
def pg_utcnow(element, compiler, **kw):
|
18
|
+
return "TIMEZONE('utc', CURRENT_TIMESTAMP)"
|
19
|
+
|
20
|
+
|
21
|
+
@compiles(CreateColumn, 'postgresql')
|
22
|
+
def use_identity(element, compiler, **kw):
|
23
|
+
result = compiler.visit_create_column(element, **kw).replace('SERIAL', 'INT GENERATED BY DEFAULT AS IDENTITY')
|
24
|
+
|
25
|
+
return result.replace('BIGSERIAL', 'BIGINT GENERATED BY DEFAULT AS IDENTITY')
|
26
|
+
|
27
|
+
|
28
|
+
class BaseDBModel:
|
29
|
+
id: Mapped[int] = mapped_column(BIGINT, primary_key=True)
|
30
|
+
created_at: Mapped[AwareDatetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=UTCNow())
|
31
|
+
updated_at: Mapped[AwareDatetime] = mapped_column(DateTime(timezone=True), onupdate=UTCNow())
|
32
|
+
|
33
|
+
|
34
|
+
class BaseDBUUIDModel:
|
35
|
+
uid: Mapped[UUID] = mapped_column(UUID, primary_key=True)
|
36
|
+
created_at: Mapped[AwareDatetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=UTCNow())
|
37
|
+
updated_at: Mapped[AwareDatetime | None] = mapped_column(DateTime(timezone=True), onupdate=UTCNow())
|
@@ -0,0 +1,101 @@
|
|
1
|
+
import uuid
|
2
|
+
|
3
|
+
from fastapi_users_db_sqlalchemy import GUID
|
4
|
+
from pydantic import AwareDatetime
|
5
|
+
from sqlalchemy import (
|
6
|
+
String, DateTime, ForeignKey, PrimaryKeyConstraint, CheckConstraint
|
7
|
+
)
|
8
|
+
from sqlalchemy.dialects.postgresql import UUID
|
9
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
10
|
+
|
11
|
+
from python3_commons.db import Base
|
12
|
+
|
13
|
+
|
14
|
+
class RBACRole(Base):
|
15
|
+
__tablename__ = 'rbac_roles'
|
16
|
+
|
17
|
+
uid: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True)
|
18
|
+
name: Mapped[str] = mapped_column(String, unique=True, nullable=False)
|
19
|
+
|
20
|
+
|
21
|
+
class RBACPermission(Base):
|
22
|
+
__tablename__ = 'rbac_permissions'
|
23
|
+
|
24
|
+
uid: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True)
|
25
|
+
name: Mapped[str] = mapped_column(String, unique=True, nullable=False)
|
26
|
+
|
27
|
+
__table_args__ = (
|
28
|
+
CheckConstraint("name ~ '^[a-z0-9_.]+$'", name='check_rbac_permissions_name'),
|
29
|
+
)
|
30
|
+
|
31
|
+
|
32
|
+
class RBACRolePermission(Base):
|
33
|
+
__tablename__ = 'rbac_role_permissions'
|
34
|
+
|
35
|
+
role_uid: Mapped[uuid.UUID | None] = mapped_column(
|
36
|
+
UUID,
|
37
|
+
ForeignKey('rbac_roles.uid', name='fk_rbac_role_permissions_role', ondelete='CASCADE'),
|
38
|
+
index=True,
|
39
|
+
)
|
40
|
+
permission_uid: Mapped[uuid.UUID | None] = mapped_column(
|
41
|
+
UUID,
|
42
|
+
ForeignKey('rbac_permissions.uid', name='fk_rbac_role_permissions_permission', ondelete='CASCADE'),
|
43
|
+
index=True,
|
44
|
+
)
|
45
|
+
|
46
|
+
__table_args__ = (
|
47
|
+
PrimaryKeyConstraint('role_uid', 'permission_uid', name='pk_rbac_role_permissions'),
|
48
|
+
)
|
49
|
+
|
50
|
+
|
51
|
+
class RBACUserRole(Base):
|
52
|
+
__tablename__ = 'rbac_user_roles'
|
53
|
+
|
54
|
+
user_id: Mapped[uuid.UUID | None] = mapped_column(
|
55
|
+
GUID,
|
56
|
+
ForeignKey('users.id', name='fk_rbac_user_roles_user', ondelete='CASCADE'),
|
57
|
+
index=True,
|
58
|
+
)
|
59
|
+
role_uid: Mapped[uuid.UUID | None] = mapped_column(
|
60
|
+
UUID,
|
61
|
+
ForeignKey('rbac_roles.uid', name='fk_rbac_user_roles_role', ondelete='CASCADE'),
|
62
|
+
index=True,
|
63
|
+
)
|
64
|
+
starts_at: Mapped[AwareDatetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
65
|
+
expires_at: Mapped[AwareDatetime | None] = mapped_column(DateTime(timezone=True))
|
66
|
+
|
67
|
+
__table_args__ = (
|
68
|
+
PrimaryKeyConstraint('user_id', 'role_uid', name='pk_rbac_user_roles'),
|
69
|
+
)
|
70
|
+
|
71
|
+
|
72
|
+
class RBACApiKeyRole(Base):
|
73
|
+
__tablename__ = 'rbac_api_key_roles'
|
74
|
+
|
75
|
+
api_key_uid: Mapped[uuid.UUID | None] = mapped_column(
|
76
|
+
UUID,
|
77
|
+
ForeignKey('api_keys.uid', name='fk_rbac_api_key_roles_user', ondelete='CASCADE'),
|
78
|
+
index=True,
|
79
|
+
)
|
80
|
+
role_uid: Mapped[uuid.UUID | None] = mapped_column(
|
81
|
+
UUID,
|
82
|
+
ForeignKey('rbac_roles.uid', name='fk_rbac_api_key_roles_role', ondelete='CASCADE'),
|
83
|
+
index=True,
|
84
|
+
)
|
85
|
+
starts_at: Mapped[AwareDatetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
86
|
+
expires_at: Mapped[AwareDatetime | None] = mapped_column(DateTime(timezone=True))
|
87
|
+
|
88
|
+
__table_args__ = (
|
89
|
+
PrimaryKeyConstraint('api_key_uid', 'role_uid', name='pk_rbac_api_key_roles'),
|
90
|
+
)
|
91
|
+
|
92
|
+
|
93
|
+
# class RBACRoleRelation(Base):
|
94
|
+
# __tablename__ = 'rbac_role_relations'
|
95
|
+
#
|
96
|
+
# parent_uid: Mapped[uuid.UUID] = mapped_column(UUID)
|
97
|
+
# child_uid: Mapped[uuid.UUID] = mapped_column(UUID)
|
98
|
+
#
|
99
|
+
# __table_args__ = (
|
100
|
+
# PrimaryKeyConstraint('parent_uid', 'child_uid', name='pk_rbac_role_relations'),
|
101
|
+
# )
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import logging
|
2
|
+
from uuid import UUID
|
3
|
+
|
4
|
+
import sqlalchemy as sa
|
5
|
+
from sqlalchemy import func, exists, and_
|
6
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
7
|
+
|
8
|
+
from python3_commons.db.models import RBACPermission, RBACApiKeyRole, RBACRolePermission
|
9
|
+
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
async def has_api_key_permission(session: AsyncSession, api_key_uid: UUID, permission: str) -> bool:
|
14
|
+
query = sa.select(
|
15
|
+
exists().where(
|
16
|
+
and_(
|
17
|
+
RBACApiKeyRole.api_key_uid == api_key_uid,
|
18
|
+
(RBACApiKeyRole.expires_at.is_(None) | (RBACApiKeyRole.expires_at > func.now())),
|
19
|
+
RBACApiKeyRole.role_uid == RBACRolePermission.role_uid,
|
20
|
+
RBACRolePermission.permission_uid == RBACPermission.uid,
|
21
|
+
RBACPermission.name == permission
|
22
|
+
)
|
23
|
+
)
|
24
|
+
)
|
25
|
+
|
26
|
+
cursor = await session.execute(query)
|
27
|
+
result = cursor.scalar()
|
28
|
+
|
29
|
+
return result
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: python3-commons
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.7.1
|
4
4
|
Summary: Re-usable Python3 code
|
5
5
|
Author-email: Oleg Korsak <kamikaze.is.waiting.you@gmail.com>
|
6
6
|
License: gpl-3
|
@@ -14,12 +14,15 @@ License-File: LICENSE
|
|
14
14
|
License-File: AUTHORS.rst
|
15
15
|
Requires-Dist: aiohttp[speedups]~=3.11.13
|
16
16
|
Requires-Dist: asyncpg~=0.30.0
|
17
|
+
Requires-Dist: fastapi-users-db-sqlalchemy~=7.0.0
|
18
|
+
Requires-Dist: fastapi-users[sqlalchemy]~=14.0.1
|
17
19
|
Requires-Dist: lxml~=5.3.1
|
18
20
|
Requires-Dist: minio~=7.2.15
|
19
21
|
Requires-Dist: msgpack~=1.1.0
|
20
22
|
Requires-Dist: msgspec~=0.19.0
|
21
23
|
Requires-Dist: pydantic[email]~=2.10.6
|
22
24
|
Requires-Dist: pydantic-settings~=2.8.1
|
25
|
+
Requires-Dist: SQLAlchemy[asyncio]~=2.0.38
|
23
26
|
Requires-Dist: zeep~=4.3.1
|
24
27
|
Provides-Extra: testing
|
25
28
|
Requires-Dist: pytest; extra == "testing"
|
@@ -22,15 +22,21 @@ src/python3_commons/__init__.py
|
|
22
22
|
src/python3_commons/api_client.py
|
23
23
|
src/python3_commons/audit.py
|
24
24
|
src/python3_commons/conf.py
|
25
|
-
src/python3_commons/db.py
|
26
25
|
src/python3_commons/fs.py
|
27
26
|
src/python3_commons/helpers.py
|
28
27
|
src/python3_commons/object_storage.py
|
28
|
+
src/python3_commons/permissions.py
|
29
29
|
src/python3_commons.egg-info/PKG-INFO
|
30
30
|
src/python3_commons.egg-info/SOURCES.txt
|
31
31
|
src/python3_commons.egg-info/dependency_links.txt
|
32
32
|
src/python3_commons.egg-info/requires.txt
|
33
33
|
src/python3_commons.egg-info/top_level.txt
|
34
|
+
src/python3_commons/db/__init__.py
|
35
|
+
src/python3_commons/db/helpers.py
|
36
|
+
src/python3_commons/db/models/__init__.py
|
37
|
+
src/python3_commons/db/models/auth.py
|
38
|
+
src/python3_commons/db/models/common.py
|
39
|
+
src/python3_commons/db/models/rbac.py
|
34
40
|
src/python3_commons/logging/__init__.py
|
35
41
|
src/python3_commons/logging/filters.py
|
36
42
|
src/python3_commons/logging/formatters.py
|
@@ -1,11 +1,14 @@
|
|
1
1
|
aiohttp[speedups]~=3.11.13
|
2
2
|
asyncpg~=0.30.0
|
3
|
+
fastapi-users-db-sqlalchemy~=7.0.0
|
4
|
+
fastapi-users[sqlalchemy]~=14.0.1
|
3
5
|
lxml~=5.3.1
|
4
6
|
minio~=7.2.15
|
5
7
|
msgpack~=1.1.0
|
6
8
|
msgspec~=0.19.0
|
7
9
|
pydantic[email]~=2.10.6
|
8
10
|
pydantic-settings~=2.8.1
|
11
|
+
SQLAlchemy[asyncio]~=2.0.38
|
9
12
|
zeep~=4.3.1
|
10
13
|
|
11
14
|
[testing]
|
@@ -1,28 +0,0 @@
|
|
1
|
-
import asyncio
|
2
|
-
import logging
|
3
|
-
|
4
|
-
from asyncpg import CannotConnectNowError
|
5
|
-
from pydantic import PostgresDsn
|
6
|
-
|
7
|
-
logger = logging.getLogger(__name__)
|
8
|
-
|
9
|
-
|
10
|
-
async def connect_to_db(database, dsn: PostgresDsn):
|
11
|
-
logger.info('Waiting for services')
|
12
|
-
logger.debug(f'DB_DSN: {dsn}')
|
13
|
-
timeout = 0.001
|
14
|
-
total_timeout = 0
|
15
|
-
|
16
|
-
for i in range(15):
|
17
|
-
try:
|
18
|
-
await database.connect()
|
19
|
-
except (ConnectionRefusedError, CannotConnectNowError):
|
20
|
-
timeout *= 2
|
21
|
-
await asyncio.sleep(timeout)
|
22
|
-
total_timeout += timeout
|
23
|
-
else:
|
24
|
-
break
|
25
|
-
else:
|
26
|
-
msg = f'Unable to connect database for {int(total_timeout)}s'
|
27
|
-
logger.error(msg)
|
28
|
-
raise ConnectionRefusedError(msg)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons/serializers/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{python3_commons-0.6.20 → python3_commons-0.7.1}/src/python3_commons.egg-info/dependency_links.txt
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|