fast-clean 1.3.0__tar.gz → 1.4.0__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.
- {fast_clean-1.3.0 → fast_clean-1.4.0}/PKG-INFO +2 -3
- fast_clean-1.4.0/fast_clean/cli/__init__.py +6 -0
- fast_clean-1.4.0/fast_clean/cli/cryptography.py +53 -0
- fast_clean-1.4.0/fast_clean/cli/load_seed.py +31 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/healthcheck/schemas.py +0 -1
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/monitoring/middleware.py +1 -1
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/db.py +37 -22
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/depends.py +20 -19
- fast_clean-1.4.0/fast_clean/middleware.py +40 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/models.py +11 -4
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/crud/db.py +2 -2
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/storage/__init__.py +20 -2
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/storage/local.py +16 -6
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/storage/reader.py +8 -0
- fast_clean-1.4.0/fast_clean/repositories/storage/s3.py +169 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/storage/schemas.py +8 -3
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/services/cryptography/__init__.py +1 -1
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/settings.py +4 -3
- fast_clean-1.4.0/fast_clean/utils/toml.py +34 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean.egg-info/PKG-INFO +2 -3
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean.egg-info/SOURCES.txt +4 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean.egg-info/requires.txt +1 -2
- {fast_clean-1.3.0 → fast_clean-1.4.0}/pyproject.toml +5 -5
- fast_clean-1.3.0/fast_clean/middleware.py +0 -23
- fast_clean-1.3.0/fast_clean/repositories/storage/s3.py +0 -118
- {fast_clean-1.3.0 → fast_clean-1.4.0}/README.md +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/__init__.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/broker.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/container.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/__init__.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/healthcheck/__init__.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/healthcheck/router.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/logging/__init__.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/logging/enums.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/logging/sentry.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/monitoring/__init__.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/monitoring/router.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/sqlalchemy_utils/__init__.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/sqlalchemy_utils/utils.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/enums.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/exceptions.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/loggers.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/py.typed +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/redis.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/__init__.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/cache/__init__.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/cache/in_memory.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/cache/redis.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/crud/__init__.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/crud/in_memory.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/crud/type_vars.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/settings/__init__.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/settings/enums.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/settings/env.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/settings/exceptions.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/settings/type_vars.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/storage/enums.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/schemas/__init__.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/schemas/exceptions.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/schemas/pagination.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/schemas/repository.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/schemas/request_response.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/services/__init__.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/services/cryptography/aes.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/services/cryptography/enums.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/services/lock.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/services/seed.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/services/transaction.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/utils/__init__.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/utils/process.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/utils/pydantic.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/utils/ssl_context.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/utils/string.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/utils/thread.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/utils/time.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/utils/type_converters.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/utils/typer.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean.egg-info/dependency_links.txt +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean.egg-info/top_level.txt +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/setup.cfg +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/tests/test_broker.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/tests/test_db.py +0 -0
- {fast_clean-1.3.0 → fast_clean-1.4.0}/tests/test_exceptions.py +0 -0
@@ -1,10 +1,11 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: fast-clean
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.4.0
|
4
4
|
Summary: FastAPI Clean Architecture implementation
|
5
5
|
Author-email: Luferov Victor <luferovvs@yandex.ru>, Orlov Artem <squakrazv@yandex.ru>, Kashapov Rustam <hardtechnik91@gmail.com>
|
6
6
|
Requires-Python: >=3.13
|
7
7
|
Description-Content-Type: text/markdown
|
8
|
+
Requires-Dist: aiobotocore>=2.23.2
|
8
9
|
Requires-Dist: aiofiles>=24.1.0
|
9
10
|
Requires-Dist: aiokafka>=0.12.0
|
10
11
|
Requires-Dist: aioprometheus>=23.12.0
|
@@ -15,14 +16,12 @@ Requires-Dist: fastapi>=0.115.8
|
|
15
16
|
Requires-Dist: fastapi-cache2[redis]>=0.2.2
|
16
17
|
Requires-Dist: faststream>=0.5.34
|
17
18
|
Requires-Dist: flatten-dict>=0.4.2
|
18
|
-
Requires-Dist: miniopy-async>=1.21.1
|
19
19
|
Requires-Dist: overrides>=7.7.0
|
20
20
|
Requires-Dist: psycopg[binary]>=3.2.4
|
21
21
|
Requires-Dist: pydantic>=2.10.6
|
22
22
|
Requires-Dist: pydantic-settings>=2.8.0
|
23
23
|
Requires-Dist: pyyaml>=6.0.2
|
24
24
|
Requires-Dist: sentry-sdk[fastapi]>=2.32.0
|
25
|
-
Requires-Dist: snakecase>=1.0.1
|
26
25
|
Requires-Dist: sqlalchemy-utils>=0.41.2
|
27
26
|
Requires-Dist: sqlalchemy[asyncio]>=2.0.38
|
28
27
|
Requires-Dist: stringcase>=1.2.0
|
@@ -0,0 +1,53 @@
|
|
1
|
+
"""
|
2
|
+
Модуль, содержащий команды криптографии для шифрования секретных параметров.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Annotated
|
6
|
+
|
7
|
+
import typer
|
8
|
+
from rich import print
|
9
|
+
|
10
|
+
from fast_clean.container import get_container
|
11
|
+
from fast_clean.services import CryptographicAlgorithmEnum, CryptographyServiceFactory
|
12
|
+
from fast_clean.utils import typer_async
|
13
|
+
|
14
|
+
|
15
|
+
@typer_async
|
16
|
+
async def encrypt(
|
17
|
+
data: Annotated[str, typer.Argument(help='Данные для шифровки.')],
|
18
|
+
algorithm: Annotated[
|
19
|
+
CryptographicAlgorithmEnum, typer.Option(help='Криптографический алгоритм')
|
20
|
+
] = CryptographicAlgorithmEnum.AES_GCM,
|
21
|
+
) -> None:
|
22
|
+
"""
|
23
|
+
Зашифровываем данные.
|
24
|
+
"""
|
25
|
+
async with get_container() as container:
|
26
|
+
cryptography_service_factory = await container.get(CryptographyServiceFactory)
|
27
|
+
cryptography_service = await cryptography_service_factory.make(algorithm)
|
28
|
+
print(cryptography_service.encrypt(data))
|
29
|
+
|
30
|
+
|
31
|
+
@typer_async
|
32
|
+
async def decrypt(
|
33
|
+
data: Annotated[str, typer.Argument(help='Данные для расшифровки.')],
|
34
|
+
algorithm: Annotated[
|
35
|
+
CryptographicAlgorithmEnum, typer.Option(help='Криптографический алгоритм')
|
36
|
+
] = CryptographicAlgorithmEnum.AES_GCM,
|
37
|
+
) -> None:
|
38
|
+
"""
|
39
|
+
Расшифровываем данные.
|
40
|
+
"""
|
41
|
+
async with get_container() as container:
|
42
|
+
cryptography_service_factory = await container.get(CryptographyServiceFactory)
|
43
|
+
cryptography_service = await cryptography_service_factory.make(algorithm)
|
44
|
+
print(cryptography_service.decrypt(data))
|
45
|
+
|
46
|
+
|
47
|
+
def use_cryptography(app: typer.Typer) -> None:
|
48
|
+
"""
|
49
|
+
Регистрируем команды криптографии для шифрования секретных параметров.
|
50
|
+
"""
|
51
|
+
|
52
|
+
app.command()(encrypt)
|
53
|
+
app.command()(decrypt)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
"""
|
2
|
+
Модуль, содержащий команды загрузки данных из файлов.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Annotated
|
6
|
+
|
7
|
+
import typer
|
8
|
+
|
9
|
+
from fast_clean.container import get_container
|
10
|
+
from fast_clean.services import SeedService
|
11
|
+
from fast_clean.utils import typer_async
|
12
|
+
|
13
|
+
|
14
|
+
@typer_async
|
15
|
+
async def load_seed(
|
16
|
+
path: Annotated[str | None, typer.Argument(help='Путь к директории для загрузки данных.')] = None,
|
17
|
+
) -> None:
|
18
|
+
"""
|
19
|
+
Загружаем данные из файлов.
|
20
|
+
"""
|
21
|
+
async with get_container() as container:
|
22
|
+
seed_service = await container.get(SeedService)
|
23
|
+
await seed_service.load_data(path)
|
24
|
+
|
25
|
+
|
26
|
+
def use_load_seed(app: typer.Typer) -> None:
|
27
|
+
"""
|
28
|
+
Регистрируем команды загрузки данных из файлов.
|
29
|
+
"""
|
30
|
+
|
31
|
+
app.command()(load_seed)
|
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|
7
7
|
import uuid
|
8
8
|
from collections.abc import AsyncIterator
|
9
9
|
from contextlib import asynccontextmanager
|
10
|
-
from typing import TYPE_CHECKING, AsyncContextManager, Protocol, Self
|
10
|
+
from typing import TYPE_CHECKING, Any, AsyncContextManager, Protocol, Self
|
11
11
|
|
12
12
|
import sqlalchemy as sa
|
13
13
|
from sqlalchemy import MetaData
|
@@ -18,11 +18,9 @@ from sqlalchemy.ext.asyncio import (
|
|
18
18
|
async_sessionmaker,
|
19
19
|
create_async_engine,
|
20
20
|
)
|
21
|
-
from sqlalchemy.ext.declarative import declared_attr
|
22
21
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
23
22
|
from sqlalchemy.sql import func
|
24
|
-
from sqlalchemy_utils.types
|
25
|
-
from stringcase import snakecase
|
23
|
+
from sqlalchemy_utils.types import UUIDType
|
26
24
|
|
27
25
|
from .settings import CoreDbSettingsSchema, CoreSettingsSchema
|
28
26
|
|
@@ -40,24 +38,46 @@ POSTGRES_INDEXES_NAMING_CONVENTION = {
|
|
40
38
|
metadata = MetaData(naming_convention=POSTGRES_INDEXES_NAMING_CONVENTION)
|
41
39
|
|
42
40
|
|
43
|
-
def make_async_engine(
|
41
|
+
def make_async_engine(
|
42
|
+
db_dsn: str,
|
43
|
+
*,
|
44
|
+
scheme: str = 'public',
|
45
|
+
echo: bool = False,
|
46
|
+
pool_pre_ping: bool = True,
|
47
|
+
disable_prepared_statements: bool = True,
|
48
|
+
) -> AsyncEngine:
|
44
49
|
"""
|
45
50
|
Создаем асинхронный движок.
|
46
51
|
"""
|
52
|
+
connect_args: dict[str, Any] = {}
|
53
|
+
if disable_prepared_statements:
|
54
|
+
connect_args['prepare_threshold'] = None
|
47
55
|
return create_async_engine(
|
48
56
|
db_dsn,
|
49
|
-
connect_args={'options': f'-csearch_path={scheme}'},
|
50
57
|
echo=echo,
|
58
|
+
pool_pre_ping=pool_pre_ping,
|
59
|
+
connect_args=connect_args,
|
51
60
|
)
|
52
61
|
|
53
62
|
|
54
63
|
def make_async_session_factory(
|
55
|
-
db_dsn: str,
|
64
|
+
db_dsn: str,
|
65
|
+
*,
|
66
|
+
scheme: str = 'public',
|
67
|
+
echo: bool = False,
|
68
|
+
pool_pre_ping: bool = True,
|
69
|
+
disable_prepared_statements: bool = True,
|
56
70
|
) -> async_sessionmaker[AsyncSession]:
|
57
71
|
"""
|
58
72
|
Создаем фабрику асинхронных сессий.
|
59
73
|
"""
|
60
|
-
asyncio_engine = make_async_engine(
|
74
|
+
asyncio_engine = make_async_engine(
|
75
|
+
db_dsn,
|
76
|
+
scheme=scheme,
|
77
|
+
echo=echo,
|
78
|
+
pool_pre_ping=pool_pre_ping,
|
79
|
+
disable_prepared_statements=disable_prepared_statements,
|
80
|
+
)
|
61
81
|
return async_sessionmaker(asyncio_engine, expire_on_commit=False, autoflush=False)
|
62
82
|
|
63
83
|
|
@@ -85,10 +105,6 @@ class BaseUUID(Base):
|
|
85
105
|
server_default=func.gen_random_uuid(),
|
86
106
|
)
|
87
107
|
|
88
|
-
@declared_attr.directive
|
89
|
-
def __tablename__(cls) -> str:
|
90
|
-
return snakecase(cls.__name__)
|
91
|
-
|
92
108
|
|
93
109
|
class BaseInt(Base):
|
94
110
|
"""
|
@@ -100,10 +116,6 @@ class BaseInt(Base):
|
|
100
116
|
id: Mapped[int] = mapped_column(primary_key=True)
|
101
117
|
|
102
118
|
|
103
|
-
@declared_attr.directive
|
104
|
-
def __tablename__(cls) -> str:
|
105
|
-
return snakecase(cls.__name__)
|
106
|
-
|
107
119
|
class SessionFactory:
|
108
120
|
"""
|
109
121
|
Фабрика сессий.
|
@@ -125,16 +137,13 @@ class SessionFactory:
|
|
125
137
|
yield session
|
126
138
|
|
127
139
|
@classmethod
|
128
|
-
@asynccontextmanager
|
129
140
|
async def make_async_session_dynamic(
|
130
141
|
cls, settings_repository: SettingsRepositoryProtocol
|
131
|
-
) ->
|
142
|
+
) -> async_sessionmaker[AsyncSession]:
|
132
143
|
"""
|
133
144
|
Создаем асинхронную сессию с помощью динамической фабрики.
|
134
145
|
"""
|
135
|
-
|
136
|
-
async with async_session_factory() as session:
|
137
|
-
yield session
|
146
|
+
return await cls.make_async_session_factory(settings_repository)
|
138
147
|
|
139
148
|
@staticmethod
|
140
149
|
async def make_async_session_factory(
|
@@ -145,7 +154,13 @@ class SessionFactory:
|
|
145
154
|
"""
|
146
155
|
settings = await settings_repository.get(CoreSettingsSchema)
|
147
156
|
db_settings = await settings_repository.get(CoreDbSettingsSchema)
|
148
|
-
return make_async_session_factory(
|
157
|
+
return make_async_session_factory(
|
158
|
+
db_settings.dsn,
|
159
|
+
scheme=db_settings.scheme,
|
160
|
+
echo=settings.debug,
|
161
|
+
pool_pre_ping=db_settings.pool_pre_ping,
|
162
|
+
disable_prepared_statements=db_settings.disable_prepared_statements,
|
163
|
+
)
|
149
164
|
|
150
165
|
|
151
166
|
class SessionManagerProtocol(Protocol):
|
@@ -82,18 +82,14 @@ class CoreProvider(Provider):
|
|
82
82
|
Провайдер зависимостей.
|
83
83
|
"""
|
84
84
|
|
85
|
-
scope = Scope.
|
85
|
+
scope = Scope.APP
|
86
86
|
|
87
87
|
# --- repositories ---
|
88
88
|
|
89
|
-
settings_repository_factory = provide(
|
90
|
-
|
91
|
-
)
|
92
|
-
storage_repository_factory = provide(
|
93
|
-
StorageRepositoryFactoryImpl, provides=StorageRepositoryFactoryProtocol, scope=Scope.APP
|
94
|
-
)
|
89
|
+
settings_repository_factory = provide(SettingsRepositoryFactoryImpl, provides=SettingsRepositoryFactoryProtocol)
|
90
|
+
storage_repository_factory = provide(StorageRepositoryFactoryImpl, provides=StorageRepositoryFactoryProtocol)
|
95
91
|
|
96
|
-
@provide
|
92
|
+
@provide
|
97
93
|
@staticmethod
|
98
94
|
async def get_settings_repository(
|
99
95
|
settings_repository_factory: SettingsRepositoryFactoryProtocol,
|
@@ -103,7 +99,7 @@ class CoreProvider(Provider):
|
|
103
99
|
"""
|
104
100
|
return await settings_repository_factory.make(SettingsSourceEnum.ENV)
|
105
101
|
|
106
|
-
@provide
|
102
|
+
@provide
|
107
103
|
@staticmethod
|
108
104
|
async def get_settings(settings_repository: SettingsRepositoryProtocol) -> CoreSettingsSchema:
|
109
105
|
"""
|
@@ -119,7 +115,7 @@ class CoreProvider(Provider):
|
|
119
115
|
kafka_settings = await settings_repository.get(CoreKafkaSettingsSchema)
|
120
116
|
yield BrokerFactory.make_static(kafka_settings)
|
121
117
|
|
122
|
-
@provide
|
118
|
+
@provide
|
123
119
|
@staticmethod
|
124
120
|
async def get_cache_repository(settings_repository: SettingsRepositoryProtocol) -> CacheRepositoryProtocol:
|
125
121
|
"""
|
@@ -135,25 +131,29 @@ class CoreProvider(Provider):
|
|
135
131
|
return CacheManager.cache_repository
|
136
132
|
raise ValueError('Cache is not initialized')
|
137
133
|
|
138
|
-
@provide(scope=Scope.
|
134
|
+
@provide(scope=Scope.REQUEST)
|
139
135
|
@staticmethod
|
140
136
|
async def get_storage_repository(
|
141
137
|
settings_repository: SettingsRepositoryProtocol,
|
142
138
|
storage_repository_factory: StorageRepositoryFactoryProtocol,
|
143
|
-
) -> StorageRepositoryProtocol:
|
139
|
+
) -> AsyncIterator[StorageRepositoryProtocol]:
|
144
140
|
"""
|
145
141
|
Получаем репозиторий файлового хранилища.
|
146
142
|
"""
|
147
143
|
storage_settings = await settings_repository.get(CoreStorageSettingsSchema)
|
148
144
|
if storage_settings.provider == 's3' and storage_settings.s3 is not None:
|
149
|
-
|
145
|
+
storage_repository = await storage_repository_factory.make(
|
150
146
|
StorageTypeEnum.S3,
|
151
147
|
S3StorageParamsSchema.model_validate(storage_settings.s3.model_dump()),
|
152
148
|
)
|
153
|
-
|
154
|
-
|
149
|
+
async with storage_repository:
|
150
|
+
yield storage_repository
|
151
|
+
elif storage_settings.provider == 'local':
|
152
|
+
storage_repository = await storage_repository_factory.make(
|
155
153
|
StorageTypeEnum.LOCAL, LocalStorageParamsSchema(path=storage_settings.dir)
|
156
154
|
)
|
155
|
+
async with storage_repository:
|
156
|
+
yield storage_repository
|
157
157
|
raise NotImplementedError(f'Storage {storage_settings.provider} not allowed')
|
158
158
|
|
159
159
|
# --- db ---
|
@@ -164,7 +164,8 @@ class CoreProvider(Provider):
|
|
164
164
|
"""
|
165
165
|
Получаем асинхронную сессию.
|
166
166
|
"""
|
167
|
-
|
167
|
+
session_maker = await SessionFactory.make_async_session_dynamic(settings_repository)
|
168
|
+
async with session_maker() as session:
|
168
169
|
yield session
|
169
170
|
|
170
171
|
@provide
|
@@ -180,7 +181,7 @@ class CoreProvider(Provider):
|
|
180
181
|
seed_service = provide(SeedService)
|
181
182
|
transaction_service = provide(TransactionService)
|
182
183
|
|
183
|
-
@provide
|
184
|
+
@provide
|
184
185
|
@staticmethod
|
185
186
|
def get_cryptography_service_factory(settings: CoreSettingsSchema) -> CryptographyServiceFactory:
|
186
187
|
"""
|
@@ -188,7 +189,7 @@ class CoreProvider(Provider):
|
|
188
189
|
"""
|
189
190
|
return CryptographyServiceFactory(settings.secret_key)
|
190
191
|
|
191
|
-
@provide
|
192
|
+
@provide
|
192
193
|
@staticmethod
|
193
194
|
async def get_cryptography_service(
|
194
195
|
cryptography_service_factory: CryptographyServiceFactory,
|
@@ -198,7 +199,7 @@ class CoreProvider(Provider):
|
|
198
199
|
"""
|
199
200
|
return await cryptography_service_factory.make(CryptographicAlgorithmEnum.AES_GCM)
|
200
201
|
|
201
|
-
@provide
|
202
|
+
@provide
|
202
203
|
@staticmethod
|
203
204
|
def get_lock_service(settings: CoreSettingsSchema) -> LockServiceProtocol:
|
204
205
|
"""
|
@@ -0,0 +1,40 @@
|
|
1
|
+
"""
|
2
|
+
Модуль, содержащий middleware.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import time
|
6
|
+
from typing import Awaitable, Callable
|
7
|
+
|
8
|
+
from fastapi import FastAPI, Request, Response
|
9
|
+
from starlette.middleware.cors import CORSMiddleware
|
10
|
+
|
11
|
+
|
12
|
+
async def add_process_time_header(request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
13
|
+
start_time = time.perf_counter()
|
14
|
+
response = await call_next(request)
|
15
|
+
response.headers['x-process-time'] = f'{time.perf_counter() - start_time}'
|
16
|
+
return response
|
17
|
+
|
18
|
+
|
19
|
+
def use_middleware(
|
20
|
+
app: FastAPI,
|
21
|
+
name: str,
|
22
|
+
cors_origins: list[str],
|
23
|
+
*,
|
24
|
+
allow_methods: list[str] | None = None,
|
25
|
+
allow_headers: list[str] | None = None,
|
26
|
+
) -> FastAPI:
|
27
|
+
"""
|
28
|
+
Регистрируем middleware.
|
29
|
+
"""
|
30
|
+
|
31
|
+
app.add_middleware(
|
32
|
+
CORSMiddleware,
|
33
|
+
allow_origins=cors_origins,
|
34
|
+
allow_credentials=True,
|
35
|
+
allow_methods=allow_methods or ['*'],
|
36
|
+
allow_headers=allow_headers or ['*'],
|
37
|
+
)
|
38
|
+
|
39
|
+
app.middleware('http')(add_process_time_header)
|
40
|
+
return app
|
@@ -9,9 +9,9 @@ from sqlalchemy.orm import Mapped, mapped_column
|
|
9
9
|
from sqlalchemy.sql import func
|
10
10
|
|
11
11
|
|
12
|
-
class
|
12
|
+
class CreatedAtMixin:
|
13
13
|
"""
|
14
|
-
Миксин, содержащий дату и время создания
|
14
|
+
Миксин, содержащий дату и время создания записи.
|
15
15
|
"""
|
16
16
|
|
17
17
|
created_at: Mapped[dt.datetime] = mapped_column(
|
@@ -19,15 +19,22 @@ class TimestampMixin:
|
|
19
19
|
default=lambda: dt.datetime.now(dt.UTC),
|
20
20
|
server_default=func.now(),
|
21
21
|
)
|
22
|
+
|
23
|
+
|
24
|
+
class UpdatedAtMixin:
|
22
25
|
"""
|
23
|
-
|
26
|
+
Миксин, содержащий дату и время обновления записи.
|
24
27
|
"""
|
28
|
+
|
25
29
|
updated_at: Mapped[dt.datetime] = mapped_column(
|
26
30
|
DateTime(timezone=True),
|
27
31
|
default=lambda: dt.datetime.now(dt.UTC),
|
28
32
|
server_default=func.now(),
|
29
33
|
onupdate=lambda: dt.datetime.now(dt.UTC),
|
30
34
|
)
|
35
|
+
|
36
|
+
|
37
|
+
class TimestampMixin(CreatedAtMixin, UpdatedAtMixin):
|
31
38
|
"""
|
32
|
-
|
39
|
+
Миксин, содержащий дату и время создания и обновления записи.
|
33
40
|
"""
|
@@ -502,11 +502,11 @@ class DbCrudRepositoryBase(
|
|
502
502
|
count = (await s.execute(count_statement)).scalar_one()
|
503
503
|
return PaginationResultSchema(count=count, objects=objects)
|
504
504
|
|
505
|
-
def get_order_by_expr(self: Self, sorting: Iterable[str]) -> list[sa.UnaryExpression]:
|
505
|
+
def get_order_by_expr(self: Self, sorting: Iterable[str]) -> list[sa.UnaryExpression[Any]]:
|
506
506
|
"""
|
507
507
|
Получаем выражение сортировки.
|
508
508
|
"""
|
509
|
-
order_by_expr: list[sa.UnaryExpression] = []
|
509
|
+
order_by_expr: list[sa.UnaryExpression[Any]] = []
|
510
510
|
for st in sorting:
|
511
511
|
try:
|
512
512
|
if st[0] == '-':
|
@@ -6,11 +6,13 @@
|
|
6
6
|
- S3
|
7
7
|
"""
|
8
8
|
|
9
|
+
from collections.abc import AsyncIterator
|
9
10
|
from pathlib import Path
|
10
11
|
from typing import AsyncContextManager, Protocol, Self
|
11
12
|
|
12
13
|
from .enums import StorageTypeEnum
|
13
14
|
from .local import LocalStorageRepository
|
15
|
+
from .reader import AsyncStreamReaderProtocol as AsyncStreamReaderProtocol
|
14
16
|
from .reader import StreamReaderProtocol, StreamReadProtocol
|
15
17
|
from .s3 import S3StorageRepository
|
16
18
|
from .schemas import (
|
@@ -25,6 +27,18 @@ class StorageRepositoryProtocol(Protocol):
|
|
25
27
|
Протокол репозитория файлового хранилища.
|
26
28
|
"""
|
27
29
|
|
30
|
+
async def __aenter__(self: Self) -> Self:
|
31
|
+
"""
|
32
|
+
Вход в контекст менеджера.
|
33
|
+
"""
|
34
|
+
...
|
35
|
+
|
36
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
37
|
+
"""
|
38
|
+
Выход из контекст менеджера.
|
39
|
+
"""
|
40
|
+
...
|
41
|
+
|
28
42
|
async def exists(self: Self, path: str | Path) -> bool:
|
29
43
|
"""
|
30
44
|
Проверяем существует ли файл.
|
@@ -71,14 +85,18 @@ class StorageRepositoryProtocol(Protocol):
|
|
71
85
|
self: Self,
|
72
86
|
path: str | Path,
|
73
87
|
stream: StreamReadProtocol,
|
74
|
-
length: int = -1,
|
75
|
-
part_size: int = 0,
|
76
88
|
) -> None:
|
77
89
|
"""
|
78
90
|
Создаем файл или переписываем существующий в потоковом режиме.
|
79
91
|
"""
|
80
92
|
...
|
81
93
|
|
94
|
+
def straming_read(self: Self, path: str | Path) -> AsyncIterator[bytes]:
|
95
|
+
"""
|
96
|
+
Возвращаем асинхронные итератор потока байт.
|
97
|
+
"""
|
98
|
+
...
|
99
|
+
|
82
100
|
async def delete(self: Self, path: str | Path) -> None:
|
83
101
|
"""
|
84
102
|
Удаляем файл.
|
@@ -4,7 +4,7 @@
|
|
4
4
|
|
5
5
|
import asyncio
|
6
6
|
import os
|
7
|
-
from collections.abc import AsyncGenerator, Awaitable, Callable
|
7
|
+
from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable
|
8
8
|
from contextlib import asynccontextmanager
|
9
9
|
from logging import getLogger
|
10
10
|
from pathlib import Path
|
@@ -28,6 +28,14 @@ class LocalStorageRepository:
|
|
28
28
|
os.makedirs(self.work_dir)
|
29
29
|
self.logger = getLogger(__name__)
|
30
30
|
|
31
|
+
async def __aenter__(self: Self) -> Self:
|
32
|
+
"""
|
33
|
+
Вход в контекст менеджера.
|
34
|
+
"""
|
35
|
+
return self
|
36
|
+
|
37
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb): ...
|
38
|
+
|
31
39
|
async def exists(self: Self, path: str | Path) -> bool:
|
32
40
|
"""
|
33
41
|
Проверяем существует ли файл.
|
@@ -78,6 +86,12 @@ class LocalStorageRepository:
|
|
78
86
|
async with open(path, 'rb') as f:
|
79
87
|
yield AiofilesStreamReader(f)
|
80
88
|
|
89
|
+
async def straming_read(self: Self, path: str | Path) -> AsyncIterator[bytes]:
|
90
|
+
path = self.work_dir / path
|
91
|
+
async with open(path, 'rb') as f:
|
92
|
+
async for chunk in f:
|
93
|
+
yield chunk
|
94
|
+
|
81
95
|
async def write(self: Self, path: str | Path, content: str | bytes) -> None:
|
82
96
|
"""
|
83
97
|
Создаем файл или переписываем существующий.
|
@@ -91,15 +105,11 @@ class LocalStorageRepository:
|
|
91
105
|
self: Self,
|
92
106
|
path: str | Path,
|
93
107
|
stream: StreamReadProtocol,
|
94
|
-
length: int = -1,
|
95
|
-
part_size: int = 0,
|
96
108
|
) -> None:
|
97
109
|
"""
|
98
110
|
Создаем файл или переписываем существующий в потоковом режиме.
|
99
111
|
"""
|
100
|
-
|
101
|
-
self.logger.warning('Параметр length не используется для LocalStorage.')
|
102
|
-
part_size = part_size or 1024
|
112
|
+
part_size = 1024 * 1024
|
103
113
|
path = self.work_dir / path
|
104
114
|
is_co_function = asyncio.iscoroutinefunction(stream.read)
|
105
115
|
async with open(path, 'wb') as f:
|
@@ -35,6 +35,14 @@ class StreamReadAsyncProtocol(Protocol):
|
|
35
35
|
...
|
36
36
|
|
37
37
|
|
38
|
+
class AsyncStreamReaderProtocol(Protocol):
|
39
|
+
async def read(self: Self, size: int = -1) -> bytes:
|
40
|
+
"""
|
41
|
+
Потоковое чтение файлов.
|
42
|
+
"""
|
43
|
+
...
|
44
|
+
|
45
|
+
|
38
46
|
StreamReadProtocol = StreamReadAsyncProtocol | StreamReadSyncProtocol
|
39
47
|
|
40
48
|
|