Skip to content

Commit

Permalink
doc: ref docs
Browse files Browse the repository at this point in the history
  • Loading branch information
e-kondr01 committed Feb 9, 2024
1 parent 2fa95d4 commit 75cf180
Show file tree
Hide file tree
Showing 19 changed files with 297 additions and 192 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ test:
- docker run --name toolkit-test --network toolkit-test test_toolkit:latest
docker rm toolkit-test
docker compose -f tests/docker-compose.yml down

docs_publish:
mkdocs build
mkdocs gh-deploy
64 changes: 64 additions & 0 deletions docs/benefits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
### Benefits
An optional section demonstrating the reduction of boilerplate code when using `fastapi_sqlalchemy_toolkit`.

If you need to add filters based on field values in a `FastAPI` endpoint, the code would look something like this:

```python
from typing import Annotated
from uuid import UUID

from fastapi import APIRouter, Depends, Response, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.deps import get_async_session
from app.models import MyModel, MyParentModel
from app.schemas import MyObjectListSchema

router = APIRouter()
Session = Annotated[AsyncSession, Depends(get_async_session)]


@router.get("/my-objects")
async def get_my_objects(
session: Session,
user_id: UUID | None = None,
name: str | None = None,
parent_name: str | None = None,
) -> list[MyObjectListSchema]:
stmt = select(MyModel)
if user_id is not None:
stmt = stmt.filter_by(user_id=user_id)
if name is not None:
stmt = stmt.filter(MyModel.name.ilike == f"%{name}%")
if parent_name is not None:
stmt = stmt.join(MyModel.parent)
stmt = stmt.filter(ParentModel.name.ilike == f"%{parent_name}%")
result = await session.execute(stmt)
return result.scalars().all()
```
As you can see, implementing filtering requires duplicating template code.

With `fastapi-sqlalchemy-toolkit`, this endpoint looks like this:

```python
from fastapi_sqlalchemy_toolkit import FieldFilter

from app.managers import my_object_manager

@router.get("/my-objects")
async def get_my_objects(
session: Session,
user_id: UUID | None = None,
name: str | None = None,
parent_name: str | None = None,
) -> list[MyObjectListSchema]:
return await my_object_manager.list(
session,
user_id=user_id,
filter_expressions={
MyObject.name: name,
MyObjectParent.name: parent_name
}
)
```
1 change: 1 addition & 0 deletions docs/db_validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Database validation
65 changes: 0 additions & 65 deletions docs/filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,68 +248,3 @@ Also, there is support for filtering by reverse relationships
await parent_manager.list(session, children=[1, 2])
# Returns Parent objects that have a relationship with ChildModel with ids 1 or 2
```

### Prerequisites
An optional section demonstrating the reduction of boilerplate code when using `fastapi_sqlalchemy_toolkit`.

If you need to add filters based on field values in a `FastAPI` endpoint, the code would look something like this:

```python
from typing import Annotated
from uuid import UUID

from fastapi import APIRouter, Depends, Response, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.deps import get_async_session
from app.models import MyModel, MyParentModel
from app.schemas import MyObjectListSchema

router = APIRouter()
Session = Annotated[AsyncSession, Depends(get_async_session)]


@router.get("/my-objects")
async def get_my_objects(
session: Session,
user_id: UUID | None = None,
name: str | None = None,
parent_name: str | None = None,
) -> list[MyObjectListSchema]:
stmt = select(MyModel)
if user_id is not None:
stmt = stmt.filter_by(user_id=user_id)
if name is not None:
stmt = stmt.filter(MyModel.name.ilike == f"%{name}%")
if parent_name is not None:
stmt = stmt.join(MyModel.parent)
stmt = stmt.filter(ParentModel.name.ilike == f"%{parent_name}%")
result = await session.execute(stmt)
return result.scalars().all()
```
As you can see, implementing filtering requires duplicating template code.

With `fastapi-sqlalchemy-toolkit`, this endpoint looks like this:

```python
from fastapi_sqlalchemy_toolkit import FieldFilter

from app.managers import my_object_manager

@router.get("/my-objects")
async def get_my_objects(
session: Session,
user_id: UUID | None = None,
name: str | None = None,
parent_name: str | None = None,
) -> list[MyObjectListSchema]:
return await my_object_manager.list(
session,
user_id=user_id,
filter_expressions={
MyObject.name: name,
MyObjectParent.name: parent_name
}
)
```
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ Example of `fastapi-sqlalchemy-toolkit` usage is available in the `examples/app`


## Read More
- [ModelManager](./model_manager.md)
- [ModelManager](./usage.md)
- [Filtering](./filtering.md)
- [Sorting](./sorting.md)
- [Extension](./extension.md)
- [Other useful features](./features.md)
- [Other utilities](./utils.md)
64 changes: 64 additions & 0 deletions docs/ru/benefits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Предпосылки
Необязательный раздел с демо сокращения количества шаблонного кода при использовании `fastapi_sqlalchemy_toolkit`.

Если в эндпоинт `FastAPI` нужно добавить фильтры по значениям полей, то код будет выглядеть примерно так:

```python
from typing import Annotated
from uuid import UUID

