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/depends.py ADDED
@@ -0,0 +1,255 @@
1
+ """
2
+ Модуль, содержащий зависимости.
3
+ """
4
+
5
+ import json
6
+ from collections.abc import AsyncIterator, Sequence
7
+ from typing import Annotated
8
+
9
+ from fastapi import Depends, Request
10
+ from faststream.kafka import KafkaBroker
11
+ from flatten_dict import unflatten
12
+ from sqlalchemy.ext.asyncio import AsyncSession
13
+ from starlette.datastructures import FormData
14
+ from stringcase import snakecase
15
+
16
+ from .broker import BrokerFactory
17
+ from .container import ContainerImpl, ContainerProtocol
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
+ CryptographyServiceFactoryImpl,
38
+ CryptographyServiceFactoryProtocol,
39
+ CryptographyServiceProtocol,
40
+ LockServiceProtocol,
41
+ RedisLockService,
42
+ SeedServiceImpl,
43
+ SeedServiceProtocol,
44
+ TransactionServiceImpl,
45
+ TransactionServiceProtocol,
46
+ )
47
+ from .settings import CoreCacheSettingsSchema, CoreKafkaSettingsSchema, CoreSettingsSchema, CoreStorageSettingsSchema
48
+
49
+ # --- utils ---
50
+
51
+
52
+ def get_container() -> ContainerProtocol:
53
+ """
54
+ Получаем контейнер зависимостей.
55
+ """
56
+ ContainerImpl.init()
57
+ return ContainerImpl()
58
+
59
+
60
+ async def get_nested_form_data(request: Request) -> FormData:
61
+ """
62
+ Получаем форму, позволяющую использовать вложенные словари.
63
+ """
64
+ dot_data = {k.replace('[', '.').replace(']', ''): v for k, v in (await request.form()).items()}
65
+ nested_data = unflatten(dot_data, 'dot')
66
+ for k, v in nested_data.items():
67
+ if isinstance(v, dict):
68
+ nested_data[k] = json.dumps(v)
69
+ return FormData(nested_data)
70
+
71
+
72
+ def get_pagination(page: int | None = None, page_size: int | None = None) -> PaginationRequestSchema:
73
+ """
74
+ Получаем входные данные пагинации.
75
+ """
76
+ return PaginationRequestSchema(page=page or 1, page_size=page_size or 10)
77
+
78
+
79
+ def get_sorting(sorting: str | None = None) -> Sequence[str]:
80
+ """
81
+ Получаем входные данные сортировки.
82
+ """
83
+ if not sorting:
84
+ return []
85
+ return [s[0] + snakecase(s[1:]) if s[0] == '-' else snakecase(s) for s in sorting.split(',')]
86
+
87
+
88
+ Container = Annotated[ContainerProtocol, Depends(get_container)]
89
+ NestedFormData = Annotated[FormData, Depends(get_nested_form_data)]
90
+ Pagination = Annotated[PaginationRequestSchema, Depends(get_pagination)]
91
+ Sorting = Annotated[Sequence[str], Depends(get_sorting)]
92
+
93
+ # --- repositories ---
94
+
95
+
96
+ def get_settings_repository_factory() -> SettingsRepositoryFactoryProtocol:
97
+ """
98
+ Получаем фабрику репозиториев настроек.
99
+ """
100
+ return SettingsRepositoryFactoryImpl()
101
+
102
+
103
+ SettingsRepositoryFactory = Annotated[SettingsRepositoryFactoryProtocol, Depends(get_settings_repository_factory)]
104
+
105
+
106
+ async def get_settings_repository(settings_repository_factory: SettingsRepositoryFactory) -> SettingsRepositoryProtocol:
107
+ """
108
+ Получаем репозиторий настроек.
109
+ """
110
+ return await settings_repository_factory.make(SettingsSourceEnum.ENV)
111
+
112
+
113
+ SettingsRepository = Annotated[SettingsRepositoryProtocol, Depends(get_settings_repository)]
114
+
115
+
116
+ async def get_settings(settings_repository: SettingsRepository) -> CoreSettingsSchema:
117
+ """
118
+ Получаем настройки.
119
+ """
120
+ return await settings_repository.get(CoreSettingsSchema)
121
+
122
+
123
+ Settings = Annotated[CoreSettingsSchema, Depends(get_settings)]
124
+
125
+
126
+ async def get_broker_repository(settings_repository: SettingsRepository) -> AsyncIterator[KafkaBroker]:
127
+ """
128
+ Получаем репозиторий брокера сообщений.
129
+ """
130
+ kafka_settings = await settings_repository.get(CoreKafkaSettingsSchema)
131
+ yield BrokerFactory.make_static(kafka_settings)
132
+
133
+
134
+ async def get_cache_repository(settings_repository: SettingsRepository) -> CacheRepositoryProtocol:
135
+ """
136
+ Получаем репозиторий кеша.
137
+ """
138
+ settings = await settings_repository.get(CoreSettingsSchema)
139
+ if settings.redis_dsn is not None:
140
+ RedisManager.init(settings.redis_dsn)
141
+ cache_settings = await settings_repository.get(CoreCacheSettingsSchema)
142
+ if CacheManager.cache is None:
143
+ CacheManager.init(cache_settings, RedisManager.redis)
144
+ if CacheManager.cache is not None:
145
+ return CacheManager.cache
146
+ raise ValueError('Cache is not initialized')
147
+
148
+
149
+ def get_storage_repository_factory() -> StorageRepositoryFactoryProtocol:
150
+ """
151
+ Получаем фабрику репозиториев файлового хранилища.
152
+ """
153
+ return StorageRepositoryFactoryImpl()
154
+
155
+
156
+ BrokerRepository = Annotated[KafkaBroker, Depends(get_broker_repository)]
157
+ CacheRepository = Annotated[CacheRepositoryProtocol, Depends(get_cache_repository)]
158
+ StorageRepositoryFactory = Annotated[StorageRepositoryFactoryProtocol, Depends(get_storage_repository_factory)]
159
+
160
+
161
+ async def get_storage_repository(
162
+ settings_repository: SettingsRepository,
163
+ storage_repository_factory: StorageRepositoryFactory,
164
+ ) -> StorageRepositoryProtocol:
165
+ """
166
+ Получаем репозиторий файлового хранилища.
167
+ """
168
+ storage_settings = await settings_repository.get(CoreStorageSettingsSchema)
169
+ if storage_settings.provider == 's3' and storage_settings.s3 is not None:
170
+ return await storage_repository_factory.make(
171
+ StorageTypeEnum.S3,
172
+ S3StorageParamsSchema.model_validate(storage_settings.s3.model_dump()),
173
+ )
174
+ elif storage_settings.provider == 'local':
175
+ return await storage_repository_factory.make(
176
+ StorageTypeEnum.LOCAL, LocalStorageParamsSchema(path=storage_settings.dir)
177
+ )
178
+ raise NotImplementedError(f'Storage {storage_settings.provider} not allowed')
179
+
180
+
181
+ StorageRepository = Annotated[StorageRepositoryProtocol, Depends(get_storage_repository)]
182
+
183
+ # --- db ---
184
+
185
+
186
+ async def get_async_session(settings_repository: SettingsRepository) -> AsyncIterator[AsyncSession]:
187
+ """
188
+ Получаем асинхронную сессию.
189
+ """
190
+ async with SessionFactory.make_async_session_static(settings_repository) as session:
191
+ yield session
192
+
193
+
194
+ Session = Annotated[AsyncSession, Depends(get_async_session)]
195
+
196
+
197
+ def get_session_manager(session: Session) -> SessionManagerProtocol:
198
+ """
199
+ Получаем менеджер сессий.
200
+ """
201
+ return SessionManagerImpl(session)
202
+
203
+
204
+ SessionManager = Annotated[SessionManagerProtocol, Depends(get_session_manager)]
205
+
206
+ # --- services ---
207
+
208
+
209
+ def get_cryptography_service_factory(settings: Settings) -> CryptographyServiceFactoryProtocol:
210
+ """
211
+ Получаем фабрику сервисов криптографии.
212
+ """
213
+ return CryptographyServiceFactoryImpl(settings.secret_key)
214
+
215
+
216
+ CryptographyServiceFactory = Annotated[CryptographyServiceFactoryProtocol, Depends(get_cryptography_service_factory)]
217
+
218
+
219
+ async def get_cryptography_service(
220
+ cryptography_service_factory: CryptographyServiceFactory,
221
+ ) -> CryptographyServiceProtocol:
222
+ """
223
+ Получаем сервис криптографии.
224
+ """
225
+ return await cryptography_service_factory.make(CryptographicAlgorithmEnum.AES_GCM)
226
+
227
+
228
+ def get_lock_service(settings: Settings) -> LockServiceProtocol:
229
+ """
230
+ Получаем сервис распределенной блокировки.
231
+ """
232
+ assert settings.redis_dsn is not None
233
+ RedisManager.init(settings.redis_dsn)
234
+ assert RedisManager.redis is not None
235
+ return RedisLockService(RedisManager.redis)
236
+
237
+
238
+ def get_seed_service(session_manager: SessionManager) -> SeedServiceProtocol:
239
+ """
240
+ Получаем сервис для загрузки данных из файлов.
241
+ """
242
+ return SeedServiceImpl(session_manager)
243
+
244
+
245
+ def get_transaction_service(session: Session) -> TransactionServiceProtocol:
246
+ """
247
+ Получаем сервис транзакций.
248
+ """
249
+ return TransactionServiceImpl(session)
250
+
251
+
252
+ CryptographyService = Annotated[CryptographyServiceProtocol, Depends(get_cryptography_service)]
253
+ LockService = Annotated[LockServiceProtocol, Depends(get_lock_service)]
254
+ SeedService = Annotated[SeedServiceProtocol, Depends(get_seed_service)]
255
+ TransactionService = Annotated[TransactionServiceProtocol, Depends(get_transaction_service)]
fast_clean/enums.py ADDED
@@ -0,0 +1,39 @@
1
+ """
2
+ Модуль, содержащий перечисления.
3
+ """
4
+
5
+ from enum import StrEnum, auto
6
+ from typing import Self
7
+
8
+
9
+ class CascadeEnum(StrEnum):
10
+ """
11
+ Настройки каскадного поведения для SQLAlchemy.
12
+ """
13
+
14
+ SAVE_UPDATE = 'save-update'
15
+ MERGE = 'merge'
16
+ REFRESH_EXPIRE = 'refresh-expire'
17
+ EXPUNGE = 'expunge'
18
+ DELETE = 'delete'
19
+ ALL = 'all'
20
+ DELETE_ORPHAN = 'delete-orphan'
21
+
22
+ ALL_DELETE_ORPHAN = 'all, delete-orphan'
23
+
24
+ def __add__(self: Self, value: str) -> str:
25
+ return f'{self}, {value}'
26
+
27
+ def __radd__(self: Self, value: str) -> str:
28
+ return f'{value}, {self}'
29
+
30
+
31
+ class ModelActionEnum(StrEnum):
32
+ """
33
+ Действие с моделью.
34
+ """
35
+
36
+ INSERT = auto()
37
+ UPDATE = auto()
38
+ UPSERT = auto()
39
+ DELETE = auto()
@@ -0,0 +1,281 @@
1
+ """
2
+ Модуль, содержащий исключения.
3
+ """
4
+
5
+ import traceback
6
+ import uuid
7
+ from abc import ABC, abstractmethod
8
+ from collections.abc import Iterable, Sequence
9
+ from functools import partial
10
+ from typing import Self, TypeVar
11
+
12
+ from fastapi import FastAPI, HTTPException, Request, Response, status
13
+ from fastapi.exception_handlers import http_exception_handler
14
+ from stringcase import camelcase, snakecase
15
+
16
+ from .enums import ModelActionEnum
17
+ from .schemas import BusinessLogicExceptionSchema, ModelAlreadyExistsErrorSchema, ValidationErrorSchema
18
+ from .settings import CoreSettingsSchema
19
+
20
+ ModelType = TypeVar('ModelType')
21
+
22
+
23
+ class ContainerError(Exception):
24
+ """
25
+ Ошибка контейнера зависимостей.
26
+ """
27
+
28
+ def __init__(self, message: str, *args: object) -> None:
29
+ super().__init__(*args)
30
+ self.message = message
31
+
32
+
33
+ class LockError(Exception):
34
+ """
35
+ Ошибка распределенной блокировки.
36
+ """
37
+
38
+ message = 'Errors acquiring or releasing a lock'
39
+
40
+
41
+ class BusinessLogicException(Exception, ABC):
42
+ """
43
+ Базовое исключение бизнес-логики.
44
+ """
45
+
46
+ @property
47
+ def type(self: Self) -> str:
48
+ """
49
+ Тип ошибки.
50
+ """
51
+ return snakecase(type(self).__name__.replace('Error', ''))
52
+
53
+ @property
54
+ @abstractmethod
55
+ def msg(self: Self) -> str:
56
+ """
57
+ Сообщение ошибки.
58
+ """
59
+ ...
60
+
61
+ def __str__(self: Self) -> str:
62
+ return self.msg
63
+
64
+ def get_schema(self: Self, debug: bool) -> BusinessLogicExceptionSchema:
65
+ """
66
+ Получаем схему исключения.
67
+ """
68
+ return BusinessLogicExceptionSchema(
69
+ type=self.type,
70
+ msg=self.msg,
71
+ traceback=(''.join(traceback.format_exception(type(self), self, self.__traceback__)) if debug else None),
72
+ )
73
+
74
+
75
+ class PermissionDeniedError(BusinessLogicException):
76
+ """
77
+ Ошибка, возникающая при недостатке прав для выполнения действия.
78
+ """
79
+
80
+ @property
81
+ def msg(self: Self) -> str:
82
+ return 'Недостаточно прав для выполнения действия'
83
+
84
+
85
+ class ModelNotFoundError(BusinessLogicException):
86
+ """
87
+ Ошибка, возникающая при невозможности найти модель.
88
+ """
89
+
90
+ def __init__(
91
+ self,
92
+ model: type[ModelType] | str,
93
+ *args: object,
94
+ model_id: int | uuid.UUID | Iterable[int | uuid.UUID] | None = None,
95
+ model_name: str | Iterable[str] | None = None,
96
+ message: str | None = None,
97
+ ) -> None:
98
+ super().__init__(*args)
99
+ self.model = model
100
+ self.model_id = model_id
101
+ self.model_name = model_name
102
+ self.custom_message = message
103
+
104
+ @property
105
+ def msg(self: Self) -> str:
106
+ if self.custom_message is not None:
107
+ return self.custom_message
108
+ msg = f'Не удалось найти модель {self.model if isinstance(self.model, str) else self.model.__name__}'
109
+ if self.model_id is not None:
110
+ if isinstance(self.model_id, Iterable):
111
+ return f'{msg} по идентификаторам: [{", ".join(map(str, self.model_id))}]'
112
+ return f'{msg} по идентификатору: {self.model_id}'
113
+ if self.model_name is not None:
114
+ if isinstance(self.model_name, Iterable):
115
+ return f'{msg} по именам: [{", ".join(self.model_name)}]'
116
+ return f'{msg} по имени: {self.model_name}'
117
+ return msg
118
+
119
+
120
+ class ModelAlreadyExistsError(BusinessLogicException):
121
+ """
122
+ Ошибка, возникающая при попытке создать модель с существующим уникальным полем.
123
+ """
124
+
125
+ def __init__(self, field: str, message: str, *args: object) -> None:
126
+ super().__init__(*args)
127
+ self.field = field
128
+ self.message = message
129
+
130
+ @property
131
+ def msg(self: Self) -> str:
132
+ return self.message
133
+
134
+ def get_schema(self: Self, debug: bool) -> BusinessLogicExceptionSchema:
135
+ return ModelAlreadyExistsErrorSchema.model_validate(
136
+ {**super().get_schema(debug).model_dump(), 'field': self.field}
137
+ )
138
+
139
+
140
+ class ModelIntegrityError(BusinessLogicException):
141
+ """
142
+ Ошибка целостности данных при взаимодействии с моделью.
143
+ """
144
+
145
+ def __init__(self, model: type[ModelType] | str, action: ModelActionEnum, *args: object) -> None:
146
+ super().__init__(*args)
147
+ self.model = model
148
+ self.action = action
149
+
150
+ @property
151
+ def msg(self: Self) -> str:
152
+ """
153
+ Сообщение ошибки.
154
+ """
155
+ msg = 'Ошибка целостности данных'
156
+ model_name = self.model if isinstance(self.model, str) else self.model.__name__
157
+ match self.action:
158
+ case ModelActionEnum.INSERT:
159
+ msg += f' при создании модели {model_name}'
160
+ case ModelActionEnum.UPDATE:
161
+ msg += f' при изменении модели {model_name}'
162
+ case ModelActionEnum.UPSERT:
163
+ msg += f' при создании или изменении модели {model_name}'
164
+ case ModelActionEnum.DELETE:
165
+ msg += f' при удалении модели {model_name}'
166
+ return msg
167
+
168
+
169
+ class ValidationError(BusinessLogicException):
170
+ """
171
+ Ошибка валидации.
172
+ """
173
+
174
+ def __init__(self, field: str | Sequence[str], message: str, *args: object) -> None:
175
+ super().__init__(*args)
176
+ self.field = field
177
+ self.message = message
178
+
179
+ @property
180
+ def fields(self: Self) -> Sequence[str]:
181
+ """
182
+ Поля ошибки.
183
+ """
184
+ return [self.field] if isinstance(self.field, str) else self.field
185
+
186
+ @property
187
+ def msg(self: Self) -> str:
188
+ return self.message
189
+
190
+ def get_schema(self: Self, debug: bool) -> BusinessLogicExceptionSchema:
191
+ return ValidationErrorSchema.model_validate(
192
+ {
193
+ **super().get_schema(debug).model_dump(),
194
+ 'fields': list(map(camelcase, self.fields)),
195
+ }
196
+ )
197
+
198
+
199
+ class SortingFieldNotFoundError(BusinessLogicException):
200
+ """
201
+ Ошибка, возникающая при невозможности найти поле для сортировки.
202
+ """
203
+
204
+ def __init__(self, field: str, *args: object) -> None:
205
+ super().__init__(*args)
206
+ self.field = field
207
+
208
+ @property
209
+ def msg(self: Self) -> str:
210
+ return f'Не удалось найти поле для сортировки: {self.field}'
211
+
212
+
213
+ async def business_logic_exception_handler(
214
+ settings: CoreSettingsSchema, request: Request, exception: BusinessLogicException
215
+ ) -> Response:
216
+ """
217
+ Обработчик базового исключения бизнес-логики.
218
+ """
219
+ return await http_exception_handler(
220
+ request,
221
+ HTTPException(
222
+ status_code=status.HTTP_400_BAD_REQUEST,
223
+ detail=[exception.get_schema(settings.debug).model_dump()],
224
+ ),
225
+ )
226
+
227
+
228
+ async def permission_denied_error_handler(
229
+ settings: CoreSettingsSchema, request: Request, error: PermissionDeniedError
230
+ ) -> Response:
231
+ """
232
+ Обработчик ошибки, возникающей при недостатке прав для выполнения действия.
233
+ """
234
+ return await http_exception_handler(
235
+ request,
236
+ HTTPException(
237
+ status_code=status.HTTP_403_FORBIDDEN,
238
+ detail=[error.get_schema(settings.debug).model_dump()],
239
+ ),
240
+ )
241
+
242
+
243
+ async def model_not_found_error_handler(
244
+ settings: CoreSettingsSchema, request: Request, error: ModelNotFoundError
245
+ ) -> Response:
246
+ """
247
+ Обработчик ошибки, возникающей при невозможности найти модель.
248
+ """
249
+ return await http_exception_handler(
250
+ request,
251
+ HTTPException(
252
+ status_code=status.HTTP_404_NOT_FOUND,
253
+ detail=[error.get_schema(settings.debug).model_dump()],
254
+ ),
255
+ )
256
+
257
+
258
+ async def model_already_exists_error_handler(
259
+ settings: CoreSettingsSchema, request: Request, error: ModelAlreadyExistsError
260
+ ) -> Response:
261
+ """
262
+ Обработчик ошибки, возникающей при попытке создать модель с существующим уникальным
263
+ полем.
264
+ """
265
+ return await http_exception_handler(
266
+ request,
267
+ HTTPException(
268
+ status_code=status.HTTP_409_CONFLICT,
269
+ detail=[error.get_schema(settings.debug).model_dump()],
270
+ ),
271
+ )
272
+
273
+
274
+ def use_exceptions_handlers(app: FastAPI, settings: CoreSettingsSchema) -> None:
275
+ """
276
+ Регистрируем глобальные обработчики исключений.
277
+ """
278
+ app.exception_handler(BusinessLogicException)(partial(business_logic_exception_handler, settings))
279
+ app.exception_handler(PermissionDeniedError)(partial(permission_denied_error_handler, settings))
280
+ app.exception_handler(ModelNotFoundError)(partial(model_not_found_error_handler, settings))
281
+ app.exception_handler(ModelAlreadyExistsError)(partial(model_already_exists_error_handler, settings))
fast_clean/loggers.py ADDED
@@ -0,0 +1,26 @@
1
+ """
2
+ Модуль, содержащий функционал, связанный логированием.
3
+ """
4
+
5
+ import logging
6
+ import logging.config
7
+ from pathlib import Path
8
+
9
+ import yaml
10
+
11
+
12
+ def use_logging(base_dir: Path) -> None:
13
+ """
14
+ Применяем настройки логирования.
15
+ """
16
+ files = ['.logging.dev.yaml', '.logging.yaml']
17
+ for file in files:
18
+ ls_file = base_dir / file
19
+ if ls_file.exists():
20
+ with open(ls_file, 'rt') as f:
21
+ config = yaml.safe_load(f.read())
22
+ logging.config.dictConfig(config)
23
+ break
24
+ else:
25
+ print(f'Missing configuration logging files: {", ".join(files)}')
26
+ exit(0)
@@ -0,0 +1,20 @@
1
+ """
2
+ Модуль, содержащий middleware.
3
+ """
4
+
5
+ from fastapi import FastAPI
6
+ from starlette.middleware.cors import CORSMiddleware
7
+
8
+
9
+ def use_middleware(app: FastAPI, cors_origins: list[str]) -> FastAPI:
10
+ """
11
+ Регистрируем middleware.
12
+ """
13
+ app.add_middleware(
14
+ CORSMiddleware,
15
+ allow_origins=cors_origins,
16
+ allow_credentials=True,
17
+ allow_methods=['*'],
18
+ allow_headers=['*'],
19
+ )
20
+ return app
fast_clean/models.py ADDED
@@ -0,0 +1,33 @@
1
+ """
2
+ Модуль, содержащий модели.
3
+ """
4
+
5
+ import datetime as dt
6
+
7
+ from sqlalchemy import DateTime
8
+ from sqlalchemy.orm import Mapped, mapped_column
9
+ from sqlalchemy.sql import func
10
+
11
+
12
+ class TimestampMixin:
13
+ """
14
+ Миксин, содержащий дату и время создания и обновления модели.
15
+ """
16
+
17
+ created_at: Mapped[dt.datetime] = mapped_column(
18
+ DateTime(timezone=True),
19
+ default=lambda: dt.datetime.now(dt.UTC),
20
+ server_default=func.now(),
21
+ )
22
+ """
23
+ Дата и время создания.
24
+ """
25
+ updated_at: Mapped[dt.datetime] = mapped_column(
26
+ DateTime(timezone=True),
27
+ default=lambda: dt.datetime.now(dt.UTC),
28
+ server_default=func.now(),
29
+ onupdate=lambda: dt.datetime.now(dt.UTC),
30
+ )
31
+ """
32
+ Дата и время обновления.
33
+ """
fast_clean/py.typed ADDED
File without changes
fast_clean/redis.py ADDED
@@ -0,0 +1,23 @@
1
+ """
2
+ Модуль, содержащий функционал, связанный с Redis.
3
+ """
4
+
5
+ from pydantic import RedisDsn
6
+
7
+ from redis import asyncio as aioredis
8
+
9
+
10
+ class RedisManager:
11
+ """
12
+ Менеджер для управления клиентом Redis.
13
+ """
14
+
15
+ redis: aioredis.Redis | None = None
16
+
17
+ @classmethod
18
+ def init(cls, redis_dsn: RedisDsn) -> None:
19
+ """
20
+ Инициализируем клиент Redis.
21
+ """
22
+ if cls.redis is None:
23
+ cls.redis = aioredis.from_url(url=str(redis_dsn), decode_responses=True)