fastapi-sqlalchemy-toolkit 0.0.2.2__tar.gz → 0.1__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.
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/PKG-INFO +137 -68
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/README.md +135 -66
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/examples/app/.env +2 -2
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/examples/app/api/api.py +1 -1
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/examples/app/api/endpoints/child.py +27 -10
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/examples/app/config.py +12 -10
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/examples/app/db.py +2 -4
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/examples/app/deps.py +1 -1
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/examples/app/main.py +1 -2
- fastapi_sqlalchemy_toolkit-0.1/examples/app/managers.py +8 -0
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/examples/app/models.py +2 -1
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/examples/app/schemas.py +5 -1
- fastapi_sqlalchemy_toolkit-0.1/fastapi_sqlalchemy_toolkit/__init__.py +4 -0
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/fastapi_sqlalchemy_toolkit/filters.py +2 -2
- fastapi_sqlalchemy_toolkit-0.0.2.2/fastapi_sqlalchemy_toolkit/db_crud.py → fastapi_sqlalchemy_toolkit-0.1/fastapi_sqlalchemy_toolkit/model_manager.py +11 -10
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/fastapi_sqlalchemy_toolkit/utils.py +9 -6
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/pyproject.toml +2 -2
- fastapi_sqlalchemy_toolkit-0.0.2.2/examples/app/api/endpoints/parent.py +0 -0
- fastapi_sqlalchemy_toolkit-0.0.2.2/examples/app/db_crud.py +0 -7
- fastapi_sqlalchemy_toolkit-0.0.2.2/fastapi_sqlalchemy_toolkit/__init__.py +0 -4
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/.flake8 +0 -0
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/.gitignore +0 -0
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/LICENSE +0 -0
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/examples/app/__init__.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/examples/app/api/__init__.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/examples/app/api/endpoints/__init__.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/fastapi_sqlalchemy_toolkit/base_model.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/fastapi_sqlalchemy_toolkit/ordering.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/requirements.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fastapi_sqlalchemy_toolkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1
|
|
4
4
|
Summary: FastAPI SQLAlchemy Toolkit
|
|
5
5
|
Project-URL: Homepage, https://github.com/e-kondr01/fastapi-sqlalchemy-toolkit
|
|
6
6
|
Author-email: Egor Kondrashov <e.kondr01@gmail.com>
|
|
@@ -8,10 +8,10 @@ License-File: LICENSE
|
|
|
8
8
|
Classifier: Operating System :: OS Independent
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
10
|
Requires-Python: >=3.11
|
|
11
|
-
Requires-Dist: dateutil>=2.8.2
|
|
12
11
|
Requires-Dist: fastapi-pagination>=0.12.6
|
|
13
12
|
Requires-Dist: fastapi>=0.100.0
|
|
14
13
|
Requires-Dist: pydantic>=2.0.0
|
|
14
|
+
Requires-Dist: python-dateutil>=2.8.2
|
|
15
15
|
Requires-Dist: sqlalchemy>=2.0.0
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
|
|
@@ -49,61 +49,59 @@ pip install fastapi-sqlalchemy-toolkit
|
|
|
49
49
|
|
|
50
50
|
Пример использования `fastapi-sqlalchemy-toolkit` доступен в директории `examples/app`
|
|
51
51
|
|
|
52
|
-
##
|
|
52
|
+
## Инициализация ModelManager
|
|
53
53
|
|
|
54
|
-
Для использования `fastapi-sqlaclhemy-toolkit` необходимо создать экземпляр `
|
|
54
|
+
Для использования `fastapi-sqlaclhemy-toolkit` необходимо создать экземпляр `ModelManager` для своей модели:
|
|
55
55
|
|
|
56
56
|
```python
|
|
57
|
-
from fastapi_sqlalchemy_toolkit import
|
|
57
|
+
from fastapi_sqlalchemy_toolkit import ModelManager
|
|
58
58
|
|
|
59
59
|
from .models import MyModel
|
|
60
60
|
from .schemas import MyModelCreateSchema, MyModelUpdateSchema
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
my_model_manager = ModelManager[MyModel, MyModelCreateSchema, MyModelUpdateSchema](MyModel)
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
При инициализации
|
|
66
|
-
`fk_mapping` — это словарь, в котором ключи — это названия
|
|
65
|
+
При инициализации ModelManager можно задать параметр `fk_mapping`, необходимый для валидации внешних ключей.
|
|
66
|
+
`fk_mapping` — это словарь, в котором ключи — это названия внешних ключей, а значения — модели SQLAlchemy, на которые эти ключи ссылаются.
|
|
67
67
|
|
|
68
68
|
```python
|
|
69
|
-
from fastapi_sqlalchemy_toolkit import
|
|
69
|
+
from fastapi_sqlalchemy_toolkit import ModelManager
|
|
70
70
|
|
|
71
71
|
from .models import MyModel, MyParentModel
|
|
72
72
|
from .schemas import MyModelCreateSchema, MyModelUpdateSchema
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
MyModel,
|
|
76
|
-
fk_mapping={"parent_id": MyParentModel}
|
|
74
|
+
my_model_manager = ModelManager[MyModel, MyModelCreateSchema, MyModelUpdateSchema](
|
|
75
|
+
MyModel, fk_mapping={"parent_id": MyParentModel}
|
|
77
76
|
)
|
|
78
77
|
```
|
|
79
78
|
|
|
80
|
-
Атрибут `default_ordering` определяет сортировку по умолчанию при получении
|
|
79
|
+
Атрибут `default_ordering` определяет сортировку по умолчанию при получении списка объектов. В него нужно передать поле основной модели.
|
|
81
80
|
|
|
82
81
|
```python
|
|
83
|
-
from fastapi_sqlalchemy_toolkit import
|
|
82
|
+
from fastapi_sqlalchemy_toolkit import ModelManager
|
|
84
83
|
|
|
85
84
|
from .models import MyModel
|
|
86
85
|
from .schemas import MyModelCreateSchema, MyModelUpdateSchema
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
MyModel,
|
|
90
|
-
default_ordering=MyModel.title
|
|
87
|
+
my_model_manager = ModelManager[MyModel, MyModelCreateSchema, MyModelUpdateSchema](
|
|
88
|
+
MyModel, default_ordering=MyModel.title
|
|
91
89
|
)
|
|
92
90
|
```
|
|
93
91
|
|
|
94
|
-
## Доступные методы `
|
|
92
|
+
## Доступные методы `ModelManager`
|
|
95
93
|
|
|
96
|
-
Ниже перечислены
|
|
94
|
+
Ниже перечислены CRUD методы, предоставляемые `ModelManager`.
|
|
97
95
|
Документация параметров, принимаемых методами, находится в докстрингах методов.
|
|
98
96
|
|
|
99
|
-
- `create` - создание
|
|
97
|
+
- `create` - создание объекта; выполняет валидацию значений полей на уровне БД
|
|
100
98
|
- `get` - получение объекта
|
|
101
|
-
- `get_or_404` - получение объекта или ошибки 404
|
|
99
|
+
- `get_or_404` - получение объекта или ошибки HTTP 404
|
|
102
100
|
- `exists` - проверка существования объекта
|
|
103
|
-
- `paginated_filter` - получение списка объектов с пагинацией через `fastapi_pagination`
|
|
104
|
-
- `filter` - получение списка объектов
|
|
101
|
+
- `paginated_filter` - получение списка объектов с фильтрами и пагинацией через `fastapi_pagination`
|
|
102
|
+
- `filter` - получение списка объектов с фильтрами
|
|
105
103
|
- `count` - получение количества объектов
|
|
106
|
-
- `update` - обновление
|
|
104
|
+
- `update` - обновление объекта; выполняет валидацию значений полей на уровне БД
|
|
107
105
|
- `delete` - удаление объекта
|
|
108
106
|
|
|
109
107
|
## Фильтрация
|
|
@@ -113,69 +111,86 @@ my_model_db = BaseCRUD[MyModel, MyModelCreateSchema, MyModelUpdateSchema](
|
|
|
113
111
|
```python
|
|
114
112
|
from typing import Annotated
|
|
115
113
|
from uuid import UUID
|
|
114
|
+
|
|
116
115
|
from fastapi import APIRouter, Depends, Response, status
|
|
117
116
|
from sqlalchemy import select
|
|
118
117
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
118
|
+
|
|
119
119
|
from app.deps import get_async_session
|
|
120
|
-
from app.models import MyModel
|
|
120
|
+
from app.models import MyModel, MyParentModel
|
|
121
121
|
from app.schemas import MyObjectListSchema
|
|
122
122
|
|
|
123
|
-
|
|
124
123
|
router = APIRouter()
|
|
125
124
|
CurrentSession = Annotated[AsyncSession, Depends(get_async_session)]
|
|
126
125
|
|
|
126
|
+
|
|
127
127
|
@router.get("/my-objects")
|
|
128
128
|
async def get_my_objects(
|
|
129
129
|
session: CurrentSession,
|
|
130
130
|
user_id: UUID | None = None,
|
|
131
|
-
name: str | None = None
|
|
131
|
+
name: str | None = None,
|
|
132
|
+
parent_name: str | None = None,
|
|
132
133
|
) -> list[MyObjectListSchema]:
|
|
133
134
|
stmt = select(MyModel)
|
|
134
135
|
if user_id is not None:
|
|
135
136
|
stmt = stmt.filter_by(user_id=user_id)
|
|
136
137
|
if name is not None:
|
|
137
|
-
stmt = stmt.
|
|
138
|
+
stmt = stmt.filter(MyModel.name.ilike == name)
|
|
139
|
+
if parent_name is not None:
|
|
140
|
+
stmt = stmt.join(MyModel.parent)
|
|
141
|
+
stmt = stmt.filter(ParentModel.name.ilike == parent_name)
|
|
138
142
|
result = await session.execute(stmt)
|
|
139
|
-
return
|
|
143
|
+
return result.scalars().all()
|
|
140
144
|
```
|
|
141
|
-
Как можно заметить,
|
|
145
|
+
Как можно заметить, для реализации фильтрации необходима дубликация шаблонного кода.
|
|
142
146
|
|
|
143
147
|
В `fastapi-sqlalchemy-toolkit` этот эндпоинт выглядит так:
|
|
144
148
|
|
|
145
149
|
```python
|
|
146
|
-
from
|
|
150
|
+
from fastapi_sqlalchemy_toolkit import FieldFilter
|
|
151
|
+
|
|
152
|
+
from app.managers import my_object_manager
|
|
147
153
|
|
|
148
154
|
@router.get("/my-objects")
|
|
149
155
|
async def get_my_objects(
|
|
150
156
|
session: CurrentSession,
|
|
151
157
|
user_id: UUID | None = None,
|
|
152
|
-
name: str | None = None
|
|
158
|
+
name: str | None = None,
|
|
159
|
+
parent_name: str | None = None,
|
|
153
160
|
) -> list[MyObjectListSchema]:
|
|
154
|
-
return await
|
|
161
|
+
return await my_object_manager.filter(
|
|
162
|
+
session,
|
|
163
|
+
user_id=user_id,
|
|
164
|
+
name=FieldFilter(name, operator="ilike"),
|
|
165
|
+
parent_name=FieldFilter(parent_name, operator="ilike", model=ParentModel),
|
|
166
|
+
)
|
|
155
167
|
```
|
|
156
168
|
### Использование FieldFilter
|
|
157
169
|
Дополнительные возможности декларативной фильтрации поддерживаются использованием класса `FieldFilter`.
|
|
158
170
|
`FieldFilter` позволяет:
|
|
159
171
|
- фильтровать по значениям полей связанных моделей при установке атрибута `model`.
|
|
160
|
-
При этом `
|
|
161
|
-
- использовать любые
|
|
172
|
+
При этом `ModelManager` автоматически сделает необходимые join'ы, если это модель, которая напрямую связана с главной
|
|
173
|
+
- использовать любые методы и атрибуты полей SQLAlchemy через атрибут `operator`
|
|
162
174
|
- применять функции SQLAlchemy к полям (например, `date()`) через атрибут `func`
|
|
163
175
|
|
|
164
176
|
```python
|
|
165
|
-
from
|
|
166
|
-
from app.db_crud import parent_db
|
|
177
|
+
from app.managers import parent_manager
|
|
167
178
|
from app.models import Child
|
|
168
179
|
|
|
169
|
-
|
|
180
|
+
from fastapi_sqlalchemy_toolkit import FieldFilter
|
|
181
|
+
|
|
182
|
+
await parent_manager.filter(
|
|
183
|
+
session, child_title=FieldFilter(child_title, model=Child, operator="ilike")
|
|
184
|
+
)
|
|
170
185
|
```
|
|
171
186
|
### Фильтрация по обратным связям
|
|
172
187
|
Также в методах `filter` и `paginated_filter` есть поддержка фильтрации
|
|
173
188
|
по обратным связям (`relationship()` в направлении один ко многим) с использованием метода `.any()`.
|
|
174
189
|
|
|
175
190
|
```python
|
|
176
|
-
# Если
|
|
177
|
-
await
|
|
178
|
-
# Вернёт объекты Parent, у которых есть связь с
|
|
191
|
+
# Если ParentModel.children -- это связь один ко многим
|
|
192
|
+
await parent_manager.filter(session, children=[1, 2])
|
|
193
|
+
# Вернёт объекты Parent, у которых есть связь с ChildModel с id 1 или 2
|
|
179
194
|
```
|
|
180
195
|
### Фильтрация по null
|
|
181
196
|
Для того чтобы осуществить фильтрацию по `null`, квери параметр должен принимать
|
|
@@ -187,18 +202,19 @@ from fastapi_sqlalchemy_toolkit import NullableQuery
|
|
|
187
202
|
@router.get("")
|
|
188
203
|
async def get_children(
|
|
189
204
|
session: CurrentSession,
|
|
190
|
-
|
|
205
|
+
activated_at: NullableQuery | datetime.datetime | None = None,
|
|
191
206
|
) -> Page[ChildRetrieveSchema]:
|
|
207
|
+
...
|
|
192
208
|
```
|
|
193
|
-
`NullableQuery` это пустая строка.
|
|
194
|
-
`GET /children?
|
|
209
|
+
`NullableQuery` -- это пустая строка. Запрос с фильтрацией по `activated_at == None` должен выглядеть так:
|
|
210
|
+
`GET /children?activated_at=`
|
|
195
211
|
|
|
196
212
|
*Почему так?*
|
|
197
213
|
|
|
198
214
|
|
|
199
|
-
При запросе `GET /children?
|
|
200
|
-
объекты с `
|
|
201
|
-
объекты с `
|
|
215
|
+
При запросе `GET /children?activated_at=2023-08-01` ожидается, что будут возвращены
|
|
216
|
+
объекты с `activated_at == 2023-08-01`, но при запросе GET `/children` мы не ожидаем, что будут возвращены
|
|
217
|
+
объекты с `activated_at == None` (ожидаемым поведением является отсутствие фильтрации по `activated_at`).
|
|
202
218
|
|
|
203
219
|
Если в эндпоинте FastAPI определён необязательный квери параметр, и он не передан
|
|
204
220
|
в запросе, то значение этого параметра будет равно `None`. Чтобы не возникала описанная выше некорректная фильтрация, фильтр
|
|
@@ -210,7 +226,7 @@ async def get_children(
|
|
|
210
226
|
а также по полям связанных моделей. При этом необходимые для сортировки по полям
|
|
211
227
|
связанных моделей join'ы будут сделаны автоматически.
|
|
212
228
|
|
|
213
|
-
Для применения декларативной сортировки
|
|
229
|
+
Для применения декларативной сортировки нужно:
|
|
214
230
|
1. Определить список полей, по которым доступна фильтрация. Поле может быть
|
|
215
231
|
строкой, если это поле основной модели, или атрибутом модели, если оно находится
|
|
216
232
|
на связанной модели.
|
|
@@ -218,12 +234,12 @@ async def get_children(
|
|
|
218
234
|
```python
|
|
219
235
|
from app.models import Parent
|
|
220
236
|
|
|
221
|
-
child_ordering_fields =
|
|
237
|
+
child_ordering_fields = (
|
|
222
238
|
"title",
|
|
223
239
|
"created_at",
|
|
224
240
|
Parent.title,
|
|
225
241
|
Parent.created_at
|
|
226
|
-
|
|
242
|
+
)
|
|
227
243
|
```
|
|
228
244
|
|
|
229
245
|
Для каждого из указаных полей будет доступна сортировка по возрастанию и убыванию.
|
|
@@ -243,39 +259,40 @@ async def get_child_objects(
|
|
|
243
259
|
session: CurrentSession,
|
|
244
260
|
order_by: ordering_dep(child_ordering_fields)
|
|
245
261
|
) -> list[ChildListSchema]
|
|
262
|
+
...
|
|
246
263
|
```
|
|
247
264
|
|
|
248
|
-
3. Передать параметр сортировки как параметр `order_by` в методы `
|
|
265
|
+
3. Передать параметр сортировки как параметр `order_by` в методы `ModelManager`
|
|
249
266
|
|
|
250
267
|
```python
|
|
251
|
-
return await
|
|
268
|
+
return await child_manager.filter(session=session, order_by=order_by)
|
|
252
269
|
```
|
|
253
270
|
|
|
254
271
|
|
|
255
272
|
## Расширение
|
|
256
|
-
Методы `
|
|
273
|
+
Методы `ModelManager` легко расширить дополнительной логикой.
|
|
257
274
|
|
|
258
275
|
|
|
259
|
-
В первую очередь необходимо определить свой класс
|
|
276
|
+
В первую очередь необходимо определить свой класс ModelManager:
|
|
260
277
|
|
|
261
278
|
```python
|
|
262
|
-
from fastapi_sqlalchemy_toolkit import
|
|
279
|
+
from fastapi_sqlalchemy_toolkit import ModelManager
|
|
263
280
|
|
|
264
281
|
|
|
265
|
-
class
|
|
282
|
+
class MyModelManager[MyModel, MyModelCreateSchema, MyModelUpdateSchema](ModelManager):
|
|
266
283
|
...
|
|
267
284
|
```
|
|
268
285
|
### Дополнительная валидация
|
|
269
286
|
Дополнительную валидацию можно добавить, переопределив метод `validate`:
|
|
270
287
|
|
|
271
288
|
```python
|
|
272
|
-
class
|
|
289
|
+
class MyModelManager[MyModel, MyModelCreateSchema, MyModelUpdateSchema](ModelManager):
|
|
273
290
|
async def validate_parent_type(self, session: AsyncSession, validated_data: ModelDict) -> None:
|
|
274
291
|
"""
|
|
275
292
|
Проверяет тип выбранного объекта Parent
|
|
276
293
|
"""
|
|
277
294
|
# объект Parent с таким ID точно есть, так как это проверяется ранее в super().validate
|
|
278
|
-
parent = await
|
|
295
|
+
parent = await parent_manager.get(session, id=in_obj["parent_id"])
|
|
279
296
|
if parent.type != ParentTypes.CanHaveChildren:
|
|
280
297
|
raise HTTPException(
|
|
281
298
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
@@ -285,19 +302,36 @@ class MyModelCRUDB[MyModel, MyModelCreateSchema, MyModelUpdateSchema](BaseCRUD):
|
|
|
285
302
|
async def run_db_validation(
|
|
286
303
|
self,
|
|
287
304
|
session: AsyncSession,
|
|
288
|
-
db_obj:
|
|
305
|
+
db_obj: MyModel | None = None,
|
|
289
306
|
in_obj: ModelDict | None = None,
|
|
290
307
|
) -> ModelDict:
|
|
291
308
|
validated_data = await super().validate(session, db_obj, in_obj)
|
|
292
309
|
await self.validate_parent_type(session, validated_data)
|
|
293
310
|
return validated_data
|
|
294
311
|
```
|
|
312
|
+
|
|
313
|
+
### Дополнительная бизнес логика при CRUD операциях
|
|
314
|
+
Если при CRUD операциях с моделью необходимо выполнить какую-то дополнительную бизнес логику,
|
|
315
|
+
это можно сделать, переопределив соответствующие методы ModelManager:
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
class MyModelManager[MyModel, MyModelCreateSchema, MyModelUpdateSchema](ModelManager):
|
|
319
|
+
async def create(
|
|
320
|
+
self, *args, background_tasks: BackgroundTasks | None = None, **kwargs
|
|
321
|
+
) -> MyModel:
|
|
322
|
+
created = await super().create(*args, **kwargs)
|
|
323
|
+
background_tasks.add_task(send_email, created.id)
|
|
324
|
+
return created
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Такой подход соответствует принципу "Fat Models, Skinny Views" из Django.
|
|
328
|
+
|
|
295
329
|
### Использование декларативных фильтров в нестандартных списочных запросах
|
|
296
330
|
Если необходимо получить не просто список объектов, но и какие-то другие поля (допустим, кол-во дочерних объектов)
|
|
297
|
-
или агрегации, но также необходима декларативная фильтрация, то можно
|
|
331
|
+
или агрегации, но также необходима декларативная фильтрация, то можно новый свой метод менеджера,
|
|
298
332
|
вызвав в нём метод `super().get_filter_expression`:
|
|
299
333
|
```python
|
|
300
|
-
class
|
|
334
|
+
class MyModelManager[MyModel, MyModelCreateSchema, MyModelUpdateSchema](MyModel):
|
|
301
335
|
async def get_parents_with_children_count(
|
|
302
336
|
self, session: AsyncSession, **kwargs
|
|
303
337
|
) -> list[RetrieveParentWithChildrenCountSchema]:
|
|
@@ -311,29 +345,64 @@ class MyModelCRUDB[MyModel, MyModelCreateSchema, MyModelUpdateSchema](MyModel):
|
|
|
311
345
|
)
|
|
312
346
|
|
|
313
347
|
# Вызываем метод для получения фильтров SQLAlchemy из аргументов методов
|
|
314
|
-
# filter и paginated_filter
|
|
348
|
+
# filter и paginated_filter
|
|
315
349
|
query = query.filter(self.get_filter_expression(**kwargs))
|
|
316
350
|
|
|
317
351
|
result = await session.execute(query)
|
|
318
352
|
result = result.unique().all()
|
|
319
|
-
for row in result:
|
|
353
|
+
for i, row in enumerate(result):
|
|
320
354
|
row.Parent.children_count = row.children_count
|
|
321
|
-
|
|
355
|
+
result[i] = row.Parent
|
|
356
|
+
return result
|
|
322
357
|
```
|
|
323
358
|
|
|
324
359
|
## Другие полезности
|
|
325
360
|
### Сохранение пользователя запроса
|
|
326
361
|
|
|
327
|
-
|
|
362
|
+
Пользователя запроса можно задать в создаваемом/обновляемом объекте,
|
|
363
|
+
передав дополнительный параметр в метод `create` (`update`):
|
|
328
364
|
```python
|
|
329
365
|
@router.post("")
|
|
330
366
|
async def create_child(
|
|
331
367
|
child_in: CreateUpdateChildSchema, session: CurrentSession, user: CurrentUser
|
|
332
368
|
) -> CreateUpdateChildSchema:
|
|
333
|
-
return await
|
|
369
|
+
return await child_manager.create(session=session, in_obj=child_in, author_id=user.id)
|
|
334
370
|
```
|
|
335
371
|
|
|
336
372
|
### Создание и обновление объектов с M2M связями
|
|
337
|
-
Если на модели определена M2M связь, то использование `
|
|
373
|
+
Если на модели определена M2M связь, то использование `ModelManager` позволяет передать в это поле список ID объектов.
|
|
374
|
+
|
|
375
|
+
`fastapi-sqlalchemy-toolkit` провалидирует существование этих объектов и установит им M2M связь,
|
|
376
|
+
без необходимости создавать отдельные эндпоинты для работы с M2M связями.
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
# Пусть модели Person и House имеют M2M связь
|
|
380
|
+
from pydantic import BaseModel
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class PersonCreateSchema(BaseModel):
|
|
384
|
+
house_ids: list[int]
|
|
385
|
+
|
|
386
|
+
...
|
|
387
|
+
|
|
388
|
+
in_obj = PersonCreateSchema(house_ids=[1, 2, 3])
|
|
389
|
+
await person_manager.create(session, in_obj)
|
|
390
|
+
# Создаст объект Person и установит ему M2M связь с House с id 1, 2 и 3
|
|
391
|
+
```
|
|
338
392
|
|
|
339
|
-
|
|
393
|
+
### Фильтрация по списку значений
|
|
394
|
+
Один из способов фильтрации по списку значений -- передать этот список в качестве
|
|
395
|
+
квери параметра в строку через запятую.
|
|
396
|
+
`fastapi-sqlalchemy-toolkit` предоставляет утилиту для фильтрации по списку значений, переданного в строку через запятую:
|
|
397
|
+
```python
|
|
398
|
+
from uuid import UUID
|
|
399
|
+
from fastapi_sqlalchemy_toolkit.utils import comma_list_query, get_comma_list_values
|
|
400
|
+
|
|
401
|
+
@router.get("/children")
|
|
402
|
+
async def get_child_objects(
|
|
403
|
+
session: CurrentSession,
|
|
404
|
+
ids: comma_list_query = None,
|
|
405
|
+
) -> list[ChildListSchema]
|
|
406
|
+
ids = get_comma_list_values(ids, UUID)
|
|
407
|
+
return await child_manager.filter(session, id=FieldFilter(ids, operator="in_"))
|
|
408
|
+
```
|
|
@@ -32,61 +32,59 @@ pip install fastapi-sqlalchemy-toolkit
|
|
|
32
32
|
|
|
33
33
|
Пример использования `fastapi-sqlalchemy-toolkit` доступен в директории `examples/app`
|
|
34
34
|
|
|
35
|
-
##
|
|
35
|
+
## Инициализация ModelManager
|
|
36
36
|
|
|
37
|
-
Для использования `fastapi-sqlaclhemy-toolkit` необходимо создать экземпляр `
|
|
37
|
+
Для использования `fastapi-sqlaclhemy-toolkit` необходимо создать экземпляр `ModelManager` для своей модели:
|
|
38
38
|
|
|
39
39
|
```python
|
|
40
|
-
from fastapi_sqlalchemy_toolkit import
|
|
40
|
+
from fastapi_sqlalchemy_toolkit import ModelManager
|
|
41
41
|
|
|
42
42
|
from .models import MyModel
|
|
43
43
|
from .schemas import MyModelCreateSchema, MyModelUpdateSchema
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
my_model_manager = ModelManager[MyModel, MyModelCreateSchema, MyModelUpdateSchema](MyModel)
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
При инициализации
|
|
49
|
-
`fk_mapping` — это словарь, в котором ключи — это названия
|
|
48
|
+
При инициализации ModelManager можно задать параметр `fk_mapping`, необходимый для валидации внешних ключей.
|
|
49
|
+
`fk_mapping` — это словарь, в котором ключи — это названия внешних ключей, а значения — модели SQLAlchemy, на которые эти ключи ссылаются.
|
|
50
50
|
|
|
51
51
|
```python
|
|
52
|
-
from fastapi_sqlalchemy_toolkit import
|
|
52
|
+
from fastapi_sqlalchemy_toolkit import ModelManager
|
|
53
53
|
|
|
54
54
|
from .models import MyModel, MyParentModel
|
|
55
55
|
from .schemas import MyModelCreateSchema, MyModelUpdateSchema
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
MyModel,
|
|
59
|
-
fk_mapping={"parent_id": MyParentModel}
|
|
57
|
+
my_model_manager = ModelManager[MyModel, MyModelCreateSchema, MyModelUpdateSchema](
|
|
58
|
+
MyModel, fk_mapping={"parent_id": MyParentModel}
|
|
60
59
|
)
|
|
61
60
|
```
|
|
62
61
|
|
|
63
|
-
Атрибут `default_ordering` определяет сортировку по умолчанию при получении
|
|
62
|
+
Атрибут `default_ordering` определяет сортировку по умолчанию при получении списка объектов. В него нужно передать поле основной модели.
|
|
64
63
|
|
|
65
64
|
```python
|
|
66
|
-
from fastapi_sqlalchemy_toolkit import
|
|
65
|
+
from fastapi_sqlalchemy_toolkit import ModelManager
|
|
67
66
|
|
|
68
67
|
from .models import MyModel
|
|
69
68
|
from .schemas import MyModelCreateSchema, MyModelUpdateSchema
|
|
70
69
|
|
|
71
|
-
|
|
72
|
-
MyModel,
|
|
73
|
-
default_ordering=MyModel.title
|
|
70
|
+
my_model_manager = ModelManager[MyModel, MyModelCreateSchema, MyModelUpdateSchema](
|
|
71
|
+
MyModel, default_ordering=MyModel.title
|
|
74
72
|
)
|
|
75
73
|
```
|
|
76
74
|
|
|
77
|
-
## Доступные методы `
|
|
75
|
+
## Доступные методы `ModelManager`
|
|
78
76
|
|
|
79
|
-
Ниже перечислены
|
|
77
|
+
Ниже перечислены CRUD методы, предоставляемые `ModelManager`.
|
|
80
78
|
Документация параметров, принимаемых методами, находится в докстрингах методов.
|
|
81
79
|
|
|
82
|
-
- `create` - создание
|
|
80
|
+
- `create` - создание объекта; выполняет валидацию значений полей на уровне БД
|
|
83
81
|
- `get` - получение объекта
|
|
84
|
-
- `get_or_404` - получение объекта или ошибки 404
|
|
82
|
+
- `get_or_404` - получение объекта или ошибки HTTP 404
|
|
85
83
|
- `exists` - проверка существования объекта
|
|
86
|
-
- `paginated_filter` - получение списка объектов с пагинацией через `fastapi_pagination`
|
|
87
|
-
- `filter` - получение списка объектов
|
|
84
|
+
- `paginated_filter` - получение списка объектов с фильтрами и пагинацией через `fastapi_pagination`
|
|
85
|
+
- `filter` - получение списка объектов с фильтрами
|
|
88
86
|
- `count` - получение количества объектов
|
|
89
|
-
- `update` - обновление
|
|
87
|
+
- `update` - обновление объекта; выполняет валидацию значений полей на уровне БД
|
|
90
88
|
- `delete` - удаление объекта
|
|
91
89
|
|
|
92
90
|
## Фильтрация
|
|
@@ -96,69 +94,86 @@ my_model_db = BaseCRUD[MyModel, MyModelCreateSchema, MyModelUpdateSchema](
|
|
|
96
94
|
```python
|
|
97
95
|
from typing import Annotated
|
|
98
96
|
from uuid import UUID
|
|
97
|
+
|
|
99
98
|
from fastapi import APIRouter, Depends, Response, status
|
|
100
99
|
from sqlalchemy import select
|
|
101
100
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
101
|
+
|
|
102
102
|
from app.deps import get_async_session
|
|
103
|
-
from app.models import MyModel
|
|
103
|
+
from app.models import MyModel, MyParentModel
|
|
104
104
|
from app.schemas import MyObjectListSchema
|
|
105
105
|
|
|
106
|
-
|
|
107
106
|
router = APIRouter()
|
|
108
107
|
CurrentSession = Annotated[AsyncSession, Depends(get_async_session)]
|
|
109
108
|
|
|
109
|
+
|
|
110
110
|
@router.get("/my-objects")
|
|
111
111
|
async def get_my_objects(
|
|
112
112
|
session: CurrentSession,
|
|
113
113
|
user_id: UUID | None = None,
|
|
114
|
-
name: str | None = None
|
|
114
|
+
name: str | None = None,
|
|
115
|
+
parent_name: str | None = None,
|
|
115
116
|
) -> list[MyObjectListSchema]:
|
|
116
117
|
stmt = select(MyModel)
|
|
117
118
|
if user_id is not None:
|
|
118
119
|
stmt = stmt.filter_by(user_id=user_id)
|
|
119
120
|
if name is not None:
|
|
120
|
-
stmt = stmt.
|
|
121
|
+
stmt = stmt.filter(MyModel.name.ilike == name)
|
|
122
|
+
if parent_name is not None:
|
|
123
|
+
stmt = stmt.join(MyModel.parent)
|
|
124
|
+
stmt = stmt.filter(ParentModel.name.ilike == parent_name)
|
|
121
125
|
result = await session.execute(stmt)
|
|
122
|
-
return
|
|
126
|
+
return result.scalars().all()
|
|
123
127
|
```
|
|
124
|
-
Как можно заметить,
|
|
128
|
+
Как можно заметить, для реализации фильтрации необходима дубликация шаблонного кода.
|
|
125
129
|
|
|
126
130
|
В `fastapi-sqlalchemy-toolkit` этот эндпоинт выглядит так:
|
|
127
131
|
|
|
128
132
|
```python
|
|
129
|
-
from
|
|
133
|
+
from fastapi_sqlalchemy_toolkit import FieldFilter
|
|
134
|
+
|
|
135
|
+
from app.managers import my_object_manager
|
|
130
136
|
|
|
131
137
|
@router.get("/my-objects")
|
|
132
138
|
async def get_my_objects(
|
|
133
139
|
session: CurrentSession,
|
|
134
140
|
user_id: UUID | None = None,
|
|
135
|
-
name: str | None = None
|
|
141
|
+
name: str | None = None,
|
|
142
|
+
parent_name: str | None = None,
|
|
136
143
|
) -> list[MyObjectListSchema]:
|
|
137
|
-
return await
|
|
144
|
+
return await my_object_manager.filter(
|
|
145
|
+
session,
|
|
146
|
+
user_id=user_id,
|
|
147
|
+
name=FieldFilter(name, operator="ilike"),
|
|
148
|
+
parent_name=FieldFilter(parent_name, operator="ilike", model=ParentModel),
|
|
149
|
+
)
|
|
138
150
|
```
|
|
139
151
|
### Использование FieldFilter
|
|
140
152
|
Дополнительные возможности декларативной фильтрации поддерживаются использованием класса `FieldFilter`.
|
|
141
153
|
`FieldFilter` позволяет:
|
|
142
154
|
- фильтровать по значениям полей связанных моделей при установке атрибута `model`.
|
|
143
|
-
При этом `
|
|
144
|
-
- использовать любые
|
|
155
|
+
При этом `ModelManager` автоматически сделает необходимые join'ы, если это модель, которая напрямую связана с главной
|
|
156
|
+
- использовать любые методы и атрибуты полей SQLAlchemy через атрибут `operator`
|
|
145
157
|
- применять функции SQLAlchemy к полям (например, `date()`) через атрибут `func`
|
|
146
158
|
|
|
147
159
|
```python
|
|
148
|
-
from
|
|
149
|
-
from app.db_crud import parent_db
|
|
160
|
+
from app.managers import parent_manager
|
|
150
161
|
from app.models import Child
|
|
151
162
|
|
|
152
|
-
|
|
163
|
+
from fastapi_sqlalchemy_toolkit import FieldFilter
|
|
164
|
+
|
|
165
|
+
await parent_manager.filter(
|
|
166
|
+
session, child_title=FieldFilter(child_title, model=Child, operator="ilike")
|
|
167
|
+
)
|
|
153
168
|
```
|
|
154
169
|
### Фильтрация по обратным связям
|
|
155
170
|
Также в методах `filter` и `paginated_filter` есть поддержка фильтрации
|
|
156
171
|
по обратным связям (`relationship()` в направлении один ко многим) с использованием метода `.any()`.
|
|
157
172
|
|
|
158
173
|
```python
|
|
159
|
-
# Если
|
|
160
|
-
await
|
|
161
|
-
# Вернёт объекты Parent, у которых есть связь с
|
|
174
|
+
# Если ParentModel.children -- это связь один ко многим
|
|
175
|
+
await parent_manager.filter(session, children=[1, 2])
|
|
176
|
+
# Вернёт объекты Parent, у которых есть связь с ChildModel с id 1 или 2
|
|
162
177
|
```
|
|
163
178
|
### Фильтрация по null
|
|
164
179
|
Для того чтобы осуществить фильтрацию по `null`, квери параметр должен принимать
|
|
@@ -170,18 +185,19 @@ from fastapi_sqlalchemy_toolkit import NullableQuery
|
|
|
170
185
|
@router.get("")
|
|
171
186
|
async def get_children(
|
|
172
187
|
session: CurrentSession,
|
|
173
|
-
|
|
188
|
+
activated_at: NullableQuery | datetime.datetime | None = None,
|
|
174
189
|
) -> Page[ChildRetrieveSchema]:
|
|
190
|
+
...
|
|
175
191
|
```
|
|
176
|
-
`NullableQuery` это пустая строка.
|
|
177
|
-
`GET /children?
|
|
192
|
+
`NullableQuery` -- это пустая строка. Запрос с фильтрацией по `activated_at == None` должен выглядеть так:
|
|
193
|
+
`GET /children?activated_at=`
|
|
178
194
|
|
|
179
195
|
*Почему так?*
|
|
180
196
|
|
|
181
197
|
|
|
182
|
-
При запросе `GET /children?
|
|
183
|
-
объекты с `
|
|
184
|
-
объекты с `
|
|
198
|
+
При запросе `GET /children?activated_at=2023-08-01` ожидается, что будут возвращены
|
|
199
|
+
объекты с `activated_at == 2023-08-01`, но при запросе GET `/children` мы не ожидаем, что будут возвращены
|
|
200
|
+
объекты с `activated_at == None` (ожидаемым поведением является отсутствие фильтрации по `activated_at`).
|
|
185
201
|
|
|
186
202
|
Если в эндпоинте FastAPI определён необязательный квери параметр, и он не передан
|
|
187
203
|
в запросе, то значение этого параметра будет равно `None`. Чтобы не возникала описанная выше некорректная фильтрация, фильтр
|
|
@@ -193,7 +209,7 @@ async def get_children(
|
|
|
193
209
|
а также по полям связанных моделей. При этом необходимые для сортировки по полям
|
|
194
210
|
связанных моделей join'ы будут сделаны автоматически.
|
|
195
211
|
|
|
196
|
-
Для применения декларативной сортировки
|
|
212
|
+
Для применения декларативной сортировки нужно:
|
|
197
213
|
1. Определить список полей, по которым доступна фильтрация. Поле может быть
|
|
198
214
|
строкой, если это поле основной модели, или атрибутом модели, если оно находится
|
|
199
215
|
на связанной модели.
|
|
@@ -201,12 +217,12 @@ async def get_children(
|
|
|
201
217
|
```python
|
|
202
218
|
from app.models import Parent
|
|
203
219
|
|
|
204
|
-
child_ordering_fields =
|
|
220
|
+
child_ordering_fields = (
|
|
205
221
|
"title",
|
|
206
222
|
"created_at",
|
|
207
223
|
Parent.title,
|
|
208
224
|
Parent.created_at
|
|
209
|
-
|
|
225
|
+
)
|
|
210
226
|
```
|
|
211
227
|
|
|
212
228
|
Для каждого из указаных полей будет доступна сортировка по возрастанию и убыванию.
|
|
@@ -226,39 +242,40 @@ async def get_child_objects(
|
|
|
226
242
|
session: CurrentSession,
|
|
227
243
|
order_by: ordering_dep(child_ordering_fields)
|
|
228
244
|
) -> list[ChildListSchema]
|
|
245
|
+
...
|
|
229
246
|
```
|
|
230
247
|
|
|
231
|
-
3. Передать параметр сортировки как параметр `order_by` в методы `
|
|
248
|
+
3. Передать параметр сортировки как параметр `order_by` в методы `ModelManager`
|
|
232
249
|
|
|
233
250
|
```python
|
|
234
|
-
return await
|
|
251
|
+
return await child_manager.filter(session=session, order_by=order_by)
|
|
235
252
|
```
|
|
236
253
|
|
|
237
254
|
|
|
238
255
|
## Расширение
|
|
239
|
-
Методы `
|
|
256
|
+
Методы `ModelManager` легко расширить дополнительной логикой.
|
|
240
257
|
|
|
241
258
|
|
|
242
|
-
В первую очередь необходимо определить свой класс
|
|
259
|
+
В первую очередь необходимо определить свой класс ModelManager:
|
|
243
260
|
|
|
244
261
|
```python
|
|
245
|
-
from fastapi_sqlalchemy_toolkit import
|
|
262
|
+
from fastapi_sqlalchemy_toolkit import ModelManager
|
|
246
263
|
|
|
247
264
|
|
|
248
|
-
class
|
|
265
|
+
class MyModelManager[MyModel, MyModelCreateSchema, MyModelUpdateSchema](ModelManager):
|
|
249
266
|
...
|
|
250
267
|
```
|
|
251
268
|
### Дополнительная валидация
|
|
252
269
|
Дополнительную валидацию можно добавить, переопределив метод `validate`:
|
|
253
270
|
|
|
254
271
|
```python
|
|
255
|
-
class
|
|
272
|
+
class MyModelManager[MyModel, MyModelCreateSchema, MyModelUpdateSchema](ModelManager):
|
|
256
273
|
async def validate_parent_type(self, session: AsyncSession, validated_data: ModelDict) -> None:
|
|
257
274
|
"""
|
|
258
275
|
Проверяет тип выбранного объекта Parent
|
|
259
276
|
"""
|
|
260
277
|
# объект Parent с таким ID точно есть, так как это проверяется ранее в super().validate
|
|
261
|
-
parent = await
|
|
278
|
+
parent = await parent_manager.get(session, id=in_obj["parent_id"])
|
|
262
279
|
if parent.type != ParentTypes.CanHaveChildren:
|
|
263
280
|
raise HTTPException(
|
|
264
281
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
@@ -268,19 +285,36 @@ class MyModelCRUDB[MyModel, MyModelCreateSchema, MyModelUpdateSchema](BaseCRUD):
|
|
|
268
285
|
async def run_db_validation(
|
|
269
286
|
self,
|
|
270
287
|
session: AsyncSession,
|
|
271
|
-
db_obj:
|
|
288
|
+
db_obj: MyModel | None = None,
|
|
272
289
|
in_obj: ModelDict | None = None,
|
|
273
290
|
) -> ModelDict:
|
|
274
291
|
validated_data = await super().validate(session, db_obj, in_obj)
|
|
275
292
|
await self.validate_parent_type(session, validated_data)
|
|
276
293
|
return validated_data
|
|
277
294
|
```
|
|
295
|
+
|
|
296
|
+
### Дополнительная бизнес логика при CRUD операциях
|
|
297
|
+
Если при CRUD операциях с моделью необходимо выполнить какую-то дополнительную бизнес логику,
|
|
298
|
+
это можно сделать, переопределив соответствующие методы ModelManager:
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
class MyModelManager[MyModel, MyModelCreateSchema, MyModelUpdateSchema](ModelManager):
|
|
302
|
+
async def create(
|
|
303
|
+
self, *args, background_tasks: BackgroundTasks | None = None, **kwargs
|
|
304
|
+
) -> MyModel:
|
|
305
|
+
created = await super().create(*args, **kwargs)
|
|
306
|
+
background_tasks.add_task(send_email, created.id)
|
|
307
|
+
return created
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Такой подход соответствует принципу "Fat Models, Skinny Views" из Django.
|
|
311
|
+
|
|
278
312
|
### Использование декларативных фильтров в нестандартных списочных запросах
|
|
279
313
|
Если необходимо получить не просто список объектов, но и какие-то другие поля (допустим, кол-во дочерних объектов)
|
|
280
|
-
или агрегации, но также необходима декларативная фильтрация, то можно
|
|
314
|
+
или агрегации, но также необходима декларативная фильтрация, то можно новый свой метод менеджера,
|
|
281
315
|
вызвав в нём метод `super().get_filter_expression`:
|
|
282
316
|
```python
|
|
283
|
-
class
|
|
317
|
+
class MyModelManager[MyModel, MyModelCreateSchema, MyModelUpdateSchema](MyModel):
|
|
284
318
|
async def get_parents_with_children_count(
|
|
285
319
|
self, session: AsyncSession, **kwargs
|
|
286
320
|
) -> list[RetrieveParentWithChildrenCountSchema]:
|
|
@@ -294,29 +328,64 @@ class MyModelCRUDB[MyModel, MyModelCreateSchema, MyModelUpdateSchema](MyModel):
|
|
|
294
328
|
)
|
|
295
329
|
|
|
296
330
|
# Вызываем метод для получения фильтров SQLAlchemy из аргументов методов
|
|
297
|
-
# filter и paginated_filter
|
|
331
|
+
# filter и paginated_filter
|
|
298
332
|
query = query.filter(self.get_filter_expression(**kwargs))
|
|
299
333
|
|
|
300
334
|
result = await session.execute(query)
|
|
301
335
|
result = result.unique().all()
|
|
302
|
-
for row in result:
|
|
336
|
+
for i, row in enumerate(result):
|
|
303
337
|
row.Parent.children_count = row.children_count
|
|
304
|
-
|
|
338
|
+
result[i] = row.Parent
|
|
339
|
+
return result
|
|
305
340
|
```
|
|
306
341
|
|
|
307
342
|
## Другие полезности
|
|
308
343
|
### Сохранение пользователя запроса
|
|
309
344
|
|
|
310
|
-
|
|
345
|
+
Пользователя запроса можно задать в создаваемом/обновляемом объекте,
|
|
346
|
+
передав дополнительный параметр в метод `create` (`update`):
|
|
311
347
|
```python
|
|
312
348
|
@router.post("")
|
|
313
349
|
async def create_child(
|
|
314
350
|
child_in: CreateUpdateChildSchema, session: CurrentSession, user: CurrentUser
|
|
315
351
|
) -> CreateUpdateChildSchema:
|
|
316
|
-
return await
|
|
352
|
+
return await child_manager.create(session=session, in_obj=child_in, author_id=user.id)
|
|
317
353
|
```
|
|
318
354
|
|
|
319
355
|
### Создание и обновление объектов с M2M связями
|
|
320
|
-
Если на модели определена M2M связь, то использование `
|
|
356
|
+
Если на модели определена M2M связь, то использование `ModelManager` позволяет передать в это поле список ID объектов.
|
|
357
|
+
|
|
358
|
+
`fastapi-sqlalchemy-toolkit` провалидирует существование этих объектов и установит им M2M связь,
|
|
359
|
+
без необходимости создавать отдельные эндпоинты для работы с M2M связями.
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
# Пусть модели Person и House имеют M2M связь
|
|
363
|
+
from pydantic import BaseModel
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class PersonCreateSchema(BaseModel):
|
|
367
|
+
house_ids: list[int]
|
|
368
|
+
|
|
369
|
+
...
|
|
370
|
+
|
|
371
|
+
in_obj = PersonCreateSchema(house_ids=[1, 2, 3])
|
|
372
|
+
await person_manager.create(session, in_obj)
|
|
373
|
+
# Создаст объект Person и установит ему M2M связь с House с id 1, 2 и 3
|
|
374
|
+
```
|
|
321
375
|
|
|
322
|
-
|
|
376
|
+
### Фильтрация по списку значений
|
|
377
|
+
Один из способов фильтрации по списку значений -- передать этот список в качестве
|
|
378
|
+
квери параметра в строку через запятую.
|
|
379
|
+
`fastapi-sqlalchemy-toolkit` предоставляет утилиту для фильтрации по списку значений, переданного в строку через запятую:
|
|
380
|
+
```python
|
|
381
|
+
from uuid import UUID
|
|
382
|
+
from fastapi_sqlalchemy_toolkit.utils import comma_list_query, get_comma_list_values
|
|
383
|
+
|
|
384
|
+
@router.get("/children")
|
|
385
|
+
async def get_child_objects(
|
|
386
|
+
session: CurrentSession,
|
|
387
|
+
ids: comma_list_query = None,
|
|
388
|
+
) -> list[ChildListSchema]
|
|
389
|
+
ids = get_comma_list_values(ids, UUID)
|
|
390
|
+
return await child_manager.filter(session, id=FieldFilter(ids, operator="in_"))
|
|
391
|
+
```
|
|
@@ -6,10 +6,10 @@ from fastapi_pagination import Page, Params
|
|
|
6
6
|
from fastapi_sqlalchemy_toolkit import FieldFilter, ordering_dep
|
|
7
7
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
8
|
|
|
9
|
-
from app.db_crud import child_db
|
|
10
9
|
from app.deps import get_async_session
|
|
10
|
+
from app.managers import child_manager
|
|
11
11
|
from app.models import Parent
|
|
12
|
-
from app.schemas import CreateUpdateChildSchema, RetrieveChildSchema
|
|
12
|
+
from app.schemas import CreateUpdateChildSchema, HTTPErrorSchema, RetrieveChildSchema
|
|
13
13
|
|
|
14
14
|
router = APIRouter()
|
|
15
15
|
|
|
@@ -29,7 +29,7 @@ async def get_children(
|
|
|
29
29
|
parent_title: str | None = None,
|
|
30
30
|
parent_slug: str | None = None,
|
|
31
31
|
) -> Page[RetrieveChildSchema]:
|
|
32
|
-
return await
|
|
32
|
+
return await child_manager.paginated_filter(
|
|
33
33
|
session=session,
|
|
34
34
|
pagination_params=params,
|
|
35
35
|
title=FieldFilter(value=title, operator="ilike"),
|
|
@@ -40,12 +40,17 @@ async def get_children(
|
|
|
40
40
|
)
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
@router.get(
|
|
43
|
+
@router.get(
|
|
44
|
+
"/{child_id}",
|
|
45
|
+
responses={
|
|
46
|
+
status.HTTP_404_NOT_FOUND: {"model": HTTPErrorSchema},
|
|
47
|
+
},
|
|
48
|
+
)
|
|
44
49
|
async def get_child(
|
|
45
50
|
child_id: UUID,
|
|
46
51
|
session: CurrentSession,
|
|
47
52
|
) -> RetrieveChildSchema:
|
|
48
|
-
return await
|
|
53
|
+
return await child_manager.get_or_404(
|
|
49
54
|
session=session,
|
|
50
55
|
id=child_id,
|
|
51
56
|
)
|
|
@@ -55,21 +60,33 @@ async def get_child(
|
|
|
55
60
|
async def create_child(
|
|
56
61
|
child_in: CreateUpdateChildSchema, session: CurrentSession
|
|
57
62
|
) -> CreateUpdateChildSchema:
|
|
58
|
-
return await
|
|
63
|
+
return await child_manager.create(session=session, in_obj=child_in)
|
|
59
64
|
|
|
60
65
|
|
|
66
|
+
@router.get(
|
|
67
|
+
"/{child_id}",
|
|
68
|
+
responses={
|
|
69
|
+
status.HTTP_404_NOT_FOUND: {"model": HTTPErrorSchema},
|
|
70
|
+
},
|
|
71
|
+
)
|
|
61
72
|
@router.patch("/{child_id}")
|
|
62
73
|
async def update_child(
|
|
63
74
|
child_id: UUID, child_in: CreateUpdateChildSchema, session: CurrentSession
|
|
64
75
|
) -> CreateUpdateChildSchema:
|
|
65
|
-
child_to_update = await
|
|
66
|
-
return await
|
|
76
|
+
child_to_update = await child_manager.get_or_404(session=session, id=child_id)
|
|
77
|
+
return await child_manager.update(
|
|
67
78
|
session=session, db_obj=child_to_update, in_obj=child_in
|
|
68
79
|
)
|
|
69
80
|
|
|
70
81
|
|
|
82
|
+
@router.get(
|
|
83
|
+
"/{child_id}",
|
|
84
|
+
responses={
|
|
85
|
+
status.HTTP_404_NOT_FOUND: {"model": HTTPErrorSchema},
|
|
86
|
+
},
|
|
87
|
+
)
|
|
71
88
|
@router.delete("/{child_id}")
|
|
72
89
|
async def delete_child(child_id: UUID, session: CurrentSession) -> Response:
|
|
73
|
-
child_to_delete = await
|
|
74
|
-
await
|
|
90
|
+
child_to_delete = await child_manager.get_or_404(session=session, id=child_id)
|
|
91
|
+
await child_manager.delete(session=session, db_obj=child_to_delete)
|
|
75
92
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
{fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/examples/app/config.py
RENAMED
|
@@ -3,7 +3,6 @@ from pathlib import Path
|
|
|
3
3
|
from pydantic import FieldValidationInfo, PostgresDsn, field_validator
|
|
4
4
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
5
5
|
|
|
6
|
-
|
|
7
6
|
ROOT_DIR = Path(__file__).resolve(strict=True).parent
|
|
8
7
|
|
|
9
8
|
|
|
@@ -15,10 +14,11 @@ class Settings(BaseSettings):
|
|
|
15
14
|
POSTGRES_USER: str
|
|
16
15
|
POSTGRES_PASSWORD: str
|
|
17
16
|
POSTGRES_HOST: str
|
|
18
|
-
POSTGRES_PORT:
|
|
17
|
+
POSTGRES_PORT: int
|
|
19
18
|
POSTGRES_DB: str
|
|
20
|
-
SQLALCHEMY_DATABASE_URL:
|
|
19
|
+
SQLALCHEMY_DATABASE_URL: str | None = None
|
|
21
20
|
|
|
21
|
+
SENTRY_DSN: str | None = None
|
|
22
22
|
|
|
23
23
|
@field_validator("SQLALCHEMY_DATABASE_URL", mode="before")
|
|
24
24
|
def assemble_db_connection_string(
|
|
@@ -26,13 +26,15 @@ class Settings(BaseSettings):
|
|
|
26
26
|
) -> str | PostgresDsn:
|
|
27
27
|
if isinstance(value, str):
|
|
28
28
|
return value
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
return str(
|
|
30
|
+
PostgresDsn.build(
|
|
31
|
+
scheme="postgresql+asyncpg",
|
|
32
|
+
username=info.data["POSTGRES_USER"],
|
|
33
|
+
password=info.data["POSTGRES_PASSWORD"],
|
|
34
|
+
host=info.data["POSTGRES_HOST"],
|
|
35
|
+
port=info.data["POSTGRES_PORT"],
|
|
36
|
+
path=info.data["POSTGRES_DB"],
|
|
37
|
+
)
|
|
36
38
|
)
|
|
37
39
|
|
|
38
40
|
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
|
2
2
|
from sqlalchemy.orm import sessionmaker
|
|
3
3
|
|
|
4
|
-
from
|
|
4
|
+
from .config import settings
|
|
5
5
|
|
|
6
|
-
engine = create_async_engine(
|
|
7
|
-
str(settings.SQLALCHEMY_DATABASE_URL), future=True, echo=False
|
|
8
|
-
)
|
|
6
|
+
engine = create_async_engine(settings.SQLALCHEMY_DATABASE_URL, future=True, echo=False)
|
|
9
7
|
|
|
10
8
|
async_session_factory = sessionmaker(
|
|
11
9
|
engine, class_=AsyncSession, expire_on_commit=False
|
{fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/examples/app/models.py
RENAMED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from uuid import UUID
|
|
2
2
|
|
|
3
|
-
from fastapi_sqlalchemy_toolkit import Base
|
|
4
3
|
from sqlalchemy import ForeignKey
|
|
5
4
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
6
5
|
|
|
6
|
+
from fastapi_sqlalchemy_toolkit import Base
|
|
7
|
+
|
|
7
8
|
|
|
8
9
|
class Parent(Base):
|
|
9
10
|
title: Mapped[str]
|
|
@@ -30,9 +30,9 @@ class FieldFilter:
|
|
|
30
30
|
:param operator: Атрибут/метод модели, с помощью которого нужно фильтровать
|
|
31
31
|
:param func: Функция SQLAlchemy, которую нужно применить к полю
|
|
32
32
|
:param model: Модель SQLAlchemy, к которой применяется фильтр
|
|
33
|
-
По умолчанию, модель из
|
|
33
|
+
По умолчанию, модель из ModelManager, в котором вызывается метод.
|
|
34
34
|
:param alias: Название поля модели
|
|
35
|
-
(по умолчанию название параметра, по которому фильтр передаётся в
|
|
35
|
+
(по умолчанию название параметра, по которому фильтр передаётся в ModelManager)
|
|
36
36
|
"""
|
|
37
37
|
self.value = value
|
|
38
38
|
self.operator = operator
|
|
@@ -22,7 +22,7 @@ UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
|
|
|
22
22
|
ModelDict = dict[str, Any]
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
class
|
|
25
|
+
class ModelManager(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
|
26
26
|
def __init__(
|
|
27
27
|
self,
|
|
28
28
|
model: Type[ModelType],
|
|
@@ -30,7 +30,7 @@ class BaseCRUD(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
|
|
30
30
|
default_ordering: InstrumentedAttribute | None = None,
|
|
31
31
|
) -> None:
|
|
32
32
|
"""
|
|
33
|
-
Создание экземпляра
|
|
33
|
+
Создание экземпляра ModelManager под конкретную модель.
|
|
34
34
|
|
|
35
35
|
:param model: модель SQLAlchemy
|
|
36
36
|
|
|
@@ -38,7 +38,7 @@ class BaseCRUD(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
|
|
38
38
|
которые являются внешними ключами, и модели SQLAlchemy,
|
|
39
39
|
на которые ключи ссылаются.
|
|
40
40
|
Этот параметр нужно определить для валидации внешних ключей.
|
|
41
|
-
Например, если
|
|
41
|
+
Например, если ModelManager создаётся для модели Child, которая имеет
|
|
42
42
|
внешний ключ parent_id на модель Parent, то нужно передать
|
|
43
43
|
fk_mapping={"parent_id": Parent}
|
|
44
44
|
|
|
@@ -162,10 +162,11 @@ class BaseCRUD(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
|
|
162
162
|
"""
|
|
163
163
|
|
|
164
164
|
db_obj = await self.get(session=session, **attrs)
|
|
165
|
+
attrs_str = ", ".join([f"{key}={value}" for key, value in attrs.items()])
|
|
165
166
|
if not db_obj:
|
|
166
167
|
raise HTTPException(
|
|
167
168
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
168
|
-
detail=f"{self.model.__tablename__
|
|
169
|
+
detail=f"{self.model.__tablename__} with {attrs_str} not found",
|
|
169
170
|
)
|
|
170
171
|
return db_obj
|
|
171
172
|
|
|
@@ -507,9 +508,9 @@ class BaseCRUD(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
|
|
507
508
|
)
|
|
508
509
|
if not related_object_exists:
|
|
509
510
|
raise HTTPException(
|
|
510
|
-
status_code=status.
|
|
511
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
511
512
|
detail=(
|
|
512
|
-
f"{self.fk_mapping[key].__tablename__} с
|
|
513
|
+
f"{self.fk_mapping[key].__tablename__} с id "
|
|
513
514
|
f"{in_obj[key]} не существует."
|
|
514
515
|
),
|
|
515
516
|
)
|
|
@@ -528,7 +529,7 @@ class BaseCRUD(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
|
|
528
529
|
if object_exists:
|
|
529
530
|
conflicting_fields = ", ".join(unique_constraint)
|
|
530
531
|
raise HTTPException(
|
|
531
|
-
status_code=status.
|
|
532
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
532
533
|
detail=(
|
|
533
534
|
f"{self.model.__tablename__} с такими "
|
|
534
535
|
+ conflicting_fields
|
|
@@ -553,7 +554,7 @@ class BaseCRUD(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
|
|
553
554
|
check_unique = await self.exists(session=session, **attrs_to_check)
|
|
554
555
|
if check_unique:
|
|
555
556
|
raise HTTPException(
|
|
556
|
-
status_code=status.
|
|
557
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
557
558
|
detail=(
|
|
558
559
|
f"{self.model.__tablename__} c {column.name} "
|
|
559
560
|
f"{in_obj[column.name]} уже существует"
|
|
@@ -569,9 +570,9 @@ class BaseCRUD(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
|
|
569
570
|
related_object = await session.get(related_model, related_object_id)
|
|
570
571
|
if not related_object:
|
|
571
572
|
raise HTTPException(
|
|
572
|
-
status_code=status.
|
|
573
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
573
574
|
detail=(
|
|
574
|
-
f"{related_model.__tablename__} с
|
|
575
|
+
f"{related_model.__tablename__} с id "
|
|
575
576
|
f"{related_object_id} не существует."
|
|
576
577
|
),
|
|
577
578
|
)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
from typing import Any, Optional, Type
|
|
1
|
+
from typing import Annotated, Any, Optional, Type
|
|
2
2
|
|
|
3
3
|
import pydantic
|
|
4
4
|
from fastapi import Query
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
class AllOptional(pydantic.
|
|
7
|
+
class AllOptional(pydantic._internal._model_construction.ModelMetaclass):
|
|
8
8
|
"""
|
|
9
9
|
Метакласс, который делает все поля модели Pydantic необязательными.
|
|
10
10
|
Полезно для схем PATCH запросов.
|
|
@@ -33,13 +33,16 @@ class AllOptional(pydantic.main.ModelMetaclass):
|
|
|
33
33
|
|
|
34
34
|
# Утилиты для передачи нескольких значений для фильтрации в одном
|
|
35
35
|
# квери параметре через запятую
|
|
36
|
-
comma_list_query =
|
|
37
|
-
description="Несколько значений можно передать через запятую"
|
|
38
|
-
|
|
39
|
-
)
|
|
36
|
+
comma_list_query = Annotated[
|
|
37
|
+
str | None, Query(description="Несколько значений можно передать через запятую")
|
|
38
|
+
]
|
|
40
39
|
|
|
41
40
|
|
|
42
41
|
def get_comma_list_values(query: str | None, type_: Type) -> list | None:
|
|
42
|
+
"""
|
|
43
|
+
:param query: Значение квери параметра
|
|
44
|
+
:param type_: Тип значений в списке
|
|
45
|
+
"""
|
|
43
46
|
if query:
|
|
44
47
|
return [type_(query_value) for query_value in query.split(",")]
|
|
45
48
|
return None
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fastapi_sqlalchemy_toolkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.1"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Egor Kondrashov", email="e.kondr01@gmail.com" },
|
|
10
10
|
]
|
|
@@ -20,7 +20,7 @@ dependencies = [
|
|
|
20
20
|
"sqlalchemy>=2.0.0",
|
|
21
21
|
"fastapi_pagination>=0.12.6",
|
|
22
22
|
"pydantic>=2.0.0",
|
|
23
|
-
"dateutil>=2.8.2"
|
|
23
|
+
"python-dateutil>=2.8.2"
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
[project.urls]
|
|
File without changes
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
from fastapi_sqlalchemy_toolkit import BaseCRUD
|
|
2
|
-
|
|
3
|
-
from app.models import Child, Parent
|
|
4
|
-
from app.schemas import CreateUpdateChildSchema, ParentBaseSchema
|
|
5
|
-
|
|
6
|
-
child_db = BaseCRUD[Child, CreateUpdateChildSchema, CreateUpdateChildSchema](Child)
|
|
7
|
-
parent_db = BaseCRUD[Parent, ParentBaseSchema, ParentBaseSchema](Parent)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/examples/app/__init__.py
RENAMED
|
File without changes
|
{fastapi_sqlalchemy_toolkit-0.0.2.2 → fastapi_sqlalchemy_toolkit-0.1}/examples/app/api/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|