fast-clean 1.0.0__tar.gz → 1.1.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.0.0 → fast_clean-1.1.0}/PKG-INFO +1 -1
- fast_clean-1.1.0/fast_clean/container.py +100 -0
- fast_clean-1.1.0/fast_clean/depends.py +213 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/repositories/cache/__init__.py +6 -6
- fast_clean-1.1.0/fast_clean/services/__init__.py +12 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/services/cryptography/__init__.py +5 -17
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/services/lock.py +5 -5
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/services/seed.py +3 -15
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/services/transaction.py +3 -15
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/tools/cryptography.py +10 -14
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/tools/load_seed.py +3 -3
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean.egg-info/PKG-INFO +1 -1
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean.egg-info/SOURCES.txt +0 -1
- {fast_clean-1.0.0 → fast_clean-1.1.0}/pyproject.toml +4 -2
- fast_clean-1.0.0/fast_clean/container.py +0 -235
- fast_clean-1.0.0/fast_clean/depends.py +0 -255
- fast_clean-1.0.0/fast_clean/services/__init__.py +0 -16
- fast_clean-1.0.0/tests/test_container.py +0 -126
- {fast_clean-1.0.0 → fast_clean-1.1.0}/README.md +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/__init__.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/broker.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/contrib/__init__.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/contrib/healthcheck/__init__.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/contrib/healthcheck/router.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/db.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/enums.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/exceptions.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/loggers.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/middleware.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/models.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/py.typed +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/redis.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/repositories/__init__.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/repositories/cache/in_memory.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/repositories/cache/redis.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/repositories/crud/__init__.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/repositories/crud/db.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/repositories/crud/in_memory.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/repositories/crud/type_vars.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/repositories/settings/__init__.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/repositories/settings/enums.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/repositories/settings/env.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/repositories/settings/exceptions.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/repositories/settings/type_vars.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/repositories/storage/__init__.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/repositories/storage/enums.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/repositories/storage/local.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/repositories/storage/reader.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/repositories/storage/s3.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/repositories/storage/schemas.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/schemas/__init__.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/schemas/exceptions.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/schemas/pagination.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/schemas/repository.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/schemas/request_response.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/schemas/status_response.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/services/cryptography/aes.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/services/cryptography/enums.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/settings.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/tools/__init__.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/use_cases.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/utils/__init__.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/utils/process.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/utils/pydantic.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/utils/ssl_context.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/utils/string.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/utils/thread.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/utils/time.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/utils/type_converters.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean/utils/typer.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean.egg-info/dependency_links.txt +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean.egg-info/requires.txt +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/fast_clean.egg-info/top_level.txt +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/setup.cfg +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/tests/test_broker.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/tests/test_db.py +0 -0
- {fast_clean-1.0.0 → fast_clean-1.1.0}/tests/test_exceptions.py +0 -0
@@ -0,0 +1,100 @@
|
|
1
|
+
"""
|
2
|
+
Модуль, содержащий контейнер зависимостей.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import importlib
|
6
|
+
import os
|
7
|
+
import sys
|
8
|
+
from collections.abc import AsyncIterator
|
9
|
+
from contextlib import asynccontextmanager
|
10
|
+
from pathlib import Path
|
11
|
+
|
12
|
+
from dishka import AsyncContainer, Provider, make_async_container
|
13
|
+
from dishka.integrations.fastapi import FastapiProvider, setup_dishka
|
14
|
+
from fastapi import FastAPI
|
15
|
+
|
16
|
+
|
17
|
+
class ContainerManager:
|
18
|
+
"""
|
19
|
+
Менеджер для управления контейнером зависимостей.
|
20
|
+
"""
|
21
|
+
|
22
|
+
DEPENDS_MODULE = 'depends'
|
23
|
+
|
24
|
+
container: AsyncContainer | None = None
|
25
|
+
|
26
|
+
@classmethod
|
27
|
+
def init(cls, module_names: set[str] | None = None) -> AsyncContainer:
|
28
|
+
"""
|
29
|
+
Инициализируем контейнер зависимостей.
|
30
|
+
"""
|
31
|
+
if cls.container is None:
|
32
|
+
cls.container = cls.create(module_names)
|
33
|
+
return cls.container
|
34
|
+
|
35
|
+
@classmethod
|
36
|
+
def init_for_fastapi(cls, app: FastAPI, module_names: set[str] | None = None) -> AsyncContainer:
|
37
|
+
"""
|
38
|
+
Инициализируем контейнер зависимостей для приложения FastAPI.
|
39
|
+
"""
|
40
|
+
container = cls.init(module_names)
|
41
|
+
setup_dishka(container=container, app=app)
|
42
|
+
return container
|
43
|
+
|
44
|
+
@classmethod
|
45
|
+
async def close(cls) -> None:
|
46
|
+
"""
|
47
|
+
Закрываем контейнер зависимостей.
|
48
|
+
"""
|
49
|
+
if cls.container is None:
|
50
|
+
return
|
51
|
+
await cls.container.close()
|
52
|
+
cls.container = None
|
53
|
+
|
54
|
+
@classmethod
|
55
|
+
def create(cls, module_names: set[str] | None = None) -> AsyncContainer:
|
56
|
+
"""
|
57
|
+
Создаем контейнер зависимостей.
|
58
|
+
"""
|
59
|
+
module_names = module_names or set()
|
60
|
+
module_names.update(cls.get_default_module_names())
|
61
|
+
return make_async_container(FastapiProvider(), *cls.get_providers(module_names))
|
62
|
+
|
63
|
+
@classmethod
|
64
|
+
def get_default_module_names(cls) -> set[str]:
|
65
|
+
"""
|
66
|
+
Получаем список модулей с зависимостями по умолчанию.
|
67
|
+
"""
|
68
|
+
cwd = Path(os.getcwd())
|
69
|
+
virtual_env_paths = {path.parent for path in cwd.rglob('pyvenv.cfg')}
|
70
|
+
module_names: set[str] = set()
|
71
|
+
for path in cwd.rglob(f'{cls.DEPENDS_MODULE}.py'):
|
72
|
+
if not any(path.is_relative_to(venv) for venv in virtual_env_paths):
|
73
|
+
module_names.add('.'.join(str(path.relative_to(cwd).with_suffix('')).split('/')))
|
74
|
+
module_names.add(f'fast_clean.{cls.DEPENDS_MODULE}')
|
75
|
+
return module_names
|
76
|
+
|
77
|
+
@staticmethod
|
78
|
+
def get_providers(module_names: set[str]) -> list[Provider]:
|
79
|
+
"""
|
80
|
+
Получаем провайдеры зависимостей.
|
81
|
+
"""
|
82
|
+
providers: list[Provider] = []
|
83
|
+
for module_name in module_names:
|
84
|
+
module = sys.modules[module_name] if module_name in sys.modules else importlib.import_module(module_name)
|
85
|
+
for obj in module.__dict__.values():
|
86
|
+
if isinstance(obj, Provider):
|
87
|
+
providers.append(obj)
|
88
|
+
return providers
|
89
|
+
|
90
|
+
|
91
|
+
@asynccontextmanager
|
92
|
+
async def get_container() -> AsyncIterator[AsyncContainer]:
|
93
|
+
"""
|
94
|
+
Получаем контейнер зависимостей.
|
95
|
+
"""
|
96
|
+
container = ContainerManager.container
|
97
|
+
if container is None:
|
98
|
+
container = ContainerManager.init()
|
99
|
+
async with container() as nested_container:
|
100
|
+
yield nested_container
|
@@ -0,0 +1,213 @@
|
|
1
|
+
"""
|
2
|
+
Модуль, содержащий зависимости.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
from collections.abc import AsyncIterator, Sequence
|
7
|
+
from typing import Annotated
|
8
|
+
|
9
|
+
from dishka import Provider, Scope, provide
|
10
|
+
from fastapi import Depends, Request
|
11
|
+
from faststream.kafka import KafkaBroker
|
12
|
+
from flatten_dict import unflatten
|
13
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
14
|
+
from starlette.datastructures import FormData
|
15
|
+
from stringcase import snakecase
|
16
|
+
|
17
|
+
from .broker import BrokerFactory
|
18
|
+
from .db import SessionFactory, SessionManagerImpl, SessionManagerProtocol
|
19
|
+
from .redis import RedisManager
|
20
|
+
from .repositories import (
|
21
|
+
CacheManager,
|
22
|
+
CacheRepositoryProtocol,
|
23
|
+
LocalStorageParamsSchema,
|
24
|
+
S3StorageParamsSchema,
|
25
|
+
SettingsRepositoryFactoryImpl,
|
26
|
+
SettingsRepositoryFactoryProtocol,
|
27
|
+
SettingsRepositoryProtocol,
|
28
|
+
SettingsSourceEnum,
|
29
|
+
StorageRepositoryFactoryImpl,
|
30
|
+
StorageRepositoryFactoryProtocol,
|
31
|
+
StorageRepositoryProtocol,
|
32
|
+
StorageTypeEnum,
|
33
|
+
)
|
34
|
+
from .schemas import PaginationRequestSchema
|
35
|
+
from .services import (
|
36
|
+
CryptographicAlgorithmEnum,
|
37
|
+
CryptographyServiceFactory,
|
38
|
+
CryptographyServiceProtocol,
|
39
|
+
LockServiceProtocol,
|
40
|
+
RedisLockService,
|
41
|
+
SeedService,
|
42
|
+
TransactionService,
|
43
|
+
)
|
44
|
+
from .settings import CoreCacheSettingsSchema, CoreKafkaSettingsSchema, CoreSettingsSchema, CoreStorageSettingsSchema
|
45
|
+
|
46
|
+
|
47
|
+
async def get_nested_form_data(request: Request) -> FormData:
|
48
|
+
"""
|
49
|
+
Получаем форму, позволяющую использовать вложенные словари.
|
50
|
+
"""
|
51
|
+
dot_data = {k.replace('[', '.').replace(']', ''): v for k, v in (await request.form()).items()}
|
52
|
+
nested_data = unflatten(dot_data, 'dot')
|
53
|
+
for k, v in nested_data.items():
|
54
|
+
if isinstance(v, dict):
|
55
|
+
nested_data[k] = json.dumps(v)
|
56
|
+
return FormData(nested_data)
|
57
|
+
|
58
|
+
|
59
|
+
def get_pagination(page: int | None = None, page_size: int | None = None) -> PaginationRequestSchema:
|
60
|
+
"""
|
61
|
+
Получаем входные данные пагинации.
|
62
|
+
"""
|
63
|
+
return PaginationRequestSchema(page=page or 1, page_size=page_size or 10)
|
64
|
+
|
65
|
+
|
66
|
+
def get_sorting(sorting: str | None = None) -> Sequence[str]:
|
67
|
+
"""
|
68
|
+
Получаем входные данные сортировки.
|
69
|
+
"""
|
70
|
+
if not sorting:
|
71
|
+
return []
|
72
|
+
return [s[0] + snakecase(s[1:]) if s[0] == '-' else snakecase(s) for s in sorting.split(',')]
|
73
|
+
|
74
|
+
|
75
|
+
NestedFormData = Annotated[FormData, Depends(get_nested_form_data)]
|
76
|
+
Pagination = Annotated[PaginationRequestSchema, Depends(get_pagination)]
|
77
|
+
Sorting = Annotated[Sequence[str], Depends(get_sorting)]
|
78
|
+
|
79
|
+
|
80
|
+
class CoreProvider(Provider):
|
81
|
+
"""
|
82
|
+
Провайдер зависимостей.
|
83
|
+
"""
|
84
|
+
|
85
|
+
scope = Scope.REQUEST
|
86
|
+
|
87
|
+
# --- repositories ---
|
88
|
+
|
89
|
+
settings_repository_factory = provide(
|
90
|
+
SettingsRepositoryFactoryImpl, provides=SettingsRepositoryFactoryProtocol, scope=Scope.APP
|
91
|
+
)
|
92
|
+
storage_repository_factory = provide(
|
93
|
+
StorageRepositoryFactoryImpl, provides=StorageRepositoryFactoryProtocol, scope=Scope.APP
|
94
|
+
)
|
95
|
+
|
96
|
+
@provide(scope=Scope.APP)
|
97
|
+
@staticmethod
|
98
|
+
async def get_settings_repository(
|
99
|
+
settings_repository_factory: SettingsRepositoryFactoryProtocol,
|
100
|
+
) -> SettingsRepositoryProtocol:
|
101
|
+
"""
|
102
|
+
Получаем репозиторий настроек.
|
103
|
+
"""
|
104
|
+
return await settings_repository_factory.make(SettingsSourceEnum.ENV)
|
105
|
+
|
106
|
+
@provide(scope=Scope.APP)
|
107
|
+
@staticmethod
|
108
|
+
async def get_settings(settings_repository: SettingsRepositoryProtocol) -> CoreSettingsSchema:
|
109
|
+
"""
|
110
|
+
Получаем настройки.
|
111
|
+
"""
|
112
|
+
return await settings_repository.get(CoreSettingsSchema)
|
113
|
+
|
114
|
+
@staticmethod
|
115
|
+
async def get_broker_repository(settings_repository: SettingsRepositoryProtocol) -> AsyncIterator[KafkaBroker]:
|
116
|
+
"""
|
117
|
+
Получаем репозиторий брокера сообщений.
|
118
|
+
"""
|
119
|
+
kafka_settings = await settings_repository.get(CoreKafkaSettingsSchema)
|
120
|
+
yield BrokerFactory.make_static(kafka_settings)
|
121
|
+
|
122
|
+
@provide(scope=Scope.APP)
|
123
|
+
@staticmethod
|
124
|
+
async def get_cache_repository(settings_repository: SettingsRepositoryProtocol) -> CacheRepositoryProtocol:
|
125
|
+
"""
|
126
|
+
Получаем репозиторий кеша.
|
127
|
+
"""
|
128
|
+
settings = await settings_repository.get(CoreSettingsSchema)
|
129
|
+
if settings.redis_dsn is not None:
|
130
|
+
RedisManager.init(settings.redis_dsn)
|
131
|
+
cache_settings = await settings_repository.get(CoreCacheSettingsSchema)
|
132
|
+
if CacheManager.cache_repository is None:
|
133
|
+
CacheManager.init(cache_settings, RedisManager.redis)
|
134
|
+
if CacheManager.cache_repository is not None:
|
135
|
+
return CacheManager.cache_repository
|
136
|
+
raise ValueError('Cache is not initialized')
|
137
|
+
|
138
|
+
@provide(scope=Scope.APP)
|
139
|
+
@staticmethod
|
140
|
+
async def get_storage_repository(
|
141
|
+
settings_repository: SettingsRepositoryProtocol,
|
142
|
+
storage_repository_factory: StorageRepositoryFactoryProtocol,
|
143
|
+
) -> StorageRepositoryProtocol:
|
144
|
+
"""
|
145
|
+
Получаем репозиторий файлового хранилища.
|
146
|
+
"""
|
147
|
+
storage_settings = await settings_repository.get(CoreStorageSettingsSchema)
|
148
|
+
if storage_settings.provider == 's3' and storage_settings.s3 is not None:
|
149
|
+
return await storage_repository_factory.make(
|
150
|
+
StorageTypeEnum.S3,
|
151
|
+
S3StorageParamsSchema.model_validate(storage_settings.s3.model_dump()),
|
152
|
+
)
|
153
|
+
elif storage_settings.provider == 'local' and storage_settings.dir is not None:
|
154
|
+
return await storage_repository_factory.make(
|
155
|
+
StorageTypeEnum.LOCAL, LocalStorageParamsSchema(path=storage_settings.dir)
|
156
|
+
)
|
157
|
+
raise NotImplementedError(f'Storage {storage_settings.provider} not allowed')
|
158
|
+
|
159
|
+
# --- db ---
|
160
|
+
|
161
|
+
@provide
|
162
|
+
@staticmethod
|
163
|
+
async def get_async_session(settings_repository: SettingsRepositoryProtocol) -> AsyncIterator[AsyncSession]:
|
164
|
+
"""
|
165
|
+
Получаем асинхронную сессию.
|
166
|
+
"""
|
167
|
+
async with SessionFactory.make_async_session_static(settings_repository) as session:
|
168
|
+
yield session
|
169
|
+
|
170
|
+
@provide
|
171
|
+
@staticmethod
|
172
|
+
def get_session_manager(session: AsyncSession) -> SessionManagerProtocol:
|
173
|
+
"""
|
174
|
+
Получаем менеджер сессий.
|
175
|
+
"""
|
176
|
+
return SessionManagerImpl(session)
|
177
|
+
|
178
|
+
# --- services ---
|
179
|
+
|
180
|
+
seed_service = provide(SeedService)
|
181
|
+
transaction_service = provide(TransactionService)
|
182
|
+
|
183
|
+
@provide(scope=Scope.APP)
|
184
|
+
@staticmethod
|
185
|
+
def get_cryptography_service_factory(settings: CoreSettingsSchema) -> CryptographyServiceFactory:
|
186
|
+
"""
|
187
|
+
Получаем фабрику сервисов криптографии.
|
188
|
+
"""
|
189
|
+
return CryptographyServiceFactory(settings.secret_key)
|
190
|
+
|
191
|
+
@provide(scope=Scope.APP)
|
192
|
+
@staticmethod
|
193
|
+
async def get_cryptography_service(
|
194
|
+
cryptography_service_factory: CryptographyServiceFactory,
|
195
|
+
) -> CryptographyServiceProtocol:
|
196
|
+
"""
|
197
|
+
Получаем сервис криптографии.
|
198
|
+
"""
|
199
|
+
return await cryptography_service_factory.make(CryptographicAlgorithmEnum.AES_GCM)
|
200
|
+
|
201
|
+
@provide(scope=Scope.APP)
|
202
|
+
@staticmethod
|
203
|
+
def get_lock_service(settings: CoreSettingsSchema) -> LockServiceProtocol:
|
204
|
+
"""
|
205
|
+
Получаем сервис распределенной блокировки.
|
206
|
+
"""
|
207
|
+
assert settings.redis_dsn is not None
|
208
|
+
RedisManager.init(settings.redis_dsn)
|
209
|
+
assert RedisManager.redis is not None
|
210
|
+
return RedisLockService(RedisManager.redis)
|
211
|
+
|
212
|
+
|
213
|
+
provider = CoreProvider()
|
@@ -6,7 +6,7 @@
|
|
6
6
|
- Redis
|
7
7
|
"""
|
8
8
|
|
9
|
-
from typing import ClassVar, Protocol, Self,
|
9
|
+
from typing import ClassVar, Protocol, Self, cast
|
10
10
|
|
11
11
|
from fastapi_cache import FastAPICache
|
12
12
|
|
@@ -34,7 +34,7 @@ class CacheRepositoryProtocol(Protocol):
|
|
34
34
|
"""
|
35
35
|
...
|
36
36
|
|
37
|
-
async def get_with_ttl(self: Self, key: str) ->
|
37
|
+
async def get_with_ttl(self: Self, key: str) -> tuple[int, str | None]:
|
38
38
|
"""
|
39
39
|
Получаем значение со сроком жизни.
|
40
40
|
"""
|
@@ -61,17 +61,17 @@ class CacheRepositoryProtocol(Protocol):
|
|
61
61
|
|
62
62
|
class CacheManager:
|
63
63
|
"""
|
64
|
-
Менеджер для
|
64
|
+
Менеджер для работы с репозиторием кеша.
|
65
65
|
"""
|
66
66
|
|
67
|
-
|
67
|
+
cache_repository: ClassVar[CacheRepositoryProtocol | None] = None
|
68
68
|
|
69
69
|
@classmethod
|
70
70
|
def init(cls, cache_settings: CoreCacheSettingsSchema, redis: aioredis.Redis | None) -> None:
|
71
71
|
"""
|
72
72
|
Инициализируем кеш.
|
73
73
|
"""
|
74
|
-
if cls.
|
74
|
+
if cls.cache_repository is None:
|
75
75
|
cache_backend: InMemoryCacheRepository | RedisCacheRepository
|
76
76
|
match cache_settings.provider:
|
77
77
|
case 'in_memory':
|
@@ -80,4 +80,4 @@ class CacheManager:
|
|
80
80
|
assert redis is not None
|
81
81
|
cache_backend = RedisCacheRepository(redis)
|
82
82
|
FastAPICache.init(cache_backend, prefix=cache_settings.prefix)
|
83
|
-
cls.
|
83
|
+
cls.cache_repository = cast(CacheRepositoryProtocol, cache_backend)
|
@@ -0,0 +1,12 @@
|
|
1
|
+
"""
|
2
|
+
Пакет, содержащий сервисы.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from .cryptography import AesGcmCryptographyService as AesGcmCryptographyService
|
6
|
+
from .cryptography import CryptographicAlgorithmEnum as CryptographicAlgorithmEnum
|
7
|
+
from .cryptography import CryptographyServiceFactory as CryptographyServiceFactory
|
8
|
+
from .cryptography import CryptographyServiceProtocol as CryptographyServiceProtocol
|
9
|
+
from .lock import LockServiceProtocol as LockServiceProtocol
|
10
|
+
from .lock import RedisLockService as RedisLockService
|
11
|
+
from .seed import SeedService as SeedService
|
12
|
+
from .transaction import TransactionService as TransactionService
|
@@ -4,7 +4,8 @@
|
|
4
4
|
|
5
5
|
from typing import Protocol, Self
|
6
6
|
|
7
|
-
from .aes import AesCbcCryptographyService
|
7
|
+
from .aes import AesCbcCryptographyService as AesCbcCryptographyService
|
8
|
+
from .aes import AesGcmCryptographyService
|
8
9
|
from .enums import CryptographicAlgorithmEnum
|
9
10
|
|
10
11
|
|
@@ -26,21 +27,9 @@ class CryptographyServiceProtocol(Protocol):
|
|
26
27
|
...
|
27
28
|
|
28
29
|
|
29
|
-
class
|
30
|
+
class CryptographyServiceFactory:
|
30
31
|
"""
|
31
|
-
|
32
|
-
"""
|
33
|
-
|
34
|
-
async def make(self: Self, algorithm: CryptographicAlgorithmEnum) -> CryptographyServiceProtocol:
|
35
|
-
"""
|
36
|
-
Создаем сервис криптографии для шифрования секретных параметров.
|
37
|
-
"""
|
38
|
-
...
|
39
|
-
|
40
|
-
|
41
|
-
class CryptographyServiceFactoryImpl:
|
42
|
-
"""
|
43
|
-
Реализация фабрики сервисов криптографии для шифрования секретных параметров.
|
32
|
+
Фабрика сервисов криптографии для шифрования секретных параметров.
|
44
33
|
"""
|
45
34
|
|
46
35
|
def __init__(self, secret_key: str) -> None:
|
@@ -53,5 +42,4 @@ class CryptographyServiceFactoryImpl:
|
|
53
42
|
match algorithm:
|
54
43
|
case CryptographicAlgorithmEnum.AES_GCM:
|
55
44
|
return AesGcmCryptographyService(self.secret_key)
|
56
|
-
|
57
|
-
return AesCbcCryptographyService(self.secret_key)
|
45
|
+
raise NotImplementedError(algorithm)
|
@@ -4,11 +4,11 @@
|
|
4
4
|
|
5
5
|
from collections.abc import AsyncIterator
|
6
6
|
from contextlib import asynccontextmanager
|
7
|
-
from typing import AsyncContextManager, Protocol
|
7
|
+
from typing import AsyncContextManager, Protocol
|
8
8
|
|
9
|
+
import redis.asyncio.lock as aioredis_lock
|
9
10
|
from fast_clean.exceptions import LockError
|
10
11
|
from redis import asyncio as aioredis
|
11
|
-
from redis.exceptions import LockError as RedisLockError
|
12
12
|
|
13
13
|
|
14
14
|
class LockServiceProtocol(Protocol):
|
@@ -17,7 +17,7 @@ class LockServiceProtocol(Protocol):
|
|
17
17
|
"""
|
18
18
|
|
19
19
|
def lock(
|
20
|
-
self
|
20
|
+
self,
|
21
21
|
name: str,
|
22
22
|
*,
|
23
23
|
timeout: float | None = None,
|
@@ -40,7 +40,7 @@ class RedisLockService:
|
|
40
40
|
|
41
41
|
@asynccontextmanager
|
42
42
|
async def lock(
|
43
|
-
self
|
43
|
+
self,
|
44
44
|
name: str,
|
45
45
|
*,
|
46
46
|
timeout: float | None = None,
|
@@ -53,5 +53,5 @@ class RedisLockService:
|
|
53
53
|
try:
|
54
54
|
async with self.redis.lock(name, timeout=timeout, sleep=sleep, blocking_timeout=blocking_timeout):
|
55
55
|
yield
|
56
|
-
except
|
56
|
+
except aioredis_lock.LockError as lock_error:
|
57
57
|
raise LockError() from lock_error
|
@@ -6,7 +6,7 @@ import importlib
|
|
6
6
|
import json
|
7
7
|
import os
|
8
8
|
from pathlib import Path
|
9
|
-
from typing import Any,
|
9
|
+
from typing import Any, cast
|
10
10
|
|
11
11
|
import sqlalchemy as sa
|
12
12
|
from sqlalchemy.dialects.postgresql import insert
|
@@ -15,19 +15,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|
15
15
|
from ..db import SessionManagerProtocol
|
16
16
|
|
17
17
|
|
18
|
-
class
|
19
|
-
"""
|
20
|
-
Протокол сервиса для загрузки данных из файлов.
|
21
|
-
"""
|
22
|
-
|
23
|
-
async def load_data(self: Self, directory: str | Path | None = None) -> None:
|
24
|
-
"""
|
25
|
-
Загружаем данные из файлов по пути.
|
26
|
-
"""
|
27
|
-
...
|
28
|
-
|
29
|
-
|
30
|
-
class SeedServiceImpl:
|
18
|
+
class SeedService:
|
31
19
|
"""
|
32
20
|
Реализация сервиса для загрузки данных из файлов.
|
33
21
|
"""
|
@@ -35,7 +23,7 @@ class SeedServiceImpl:
|
|
35
23
|
def __init__(self, session_manager: SessionManagerProtocol) -> None:
|
36
24
|
self.session_manager = session_manager
|
37
25
|
|
38
|
-
async def load_data(self
|
26
|
+
async def load_data(self, directory: str | Path | None = None) -> None:
|
39
27
|
"""
|
40
28
|
Загружаем данные из файлов по пути.
|
41
29
|
"""
|
@@ -3,25 +3,13 @@
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
from contextlib import asynccontextmanager
|
6
|
-
from typing import
|
6
|
+
from typing import AsyncIterator
|
7
7
|
|
8
8
|
import sqlalchemy as sa
|
9
9
|
from sqlalchemy.ext.asyncio import AsyncSession
|
10
10
|
|
11
11
|
|
12
|
-
class
|
13
|
-
"""
|
14
|
-
Протокол сервиса транзакций.
|
15
|
-
"""
|
16
|
-
|
17
|
-
def begin(self: Self, immediate: bool = True) -> AsyncContextManager[None]:
|
18
|
-
"""
|
19
|
-
Начинаем транзакцию.
|
20
|
-
"""
|
21
|
-
...
|
22
|
-
|
23
|
-
|
24
|
-
class TransactionServiceImpl:
|
12
|
+
class TransactionService:
|
25
13
|
"""
|
26
14
|
Реализация сервиса транзакций.
|
27
15
|
"""
|
@@ -30,7 +18,7 @@ class TransactionServiceImpl:
|
|
30
18
|
self.session = session
|
31
19
|
|
32
20
|
@asynccontextmanager
|
33
|
-
async def begin(self
|
21
|
+
async def begin(self, immediate: bool = True) -> AsyncIterator[None]:
|
34
22
|
"""
|
35
23
|
Начинаем транзакцию.
|
36
24
|
"""
|
@@ -6,8 +6,8 @@ from typing import Annotated
|
|
6
6
|
|
7
7
|
import typer
|
8
8
|
|
9
|
-
from fast_clean.
|
10
|
-
from fast_clean.services import CryptographicAlgorithmEnum,
|
9
|
+
from fast_clean.container import get_container
|
10
|
+
from fast_clean.services import CryptographicAlgorithmEnum, CryptographyServiceFactory
|
11
11
|
from fast_clean.utils import typer_async
|
12
12
|
|
13
13
|
|
@@ -21,12 +21,10 @@ async def encrypt(
|
|
21
21
|
"""
|
22
22
|
Зашифровываем данные.
|
23
23
|
"""
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
cryptography_service = await cryptography_service_factory.make(algorithm)
|
29
|
-
print(cryptography_service.encrypt(data))
|
24
|
+
async with get_container() as container:
|
25
|
+
cryptography_service_factory = await container.get(CryptographyServiceFactory)
|
26
|
+
cryptography_service = await cryptography_service_factory.make(algorithm)
|
27
|
+
print(cryptography_service.encrypt(data))
|
30
28
|
|
31
29
|
|
32
30
|
@typer_async
|
@@ -39,12 +37,10 @@ async def decrypt(
|
|
39
37
|
"""
|
40
38
|
Расшифровываем данные.
|
41
39
|
"""
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
cryptography_service = await cryptography_service_factory.make(algorithm)
|
47
|
-
print(cryptography_service.decrypt(data))
|
40
|
+
async with get_container() as container:
|
41
|
+
cryptography_service_factory = await container.get(CryptographyServiceFactory)
|
42
|
+
cryptography_service = await cryptography_service_factory.make(algorithm)
|
43
|
+
print(cryptography_service.decrypt(data))
|
48
44
|
|
49
45
|
|
50
46
|
def use_cryptography(app: typer.Typer) -> None:
|
@@ -6,8 +6,8 @@ from typing import Annotated
|
|
6
6
|
|
7
7
|
import typer
|
8
8
|
|
9
|
-
from fast_clean.
|
10
|
-
from fast_clean.services import
|
9
|
+
from fast_clean.container import get_container
|
10
|
+
from fast_clean.services import SeedService
|
11
11
|
from fast_clean.utils import typer_async
|
12
12
|
|
13
13
|
|
@@ -19,7 +19,7 @@ async def load_seed(
|
|
19
19
|
Загружаем данные из файлов.
|
20
20
|
"""
|
21
21
|
async with get_container() as container:
|
22
|
-
seed_service
|
22
|
+
seed_service = await container.get(SeedService)
|
23
23
|
await seed_service.load_data(path)
|
24
24
|
|
25
25
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "fast-clean"
|
3
|
-
version = "1.
|
3
|
+
version = "1.1.0"
|
4
4
|
description = "FastAPI Clean Architecture implementation"
|
5
5
|
readme = "README.md"
|
6
6
|
requires-python = ">=3.12"
|
@@ -44,6 +44,8 @@ dev = [
|
|
44
44
|
"ruff>=0.9.7",
|
45
45
|
]
|
46
46
|
|
47
|
+
[tool.ruff.format]
|
48
|
+
quote-style = "single"
|
47
49
|
|
48
50
|
[tool.black]
|
49
51
|
skip-string-normalization = true
|
@@ -103,4 +105,4 @@ version_toml = [
|
|
103
105
|
commit_message = "chore(release): {version} [skip ci]"
|
104
106
|
branch = "main"
|
105
107
|
upload_to_pypi = false
|
106
|
-
upload_to_release = true
|
108
|
+
upload_to_release = true
|