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.
Files changed (65) hide show
  1. fast_clean/__init__.py +3 -0
  2. fast_clean/broker.py +123 -0
  3. fast_clean/container.py +235 -0
  4. fast_clean/contrib/__init__.py +3 -0
  5. fast_clean/contrib/healthcheck/__init__.py +3 -0
  6. fast_clean/contrib/healthcheck/router.py +17 -0
  7. fast_clean/db.py +179 -0
  8. fast_clean/depends.py +255 -0
  9. fast_clean/enums.py +39 -0
  10. fast_clean/exceptions.py +281 -0
  11. fast_clean/loggers.py +26 -0
  12. fast_clean/middleware.py +20 -0
  13. fast_clean/models.py +33 -0
  14. fast_clean/py.typed +0 -0
  15. fast_clean/redis.py +23 -0
  16. fast_clean/repositories/__init__.py +30 -0
  17. fast_clean/repositories/cache/__init__.py +83 -0
  18. fast_clean/repositories/cache/in_memory.py +62 -0
  19. fast_clean/repositories/cache/redis.py +58 -0
  20. fast_clean/repositories/crud/__init__.py +149 -0
  21. fast_clean/repositories/crud/db.py +559 -0
  22. fast_clean/repositories/crud/in_memory.py +369 -0
  23. fast_clean/repositories/crud/type_vars.py +35 -0
  24. fast_clean/repositories/settings/__init__.py +52 -0
  25. fast_clean/repositories/settings/enums.py +16 -0
  26. fast_clean/repositories/settings/env.py +55 -0
  27. fast_clean/repositories/settings/exceptions.py +13 -0
  28. fast_clean/repositories/settings/type_vars.py +9 -0
  29. fast_clean/repositories/storage/__init__.py +114 -0
  30. fast_clean/repositories/storage/enums.py +20 -0
  31. fast_clean/repositories/storage/local.py +118 -0
  32. fast_clean/repositories/storage/reader.py +122 -0
  33. fast_clean/repositories/storage/s3.py +118 -0
  34. fast_clean/repositories/storage/schemas.py +31 -0
  35. fast_clean/schemas/__init__.py +25 -0
  36. fast_clean/schemas/exceptions.py +32 -0
  37. fast_clean/schemas/pagination.py +65 -0
  38. fast_clean/schemas/repository.py +43 -0
  39. fast_clean/schemas/request_response.py +36 -0
  40. fast_clean/schemas/status_response.py +13 -0
  41. fast_clean/services/__init__.py +16 -0
  42. fast_clean/services/cryptography/__init__.py +57 -0
  43. fast_clean/services/cryptography/aes.py +120 -0
  44. fast_clean/services/cryptography/enums.py +20 -0
  45. fast_clean/services/lock.py +57 -0
  46. fast_clean/services/seed.py +91 -0
  47. fast_clean/services/transaction.py +40 -0
  48. fast_clean/settings.py +189 -0
  49. fast_clean/tools/__init__.py +6 -0
  50. fast_clean/tools/cryptography.py +56 -0
  51. fast_clean/tools/load_seed.py +31 -0
  52. fast_clean/use_cases.py +38 -0
  53. fast_clean/utils/__init__.py +15 -0
  54. fast_clean/utils/process.py +31 -0
  55. fast_clean/utils/pydantic.py +23 -0
  56. fast_clean/utils/ssl_context.py +31 -0
  57. fast_clean/utils/string.py +28 -0
  58. fast_clean/utils/thread.py +21 -0
  59. fast_clean/utils/time.py +14 -0
  60. fast_clean/utils/type_converters.py +18 -0
  61. fast_clean/utils/typer.py +23 -0
  62. fast_clean-0.4.0.dist-info/METADATA +38 -0
  63. fast_clean-0.4.0.dist-info/RECORD +65 -0
  64. fast_clean-0.4.0.dist-info/WHEEL +5 -0
  65. fast_clean-0.4.0.dist-info/top_level.txt +1 -0
fast_clean/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """
2
+ FastAPI clean architecture implementation.
3
+ """
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
+ )
@@ -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,3 @@
1
+ """
2
+ Пакет, содержащий приложения.
3
+ """
@@ -0,0 +1,3 @@
1
+ """
2
+ Приложение для получения состояния и метрик сервера.
3
+ """
@@ -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