Skip to content

Commit ff3b395

Browse files
фильтрация, пагинация, сортировка
1 parent 705233f commit ff3b395

File tree

10 files changed

+231
-9
lines changed

10 files changed

+231
-9
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- [Пермишены](./docs/Пермишены.md)
1717
- [Аутентификация](./docs/Аутентификация.md)
1818
- [Валидация входящих данных](./docs/Валидация%20входящих%20данных.md)
19+
- [Фильтрация, пагинация, сортировка.md](docs/%D0%A4%D0%B8%D0%BB%D1%8C%D1%82%D1%80%D0%B0%D1%86%D0%B8%D1%8F%2C%20%D0%BF%D0%B0%D0%B3%D0%B8%D0%BD%D0%B0%D1%86%D0%B8%D1%8F%2C%20%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0.md)
1920
- [TODO](#todo)
2021

2122
## TODO
1.03 MB
Loading
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Фильтрация, пагинация, сортировка
2+
3+
Предлагается объединить в одном сервисе возможности для пагинации, фильтрации, сортировки.
4+
5+
Это представляет собой класс, который принимает объекты запроса (Request), а также экземпляры объектов фильтрации,
6+
сортировки, пагинации, которые предоставляют метод `Pagination.paginate_queryset()`, `FilterSet.filter_queryset()` и
7+
`Ordering.order_queryset()` соответственно.
8+
9+
Базовые классы сервиса и фильтрации, пагинации, сортировки:
10+
11+
```python
12+
from typing import Any
13+
14+
from fastapi import Query, Request
15+
from pydantic import BaseModel, Field
16+
17+
from fastapi_django.db.repositories.queryset import QuerySet
18+
19+
20+
class Ordering(BaseModel):
21+
ordering: list[str] = Field(Query(default_factory=list))
22+
23+
def order_queryset(self, queryset: QuerySet) -> QuerySet:
24+
return queryset.order_by(*self.ordering)
25+
26+
27+
class Pagination(BaseModel):
28+
29+
async def paginate_queryset(self, queryset: QuerySet) -> Any:
30+
raise NotImplementedError
31+
32+
33+
class LimitOffsetPagination(Pagination):
34+
limit: int = Query(10, gt=0, le=100)
35+
offset: int = Query(0, ge=0)
36+
37+
async def paginate_queryset(self, queryset: QuerySet) -> Any:
38+
# пример логики пагинации
39+
# пагинация знает, какие поля используются при пагинации
40+
count = await queryset.count()
41+
data = await queryset[self.offset:self.offset + self.limit]
42+
return {"count": count, "results": data}
43+
44+
45+
class FilterSet(BaseModel):
46+
47+
def filter_queryset(self, queryset: QuerySet) -> QuerySet:
48+
conditions = self.model_dump(exclude_unset=True, exclude_none=True)
49+
return queryset.filter(**conditions)
50+
51+
52+
class ListService:
53+
54+
def __init__(self, request: Request | None = None, filterset=None, ordering=None, pagination=None):
55+
self._request = request
56+
self._filterset = filterset
57+
self._ordering = ordering
58+
self._pagination = pagination
59+
60+
async def list(self, *args, **kwargs) -> Any:
61+
queryset = self.get_queryset()
62+
if self._filterset is not None:
63+
queryset = self._filterset.filter_queryset(queryset)
64+
if self._ordering is not None:
65+
queryset = self._ordering.order_queryset(queryset)
66+
if self._pagination:
67+
data = await self._pagination.paginate_queryset(queryset)
68+
else:
69+
data = await queryset
70+
return data
71+
72+
def get_queryset(self, *args: Any, **kwargs: Any) -> QuerySet:
73+
raise NotImplementedError
74+
75+
@classmethod
76+
def init(cls, *args: Any, **kw: Any) -> Any:
77+
return cls(*args, **kw)
78+
```
79+
80+
Обратите внимание, что классы FilterSet, Pagination, Ordering являются наследниками pydantic.BaseModel. Это необходимо
81+
для валидации и отображения в свагере.
82+
83+
Реализация:
84+
85+
```python
86+
class UsersFilterSet(FilterSet):
87+
# TODO: не смог сделать вложенные фильтры
88+
name__ilike: str | None = Query(None)
89+
role_id: int | None = Query(None)
90+
role__code__in: list[str] | None = Field(Query(None, alias="role__code")) # нужно именно прописывать Field(Query(...)), чтобы корректно отображалось в сваггере
91+
92+
93+
class UsersOrdering(Ordering):
94+
ordering: list[str] = Field(Query(["id"])) # нужно именно прописывать Field(Query(...)), чтобы корректно отображалось в сваггере
95+
96+
97+
class UsersListService(ListService):
98+
# реализация сервиса, который возвращает список пользователей
99+
# предоставляет возможности для фильтрации, пагинации, сортировки
100+
101+
def __init__(self, users: UsersRepository, **kw: Any) -> None:
102+
super().__init__(**kw)
103+
self._users = users
104+
105+
def get_queryset(self) -> QuerySet:
106+
return self._users.objects.all()
107+
108+
@classmethod
109+
def init(
110+
cls,
111+
request: Request,
112+
users: UsersRepository = Depends(),
113+
filterset: UsersFilterSet = Depends(),
114+
ordering: UsersOrdering = Depends(),
115+
pagination: LimitOffsetPagination = Depends(),
116+
) -> Self:
117+
# метод, который использует fastapi для инициализации сервиса
118+
return cls(request=request, users=users, filterset=filterset, ordering=ordering, pagination=pagination)
119+
```
120+
121+
Метод UsersListService.init позволяет прописать все dependency таким образом, чтобы они корректно оторажались в сваггере.
122+
123+
Наконец вьюха:
124+
125+
```python
126+
@router.get(
127+
"/filtering-ordering-paginate-users",
128+
dependencies=[Depends(contextify_autocommit_session())]
129+
)
130+
async def get_users(service: UsersListService = Depends(UsersListService.init)):
131+
return await service.list()
132+
```
133+
134+
Сваггер:
135+
136+
![filtering-ordering-pagination.png](assets/images/filtering-ordering-pagination.png)
137+
138+
Пример реализации по ссылке https://github.com/albertalexandrov/fastapi-django-example/blob/main/src/web/api/crud/views.py#L60

fastapi_django/app.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from pathlib import Path
33

44
import pkg_resources
5-
from fastapi import APIRouter, FastAPI, Depends
5+
from fastapi import APIRouter, FastAPI
66
from starlette.staticfiles import StaticFiles
77

88
from fastapi_django.conf import settings
@@ -15,14 +15,12 @@
1515

1616

1717
def include_docs_router(app: FastAPI, router: APIRouter) -> None:
18-
print("===> settings.API_DOCS_ENABLED", settings.API_DOCS_ENABLED)
1918
if settings.API_DOCS_ENABLED:
2019
app.mount(f"{settings.API_PREFIX}/static", StaticFiles(directory=APP_ROOT / "static"), name="static")
2120
router.include_router(docs_router)
2221

2322

2423
def setup_prometheus(app: FastAPI) -> None:
25-
print("===> settings.PROMETHEUS_ENABLED", settings.PROMETHEUS_ENABLED)
2624
if settings.PROMETHEUS_ENABLED and "prometheus-fastapi-instrumentator" in installed_packages_list:
2725
from prometheus_fastapi_instrumentator import PrometheusFastApiInstrumentator
2826

@@ -44,7 +42,6 @@ def include_routers(app: FastAPI) -> None:
4442

4543
def setup_middlewares(app: FastAPI) -> None:
4644
for middleware in settings.MIDDLEWARES:
47-
print("===> middleware", middleware)
4845
app.add_middleware(middleware)
4946
# TODO: продумать:
5047
# преднастроенные миддлварь, которые задаются в строковом формате и

fastapi_django/db/services/__init__.py

Whitespace-only changes.

fastapi_django/db/services/list.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from typing import Any
2+
3+
from fastapi import Query, Request
4+
from pydantic import BaseModel, Field
5+
6+
from fastapi_django.db.repositories.queryset import QuerySet
7+
8+
9+
class Ordering(BaseModel):
10+
ordering: list[str] = Field(Query(default_factory=list)) # TODO: а если нужно другое именование поля?
11+
12+
def order_queryset(self, queryset: QuerySet) -> QuerySet:
13+
return queryset.order_by(*self.ordering)
14+
15+
16+
class Pagination(BaseModel):
17+
18+
async def paginate_queryset(self, queryset: QuerySet) -> Any:
19+
raise NotImplementedError
20+
21+
22+
class LimitOffsetPagination(Pagination):
23+
limit: int = Query(10, gt=0, le=100)
24+
offset: int = Query(0, ge=0)
25+
26+
async def paginate_queryset(self, queryset: QuerySet) -> Any:
27+
# пример логики пагинации
28+
# пагинация знает, какие поля используются при пагинации
29+
count = await queryset.count()
30+
data = await queryset[self.offset:self.offset + self.limit]
31+
return {"count": count, "results": data}
32+
33+
34+
class FilterSet(BaseModel):
35+
36+
def filter_queryset(self, queryset: QuerySet) -> QuerySet:
37+
conditions = self.model_dump(exclude_unset=True, exclude_none=True)
38+
return queryset.filter(**conditions)
39+
40+
41+
class ListService:
42+
# базовый класс для сервиса, возвращающего список объектов
43+
# предоставляет возможности для фильтрации, пагинации, сортировки
44+
45+
def __init__(self, request: Request | None = None, filterset=None, ordering=None, pagination=None):
46+
self._request = request
47+
self._filterset = filterset
48+
self._ordering = ordering
49+
self._pagination = pagination
50+
51+
async def list(self, *args, **kwargs) -> Any:
52+
queryset = self.get_queryset()
53+
if self._filterset is not None:
54+
queryset = self._filterset.filter_queryset(queryset)
55+
if self._ordering is not None:
56+
queryset = self._ordering.order_queryset(queryset)
57+
if self._pagination:
58+
data = await self._pagination.paginate_queryset(queryset)
59+
else:
60+
data = await queryset
61+
return data
62+
63+
def get_queryset(self, *args: Any, **kwargs: Any) -> QuerySet:
64+
raise NotImplementedError
65+
66+
@classmethod
67+
def init(cls, *args: Any, **kw: Any) -> Any:
68+
# метод инициалиции сервиса. для использования в Depends
69+
# предполагается, что каждый класс определяет, какие именно возможности будут использованы - фильтрация,
70+
# сортировка, пагинация. например:
71+
# @classmethod
72+
# def init(
73+
# cls,
74+
# request: Request,
75+
# filterset: UsersFilterSet = Depends(),
76+
# ordering: UsersOrdering = Depends(),
77+
# ) -> Self:
78+
# # метод, который использует fastapi для инициализации сервиса
79+
# return cls(request=request, users=users, filterset=filterset, ordering=ordering, pagination=pagination)
80+
# из примера видно, что пагинация не была определена, сделаловательно, в качестве результата можно ожидать
81+
# НЕпагинированный список объекто
82+
return cls(*args, **kw)

fastapi_django/exceptions/http.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,8 @@ def __init__(self, detail: str = "Forbidden", headers: dict | None = None) -> No
1515
class HTTP404Exception(HTTPException):
1616
def __init__(self, detail: str = "Not found", headers: dict | None = None) -> None:
1717
super().__init__(status.HTTP_404_NOT_FOUND, detail=detail, headers=headers)
18+
19+
20+
class HTTP400Exception(HTTPException):
21+
def __init__(self, detail: str = "Bad request", headers: dict | None = None) -> None:
22+
super().__init__(status.HTTP_400_BAD_REQUEST, detail=detail, headers=headers)

fastapi_django/static/docs/swagger-ui-bundle.js

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fastapi_django/static/docs/swagger-ui.css

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ check_untyped_defs = true
77

88
[tool.poetry]
99
name = "fastapi-django"
10-
version = "0.7.0"
10+
version = "0.7.19"
1111
description = ""
1212
authors = ["albertalexandrov <albert-ugatu@list.ru>"]
1313
packages = [{include = "fastapi_django"}]

0 commit comments

Comments
 (0)