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
@@ -0,0 +1,559 @@
|
|
1
|
+
"""
|
2
|
+
Модуль, содержащий репозиторий для выполнения CRUD операций над моделями в базе данных.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
import contextlib
|
8
|
+
import uuid
|
9
|
+
from collections.abc import Callable, Iterable, Sequence
|
10
|
+
from itertools import groupby
|
11
|
+
from typing import Any, Generic, Self, cast, get_args
|
12
|
+
|
13
|
+
import sqlalchemy as sa
|
14
|
+
from sqlalchemy.dialects.postgresql import insert
|
15
|
+
from sqlalchemy.exc import IntegrityError
|
16
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
17
|
+
from sqlalchemy.orm import selectin_polymorphic
|
18
|
+
from sqlalchemy.sql.expression import func
|
19
|
+
|
20
|
+
from fast_clean.db import SessionManagerProtocol
|
21
|
+
from fast_clean.enums import ModelActionEnum
|
22
|
+
from fast_clean.exceptions import (
|
23
|
+
ModelIntegrityError,
|
24
|
+
ModelNotFoundError,
|
25
|
+
SortingFieldNotFoundError,
|
26
|
+
)
|
27
|
+
from fast_clean.schemas import PaginationResultSchema, PaginationSchema
|
28
|
+
|
29
|
+
from .type_vars import (
|
30
|
+
CreateSchemaBaseType,
|
31
|
+
CreateSchemaOldType,
|
32
|
+
CreateSchemaType,
|
33
|
+
IdType,
|
34
|
+
ModelBaseType,
|
35
|
+
ModelOldType,
|
36
|
+
ModelType,
|
37
|
+
ReadSchemaBaseType,
|
38
|
+
ReadSchemaOldType,
|
39
|
+
ReadSchemaType,
|
40
|
+
UpdateSchemaBaseType,
|
41
|
+
UpdateSchemaOldType,
|
42
|
+
UpdateSchemaType,
|
43
|
+
)
|
44
|
+
|
45
|
+
|
46
|
+
class DbCrudRepositoryBase(
|
47
|
+
Generic[
|
48
|
+
ModelBaseType,
|
49
|
+
ReadSchemaBaseType,
|
50
|
+
CreateSchemaBaseType,
|
51
|
+
UpdateSchemaBaseType,
|
52
|
+
IdType,
|
53
|
+
]
|
54
|
+
):
|
55
|
+
"""
|
56
|
+
Базовый репозиторий для выполнения CRUD операций над моделями в базе данных.
|
57
|
+
"""
|
58
|
+
|
59
|
+
__abstract__: bool = True
|
60
|
+
|
61
|
+
__orig_bases__: tuple[
|
62
|
+
type[
|
63
|
+
DbCrudRepositoryBase[ModelBaseType, ReadSchemaBaseType, CreateSchemaBaseType, UpdateSchemaBaseType, IdType]
|
64
|
+
]
|
65
|
+
]
|
66
|
+
__subtypes__: Sequence[
|
67
|
+
tuple[type[ModelBaseType], type[ReadSchemaBaseType], type[CreateSchemaBaseType], type[UpdateSchemaBaseType]]
|
68
|
+
]
|
69
|
+
|
70
|
+
model_types: set[type[ModelBaseType]] = set()
|
71
|
+
model_subtypes: set[type[ModelBaseType]] = set()
|
72
|
+
model_types_mapping: dict[type[ModelBaseType], type[ReadSchemaBaseType]]
|
73
|
+
create_models_mapping: dict[type[CreateSchemaBaseType], type[ModelBaseType]]
|
74
|
+
update_models_mapping: dict[type[UpdateSchemaBaseType], type[ModelBaseType]]
|
75
|
+
model_identities_mapping: dict[Any, type[ModelBaseType]] = {}
|
76
|
+
|
77
|
+
model_type: type[ModelBaseType]
|
78
|
+
|
79
|
+
def __init__(self, session_manager: SessionManagerProtocol):
|
80
|
+
if self.__dict__.get('__abstract__', False):
|
81
|
+
raise TypeError(f"Can't instantiate abstract class {type(self).__name__}")
|
82
|
+
self.session_manager = session_manager
|
83
|
+
|
84
|
+
def __init_subclass__(cls) -> None:
|
85
|
+
"""
|
86
|
+
Инициализируем класс.
|
87
|
+
|
88
|
+
Получаем используемую модель SQLAlchemy и схему Pydantic из базового типа.
|
89
|
+
"""
|
90
|
+
if cls.__dict__.get('__abstract__', False):
|
91
|
+
return super().__init_subclass__()
|
92
|
+
|
93
|
+
base_repository_generic = next(
|
94
|
+
(
|
95
|
+
base
|
96
|
+
for base in getattr(cls, '__orig_bases__', [])
|
97
|
+
if issubclass(getattr(base, '__origin__', base), DbCrudRepositoryBase)
|
98
|
+
),
|
99
|
+
None,
|
100
|
+
)
|
101
|
+
if not base_repository_generic:
|
102
|
+
raise ValueError('Repository must be implemented by DbCrudRepositoryBase')
|
103
|
+
|
104
|
+
if not hasattr(cls, '__subtypes__'):
|
105
|
+
cls.__subtypes__ = []
|
106
|
+
|
107
|
+
cls.model_subtypes = {st[0] for st in cls.__subtypes__}
|
108
|
+
|
109
|
+
cls.model_types_mapping = {}
|
110
|
+
cls.create_models_mapping = {}
|
111
|
+
cls.update_models_mapping = {}
|
112
|
+
cls.model_identities_mapping = {}
|
113
|
+
types: Sequence[
|
114
|
+
tuple[type[ModelBaseType], type[ReadSchemaBaseType], type[CreateSchemaBaseType], type[UpdateSchemaBaseType]]
|
115
|
+
] = [*cls.__subtypes__, get_args(base_repository_generic)[:4]]
|
116
|
+
for model_type, read_schema_type, create_schema_type, update_schema_type in types:
|
117
|
+
cls.model_types.add(model_type)
|
118
|
+
cls.model_types_mapping[model_type] = read_schema_type
|
119
|
+
cls.create_models_mapping[create_schema_type] = model_type
|
120
|
+
cls.update_models_mapping[update_schema_type] = model_type
|
121
|
+
cls.model_identities_mapping[model_type.__mapper__.polymorphic_identity] = model_type
|
122
|
+
|
123
|
+
cls.model_type, *_ = cast(
|
124
|
+
tuple[
|
125
|
+
type[ModelBaseType],
|
126
|
+
type[ReadSchemaBaseType],
|
127
|
+
type[CreateSchemaBaseType],
|
128
|
+
type[UpdateSchemaBaseType],
|
129
|
+
],
|
130
|
+
get_args(base_repository_generic),
|
131
|
+
)
|
132
|
+
|
133
|
+
return super().__init_subclass__()
|
134
|
+
|
135
|
+
async def get(self: Self, id: IdType) -> ReadSchemaBaseType:
|
136
|
+
"""
|
137
|
+
Получаем модель по идентификатору.
|
138
|
+
"""
|
139
|
+
async with self.session_manager.get_session() as s:
|
140
|
+
statement = self.select().where(self.model_type.id == id)
|
141
|
+
model = (await s.execute(statement)).scalar_one_or_none()
|
142
|
+
if model is None:
|
143
|
+
raise ModelNotFoundError(self.model_type, model_id=id)
|
144
|
+
return self.model_validate(model)
|
145
|
+
|
146
|
+
async def get_or_none(self: Self, id: IdType) -> ReadSchemaBaseType | None:
|
147
|
+
"""
|
148
|
+
Получаем модель или None по идентификатору.
|
149
|
+
"""
|
150
|
+
with contextlib.suppress(ModelNotFoundError):
|
151
|
+
return await self.get(id)
|
152
|
+
return None
|
153
|
+
|
154
|
+
async def get_by_ids(self: Self, ids: Sequence[IdType], *, exact: bool = False) -> list[ReadSchemaBaseType]:
|
155
|
+
"""
|
156
|
+
Получаем список моделей по идентификаторам.
|
157
|
+
"""
|
158
|
+
async with self.session_manager.get_session() as s:
|
159
|
+
statement = self.select().where(self.model_type.id.in_(ids))
|
160
|
+
models = (await s.execute(statement)).scalars().all()
|
161
|
+
self.check_get_by_ids_exact(ids, models, exact)
|
162
|
+
return [self.model_validate(model) for model in models]
|
163
|
+
|
164
|
+
async def get_all(self: Self) -> list[ReadSchemaBaseType]:
|
165
|
+
"""
|
166
|
+
Получаем все модели.
|
167
|
+
"""
|
168
|
+
async with self.session_manager.get_session() as s:
|
169
|
+
statement = self.select()
|
170
|
+
models = (await s.execute(statement)).scalars().all()
|
171
|
+
return [self.model_validate(model) for model in models]
|
172
|
+
|
173
|
+
async def paginate(
|
174
|
+
self: Self,
|
175
|
+
pagination: PaginationSchema,
|
176
|
+
user: Any,
|
177
|
+
policies: list[str],
|
178
|
+
*,
|
179
|
+
search: str | None = None,
|
180
|
+
search_by: Iterable[str] | None = None,
|
181
|
+
sorting: Iterable[str] | None = None,
|
182
|
+
) -> PaginationResultSchema[ReadSchemaBaseType]:
|
183
|
+
"""
|
184
|
+
Получаем список моделей с пагинацией, поиском и сортировкой.
|
185
|
+
"""
|
186
|
+
return await self.paginate_with_filter(
|
187
|
+
pagination,
|
188
|
+
user,
|
189
|
+
policies,
|
190
|
+
search=search,
|
191
|
+
search_by=search_by,
|
192
|
+
sorting=sorting,
|
193
|
+
)
|
194
|
+
|
195
|
+
async def create(self: Self, create_object: CreateSchemaBaseType) -> ReadSchemaBaseType:
|
196
|
+
"""
|
197
|
+
Создаем модель.
|
198
|
+
"""
|
199
|
+
async with self.session_manager.get_session() as s:
|
200
|
+
try:
|
201
|
+
model_type = self.create_models_mapping[type(create_object)]
|
202
|
+
create_dict = self.dump_create_object(create_object)
|
203
|
+
return (await self.bulk_create_with_model_type(model_type, [create_dict], s))[0]
|
204
|
+
except IntegrityError as integrity_error:
|
205
|
+
raise ModelIntegrityError(self.model_type, ModelActionEnum.INSERT) from integrity_error
|
206
|
+
|
207
|
+
async def bulk_create(self: Self, create_objects: list[CreateSchemaBaseType]) -> list[ReadSchemaBaseType]:
|
208
|
+
"""
|
209
|
+
Создаем несколько моделей.
|
210
|
+
"""
|
211
|
+
if len(create_objects) == 0:
|
212
|
+
return []
|
213
|
+
async with self.session_manager.get_session() as s:
|
214
|
+
try:
|
215
|
+
created_models: list[ReadSchemaBaseType] = []
|
216
|
+
for model_type, type_create_objects in groupby(
|
217
|
+
create_objects, key=lambda co: self.create_models_mapping[type(co)]
|
218
|
+
):
|
219
|
+
create_dicts = [self.dump_create_object(create_object) for create_object in type_create_objects]
|
220
|
+
created_models.extend(await self.bulk_create_with_model_type(model_type, create_dicts, s))
|
221
|
+
return created_models
|
222
|
+
except IntegrityError as integrity_error:
|
223
|
+
raise ModelIntegrityError(self.model_type, ModelActionEnum.INSERT) from integrity_error
|
224
|
+
|
225
|
+
async def update(self: Self, update_object: UpdateSchemaBaseType) -> ReadSchemaBaseType:
|
226
|
+
"""
|
227
|
+
Обновляем модель.
|
228
|
+
"""
|
229
|
+
async with self.session_manager.get_session() as s:
|
230
|
+
try:
|
231
|
+
model_type = self.update_models_mapping[type(update_object)]
|
232
|
+
update_dict = update_object.model_dump(exclude_unset=True)
|
233
|
+
return await self.update_with_model_type(model_type, update_dict, s)
|
234
|
+
except IntegrityError as integrity_error:
|
235
|
+
raise ModelIntegrityError(self.model_type, ModelActionEnum.UPDATE) from integrity_error
|
236
|
+
|
237
|
+
async def bulk_update(self: Self, update_objects: list[UpdateSchemaBaseType]) -> None:
|
238
|
+
"""
|
239
|
+
Обновляем несколько моделей.
|
240
|
+
"""
|
241
|
+
if len(update_objects) == 0:
|
242
|
+
return
|
243
|
+
async with self.session_manager.get_session() as s:
|
244
|
+
try:
|
245
|
+
for model_type, type_update_objects in groupby(
|
246
|
+
update_objects, key=lambda co: self.update_models_mapping[type(co)]
|
247
|
+
):
|
248
|
+
update_dicts = [update_object.model_dump() for update_object in type_update_objects]
|
249
|
+
await self.bulk_update_with_model_type(model_type, update_dicts, s)
|
250
|
+
except IntegrityError as integrity_error:
|
251
|
+
raise ModelIntegrityError(self.model_type, ModelActionEnum.UPDATE) from integrity_error
|
252
|
+
|
253
|
+
async def upsert(self: Self, create_object: CreateSchemaBaseType) -> ReadSchemaBaseType:
|
254
|
+
"""
|
255
|
+
Создаем или обновляем модель.
|
256
|
+
"""
|
257
|
+
async with self.session_manager.get_session() as s:
|
258
|
+
try:
|
259
|
+
model_type = self.create_models_mapping[type(create_object)]
|
260
|
+
create_dict = self.dump_create_object(create_object)
|
261
|
+
return await self.upsert_with_model_type(model_type, create_dict, s)
|
262
|
+
except IntegrityError as integrity_error:
|
263
|
+
raise ModelIntegrityError(self.model_type, ModelActionEnum.UPSERT) from integrity_error
|
264
|
+
|
265
|
+
async def delete(self: Self, ids: Sequence[IdType]) -> None:
|
266
|
+
"""
|
267
|
+
Удаляем модели.
|
268
|
+
"""
|
269
|
+
if len(ids) == 0:
|
270
|
+
return
|
271
|
+
async with self.session_manager.get_session() as s:
|
272
|
+
try:
|
273
|
+
model_types: list[type[ModelBaseType]] = [self.model_type]
|
274
|
+
if self.model_type.__mapper__.polymorphic_on is not None:
|
275
|
+
types_statement = (
|
276
|
+
sa.select(self.model_type.__mapper__.polymorphic_on)
|
277
|
+
.where(self.model_type.id.in_(ids))
|
278
|
+
.distinct()
|
279
|
+
)
|
280
|
+
types = (await s.execute(types_statement)).scalars().all()
|
281
|
+
for t in types:
|
282
|
+
model_types.append(self.model_identities_mapping[t])
|
283
|
+
for model_type in model_types[::-1]:
|
284
|
+
statement = sa.delete(model_type).where(model_type.id.in_(ids))
|
285
|
+
await s.execute(statement)
|
286
|
+
except IntegrityError as integrity_error:
|
287
|
+
raise ModelIntegrityError(self.model_type, ModelActionEnum.DELETE) from integrity_error
|
288
|
+
|
289
|
+
@classmethod
|
290
|
+
def select(cls) -> sa.Select[tuple[ModelBaseType]]:
|
291
|
+
"""
|
292
|
+
Выбираем базовую модели или наследника со всеми полями при наличии.
|
293
|
+
"""
|
294
|
+
statement = sa.select(cls.model_type)
|
295
|
+
if cls.model_subtypes:
|
296
|
+
return statement.options(selectin_polymorphic(cls.model_type, cls.model_subtypes))
|
297
|
+
return statement
|
298
|
+
|
299
|
+
@classmethod
|
300
|
+
def model_validate(cls, model: ModelBaseType) -> ReadSchemaBaseType:
|
301
|
+
"""
|
302
|
+
Приводим модель к схеме.
|
303
|
+
"""
|
304
|
+
read_schema_type = cls.model_types_mapping[type(model)]
|
305
|
+
return cast(
|
306
|
+
ReadSchemaBaseType,
|
307
|
+
read_schema_type.model_validate(model, from_attributes=True),
|
308
|
+
)
|
309
|
+
|
310
|
+
@classmethod
|
311
|
+
async def bulk_create_with_model_type(
|
312
|
+
cls, model_type: type[ModelBaseType], create_dicts: list[dict[str, Any]], session: AsyncSession
|
313
|
+
) -> list[ReadSchemaBaseType]:
|
314
|
+
"""
|
315
|
+
Создаем модели с помощью типа.
|
316
|
+
"""
|
317
|
+
parent_dicts = await cls.bulk_create_parent_model(model_type, create_dicts, session)
|
318
|
+
values: list[dict[str, Any]] = []
|
319
|
+
for create_dict, parent_dict in zip(create_dicts, parent_dicts, strict=True):
|
320
|
+
value = {k: v for k, v in create_dict.items() if k in model_type.__table__.columns}
|
321
|
+
if 'id' in parent_dict:
|
322
|
+
value['id'] = parent_dict['id']
|
323
|
+
values.append(value)
|
324
|
+
statement = sa.insert(model_type).values(values).returning(*model_type.__table__.columns.values())
|
325
|
+
model_dicts = (await session.execute(statement)).mappings().all()
|
326
|
+
read_schema_type = cls.model_types_mapping[model_type]
|
327
|
+
return [
|
328
|
+
cast(
|
329
|
+
ReadSchemaBaseType,
|
330
|
+
read_schema_type.model_validate({**parent_dict, **model_dict}),
|
331
|
+
)
|
332
|
+
for parent_dict, model_dict in zip(parent_dicts, model_dicts, strict=True)
|
333
|
+
]
|
334
|
+
|
335
|
+
@classmethod
|
336
|
+
async def bulk_create_parent_model(
|
337
|
+
cls, model_type: type[ModelBaseType], create_dicts: list[dict[str, Any]], session: AsyncSession
|
338
|
+
) -> list[dict[str, Any]]:
|
339
|
+
"""
|
340
|
+
Создаем родительские модели с помощью типа.
|
341
|
+
"""
|
342
|
+
parent_model_type = cls.get_parent_model_type(model_type)
|
343
|
+
if parent_model_type is None:
|
344
|
+
return [{} for _ in create_dicts]
|
345
|
+
return [
|
346
|
+
ps.model_dump() for ps in await cls.bulk_create_with_model_type(parent_model_type, create_dicts, session)
|
347
|
+
]
|
348
|
+
|
349
|
+
@classmethod
|
350
|
+
async def update_with_model_type(
|
351
|
+
cls, model_type: type[ModelBaseType], update_dict: dict[str, Any], session: AsyncSession
|
352
|
+
) -> ReadSchemaBaseType:
|
353
|
+
"""
|
354
|
+
Обновляем модель с помощью типа.
|
355
|
+
"""
|
356
|
+
parent_dict = await cls.update_parent_model(model_type, update_dict, session)
|
357
|
+
statement = (
|
358
|
+
sa.update(model_type)
|
359
|
+
.where(model_type.id == update_dict['id'])
|
360
|
+
.values({k: v for k, v in update_dict.items() if k in model_type.__table__.columns if k != 'id'})
|
361
|
+
.returning(*model_type.__table__.columns.values())
|
362
|
+
)
|
363
|
+
model_dict = (await session.execute(statement)).mappings().one()
|
364
|
+
read_schema_type = cls.model_types_mapping[model_type]
|
365
|
+
return cast(
|
366
|
+
ReadSchemaBaseType,
|
367
|
+
read_schema_type.model_validate({**parent_dict, **model_dict}),
|
368
|
+
)
|
369
|
+
|
370
|
+
@classmethod
|
371
|
+
async def update_parent_model(
|
372
|
+
cls, model_type: type[ModelBaseType], update_dict: dict[str, Any], session: AsyncSession
|
373
|
+
) -> dict[str, Any]:
|
374
|
+
"""
|
375
|
+
Обновляем родительскую модель с помощью типа.
|
376
|
+
"""
|
377
|
+
parent_model_type = cls.get_parent_model_type(model_type)
|
378
|
+
if parent_model_type is None:
|
379
|
+
return {}
|
380
|
+
return (await cls.update_with_model_type(parent_model_type, update_dict, session)).model_dump()
|
381
|
+
|
382
|
+
@classmethod
|
383
|
+
async def bulk_update_with_model_type(
|
384
|
+
cls, model_type: type[ModelBaseType], update_dicts: list[dict[str, Any]], session: AsyncSession
|
385
|
+
) -> None:
|
386
|
+
"""
|
387
|
+
Создаем модели с помощью типа.
|
388
|
+
"""
|
389
|
+
await cls.bulk_update_parent_model(model_type, update_dicts, session)
|
390
|
+
values: list[dict[str, Any]] = []
|
391
|
+
for update_dict in update_dicts:
|
392
|
+
values.append({k: v for k, v in update_dict.items() if k in model_type.__table__.columns})
|
393
|
+
statement = sa.update(model_type)
|
394
|
+
await session.execute(statement, values)
|
395
|
+
|
396
|
+
@classmethod
|
397
|
+
async def bulk_update_parent_model(
|
398
|
+
cls, model_type: type[ModelBaseType], update_dicts: list[dict[str, Any]], session: AsyncSession
|
399
|
+
) -> None:
|
400
|
+
"""
|
401
|
+
Обновляем родительские модели с помощью типа.
|
402
|
+
"""
|
403
|
+
parent_model_type = cls.get_parent_model_type(model_type)
|
404
|
+
if parent_model_type is not None:
|
405
|
+
await cls.bulk_update_with_model_type(parent_model_type, update_dicts, session)
|
406
|
+
|
407
|
+
@classmethod
|
408
|
+
async def upsert_with_model_type(
|
409
|
+
cls, model_type: type[ModelBaseType], create_dict: dict[str, Any], session: AsyncSession
|
410
|
+
) -> ReadSchemaBaseType:
|
411
|
+
"""
|
412
|
+
Создаем или обновляем модель с помощью типа.
|
413
|
+
"""
|
414
|
+
parent_dict = await cls.upsert_parent_model(model_type, create_dict, session)
|
415
|
+
primary_keys = {key.name for key in cast(Any, sa.inspect(model_type)).primary_key}
|
416
|
+
values = {k: v for k, v in create_dict.items() if k in model_type.__table__.columns}
|
417
|
+
statement = (
|
418
|
+
insert(model_type)
|
419
|
+
.values(values)
|
420
|
+
.on_conflict_do_update(
|
421
|
+
index_elements=primary_keys,
|
422
|
+
set_={k: v for k, v in values.items() if k not in primary_keys},
|
423
|
+
)
|
424
|
+
.returning(*model_type.__table__.columns.values())
|
425
|
+
)
|
426
|
+
model_dict = (await session.execute(statement)).mappings().one()
|
427
|
+
read_schema_type = cls.model_types_mapping[model_type]
|
428
|
+
return cast(
|
429
|
+
ReadSchemaBaseType,
|
430
|
+
read_schema_type.model_validate({**parent_dict, **model_dict}),
|
431
|
+
)
|
432
|
+
|
433
|
+
@classmethod
|
434
|
+
async def upsert_parent_model(
|
435
|
+
cls, model_type: type[ModelBaseType], create_dict: dict[str, Any], session: AsyncSession
|
436
|
+
) -> dict[str, Any]:
|
437
|
+
"""
|
438
|
+
Создаем или обновляем родительскую модель с помощью типа.
|
439
|
+
"""
|
440
|
+
parent_model_type = cls.get_parent_model_type(model_type)
|
441
|
+
if parent_model_type is None:
|
442
|
+
return {}
|
443
|
+
return (await cls.upsert_with_model_type(parent_model_type, create_dict, session)).model_dump()
|
444
|
+
|
445
|
+
@staticmethod
|
446
|
+
def dump_create_object(create_object: CreateSchemaBaseType) -> dict[str, Any]:
|
447
|
+
"""
|
448
|
+
Создаем словарь для схемы создания модели.
|
449
|
+
"""
|
450
|
+
create_dict = create_object.model_dump()
|
451
|
+
if create_dict['id'] is None:
|
452
|
+
del create_dict['id']
|
453
|
+
return create_dict
|
454
|
+
|
455
|
+
@classmethod
|
456
|
+
def get_parent_model_type(cls, model_type: type[ModelBaseType]) -> type[ModelBaseType] | None:
|
457
|
+
"""
|
458
|
+
Получаем тип родительской модели.
|
459
|
+
"""
|
460
|
+
if not model_type.__bases__ or model_type.__bases__[0] not in cls.model_types:
|
461
|
+
return None
|
462
|
+
return model_type.__bases__[0]
|
463
|
+
|
464
|
+
@classmethod
|
465
|
+
def check_get_by_ids_exact(cls, ids: Sequence[IdType], models: Sequence[ModelBaseType], exact: bool) -> None:
|
466
|
+
"""
|
467
|
+
Проверяем, что по идентификаторам получены все модели.
|
468
|
+
"""
|
469
|
+
if exact and len(ids) != len(models):
|
470
|
+
raise ModelNotFoundError(
|
471
|
+
cls.model_type,
|
472
|
+
model_id=set(ids) - {cast(IdType, model.id) for model in models},
|
473
|
+
)
|
474
|
+
|
475
|
+
async def paginate_with_filter(
|
476
|
+
self: Self,
|
477
|
+
pagination: PaginationSchema,
|
478
|
+
user: Any,
|
479
|
+
policies: list[str],
|
480
|
+
*,
|
481
|
+
search: str | None = None,
|
482
|
+
search_by: Iterable[str] | None = None,
|
483
|
+
sorting: Iterable[str] | None = None,
|
484
|
+
select_filter: Callable[[sa.Select[tuple[ModelBaseType]]], sa.Select[tuple[ModelBaseType]]] | None = None,
|
485
|
+
) -> PaginationResultSchema[ReadSchemaBaseType]:
|
486
|
+
"""
|
487
|
+
Получаем список моделей с пагинацией, поиском, сортировкой и фильтрами.
|
488
|
+
"""
|
489
|
+
if len(policies) == 0:
|
490
|
+
return PaginationResultSchema(objects=[], count=0)
|
491
|
+
search_by = search_by or []
|
492
|
+
sorting = sorting or []
|
493
|
+
async with self.session_manager.get_session() as s:
|
494
|
+
statement = self.select()
|
495
|
+
if select_filter:
|
496
|
+
statement = select_filter(statement)
|
497
|
+
if search:
|
498
|
+
search_where: sa.ColumnElement[Any] = sa.false()
|
499
|
+
for sb in search_by:
|
500
|
+
search_where = sa.or_(search_where, getattr(self.model_type, sb).ilike(f'%{search}%'))
|
501
|
+
statement = statement.where(search_where)
|
502
|
+
order_by_expr = self.get_order_by_expr(sorting)
|
503
|
+
models = (
|
504
|
+
(await s.execute(statement.limit(pagination.limit).offset(pagination.offset).order_by(*order_by_expr)))
|
505
|
+
.scalars()
|
506
|
+
.all()
|
507
|
+
)
|
508
|
+
objects = [self.model_validate(model) for model in models]
|
509
|
+
count_statement = statement.with_only_columns(func.count(self.model_type.id))
|
510
|
+
count = (await s.execute(count_statement)).scalar_one()
|
511
|
+
return PaginationResultSchema(count=count, objects=objects)
|
512
|
+
|
513
|
+
def get_order_by_expr(self: Self, sorting: Iterable[str]) -> list[sa.UnaryExpression]:
|
514
|
+
"""
|
515
|
+
Получаем выражение сортировки.
|
516
|
+
"""
|
517
|
+
order_by_expr: list[sa.UnaryExpression] = []
|
518
|
+
for st in sorting:
|
519
|
+
try:
|
520
|
+
if st[0] == '-':
|
521
|
+
order_by_expr.append(getattr(self.model_type, st[1:]).desc())
|
522
|
+
else:
|
523
|
+
order_by_expr.append(getattr(self.model_type, st))
|
524
|
+
except AttributeError as attribute_error:
|
525
|
+
raise SortingFieldNotFoundError(st) from attribute_error
|
526
|
+
return order_by_expr
|
527
|
+
|
528
|
+
|
529
|
+
class DbCrudRepositoryOld(
|
530
|
+
DbCrudRepositoryBase[
|
531
|
+
ModelOldType,
|
532
|
+
ReadSchemaOldType,
|
533
|
+
CreateSchemaOldType,
|
534
|
+
UpdateSchemaOldType,
|
535
|
+
int,
|
536
|
+
],
|
537
|
+
Generic[
|
538
|
+
ModelOldType,
|
539
|
+
ReadSchemaOldType,
|
540
|
+
CreateSchemaOldType,
|
541
|
+
UpdateSchemaOldType,
|
542
|
+
],
|
543
|
+
):
|
544
|
+
"""
|
545
|
+
Репозиторий для выполнения CRUD операций над моделями старого типа в базе данных.
|
546
|
+
"""
|
547
|
+
|
548
|
+
__abstract__ = True
|
549
|
+
|
550
|
+
|
551
|
+
class DbCrudRepository(
|
552
|
+
DbCrudRepositoryBase[ModelType, ReadSchemaType, CreateSchemaType, UpdateSchemaType, uuid.UUID],
|
553
|
+
Generic[ModelType, ReadSchemaType, CreateSchemaType, UpdateSchemaType],
|
554
|
+
):
|
555
|
+
"""
|
556
|
+
Репозиторий для выполнения CRUD операций над моделями нового типа в базе данных.
|
557
|
+
"""
|
558
|
+
|
559
|
+
__abstract__ = True
|