from fastapi import APIRouter, Depends, Response, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.deps import get_async_session
from app.models import MyModel, MyParentModel
from app.schemas import MyObjectListSchema

router = APIRouter()
Session = Annotated[AsyncSession, Depends(get_async_session)]


@router.get("/my-objects")
async def get_my_objects(
session: Session,
user_id: UUID | None = None,
name: str | None = None,
parent_name: str | None = None,
) -> list[MyObjectListSchema]:
stmt = select(MyModel)
if user_id is not None:
stmt = stmt.filter_by(user_id=user_id)
if name is not None:
stmt = stmt.filter(MyModel.name.ilike == f"%{name}%")
if parent_name is not None:
stmt = stmt.join(MyModel.parent)
stmt = stmt.filter(ParentModel.name.ilike == f"%{parent_name}%")
result = await session.execute(stmt)
return result.scalars().all()
```
Как можно заметить, для реализации фильтрации необходима дубликация шаблонного кода.

В `fastapi-sqlalchemy-toolkit` этот эндпоинт выглядит так:

```python
from fastapi_sqlalchemy_toolkit import FieldFilter

from app.managers import my_object_manager

@router.get("/my-objects")
async def get_my_objects(
session: Session,
user_id: UUID | None = None,
name: str | None = None,
parent_name: str | None = None,
) -> list[MyObjectListSchema]:
return await my_object_manager.list(
session,
user_id=user_id,
filter_expressions={
MyObject.name: name,
MyObjectParent.name: parent_name
}
)
```
1 change: 1 addition & 0 deletions docs/ru/db_validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Валидация на уровне базы данных
20 changes: 9 additions & 11 deletions docs/ru/extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ from fastapi_sqlalchemy_toolkit import ModelManager
class MyModelManager[MyModel, MyModelCreateSchema, MyModelUpdateSchema](ModelManager):
...
```
### Дополнительная валидация
Дополнительную валидацию можно добавить, переопределив метод `validate`:
## Дополнительная валидация
Дополнительную валидацию можно добавить, переопределив метод `run_db_valiation`:

```python
class MyModelManager[MyModel, MyModelCreateSchema, MyModelUpdateSchema](ModelManager):
Expand All @@ -39,7 +39,7 @@ class MyModelManager[MyModel, MyModelCreateSchema, MyModelUpdateSchema](ModelMan
return validated_data
```

### Дополнительная бизнес логика при CRUD операциях
## Дополнительная бизнес логика при CRUD операциях
Если при CRUD операциях с моделью необходимо выполнить какую-то дополнительную бизнес логику,
это можно сделать, переопределив соответствующие методы ModelManager:

Expand All @@ -53,29 +53,27 @@ class MyModelManager[MyModel, MyModelCreateSchema, MyModelUpdateSchema](ModelMan
return created
```

Такой подход соответствует принципу "Fat Models, Skinny Views" из Django.

### Использование декларативных фильтров в нестандартных списочных запросах
## Использование декларативных фильтров в нестандартных списочных запросах
Если необходимо получить не просто список объектов, но и какие-то другие поля (допустим, кол-во дочерних объектов)
или агрегации, но также необходима декларативная фильтрация, то можно новый свой метод менеджера,
вызвав в нём метод `super().get_filter_expression`:
или агрегации, но также необходима декларативная фильтрация, то можно определить метод менеджера,
вызвав в нём метод `assemble_stmt`:
```python
class MyModelManager[MyModel, MyModelCreateSchema, MyModelUpdateSchema](MyModel):
async def get_parents_with_children_count(
self, session: AsyncSession, **kwargs
) -> list[RetrieveParentWithChildrenCountSchema]:
children_count_query = (
children_count_stmt = (
select(func.count(Child.id))
.filter(Child.parent_id == Parent.id)
.scalar_subquery()
)
query = (
stmt = (
select(Parent, children_count_query.label("children_count"))
)

# Вызываем метод для получения фильтров SQLAlchemy из аргументов методов
# list и paginated_list
query = query.filter(self.get_filter_expression(**kwargs))
stmt = self.assemble_stmt(stmt, **kwargs)

result = await session.execute(query)
result = result.unique().all()
Expand Down
Loading

0 comments on commit 75cf180

Please sign in to comment.