fast-clean 0.4.0__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.
- fast_clean/__init__.py +3 -0
- fast_clean/broker.py +123 -0
- fast_clean/container.py +235 -0
- fast_clean/contrib/__init__.py +3 -0
- fast_clean/contrib/healthcheck/__init__.py +3 -0
- fast_clean/contrib/healthcheck/router.py +17 -0
- fast_clean/db.py +179 -0
- fast_clean/depends.py +255 -0
- fast_clean/enums.py +39 -0
- fast_clean/exceptions.py +281 -0
- fast_clean/loggers.py +26 -0
- fast_clean/middleware.py +20 -0
- fast_clean/models.py +33 -0
- fast_clean/py.typed +0 -0
- fast_clean/redis.py +23 -0
- fast_clean/repositories/__init__.py +30 -0
- fast_clean/repositories/cache/__init__.py +83 -0
- fast_clean/repositories/cache/in_memory.py +62 -0
- fast_clean/repositories/cache/redis.py +58 -0
- fast_clean/repositories/crud/__init__.py +149 -0
- fast_clean/repositories/crud/db.py +559 -0
- fast_clean/repositories/crud/in_memory.py +369 -0
- fast_clean/repositories/crud/type_vars.py +35 -0
- fast_clean/repositories/settings/__init__.py +52 -0
- fast_clean/repositories/settings/enums.py +16 -0
- fast_clean/repositories/settings/env.py +55 -0
- fast_clean/repositories/settings/exceptions.py +13 -0
- fast_clean/repositories/settings/type_vars.py +9 -0
- fast_clean/repositories/storage/__init__.py +114 -0
- fast_clean/repositories/storage/enums.py +20 -0
- fast_clean/repositories/storage/local.py +118 -0
- fast_clean/repositories/storage/reader.py +122 -0
- fast_clean/repositories/storage/s3.py +118 -0
- fast_clean/repositories/storage/schemas.py +31 -0
- fast_clean/schemas/__init__.py +25 -0
- fast_clean/schemas/exceptions.py +32 -0
- fast_clean/schemas/pagination.py +65 -0
- fast_clean/schemas/repository.py +43 -0
- fast_clean/schemas/request_response.py +36 -0
- fast_clean/schemas/status_response.py +13 -0
- fast_clean/services/__init__.py +16 -0
- fast_clean/services/cryptography/__init__.py +57 -0
- fast_clean/services/cryptography/aes.py +120 -0
- fast_clean/services/cryptography/enums.py +20 -0
- fast_clean/services/lock.py +57 -0
- fast_clean/services/seed.py +91 -0
- fast_clean/services/transaction.py +40 -0
- fast_clean/settings.py +189 -0
- fast_clean/tools/__init__.py +6 -0
- fast_clean/tools/cryptography.py +56 -0
- fast_clean/tools/load_seed.py +31 -0
- fast_clean/use_cases.py +38 -0
- fast_clean/utils/__init__.py +15 -0
- fast_clean/utils/process.py +31 -0
- fast_clean/utils/pydantic.py +23 -0
- fast_clean/utils/ssl_context.py +31 -0
- fast_clean/utils/string.py +28 -0
- fast_clean/utils/thread.py +21 -0
- fast_clean/utils/time.py +14 -0
- fast_clean/utils/type_converters.py +18 -0
- fast_clean/utils/typer.py +23 -0
- fast_clean-0.4.0.dist-info/METADATA +38 -0
- fast_clean-0.4.0.dist-info/RECORD +65 -0
- fast_clean-0.4.0.dist-info/WHEEL +5 -0
- fast_clean-0.4.0.dist-info/top_level.txt +1 -0
fast_clean/__init__.py
ADDED
fast_clean/broker.py
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
"""
|
2
|
+
Модуль, содержащий функционал, связанный с брокером сообщений.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from collections.abc import AsyncIterator
|
6
|
+
from contextlib import asynccontextmanager
|
7
|
+
from functools import partial
|
8
|
+
from typing import Callable, Self
|
9
|
+
|
10
|
+
from faststream import ExceptionMiddleware
|
11
|
+
from faststream.kafka import KafkaBroker
|
12
|
+
from faststream.kafka.fastapi import KafkaRouter
|
13
|
+
from faststream.security import BaseSecurity, SASLPlaintext
|
14
|
+
|
15
|
+
from fast_clean.settings import CoreKafkaSettingsSchema
|
16
|
+
from fast_clean.utils import CertificateSchema, make_ssl_context
|
17
|
+
|
18
|
+
|
19
|
+
class BrokerFactory:
|
20
|
+
"""
|
21
|
+
Фабрика брокеров сообщений.
|
22
|
+
"""
|
23
|
+
|
24
|
+
router: KafkaRouter | None = None
|
25
|
+
|
26
|
+
@classmethod
|
27
|
+
def get_router(cls, kafka_settings: CoreKafkaSettingsSchema) -> KafkaRouter:
|
28
|
+
if cls.router is None:
|
29
|
+
cls.router = cls.make_router(kafka_settings)
|
30
|
+
return cls.router
|
31
|
+
|
32
|
+
@classmethod
|
33
|
+
def make_static(cls, kafka_settings: CoreKafkaSettingsSchema) -> KafkaBroker:
|
34
|
+
"""
|
35
|
+
Создаем брокер сообщений с помощью статического роутера.
|
36
|
+
"""
|
37
|
+
return cls.get_router(kafka_settings).broker
|
38
|
+
|
39
|
+
@classmethod
|
40
|
+
@asynccontextmanager
|
41
|
+
async def make_dynamic(cls, kafka_settings: CoreKafkaSettingsSchema) -> AsyncIterator[KafkaBroker]:
|
42
|
+
"""
|
43
|
+
Создаем брокер сообщений с помощью динамического роутера.
|
44
|
+
"""
|
45
|
+
async with cls.make_router(kafka_settings).broker as broker:
|
46
|
+
yield broker
|
47
|
+
|
48
|
+
@classmethod
|
49
|
+
def make_router(cls, kafka_settings: CoreKafkaSettingsSchema) -> KafkaRouter:
|
50
|
+
"""
|
51
|
+
Создаем роутер.
|
52
|
+
"""
|
53
|
+
credentials = BrokerCredentials(kafka_settings)
|
54
|
+
return KafkaRouter(
|
55
|
+
kafka_settings.bootstrap_servers,
|
56
|
+
security=credentials.get(),
|
57
|
+
asyncapi_url='/asyncapi',
|
58
|
+
middlewares=[cls.make_exception_middleware()],
|
59
|
+
)
|
60
|
+
|
61
|
+
@classmethod
|
62
|
+
def make_exception_middleware(cls) -> ExceptionMiddleware:
|
63
|
+
"""
|
64
|
+
Создаем middleware для обработки исключений.
|
65
|
+
"""
|
66
|
+
exception_middleware = ExceptionMiddleware()
|
67
|
+
exception_middleware.add_handler(Exception)(cls.exception_handler)
|
68
|
+
return exception_middleware
|
69
|
+
|
70
|
+
@staticmethod
|
71
|
+
def exception_handler(exception: Exception) -> None:
|
72
|
+
"""
|
73
|
+
Обработчик исключения.
|
74
|
+
"""
|
75
|
+
print(repr(exception))
|
76
|
+
|
77
|
+
|
78
|
+
class BrokerCredentials:
|
79
|
+
"""
|
80
|
+
Класс получения параметров для подключения к Kafka.
|
81
|
+
"""
|
82
|
+
|
83
|
+
def __init__(self, kafka_settings: CoreKafkaSettingsSchema) -> None:
|
84
|
+
self.kafka_settings = kafka_settings
|
85
|
+
|
86
|
+
def get(self: Self, use_ssl: bool = True) -> BaseSecurity | SASLPlaintext | None:
|
87
|
+
"""
|
88
|
+
Получаем параметры для подключения.
|
89
|
+
"""
|
90
|
+
if self.kafka_settings.credentials is None:
|
91
|
+
return self.get_none_credentials()
|
92
|
+
|
93
|
+
credentials_mapping: dict[str, Callable[[], BaseSecurity | SASLPlaintext | None]] = {
|
94
|
+
'SSL': partial(self.get_ssl_credentials, use_ssl),
|
95
|
+
'SASL': partial(self.get_sasl_credentials, use_ssl),
|
96
|
+
}
|
97
|
+
return credentials_mapping.get(self.kafka_settings.credentials, self.get_none_credentials)()
|
98
|
+
|
99
|
+
@staticmethod
|
100
|
+
def get_none_credentials() -> None:
|
101
|
+
"""
|
102
|
+
Получаем отсутствующие параметры для подключения.
|
103
|
+
"""
|
104
|
+
return None
|
105
|
+
|
106
|
+
def get_ssl_credentials(self: Self, use_ssl: bool = True) -> BaseSecurity:
|
107
|
+
"""
|
108
|
+
Получаем SSL параметры для подключения.
|
109
|
+
"""
|
110
|
+
ssl_context = make_ssl_context(CertificateSchema.model_validate(self.kafka_settings.model_dump()))
|
111
|
+
return BaseSecurity(ssl_context, use_ssl)
|
112
|
+
|
113
|
+
def get_sasl_credentials(self: Self, use_ssl: bool = True) -> SASLPlaintext:
|
114
|
+
"""
|
115
|
+
Получаем SASL параметры для подключения.
|
116
|
+
"""
|
117
|
+
assert self.kafka_settings.broker_username
|
118
|
+
assert self.kafka_settings.broker_password
|
119
|
+
return SASLPlaintext(
|
120
|
+
username=self.kafka_settings.broker_username,
|
121
|
+
password=self.kafka_settings.broker_password,
|
122
|
+
use_ssl=use_ssl,
|
123
|
+
)
|
fast_clean/container.py
ADDED
@@ -0,0 +1,235 @@
|
|
1
|
+
"""
|
2
|
+
Модуль, содержащий контейнер зависимостей.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
import importlib
|
8
|
+
import inspect
|
9
|
+
import os
|
10
|
+
import sys
|
11
|
+
from collections.abc import Callable
|
12
|
+
from contextlib import AsyncExitStack
|
13
|
+
from pathlib import Path
|
14
|
+
from types import TracebackType
|
15
|
+
from typing import (
|
16
|
+
Annotated,
|
17
|
+
Any,
|
18
|
+
Protocol,
|
19
|
+
Self,
|
20
|
+
get_args,
|
21
|
+
get_origin,
|
22
|
+
get_type_hints,
|
23
|
+
)
|
24
|
+
|
25
|
+
from fastapi.dependencies.utils import (
|
26
|
+
is_async_gen_callable,
|
27
|
+
is_coroutine_callable,
|
28
|
+
is_gen_callable,
|
29
|
+
solve_generator,
|
30
|
+
)
|
31
|
+
from fastapi.params import Depends
|
32
|
+
from starlette.concurrency import run_in_threadpool
|
33
|
+
from stringcase import pascalcase
|
34
|
+
|
35
|
+
from .exceptions import ContainerError
|
36
|
+
|
37
|
+
|
38
|
+
class ContainerProtocol(Protocol):
|
39
|
+
"""
|
40
|
+
Протокол контейнера зависимостей.
|
41
|
+
"""
|
42
|
+
|
43
|
+
def __call__(self: Self, extra: dict[tuple[str, Any], Any] | None = None) -> ContainerProtocol:
|
44
|
+
"""
|
45
|
+
Получаем новый контейнер.
|
46
|
+
"""
|
47
|
+
...
|
48
|
+
|
49
|
+
async def __aenter__(self: Self) -> ContainerProtocol:
|
50
|
+
"""
|
51
|
+
Открываем асинхронный контекстный менеджер для управления стеком контекстных менеджеров.
|
52
|
+
"""
|
53
|
+
...
|
54
|
+
|
55
|
+
async def __aexit__(
|
56
|
+
self: Self,
|
57
|
+
exc_type: type[BaseException] | None = None,
|
58
|
+
exc_val: BaseException | None = None,
|
59
|
+
exc_tb: TracebackType | None = None,
|
60
|
+
) -> None:
|
61
|
+
"""
|
62
|
+
Закрываем асинхронный контекстный менеджер для управления стеком контекстных менеджеров.
|
63
|
+
"""
|
64
|
+
...
|
65
|
+
|
66
|
+
async def get_by_type(self: Self, dependency_type: Any, *, extra: dict[tuple[str, Any], Any] | None = None) -> Any:
|
67
|
+
"""
|
68
|
+
Получаем зависимость по типу.
|
69
|
+
"""
|
70
|
+
...
|
71
|
+
|
72
|
+
async def get_by_name(self: Self, name: str, *, extra: dict[tuple[str, Any], Any] | None = None) -> Any:
|
73
|
+
"""
|
74
|
+
Получаем зависимость по имени.
|
75
|
+
"""
|
76
|
+
...
|
77
|
+
|
78
|
+
|
79
|
+
class ContainerImpl:
|
80
|
+
"""
|
81
|
+
Реализация контейнера зависимостей.
|
82
|
+
"""
|
83
|
+
|
84
|
+
DEPENDS_MODULE = 'depends'
|
85
|
+
|
86
|
+
dependencies_name_mapping: dict[str, Annotated[Any, Depends]] | None = None
|
87
|
+
dependencies_type_mapping: dict[type, Annotated[Any, Depends]] | None = None
|
88
|
+
|
89
|
+
def __init__(self, extra: dict[tuple[str, Any], Any] | None = None) -> None:
|
90
|
+
self.extra = extra
|
91
|
+
self.instances: dict[type, Any] = {}
|
92
|
+
self.async_exit_stack: AsyncExitStack | None = None
|
93
|
+
|
94
|
+
def __call__(self: Self, extra: dict[tuple[str, Any], Any] | None = None) -> ContainerProtocol:
|
95
|
+
"""
|
96
|
+
Получаем новый контейнер.
|
97
|
+
"""
|
98
|
+
return ContainerImpl(extra)
|
99
|
+
|
100
|
+
async def __aenter__(self: Self) -> ContainerProtocol:
|
101
|
+
"""
|
102
|
+
Открываем асинхронный контекстный менеджер для управления стеком контекстных менеджеров.
|
103
|
+
"""
|
104
|
+
self.async_exit_stack = AsyncExitStack()
|
105
|
+
return self
|
106
|
+
|
107
|
+
async def __aexit__(
|
108
|
+
self: Self,
|
109
|
+
exc_type: type[BaseException] | None = None,
|
110
|
+
exc_val: BaseException | None = None,
|
111
|
+
exc_tb: TracebackType | None = None,
|
112
|
+
) -> None:
|
113
|
+
"""
|
114
|
+
Закрываем асинхронный контекстный менеджер для управления стеком контекстных менеджеров.
|
115
|
+
"""
|
116
|
+
assert self.async_exit_stack is not None
|
117
|
+
await self.async_exit_stack.__aexit__(exc_type, exc_val, exc_tb)
|
118
|
+
|
119
|
+
async def get_by_type(self: Self, dependency_type: Any, *, extra: dict[tuple[str, Any], Any] | None = None) -> Any:
|
120
|
+
"""
|
121
|
+
Получаем зависимость по типу.
|
122
|
+
"""
|
123
|
+
extra = extra or self.extra or {}
|
124
|
+
assert self.dependencies_type_mapping is not None
|
125
|
+
if dependency_type not in self.dependencies_type_mapping:
|
126
|
+
raise ContainerError(f'Dependency {dependency_type} not found')
|
127
|
+
return await self.resolve_dependency(self.dependencies_type_mapping[dependency_type], extra)
|
128
|
+
|
129
|
+
async def get_by_name(self: Self, name: str, *, extra: dict[tuple[str, Any], Any] | None = None) -> Any:
|
130
|
+
"""
|
131
|
+
Получаем зависимость по имени.
|
132
|
+
"""
|
133
|
+
extra = extra or self.extra or {}
|
134
|
+
assert self.dependencies_name_mapping is not None
|
135
|
+
name = pascalcase(name)
|
136
|
+
if name not in self.dependencies_name_mapping:
|
137
|
+
raise ContainerError(f'Dependency {name} not found')
|
138
|
+
return await self.resolve_dependency(self.dependencies_name_mapping[name], extra)
|
139
|
+
|
140
|
+
async def resolve_dependency(self: Self, annotated_type: type, extra: dict[tuple[str, Any], Any]) -> Any:
|
141
|
+
"""
|
142
|
+
Разрешаем зависимость.
|
143
|
+
"""
|
144
|
+
dependency_type: type
|
145
|
+
depends: Depends
|
146
|
+
dependency_type, depends = get_args(annotated_type)
|
147
|
+
if dependency_type in self.instances:
|
148
|
+
return self.instances[dependency_type]
|
149
|
+
call = depends.dependency
|
150
|
+
assert call is not None
|
151
|
+
resolved_params = await self.resolve_params(call, extra)
|
152
|
+
if is_gen_callable(call) or is_async_gen_callable(call):
|
153
|
+
if self.async_exit_stack is None:
|
154
|
+
raise ContainerError('Generator requires `async_exit_stack`')
|
155
|
+
resolved = await solve_generator(call=call, stack=self.async_exit_stack, sub_values=resolved_params)
|
156
|
+
elif is_coroutine_callable(call):
|
157
|
+
resolved = await call(**resolved_params)
|
158
|
+
else:
|
159
|
+
resolved = await run_in_threadpool(call, **resolved_params)
|
160
|
+
if depends.use_cache:
|
161
|
+
self.instances[dependency_type] = resolved
|
162
|
+
return resolved
|
163
|
+
|
164
|
+
async def resolve_params(self: Self, call: Callable[..., Any], extra: dict[tuple[str, Any], Any]) -> dict[str, Any]:
|
165
|
+
"""
|
166
|
+
Разрешаем параметры зависимости.
|
167
|
+
"""
|
168
|
+
signature = inspect.signature(call)
|
169
|
+
resolved_params: dict[str, Any] = {}
|
170
|
+
type_hints = get_type_hints(call, include_extras=True)
|
171
|
+
for param_name, param in signature.parameters.items():
|
172
|
+
annotation = type_hints[param_name]
|
173
|
+
args = get_args(annotation)
|
174
|
+
if (param_name, annotation) in extra:
|
175
|
+
extra_value = extra[param_name, annotation]
|
176
|
+
resolved_params[param_name] = extra_value() if callable(extra_value) else extra_value
|
177
|
+
elif get_origin(annotation) is Annotated and len(args) == 2 and isinstance(args[1], Depends):
|
178
|
+
resolved_params[param_name] = await self.resolve_dependency(annotation, extra)
|
179
|
+
elif param.default is not inspect.Parameter.empty:
|
180
|
+
resolved_params[param_name] = param.default
|
181
|
+
else:
|
182
|
+
raise ContainerError(f'Can not resolve dependency {param_name}: {annotation}')
|
183
|
+
return resolved_params
|
184
|
+
|
185
|
+
@classmethod
|
186
|
+
def init(cls, module_names: set[str] | None = None) -> None:
|
187
|
+
"""
|
188
|
+
Инициализируем контейнер.
|
189
|
+
"""
|
190
|
+
if cls.dependencies_name_mapping is None:
|
191
|
+
module_names = module_names or cls.get_default_module_names()
|
192
|
+
cls.dependencies_name_mapping = cls.get_dependencies_name_mapping(module_names)
|
193
|
+
if cls.dependencies_type_mapping is None:
|
194
|
+
cls.dependencies_type_mapping = cls.get_dependencies_type_mapping()
|
195
|
+
|
196
|
+
@classmethod
|
197
|
+
def get_default_module_names(cls) -> set[str]:
|
198
|
+
"""
|
199
|
+
Получаем список модулей с зависимостями по умолчанию.
|
200
|
+
"""
|
201
|
+
cwd = Path(os.getcwd())
|
202
|
+
virtual_env_paths = {path.parent for path in cwd.rglob('pyvenv.cfg')}
|
203
|
+
module_names: set[str] = set()
|
204
|
+
for path in cwd.rglob(f'{cls.DEPENDS_MODULE}.py'):
|
205
|
+
if not any(path.is_relative_to(venv) for venv in virtual_env_paths):
|
206
|
+
module_names.add('.'.join(str(path.relative_to(cwd).with_suffix('')).split('/')))
|
207
|
+
module_names.add(f'fast_clean.{cls.DEPENDS_MODULE}')
|
208
|
+
return module_names
|
209
|
+
|
210
|
+
@staticmethod
|
211
|
+
def get_dependencies_name_mapping(
|
212
|
+
module_names: set[str],
|
213
|
+
) -> dict[str, Annotated[Any, Depends]]:
|
214
|
+
"""
|
215
|
+
Получаем маппинг имен на зависимости.
|
216
|
+
"""
|
217
|
+
name_mapping: dict[str, Annotated[Any, Depends]] = {}
|
218
|
+
for module_name in module_names:
|
219
|
+
module = sys.modules[module_name] if module_name in sys.modules else importlib.import_module(module_name)
|
220
|
+
for name, obj in module.__dict__.items():
|
221
|
+
args = get_args(obj)
|
222
|
+
if get_origin(obj) is Annotated and len(args) == 2 and isinstance(args[1], Depends):
|
223
|
+
name_mapping[name] = obj
|
224
|
+
return name_mapping
|
225
|
+
|
226
|
+
@classmethod
|
227
|
+
def get_dependencies_type_mapping(cls) -> dict[type, Annotated[Any, Depends]]:
|
228
|
+
"""
|
229
|
+
Получаем маппинг типов на зависимости.
|
230
|
+
"""
|
231
|
+
assert cls.dependencies_name_mapping is not None
|
232
|
+
type_mapping: dict[type, Annotated[Any, Depends]] = {}
|
233
|
+
for dependency in cls.dependencies_name_mapping.values():
|
234
|
+
type_mapping[get_args(dependency)[0]] = dependency
|
235
|
+
return type_mapping
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"""
|
2
|
+
Роутер приложения healthcheck.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from fastapi import APIRouter
|
6
|
+
|
7
|
+
from fast_clean.schemas import StatusOkResponseSchema
|
8
|
+
|
9
|
+
router = APIRouter(prefix='/health', tags=['Healthcheck'], include_in_schema=False)
|
10
|
+
|
11
|
+
|
12
|
+
@router.get('')
|
13
|
+
async def get_healthcheck_status() -> StatusOkResponseSchema:
|
14
|
+
"""
|
15
|
+
Получаем статус сервера.
|
16
|
+
"""
|
17
|
+
return StatusOkResponseSchema()
|
fast_clean/db.py
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
"""
|
2
|
+
Модуль, содержащий функционал, связанный с базой данных.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
import uuid
|
8
|
+
from collections.abc import AsyncIterator
|
9
|
+
from contextlib import asynccontextmanager
|
10
|
+
from typing import TYPE_CHECKING, AsyncContextManager, Protocol, Self
|
11
|
+
|
12
|
+
import sqlalchemy as sa
|
13
|
+
from sqlalchemy import MetaData
|
14
|
+
from sqlalchemy.ext.asyncio import (
|
15
|
+
AsyncAttrs,
|
16
|
+
AsyncEngine,
|
17
|
+
AsyncSession,
|
18
|
+
async_sessionmaker,
|
19
|
+
create_async_engine,
|
20
|
+
)
|
21
|
+
from sqlalchemy.ext.declarative import declared_attr
|
22
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
23
|
+
from sqlalchemy.sql import func
|
24
|
+
from sqlalchemy_utils import UUIDType
|
25
|
+
from stringcase import snakecase
|
26
|
+
|
27
|
+
from .settings import CoreDbSettingsSchema, CoreSettingsSchema
|
28
|
+
|
29
|
+
if TYPE_CHECKING:
|
30
|
+
from .repositories import SettingsRepositoryProtocol
|
31
|
+
|
32
|
+
POSTGRES_INDEXES_NAMING_CONVENTION = {
|
33
|
+
'ix': '%(column_0_label)s_idx',
|
34
|
+
'uq': '%(table_name)s_%(column_0_name)s_key',
|
35
|
+
'ck': '%(table_name)s_%(constraint_name)s_check',
|
36
|
+
'fk': '%(table_name)s_%(column_0_name)s_fkey',
|
37
|
+
'pk': '%(table_name)s_pkey',
|
38
|
+
}
|
39
|
+
|
40
|
+
metadata = MetaData(naming_convention=POSTGRES_INDEXES_NAMING_CONVENTION)
|
41
|
+
|
42
|
+
|
43
|
+
def make_async_engine(db_dsn: str, *, scheme: str = 'public', echo: bool = False) -> AsyncEngine:
|
44
|
+
"""
|
45
|
+
Создаем асинхронный движок.
|
46
|
+
"""
|
47
|
+
return create_async_engine(
|
48
|
+
db_dsn,
|
49
|
+
connect_args={'options': f'-csearch_path={scheme}'},
|
50
|
+
echo=echo,
|
51
|
+
)
|
52
|
+
|
53
|
+
|
54
|
+
def make_async_session_factory(
|
55
|
+
db_dsn: str, *, scheme: str = 'public', echo: bool = False
|
56
|
+
) -> async_sessionmaker[AsyncSession]:
|
57
|
+
"""
|
58
|
+
Создаем фабрику асинхронных сессий.
|
59
|
+
"""
|
60
|
+
asyncio_engine = make_async_engine(db_dsn, scheme=scheme, echo=echo)
|
61
|
+
return async_sessionmaker(asyncio_engine, expire_on_commit=False, autoflush=False)
|
62
|
+
|
63
|
+
|
64
|
+
class BaseParent(AsyncAttrs, DeclarativeBase):
|
65
|
+
"""
|
66
|
+
Базовая родительская модель.
|
67
|
+
"""
|
68
|
+
|
69
|
+
__abstract__ = True
|
70
|
+
|
71
|
+
metadata = metadata
|
72
|
+
|
73
|
+
|
74
|
+
class Base(BaseParent):
|
75
|
+
"""
|
76
|
+
Базовая родительская модель нового типа.
|
77
|
+
"""
|
78
|
+
|
79
|
+
__abstract__ = True
|
80
|
+
|
81
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
82
|
+
UUIDType(binary=False),
|
83
|
+
primary_key=True,
|
84
|
+
default=uuid.uuid4,
|
85
|
+
server_default=func.gen_random_uuid(),
|
86
|
+
)
|
87
|
+
|
88
|
+
@declared_attr.directive
|
89
|
+
def __tablename__(cls) -> str:
|
90
|
+
return snakecase(cls.__name__)
|
91
|
+
|
92
|
+
|
93
|
+
class BaseOld(BaseParent):
|
94
|
+
"""
|
95
|
+
Базовая родительская модель старого типа.
|
96
|
+
"""
|
97
|
+
|
98
|
+
__abstract__ = True
|
99
|
+
|
100
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
101
|
+
|
102
|
+
|
103
|
+
class SessionFactory:
|
104
|
+
"""
|
105
|
+
Фабрика сессий.
|
106
|
+
"""
|
107
|
+
|
108
|
+
async_session_factory: async_sessionmaker[AsyncSession] | None = None
|
109
|
+
|
110
|
+
@classmethod
|
111
|
+
@asynccontextmanager
|
112
|
+
async def make_async_session_static(
|
113
|
+
cls, settings_repository: SettingsRepositoryProtocol
|
114
|
+
) -> AsyncIterator[AsyncSession]:
|
115
|
+
"""
|
116
|
+
Создаем асинхронную сессию с помощью статической фабрики.
|
117
|
+
"""
|
118
|
+
if cls.async_session_factory is None:
|
119
|
+
cls.async_session_factory = await cls.make_async_session_factory(settings_repository)
|
120
|
+
async with cls.async_session_factory() as session:
|
121
|
+
yield session
|
122
|
+
|
123
|
+
@classmethod
|
124
|
+
@asynccontextmanager
|
125
|
+
async def make_async_session_dynamic(
|
126
|
+
cls, settings_repository: SettingsRepositoryProtocol
|
127
|
+
) -> AsyncIterator[AsyncSession]:
|
128
|
+
"""
|
129
|
+
Создаем асинхронную сессию с помощью динамической фабрики.
|
130
|
+
"""
|
131
|
+
async_session_factory = await cls.make_async_session_factory(settings_repository)
|
132
|
+
async with async_session_factory() as session:
|
133
|
+
yield session
|
134
|
+
|
135
|
+
@staticmethod
|
136
|
+
async def make_async_session_factory(
|
137
|
+
settings_repository: SettingsRepositoryProtocol,
|
138
|
+
) -> async_sessionmaker[AsyncSession]:
|
139
|
+
"""
|
140
|
+
Создаем фабрику асинхронных сессий.
|
141
|
+
"""
|
142
|
+
settings = await settings_repository.get(CoreSettingsSchema)
|
143
|
+
db_settings = await settings_repository.get(CoreDbSettingsSchema)
|
144
|
+
return make_async_session_factory(db_settings.dsn, scheme=db_settings.scheme, echo=settings.debug)
|
145
|
+
|
146
|
+
|
147
|
+
class SessionManagerProtocol(Protocol):
|
148
|
+
"""
|
149
|
+
Протокол менеджера сессий.
|
150
|
+
"""
|
151
|
+
|
152
|
+
def get_session(self: Self, immediate: bool = True) -> AsyncContextManager[AsyncSession]:
|
153
|
+
"""
|
154
|
+
Получаем сессию для выполнения запроса.
|
155
|
+
"""
|
156
|
+
...
|
157
|
+
|
158
|
+
|
159
|
+
class SessionManagerImpl:
|
160
|
+
"""
|
161
|
+
Реализация менеджера сессий.
|
162
|
+
"""
|
163
|
+
|
164
|
+
def __init__(self, session: AsyncSession) -> None:
|
165
|
+
self.session = session
|
166
|
+
|
167
|
+
@asynccontextmanager
|
168
|
+
async def get_session(self: Self, immediate: bool = True) -> AsyncIterator[AsyncSession]:
|
169
|
+
"""
|
170
|
+
Получаем сессию для выполнения запроса.
|
171
|
+
"""
|
172
|
+
if self.session.in_transaction():
|
173
|
+
async with self.session.begin_nested():
|
174
|
+
yield self.session
|
175
|
+
else:
|
176
|
+
async with self.session.begin():
|
177
|
+
if immediate:
|
178
|
+
await self.session.execute(sa.text('SET CONSTRAINTS ALL IMMEDIATE'))
|
179
|
+
yield self.session
|