fastapi-accelerator


Namefastapi-accelerator JSON
Version 0.0.1 PyPI version JSON
download
home_pagehttps://github.com/denisxab/fastapi-accelerator
SummaryNone
upload_time2024-09-07 21:55:33
maintainerNone
docs_urlNone
authorDenis Kustov
requires_python>=3.9
licenseMIT
keywords fastapi django
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Common бизнес логика FastAPI

## Зачем нужен, какие проблемы решает ?

Основная цель - ускорить и упростить разработку проектов на FastAPI. Это достигается путем:

1. Предоставления переиспользуемого кода для типовых задач.
2. Внедрения универсального менеджера для работы с РСУБД.
3. Реализации ViewSet для быстрого создания представлений с базовой бизнес-логикой.
4. Интеграции аутентификации по JWT.
5. Добавления удобной админ-панели.
6. Упрощения написания и выполнения интеграционных тестов для API.
7. Оптимизации работы с Alembic для управления миграциями в production и test окружениях.

## Описание файлов в составе common

```bash
common/
│
├── db                  # Логика взаимодействия с РСУБД
│   ├── __init__.py
│   ├── dborm.py
│   └── dbsession.py
│
├── pattern                     # Шаблоны для проектов
│   ├── __init__.py
│   ├── pattern_fastapi.py      # Шаблоны для создания проекта на FastAPI
│   ├── pattern_alembic.py      # Шаблоны для создания Alembic
│   └── pattern_flask_admin.py  # Шаблоны для создания проекта Flask админ панели
│
├── testutils                   # Утилиты для тестирования FastAPI
│   ├── __init__.py
│   ├── fixture_base.py         # Основная фикстура для тестов
│   ├── fixture_db              # Фикстуры для работы с тестовой БД
│   │   ├── __init__.py
│   │   ├── apply_fixture.py
│   │   ├── db.py
│   │   └── trace_sql.py
│   ├── fixture_auth.py         # Фикстура для аутентификации клиента по JWT
│   └── utils.py
│
├── cache.py         # Реализация кеширования
├── auth_jwt.py      # Аутентификация по JWT
├── exception.py     # Обработка исключений
├── middleware.py    # Middleware компоненты
├── paginator.py     # Реализация пагинации
├── timezone.py      # Работа с временными зонами
├── viewset.py       # Реализация ViewSet
├── utils.py         # Общие утилиты
├── README.md        # Документация
└── __init__.py
```

## Примеры файлов

### Пример `main.py`

```python
from contextlib import asynccontextmanager

import uvicorn
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse

import app.api.v1.router as RouterV1
from app.core.config import BASE_DIR_PROJECT, DEBUG, SECRET_KEY
from app.core.db import DatabaseManager
from app.core.security import AuthJWT
from fastapi_accelerator.pattern_fastapi import base_pattern
from fastapi_accelerator.timezone import moscow_tz


@asynccontextmanager
async def lifespan(app):
    """Жизненный цикл проекта"""
    yield


app = FastAPI(
    title="File ddos API",
    # Функция lifespan
    lifespan=lifespan,
    # Зависимости, которые будут применяться ко всем маршрутам в этом маршрутизаторе.
    dependencies=None,
    # Класс ответа по умолчанию для всех маршрутов.
    default_response_class=ORJSONResponse,
)


# Паттерн для проекта
base_pattern(
    app,
    routers=(RouterV1.router,),
    timezone=moscow_tz,
    cache_status=True,
    debug=DEBUG,
    base_dir=BASE_DIR_PROJECT,
    database_manager=DatabaseManager,
    secret_key=SECRET_KEY,
)

# Подключить аутентификацию по JWT
AuthJWT.mount_auth(app)

if __name__ == "__main__":
    uvicorn.run(
        "main:app",
        host="0.0.0.0",
        port=8000,
        workers=4,
        reload=DEBUG,
        access_log=DEBUG,
    )
```

Запускать `python main.py`

### Пример `Makefile`

```makefile
run_test:
	pytest

run_dev_server:
	python -m main

# Создать миграцию
makemigrate:
	alembic revision --autogenerate

# Применить миграции
migrate:
	alembic upgrade head
```

### Пример `config.py`

Файл `app/core/config.py`:

```python
"""
Глобальные настройки на проект.

Может содержать дополнительно преобразование значений настроек из `.settings_local`
"""

from pathlib import Path

from .settings_local import (
    ADMIN_PASSWORD,
    ADMIN_USERNAME,
    CACHE_STATUS,
    DATABASE_URL,
    DEBUG,
    DEV_STATUS,
    REDIS_URL,
    SECRET_KEY,
    TEST_DATABASE_URL,
)

# Путь к приложению проекта
BASE_DIR_APP = Path(__file__).parent.parent
# Путь к корню проекта
BASE_DIR_PROJECT = BASE_DIR_APP.parent

__all__ = (
    # >>> Подключения к внешним системам:
    # Url для подключения к БД
    DATABASE_URL,
    # Url для подключения к тестовой БД
    TEST_DATABASE_URL,
    # Url для подключения к Redis
    REDIS_URL,
    # >>> Статусы:
    # Режим отладки, может быть включен на тестовом сервере
    DEBUG,
    # Режим разработки, включать только в локальной разработки
    DEV_STATUS,
    # Вкл/Выкл кеширование
    CACHE_STATUS,
    # >>> Безопасность:
    SECRET_KEY,
    # Данные для входа в Flask-Admin панель
    ADMIN_USERNAME,
    ADMIN_PASSWORD,
)
```

Файл `app/core/settings_local.py`:

```python
import os

DATABASE_URL = os.getenv("DATABASE_URL", "postgres://user_app:db@postgres_db:5432/db")
TEST_DATABASE_URL = "postgres://user_app:db@postgres_db:5432/testdb"

REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379")
SECRET_KEY = "your_secret_key_here"
DEBUG = True
DEV_STATUS = False
CACHE_STATUS = True
ADMIN_USERNAME = "admin"
ADMIN_PASSWORD = "password"
```

### Пример файловой структуры

```bash
Проект/
│
├── app/
│   ├── __init__.py
│   ├── utils.py        # Пере используемый функционал для проекта
│   ├── core/           # Содержит основные модули, такие как конфигурация, безопасность и общие зависимости.
│   │   ├── __init__.py
│   │   ├── settings_local.py   # Локальные значения для настроек, не должны быть в git, создавать непосредственно на сервере
│   │   ├── config.py           # Настройки проекта которые не зависят от внешних настроек
│   │   ├── security.py         # Логика безопасности проекта
│   │   ├── db.py               # Настройки и сессии базы данных.
│   │   ├── cache.py            # Настройки кеширования
│   │   └── dependencies.py
│   │
│   ├── api/                    # Содержит все API эндпоинты, разделенные по версиям.
│   │   ├── __init__.py
│   │   └── v1/
│   │       ├── __init__.py
│   │       ├── router.py       # Содержит обработчики запросов для указанной версии api
│   │       │
│   │       ├── static/         # Содержит файлы статики, если они нужны
│   │       │   ├── js
│   │       │   ├── css
│   │       │   ├── img
│   │       │   └── html
│   │       │
│   │       ├── logic/          # Содержит бизнес логику
│   │       │   ├── __init__.py
│   │       │   ├── users.py
│   │       │   └── items.py
│   │       │
│   │       ├── schemas/        # Pydantic модели для валидации запросов и ответов.
│   │       │   ├── __init__.py
│   │       │   ├── user.py
│   │       │   └── item.py
│   │       │
│   │       ├── crud/           # Функции для работы с базой данных (Create, Read, Update, Delete).
│   │       │   ├── __init__.py
│   │       │   ├── user.py
│   │       │   └── item.py
│   │       │
│   │       └── tests/          # Директория для тестов.
│   │           ├── __init__.py
│   │           ├── test_users.py
│   │           └── test_items.py
│   │
│   ├── models/         # Определения моделей базы данных (например, SQLAlchemy модели).
│   │    ├── __init__.py
│   │    ├── user.py
│   │    └── item.py
│   │
│   └── fixture/          # Хранит фикстуры для тестирования этого проекта
│       ├── __init__.py
│       ├── items_v1.py   # Тестовые записи для БД
│       └── utils.py      # Переиспользуемые фикстуры для тестов
│
├── common/         # Submodule для переиспользовать
│
├── alembic/                # Директория для миграций базы данных.
│   ├── versions/           # Папка с миграциями
│   │   ├── __init__.py
│   │   └── 0001_init.py    # Файл с миграцией
│   └── env.py              # Настройки для alembic
│
├─ conf/                            # Файлы конфигурации для prod
│   ├── settings_local.example.py   # Пример для создания settings_local.py
│   └── Dockerfile                  # Файл для prod
│
├── pytest.ini          # Конфигурация для pytest
├── conftest.py         # Настройки выполнения тестов
│
├── .gitignore          # Какие игнорировать файлы и папки в git
├── .gitlab-ci.yml      # Настройки CI pipeline
│
├── pyproject.toml      # Настройки Poetry
│
├── Makefile            # Переиспользуемые bash команды
│
├── README.md           # Описание проекта
├── CHANGELOG.md        # Изменения в проекте
├── version.toml        # Версия проекта
│
├── alembic.ini         # Конфигурации для alembic
│
├── DockerfileDev       # Файл для создания dev контейнера с APP
├── docker-compose.yml  # Используется для сборки dev окружения
│
├── admin_panel.py      # Админ панель
│
└── main.py             # Точка входа в приложение, где создается экземпляр FastAPI.
```

## Use Base Pattern

Функция `base_pattern` добавляет множество полезных функций в `app`, включая:

-   Заполнение `state` и другую информацию у `app`.
-   Разрешение `CORS`.
-   Подключение роутеров с поддержкой `ViewSet`.
-   Добавление метода `healthcheck`.
-   `Middleware` для отладки времени выполнения API-запросов.
-   Подробный вывод для `HTTP` исключений.

## Use DatabaseManager

`DatabaseManager` - это универсальный инструмент для работы с РСУБД, предоставляющий как синхронные, так и асинхронные(название начинается на `a`) методы.

`DatabaseManager` использует патер одиночка.

Пример создания менеджера БД в файле `app/db/base.py`:

```python
"""Модуль подключения к РСУБД"""

from app.core.config import DATABASE_URL, DEBUG, DEV_STATUS
from fastapi_accelerator.dbsession import MainDatabaseManager

# Менеджера для РСУБД
DatabaseManager = MainDatabaseManager(DATABASE_URL, echo=DEBUG, DEV_STATUS=DEV_STATUS)
```

### Создать модель через DatabaseManager

```python
from sqlalchemy import Column, Integer, String

from app.db.base import DatabaseManager


class User(DatabaseManager.Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)
    login = Column(String, index=True)
    pthone = Column(String, index=True)
    email = Column(String, unique=True, index=True)
```

### Выполнение CRUD через DatabaseManager

```python
# Асинхронный вариант
class FileView:
    @router.get("/file")
    async def get_files(
        skip=Query(0),
        limit=Query(100),
        aorm: OrmAsync = Depends(DatabaseManager.aget_orm),
    ) -> List[File]:
        return await aorm.get_list(FileDb, select(FileDb).offset(skip).limit(limit))

    @router.get("/file/{file_uid}")
    async def get_file(
        file_uid: str = Path(),
        aorm: OrmAsync = Depends(DatabaseManager.aget_orm),
    ) -> File:
        return await aorm.get(select(FileDb).filter(FileDb.uid == file_uid))

    @router.post("/file")
    async def create_file(
        aorm: OrmAsync = Depends(DatabaseManager.aget_orm),
    ) -> File:
        file_uid = uuid.uuid4()
        new_user = FileDb(uid=file_uid)
        return await aorm.create_item(new_user)

    @router.put("/file/{file_uid}")
    async def update_file(
        file_uid: str = Path(),
        aorm: OrmAsync = Depends(DatabaseManager.aget_orm),
    ) -> File:
        update_data = {"filename": "new"}
        return await aorm.update(
            update(FileDb).filter(FileDb.uid == file_uid), update_data
        )

    @router.delete("/file/{file_uid}")
    async def delte_file(
        file_uid: str = Path(),
        aorm: OrmAsync = Depends(DatabaseManager.aget_orm),
    ):
        return await aorm.delete(delete(FileDb).filter(FileDb.uid == file_uid))

# Синхронный вариант
@router.get("/file-sync")
async def get_file_sync(
    session: Session = Depends(DatabaseManager.get_session),
) -> List[File]:
    skip = 0
    limit = 100
    res = session.query(FileDb).offset(skip).limit(limit).all()
    return res
```

### Работа с миграциями через Alembic

1.  Установка

```bash
poetry add alembic
```

2.  Инициализация проекта

```bash
alembic init alembic
```

3.  Изменить `alembic/env.py`

```python
# Импортируем менеджера БД
from app.core.db import DatabaseManager

# > ! Импортировать модели которые нужно отлеживать
from app.models import *  # noqa F401

from fastapi_accelerator.pattern.pattern_alembic import AlembicEnv

# Преднастоенная логика для создания и выполнения миграций чрез Alembic
AlembicEnv(DatabaseManager).run()
```

4. Можем изменить `alembic.ini`

```ini
# Формат для названия файла с миграцией
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
```

> Важный аспект поиска миграций
> Нужно чтобы модели быть импортированы в `alembic/env.py` чтобы эти модели записали свои данные в `Base.metadata`
>
> Поэтому:
>
> 1. В `app.models.__init__.py` импортировать все модели
>
> ```python
> from .files import *
> from .users import *
> ```
>
> 2. Нужно в `alembic/env.py` импортировать все модели
>
> ```python
> from app.models import *
> ```

5. Создавайте миграции и применяйте их

```bash
alembic revision --autogenerate
alembic upgrade head
```

## Use Cache

-   Вы можете использовать кеширование API ответа через декоратор `@cache_redis()`

```python
from datetime import timedelta
from fastapi_accelerator.cache import cache_redis

@app.get(f"files/{{item_id}}")
@cache_redis(cache_class=redis_client, cache_ttl=timedelta(minutes=10))
async def get_item(
    request: Request,
    item_uid: str = Path(...),
    aorm: OrmAsync = Depends(DatabaseManager.aget_orm),
) -> FilesSchema:
    response = await aorm.get(
        select(Files).filter(Files.id == item_uid)
    )
    return response
```

-   Предварительная настройка, заполнить файл `app/core/cache.py`:

```python
import redis.asyncio as redis

from app.core.config import REDIS_URL

# Создаем глобальный объект Redis
redis_client = redis.from_url(REDIS_URL, encoding="utf-8", decode_responses=True)
```

## Use ViewSet

1. Создадим например `app/api/v1/router.py`

```python
from datetime import timedelta
from typing import List
from uuid import UUID

from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.orm import Session

from app.api.v1.schemas.file import File
from app.api.v1.schemas.timemeasurement import TaskExecution
from app.api.v1.schemas.user import User
from app.core.cache import redis_client
from app.core.db import DatabaseManager
from app.models.file import File as FileDb
from app.models.timemeasurement import TaskExecution as TaskExecutionDb
from app.models.users import User as UserDb
from fastapi_accelerator.auth_jwt import jwt_auth
from fastapi_accelerator.db.dbsession import OrmAsync
from fastapi_accelerator.paginator import DefaultPaginator
from fastapi_accelerator.viewset import AppOrm, FullViewSet

router = APIRouter(prefix="/api/v1")

class FileViewSet(FullViewSet):
    """
    Представление для работы с файлами
    """

    # Модель БД
    db_model = FileDb
    # Модель Схемы
    pydantic_model = File
    # Кеширование
    cache_class = redis_client
    cache_ttl = timedelta(minutes=10)
    # Пагинация
    paginator_class = DefaultPaginator

    async def db_update(
        self, item_id: str | int | UUID, item: type[BaseModel], aorm: OrmAsync
    ) -> object:
        """Переопределение метода db_update"""
        return await super().db_update(item_id, item, aorm)


class UserViewSet(FullViewSet):
    """
    Представление для работы с пользователями
    """

    # Модель БД
    db_model = UserDb
    # Модель Схемы
    pydantic_model = User

    def list(self):
        """Переопределение метода list"""

        @self.router.get(f"{self.prefix}", tags=self.tags)
        async def get_list_items(
            skip: int = Query(0),
            limit: int = Query(100),
            aorm: OrmAsync = Depends(AppOrm.aget_orm),
        ) -> List[self.pydantic_model]:
            return await aorm.get_list(
                select(self.db_model).offset(skip).limit(limit), deep=self.deep_schema
            )
        return get_list_items

class TaskExecutionViewSet(FullViewSet):
    """
    Представление для работы трудозатратами
    """

    # Модель БД
    db_model = TaskExecutionDb
    # Модель Схемы
    pydantic_model = TaskExecution

    # Пагинация
    paginator_class = DefaultPaginator

    # Включить поддержку вложенных схем pydantic
    # это значит что будет происходить рекурсивное
    # создание, обновление, удаление связанных записей
    deep_schema = True

    # Включить защиту через JWT
    dependencies = [Depends(jwt_auth)]

router.views = [
    FileViewSet().as_view(router, prefix="/file"),
    UserViewSet().as_view(router, prefix="/user"),
    TaskExecutionViewSet().as_view(router, prefix="/taskexecution"),
]
```

## Use Time Zone

Получить текущие время сервера с учётом его временной зоны

```python
from fastapi_accelerator.timezone import get_datetime_now

# Вариант 1
get_datetime_now(request.app).isoformat()
# Вариант 2
get_datetime_now(app).isoformat()
```

## Use HTTPException

-   Использование:

```python
from fastapi_accelerator.exception import HTTPException403

@router.get("/")
async def get_users():
    if True:
        raise HTTPException403()
    return [{"user_id": "user1"}, {"user_id": "user2"}]
```

## Use AuthJWT

Использование аутентификация через JWT

-   Подключить к FastAPI проекту:

```python
from fastapi_accelerator.auth_jwt import BaseAuthJWT

class AuthJWT(BaseAuthJWT):
    def check_auth(username: str, password: str) -> bool:
        """Проверка введенного логина и пароля."""
        return username == "admin" and password == "admin"

    def add_jwt_body(username: str) -> dict:
        """Функция для добавление дополнительных данных в JWT токен пользователя"""
        return {"version": username.title()}


# Подключить аутентификацию по JWT
AuthJWT.mount_auth(app)
```

-   Пример защиты API метода:

```python
from fastapi_accelerator.auth_jwt import jwt_auth

@app.get("/check_protected", summary="Проверить аутентификацию по JWT")
async def protected_route(jwt: dict = Depends(jwt_auth)):
    return {"message": "This is a protected route", "user": jwt}
```

## Use Admin Panel

1.  Установка

```bash
poetry add flask-admin
```

2. Создать файл `admin_panel.py`

```python
from flask import Flask

from app.core.config import ADMIN_PASSWORD, ADMIN_USERNAME, SECRET_KEY
from app.db.base import DatabaseManager
from app.models import File, User
from fastapi_accelerator.pattern_flask_admin import base_pattern

app = Flask(__name__)

admin = base_pattern(
    app,
    SECRET_KEY,
    ADMIN_PASSWORD,
    ADMIN_USERNAME,
    # > Модели которые нужны в админ панели
    models=[User, File],
    database_manager_sync=DatabaseManager,
)


if __name__ == "__main__":
    app.run(
        host="0.0.0.0",
        port=8001,
        debug=True,
    )
```

3. Запускать `python admin_panel.py`

4. Войти в админ панель:

-   `http://localhost:8233/admin`
-   `http://localhost:8233/login`
-   `http://localhost:8233/logout`

# Common тестирование FastAPI

## Зачем и как писать тесты

Рассмотрим тестирование: REST API, использующего реляционную СУБД и возвращающего ответы в формате JSON.

Это один из наиболее распространенных подходов к интеграционному тестированию, который эмулирует ручную проверку API методов. Такой подход позволяет покрыть значительную часть бизнес-логики тестами, поэтому автоматизация тестирования API – хороший старт. Эти тесты не только воспроизводят ручное тестирование, но и позволяют проверять побочные эффекты в базе данных, такие как создание новых записей после выполнения POST-запросов.

Реализовать такие тесты в `Django` достаточно просто благодаря встроенным инструментам, однако в `FastAPI` эта задача требует большего внимания. Поэтому я разработал компоненты, которые позволяют создавать интеграционные тесты API на `FastAPI` так же удобно и быстро, как и в `Django`.

## Предварительная настройка для тестирования

1. Установка

```bash
poetry add pytest pytest-asyncio httpx
```

2. Создать файл `app/pytest.ini`

```ini
[pytest]
; Доп аргументы к запуску
addopts = -v -l -x -s --lf --disable-warnings

; Маска для поиска файлов с тестами
python_files = tests.py test_*.py *_tests.py

; Позволяет выводить логи (logs) в консоль при запуске тестов.
log_cli = true
```

3. Создать файл `app/conftest.py`

```python
from app.core.config import TEST_DATABASE_URL
from fastapi_accelerator.db.dbsession import MainDatabaseManager

# Вы можете указать точный список импорта, это для простоты мы импортируем все
from fastapi_accelerator.testutils import *  # noqa E402

# Нужно создать менеджер БД до импорта APP
# чтобы паттерн одиночка создал только тестовое instance
# и в APP уже взялся тестовый instance
TestDatabaseManager = MainDatabaseManager(
    TEST_DATABASE_URL, echo=False, DEV_STATUS=True
)

from main import app  # noqa E402

# Отключить кеширование во время тестов
app.state.CACHE_STATUS = False

SettingTest(TestDatabaseManager, app, alembic_migrate=True, keepdb=True) # noqa F405
```

## Основные компоненты для тестирования

Для упрощения написания тестов, их стандартизацию, вы можете использовать следующие компоненты:

-   Фикстуры:

    -   `client` - Клиент для выполнения тестовых API запросов
    -   `test_app` - Тестовое FastAPI приложение
    -   `url_path_for` - Получить полный URL путь, по имени функции обработчика
    -   `engine` - Синхронный двигатель
    -   `aengine` - Асинхронный двигатель
    -   `db_session` - Соединение с тестовой БД
    -   `db_manager` - Менеджер с тестовой БД

-   Функции:

    -   `check_response_json` - Функция, которая объединяет основные проверки для API ответа
    -   `rm_key_from_deep_dict` - Функция для отчистить не нужные ключи у API ответа

-   Классы:

    -   `BasePytest` - Базовый класс для тестирования через классы
    -   `BaseAuthJwtPytest` - Добавление аутентификации по JWT(`@client_auth_jwt`) для `BasePytest`

-   Контекстный менеджер:

    -   `track_queries` - Перехват выполняемых SQL команд, во время контекста, для последующего анализ - например подсчёта количества.

-   Декораторы:

    -   `@apply_fixture_db(ФункцияВозвращающаяФикстуры)` - Декоратор, который добавляет фикстуры в БД перед тестом и удаляет их после теста.
    -   `@client_auth_jwt()` - Декоратор который аутентифицирует тестового клиента по JWT.

## Подробнее про компоненты для тестирования

### Фикстура `client`

Основная фикстура для выполнения тестовых API запросов.

Порядок работы фикстуры `client`:

-   Этапы на уровней всей тестовой сессии:

    1. (before) Создастся тестовая БД если её нет;
    2. (before) В зависимости от настройки `SettingTest.alembic_migrate`;

        - Если `True` -> Создаст таблицы через миграции `alembic`
        - Если `False` -> Создаст таблицы через `create_all()`

    3. (after) После завершение всех тестов в зависимости от настройки `SettingTest.keepdb`;
        - Если `True` -> Ничего
        - Если `False` -> Удаляться все таблицы из тестовой БД

-   Этапы на уровней каждого тестового функции/метода:

    3. В тестовую функцию/метода попадает аргумент `client: TestClient`;
    4. (after) После выхода из тестовой функции/метода, все данных во всех таблицах отчищаются(кроме таблицы `alembic_version`, так как саму БД мы не удаляем);

```python
from fastapi.testclient import TestClient

def test_имя(client: TestClient):
    response = client.get('url')
```

### Декоратор `@client_auth_jwt`

На практике нам часто приходиться тестировать API методы которые требуют аутентификацию. Делать обход аутентификации в тестах плохой вариант, так как можно упустить некоторые исключения, или логику API метода которая завязана на данных аутентифицированного пользователя. Поэтому чтобы аутентифицировать тестового клиента, укажите декоратор `@client_auth_jwt` для тестовой функции/метода

-   Пример использование декоратора для тестовой функции:

```python
from fastapi.testclient import TestClient
from fastapi_accelerator.testutils.fixture_auth import client_auth_jwt

@client_auth_jwt(username='test')
def test_имя(client: TestClient):
    print(client.headers['authorization']) # 'Bearer ...'
```

-   Пример использование декоратора для тестового метода в классе `BasePytest`:

```python
from fastapi.testclient import TestClient
from fastapi_accelerator.testutils.fixture_base import BasePytest
from fastapi_accelerator.testutils.fixture_auth import client_auth_jwt

class TestИмяКласса(BasePytest):

    @client_auth_jwt()
    def test_имя(self, client: TestClient):
        print(client.headers['authorization']) # 'Bearer ...'
```

> Если вы используете декоратор `@client_auth_jwt` в классе `BasePytest`, то он возьмет `username` из `self.TEST_USER['username']`, этот атрибут уже определен в `BasePytest` и равен по умолчанию `test`.

### Декоратор `@apply_fixture_db`

Идея взята из `Django` в котором можно указать в атрибуте `fixtures` список файлов с фикстурами, которые будут загружены для тестов, и удалены после окончания. Этот очень удобно для переиспользовать тестовых данных.

Но я решил модифицировать этот вариант и сделать фикстуры не в виде `JSON` а виде объектов `SqlAlchemy`. Использование `JSON` лучше когда нужно переносить эти данные на другие платформы, но такое встречается редко, чаще всего фикстуры для backend тестов используются только на backend, и горазда удобнее и быстрее писать в формате объектов БД, чем в формате `JSON`. Поэтому выбран формат объектов.

Порядок работы декоратора `@apply_fixture_db`:

1. Получает записи из переданной функции `export_func`;
2. Создает записи в БД;
3. Выполняется тестовая функция. Если она ожидает аргумент `fixtures`, то в него передадутся записи из `export_func`;
4. Удаляет записи из БД:
    - Если вы используете фикстуру `client`, то она автоматически отчистить все данные в таблицах, после выполнения тестовой функции.
    - Если вы не используете фикстуру `client`, то для отчистки данных укажите в декоратор аргумент `flush=True`

---

-   Оформление файлами с тестовыми данными `app.fixture.items_v1.py`:

```python
from fastapi_accelerator.testutils.utils import to_namedtuple
from app.models.timemeasurement import Task, TaskExecution, TaskUser

def export_fixture_task():
    # Создание пользователей и задач
    user1 = TaskUser(id=0, name="Alice")
    user2 = TaskUser(id=1, name="Bob")

    task1 = Task(id=9, name="Admins")
    task2 = Task(id=8, name="Users")

    # Связывание пользователей с задачами
    user1.tasks.append(task1)
    user2.tasks.append(task1)
    user2.tasks.append(task2)

    # Вернуть именований картеж
    return to_namedtuple(
        user1=user1,
        user2=user2,
        task1=task1,
        task2=task2,
        task_execution1=TaskExecution(
            id=91,
            task=task1,
            start_time="2024-09-06T10:55:43",
            end_time="2024-09-06T10:59:43",
        ),
    )
```

-   Использование декоратора в тестовых функциях:

```python
from fastapi_accelerator.test_utils import apply_fixture_db
from app.fixture.items_v1 import export_fixture_task

@apply_fixture_db(export_fixture_task)
def test_имя(client: TestClient):
    response = client.get('url')
```

-   Использование декоратора в тестовых методах, в этом варианте вы можно указывать только для `setUp`, тогда он будет применен для всех тестовых методов:

```python
from fastapi.testclient import TestClient
from fastapi_accelerator.testutils.fixture_base import BasePytest
from fastapi_accelerator.test_utils import apply_fixture_db
from app.fixture.items_v1 import export_fixture_task

class TestИмяКласса(BasePytest):

    @apply_fixture_db(export_fixture_task)
    def setUp(self, fixtures: NamedTuple):
        self.fixtures = fixtures

    def test_имя(self, client: TestClient):
        response = client.get('url')
        print(self.fixtures)
```

### Контекстный менеджер `track_queries`

Идея взята из `Django` метода `self.assertNumQueries`, который поваляет проверять количество выполненных SQL команд в контексте. Это очень полезно когда используется ORM, который может из за неаккуратного использования, генерировать сотни SQL команд. Поэтому лучше у каждого вызова тестового API метода отлеживать количество выполненных SQL команд.

-   Пример использования контекстного менеджера `track_queries`:

```python
from fastapi_accelerator.testutils.fixture_db.trace_sql import track_queries

def test_имя(client: TestClient, db_manager: MainDatabaseManager):
    with track_queries(db_manager, expected_count=3):
        response = client.get('url')
```

-   Вы можете получить полный список выполненных SQL команд из `tracker.queries`:

```python
from fastapi_accelerator.testutils.fixture_db.trace_sql import track_queries

def test_имя(client: TestClient, db_manager: MainDatabaseManager):
    with track_queries(db_manager) as tracker:
        response = client.get('url')

    # Если количество измениться, то выведется список всех выполненных SQL команд
    assert tracker.count == 3, tracker.queries
```

### Функция `check_response_json`

По опыту написания тестов, могу выделить несколько основных проверок для ответов API JSON.

1. Проверить статус ответа
2. Получить ответ в формате JSON
3. Если нужно, то удалить динамические ключи из ответа, например дату создания, дату обновления, первичный ключ новой записи. Работает через функцию `rm_key_from_deep_dict`
4. Сравнить ответ с ожидаемым

Эти проверки выполняются в функции `check_response_json`

Пример использования:

```python
def test_имя(client: TestClient):
    response = client.post('url', json={...})
    check_response_json(
        response,
        200,
        {
            "page": 1,
            "size": 10,
            "count": 1,
            "items": [
                {
                    "end_time": "2024-09-06T10:59:43",
                    "start_time": "2024-09-06T10:55:43",
                    "task": {
                        "description": None,
                        "name": "Admins",
                    },
                }
            ],
        },
        exclude_list=['id','task_id']
    )
```

### Тестирование через классы

#### Класс `BasePytest`

Удобнее и понятнее, создавать логически связанные тесты в одном классе, и указывать в методе `setUp` общую для них инициализацию, например общий url, или создание тестовых объектов в БД, или создание переменных хранящих ожидаемый JSON ответ.

-   Пример создания класса для тестирования на основание `BasePytest`:

```python
from fastapi.testclient import TestClient
from fastapi_accelerator.testutils.fixture_base import BasePytest

class TestИмяКласса(BasePytest):

    def setUp(self):
        # Метод для выполнения необходимой настройки перед каждым тестом.
        ...

    def test_имя(self, client: TestClient):
        ...
```

-   Вы можете использовать фикстуры и декораторы для тестовых методах, например аутентификаций по JWT:

```python
from fastapi.testclient import TestClient
from fastapi_accelerator.testutils.fixture_base import BasePytest
from fastapi_accelerator.testutils.fixture_auth import client_auth_jwt

class TestИмяКласса(BasePytest):

    def setUp(self):
        # Метод для выполнения необходимой настройки перед каждым тестом.
        ...

    @client_auth_jwt()
    def test_имя(self, client: TestClient):
        print(client.headers['authorization']) # 'Bearer ...'
        ...
```

#### Класс `BaseAuthJwtPytest`

Чтобы не писать для каждого тестового метода в классе, декоратор `@client_auth_jwt` вы можете наследоваться от `BaseAuthJwtPytest`, в котором эта логика уже реализована.

-   Пример создания класса для тестирования на основание `BaseAuthJwtPytest`:

```python
from fastapi.testclient import TestClient
from fastapi_accelerator.testutils.fixture_base import BaseAuthJwtPytest

class TestИмяКласса(BaseAuthJwtPytest):

    def setUp(self):
        # Метод для выполнения необходимой настройки перед каждым тестом.
        ...

    def test_имя(self, client: TestClient):
        print(client.headers['authorization']) # 'Bearer ...'
        ...
```

## Примеры тестов

### Классическая тестовой функции

Проверки REST API метода, который использует РСУБД, и возвращает ответ в формате `JSON`:

```python
from typing import Callable, NamedTuple

from fastapi.testclient import TestClient

from app.fixture.items_v1 import export_fixture_file
from fastapi_accelerator.db.dbsession import MainDatabaseManager
from fastapi_accelerator.testutils import apply_fixture_db, client_auth_jwt, track_queries, check_response_json

# Аутентифицировать тестового клиента
@client_auth_jwt(username="test")
# Создать тестовые данных из функции с фикстурами
@apply_fixture_db(export_fixture_file)
def test_имя(
    client: TestClient,  # Тестовый клиент для API запросов
    url_path_for: Callable,  # Функция для получения url по имени функции обработчика
    db_manager: MainDatabaseManager,  # Менеджер тестовой БД
    fixtures: NamedTuple,  # Хранит созданные данные из фикстур
):
    # Проверка количество выполняемых SQL команд
    with track_queries(db_manager, expected_count=3):
        # Запрос в API
        response = client.get(url_path_for("ИмяФункции"))
    # Проверка JSON API ответа
    check_response_json(
        response,
        200,
        {
            "id": fixtures.Имя.id,
        },
    )
    # TODO Можно для методов POST, UPDATE, DELETE добавить проверку на изменения записей в БД.
    ...
```

### Классический тестовый класс

Проверки REST API метода, который использует РСУБД, и возвращает ответ в формате `JSON`:

```python
from typing import Callable, NamedTuple

from fastapi.testclient import TestClient

from app.fixture.items_v1 import export_fixture_file
from fastapi_accelerator.db.dbsession import MainDatabaseManager
from fastapi_accelerator.testutils import apply_fixture_db
from fastapi_accelerator.testutils.fixture_auth import client_auth_jwt
from fastapi_accelerator.testutils.fixture_db.trace_sql import track_queries
from fastapi_accelerator.testutils.utils import BaseAuthJwtPytest, check_response_json

BASE_URL_V1 = "/api/v1/"

class TestИмя(BaseAuthJwtPytest):

    # Создать тестовые данных из функции с фикстурами
    @apply_fixture_db(export_fixture_file)
    def setUp(self, fixtures: NamedTuple):
        self.url = BASE_URL_V1 + "taskexecution"
        self.fixtures = fixtures # Хранит созданные данные из фикстур

    def test_имя(self, client: TestClient, db_manager: MainDatabaseManager):
        # Проверка количество выполняемых SQL команд
        with track_queries(db_manager, expected_count=3):
            # Запрос в API
            response = client.get(self.url)
        # Проверка JSON API ответа
        check_response_json(
            response,
            200,
            {
                "id": self.fixtures.Имя.id,
            },
        )
        # TODO Можно для методов POST, UPDATE, DELETE добавить проверку на изменения записей в БД.
        ...
```

### Пример тестового файла:

```python
from typing import Callable, NamedTuple

from fastapi.testclient import TestClient
from sqlalchemy.orm import Session

from app.fixture.items_v1 import export_fixture_file, export_fixture_task
from app.models.file import File as FileDb
from fastapi_accelerator.db.dbsession import MainDatabaseManager
from fastapi_accelerator.testutils import apply_fixture_db, rm_key_from_deep_dict
from fastapi_accelerator.testutils.fixture_auth import client_auth_jwt
from fastapi_accelerator.testutils.fixture_db.trace_sql import track_queries
from fastapi_accelerator.testutils.utils import BaseAuthJwtPytest, BasePytest, check_response_json


def test_base(
    client: TestClient,
    url_path_for: Callable,
    db_session: Session,
):
    # Создать запись в тестом методе
    file1 = FileDb(
        uid="469d4176-98f3-48a2-8794-0e2472bc2b7e",
        filename="file1.txt",
        size=100,
        format="text/plain",
        extension=".txt",
    )
    db_session.add(file1)
    db_session.commit()

    # Запрос в API
    response = client.get(url_path_for("get_file_sync"))
    assert response.status_code == 200
    res = response.json()
    rm_key_from_deep_dict(res, ["updated_at", "created_at"])
    assert res == [
        {
            "cloud_path": None,
            "extension": ".txt",
            "filename": "file1.txt",
            "format": "text/plain",
            "local_path": None,
            "size": 100,
            "uid": "469d4176-98f3-48a2-8794-0e2472bc2b7e",
        },
    ]


@apply_fixture_db(export_fixture_file)
def test_base2(client: TestClient, url_path_for: Callable):
    # Запрос в API
    response = client.get(url_path_for("get_file_sync"))
    assert response.status_code == 200
    res = response.json()

    rm_key_from_deep_dict(res, ["updated_at", "created_at"])

    assert res == [
        {
            "cloud_path": None,
            "extension": ".txt",
            "filename": "file1.txt",
            "format": "text/plain",
            "local_path": None,
            "size": 100,
            "uid": "469d4176-98f3-48a2-8794-0e2472bc2b7e",
        },
        {
            "cloud_path": None,
            "extension": ".txt",
            "filename": "file1.txt",
            "format": "text/plain",
            "local_path": None,
            "size": 100,
            "uid": "569d4176-98f3-48a2-8794-0e2472bc2b7e",
        },
    ]


BASE_URL_V1 = "/api/v1/"


class TestTaskExecution(BaseAuthJwtPytest):

    @apply_fixture_db(export_fixture_task)
    def setUp(self, fixtures: NamedTuple):
        self.url = BASE_URL_V1 + "taskexecution"
        self.fixtures = fixtures

    def test_get_list(self, client: TestClient, db_manager: MainDatabaseManager):
        with track_queries(db_manager, expected_count=1) as tracker:
            response = client.get(self.url)
        check_response_json(
            response,
            200,
            {
                "page": 1,
                "size": 10,
                "count": 1,
                "items": [
                    {
                        "id": self.fixtures.task_execution1.id,
                        "end_time": "2024-09-06T10:59:43",
                        "start_time": "2024-09-06T10:55:43",
                        "task_id": self.fixtures.task1.id,
                        "task": {
                            "description": None,
                            "name": "Admins",
                            "id": self.fixtures.task1.id,
                        },
                    }
                ],
            },
        )

    def test_get_item(self, client: TestClient):
        response = client.get(self.url + f"/{self.fixtures.task_execution1.id}")
        check_response_json(
            response,
            200,
            {
                "id": self.fixtures.task_execution1.id,
                "start_time": "2024-09-06T10:55:43",
                "end_time": "2024-09-06T10:59:43",
                "task": {
                    "id": self.fixtures.task1.id,
                    "name": "Admins",
                    "description": None,
                },
            },
        )


class TestTaskExecution2(BasePytest):

    @apply_fixture_db(export_fixture_task)
    def setUp(self, fixtures: NamedTuple):
        self.url = BASE_URL_V1 + "taskexecution"
        self.fixtures = fixtures

    @client_auth_jwt()
    def test_get_list(self, client: TestClient, db_manager: MainDatabaseManager):
        with track_queries(db_manager, expected_count=1):
            response = client.get(self.url)
        check_response_json(
            response,
            200,
            {
                "page": 1,
                "size": 10,
                "count": 1,
                "items": [
                    {
                        "id": self.fixtures.task_execution1.id,
                        "end_time": "2024-09-06T10:59:43",
                        "start_time": "2024-09-06T10:55:43",
                        "task_id": self.fixtures.task1.id,
                        "task": {
                            "description": None,
                            "name": "Admins",
                            "id": self.fixtures.task1.id,
                        },
                    }
                ],
            },
        )

    @client_auth_jwt()
    def test_get_item(self, client: TestClient):
        response = client.get(self.url + f"/{self.fixtures.task_execution1.id}")
        check_response_json(
            response,
            200,
            {
                "id": self.fixtures.task_execution1.id,
                "start_time": "2024-09-06T10:55:43",
                "end_time": "2024-09-06T10:59:43",
                "task": {
                    "id": self.fixtures.task1.id,
                    "name": "Admins",
                    "description": None,
                },
            },
        )
```

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/denisxab/fastapi-accelerator",
    "name": "fastapi-accelerator",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.9",
    "maintainer_email": null,
    "keywords": "fastapi, Django",
    "author": "Denis Kustov",
    "author_email": "pro-progerkustov@yandex.ru",
    "download_url": "https://files.pythonhosted.org/packages/85/3c/a567c0ece414cd06cf6d29175ca9439bdcb6127372e82b57e07740dc97ae/fastapi_accelerator-0.0.1.tar.gz",
    "platform": null,
    "description": "# Common \u0431\u0438\u0437\u043d\u0435\u0441 \u043b\u043e\u0433\u0438\u043a\u0430 FastAPI\n\n## \u0417\u0430\u0447\u0435\u043c \u043d\u0443\u0436\u0435\u043d, \u043a\u0430\u043a\u0438\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0440\u0435\u0448\u0430\u0435\u0442 ?\n\n\u041e\u0441\u043d\u043e\u0432\u043d\u0430\u044f \u0446\u0435\u043b\u044c - \u0443\u0441\u043a\u043e\u0440\u0438\u0442\u044c \u0438 \u0443\u043f\u0440\u043e\u0441\u0442\u0438\u0442\u044c \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0443 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432 \u043d\u0430 FastAPI. \u042d\u0442\u043e \u0434\u043e\u0441\u0442\u0438\u0433\u0430\u0435\u0442\u0441\u044f \u043f\u0443\u0442\u0435\u043c:\n\n1. \u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043f\u0435\u0440\u0435\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u043a\u043e\u0434\u0430 \u0434\u043b\u044f \u0442\u0438\u043f\u043e\u0432\u044b\u0445 \u0437\u0430\u0434\u0430\u0447.\n2. \u0412\u043d\u0435\u0434\u0440\u0435\u043d\u0438\u044f \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0420\u0421\u0423\u0411\u0414.\n3. \u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 ViewSet \u0434\u043b\u044f \u0431\u044b\u0441\u0442\u0440\u043e\u0433\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0439 \u0441 \u0431\u0430\u0437\u043e\u0432\u043e\u0439 \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u043e\u0439.\n4. \u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u043e JWT.\n5. \u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0443\u0434\u043e\u0431\u043d\u043e\u0439 \u0430\u0434\u043c\u0438\u043d-\u043f\u0430\u043d\u0435\u043b\u0438.\n6. \u0423\u043f\u0440\u043e\u0449\u0435\u043d\u0438\u044f \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0445 \u0442\u0435\u0441\u0442\u043e\u0432 \u0434\u043b\u044f API.\n7. \u041e\u043f\u0442\u0438\u043c\u0438\u0437\u0430\u0446\u0438\u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 Alembic \u0434\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u044f\u043c\u0438 \u0432 production \u0438 test \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f\u0445.\n\n## \u041e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u0444\u0430\u0439\u043b\u043e\u0432 \u0432 \u0441\u043e\u0441\u0442\u0430\u0432\u0435 common\n\n```bash\ncommon/\n\u2502\n\u251c\u2500\u2500 db                  # \u041b\u043e\u0433\u0438\u043a\u0430 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441 \u0420\u0421\u0423\u0411\u0414\n\u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u251c\u2500\u2500 dborm.py\n\u2502   \u2514\u2500\u2500 dbsession.py\n\u2502\n\u251c\u2500\u2500 pattern                     # \u0428\u0430\u0431\u043b\u043e\u043d\u044b \u0434\u043b\u044f \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432\n\u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u251c\u2500\u2500 pattern_fastapi.py      # \u0428\u0430\u0431\u043b\u043e\u043d\u044b \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u043d\u0430 FastAPI\n\u2502   \u251c\u2500\u2500 pattern_alembic.py      # \u0428\u0430\u0431\u043b\u043e\u043d\u044b \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f Alembic\n\u2502   \u2514\u2500\u2500 pattern_flask_admin.py  # \u0428\u0430\u0431\u043b\u043e\u043d\u044b \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043f\u0440\u043e\u0435\u043a\u0442\u0430 Flask \u0430\u0434\u043c\u0438\u043d \u043f\u0430\u043d\u0435\u043b\u0438\n\u2502\n\u251c\u2500\u2500 testutils                   # \u0423\u0442\u0438\u043b\u0438\u0442\u044b \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f FastAPI\n\u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u251c\u2500\u2500 fixture_base.py         # \u041e\u0441\u043d\u043e\u0432\u043d\u0430\u044f \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u0430 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432\n\u2502   \u251c\u2500\u2500 fixture_db              # \u0424\u0438\u043a\u0441\u0442\u0443\u0440\u044b \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0411\u0414\n\u2502   \u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u2502   \u251c\u2500\u2500 apply_fixture.py\n\u2502   \u2502   \u251c\u2500\u2500 db.py\n\u2502   \u2502   \u2514\u2500\u2500 trace_sql.py\n\u2502   \u251c\u2500\u2500 fixture_auth.py         # \u0424\u0438\u043a\u0441\u0442\u0443\u0440\u0430 \u0434\u043b\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043f\u043e JWT\n\u2502   \u2514\u2500\u2500 utils.py\n\u2502\n\u251c\u2500\u2500 cache.py         # \u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043a\u0435\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\n\u251c\u2500\u2500 auth_jwt.py      # \u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u043e JWT\n\u251c\u2500\u2500 exception.py     # \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439\n\u251c\u2500\u2500 middleware.py    # Middleware \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b\n\u251c\u2500\u2500 paginator.py     # \u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043f\u0430\u0433\u0438\u043d\u0430\u0446\u0438\u0438\n\u251c\u2500\u2500 timezone.py      # \u0420\u0430\u0431\u043e\u0442\u0430 \u0441 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u043c\u0438 \u0437\u043e\u043d\u0430\u043c\u0438\n\u251c\u2500\u2500 viewset.py       # \u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f ViewSet\n\u251c\u2500\u2500 utils.py         # \u041e\u0431\u0449\u0438\u0435 \u0443\u0442\u0438\u043b\u0438\u0442\u044b\n\u251c\u2500\u2500 README.md        # \u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\n\u2514\u2500\u2500 __init__.py\n```\n\n## \u041f\u0440\u0438\u043c\u0435\u0440\u044b \u0444\u0430\u0439\u043b\u043e\u0432\n\n### \u041f\u0440\u0438\u043c\u0435\u0440 `main.py`\n\n```python\nfrom contextlib import asynccontextmanager\n\nimport uvicorn\nfrom fastapi import FastAPI\nfrom fastapi.responses import ORJSONResponse\n\nimport app.api.v1.router as RouterV1\nfrom app.core.config import BASE_DIR_PROJECT, DEBUG, SECRET_KEY\nfrom app.core.db import DatabaseManager\nfrom app.core.security import AuthJWT\nfrom fastapi_accelerator.pattern_fastapi import base_pattern\nfrom fastapi_accelerator.timezone import moscow_tz\n\n\n@asynccontextmanager\nasync def lifespan(app):\n    \"\"\"\u0416\u0438\u0437\u043d\u0435\u043d\u043d\u044b\u0439 \u0446\u0438\u043a\u043b \u043f\u0440\u043e\u0435\u043a\u0442\u0430\"\"\"\n    yield\n\n\napp = FastAPI(\n    title=\"File ddos API\",\n    # \u0424\u0443\u043d\u043a\u0446\u0438\u044f lifespan\n    lifespan=lifespan,\n    # \u0417\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u0442\u044c\u0441\u044f \u043a\u043e \u0432\u0441\u0435\u043c \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0430\u043c \u0432 \u044d\u0442\u043e\u043c \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0435.\n    dependencies=None,\n    # \u041a\u043b\u0430\u0441\u0441 \u043e\u0442\u0432\u0435\u0442\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0434\u043b\u044f \u0432\u0441\u0435\u0445 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u043e\u0432.\n    default_response_class=ORJSONResponse,\n)\n\n\n# \u041f\u0430\u0442\u0442\u0435\u0440\u043d \u0434\u043b\u044f \u043f\u0440\u043e\u0435\u043a\u0442\u0430\nbase_pattern(\n    app,\n    routers=(RouterV1.router,),\n    timezone=moscow_tz,\n    cache_status=True,\n    debug=DEBUG,\n    base_dir=BASE_DIR_PROJECT,\n    database_manager=DatabaseManager,\n    secret_key=SECRET_KEY,\n)\n\n# \u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u043f\u043e JWT\nAuthJWT.mount_auth(app)\n\nif __name__ == \"__main__\":\n    uvicorn.run(\n        \"main:app\",\n        host=\"0.0.0.0\",\n        port=8000,\n        workers=4,\n        reload=DEBUG,\n        access_log=DEBUG,\n    )\n```\n\n\u0417\u0430\u043f\u0443\u0441\u043a\u0430\u0442\u044c `python main.py`\n\n### \u041f\u0440\u0438\u043c\u0435\u0440 `Makefile`\n\n```makefile\nrun_test:\n\tpytest\n\nrun_dev_server:\n\tpython -m main\n\n# \u0421\u043e\u0437\u0434\u0430\u0442\u044c \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u044e\nmakemigrate:\n\talembic revision --autogenerate\n\n# \u041f\u0440\u0438\u043c\u0435\u043d\u0438\u0442\u044c \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438\nmigrate:\n\talembic upgrade head\n```\n\n### \u041f\u0440\u0438\u043c\u0435\u0440 `config.py`\n\n\u0424\u0430\u0439\u043b `app/core/config.py`:\n\n```python\n\"\"\"\n\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u043f\u0440\u043e\u0435\u043a\u0442.\n\n\u041c\u043e\u0436\u0435\u0442 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u0438\u0437 `.settings_local`\n\"\"\"\n\nfrom pathlib import Path\n\nfrom .settings_local import (\n    ADMIN_PASSWORD,\n    ADMIN_USERNAME,\n    CACHE_STATUS,\n    DATABASE_URL,\n    DEBUG,\n    DEV_STATUS,\n    REDIS_URL,\n    SECRET_KEY,\n    TEST_DATABASE_URL,\n)\n\n# \u041f\u0443\u0442\u044c \u043a \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044e \u043f\u0440\u043e\u0435\u043a\u0442\u0430\nBASE_DIR_APP = Path(__file__).parent.parent\n# \u041f\u0443\u0442\u044c \u043a \u043a\u043e\u0440\u043d\u044e \u043f\u0440\u043e\u0435\u043a\u0442\u0430\nBASE_DIR_PROJECT = BASE_DIR_APP.parent\n\n__all__ = (\n    # >>> \u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0432\u043d\u0435\u0448\u043d\u0438\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u043c:\n    # Url \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0411\u0414\n    DATABASE_URL,\n    # Url \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0411\u0414\n    TEST_DATABASE_URL,\n    # Url \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a Redis\n    REDIS_URL,\n    # >>> \u0421\u0442\u0430\u0442\u0443\u0441\u044b:\n    # \u0420\u0435\u0436\u0438\u043c \u043e\u0442\u043b\u0430\u0434\u043a\u0438, \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u043d\u0430 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u043c \u0441\u0435\u0440\u0432\u0435\u0440\u0435\n    DEBUG,\n    # \u0420\u0435\u0436\u0438\u043c \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438, \u0432\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u0432 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438\n    DEV_STATUS,\n    # \u0412\u043a\u043b/\u0412\u044b\u043a\u043b \u043a\u0435\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\n    CACHE_STATUS,\n    # >>> \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c:\n    SECRET_KEY,\n    # \u0414\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u0432 Flask-Admin \u043f\u0430\u043d\u0435\u043b\u044c\n    ADMIN_USERNAME,\n    ADMIN_PASSWORD,\n)\n```\n\n\u0424\u0430\u0439\u043b `app/core/settings_local.py`:\n\n```python\nimport os\n\nDATABASE_URL = os.getenv(\"DATABASE_URL\", \"postgres://user_app:db@postgres_db:5432/db\")\nTEST_DATABASE_URL = \"postgres://user_app:db@postgres_db:5432/testdb\"\n\nREDIS_URL = os.getenv(\"REDIS_URL\", \"redis://redis:6379\")\nSECRET_KEY = \"your_secret_key_here\"\nDEBUG = True\nDEV_STATUS = False\nCACHE_STATUS = True\nADMIN_USERNAME = \"admin\"\nADMIN_PASSWORD = \"password\"\n```\n\n### \u041f\u0440\u0438\u043c\u0435\u0440 \u0444\u0430\u0439\u043b\u043e\u0432\u043e\u0439 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u044b\n\n```bash\n\u041f\u0440\u043e\u0435\u043a\u0442/\n\u2502\n\u251c\u2500\u2500 app/\n\u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u251c\u2500\u2500 utils.py        # \u041f\u0435\u0440\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b \u0434\u043b\u044f \u043f\u0440\u043e\u0435\u043a\u0442\u0430\n\u2502   \u251c\u2500\u2500 core/           # \u0421\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u043c\u043e\u0434\u0443\u043b\u0438, \u0442\u0430\u043a\u0438\u0435 \u043a\u0430\u043a \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f, \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c \u0438 \u043e\u0431\u0449\u0438\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438.\n\u2502   \u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u2502   \u251c\u2500\u2500 settings_local.py   # \u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a, \u043d\u0435 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0432 git, \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u043d\u0435\u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435\n\u2502   \u2502   \u251c\u2500\u2500 config.py           # \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0435 \u0437\u0430\u0432\u0438\u0441\u044f\u0442 \u043e\u0442 \u0432\u043d\u0435\u0448\u043d\u0438\u0445 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a\n\u2502   \u2502   \u251c\u2500\u2500 security.py         # \u041b\u043e\u0433\u0438\u043a\u0430 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 \u043f\u0440\u043e\u0435\u043a\u0442\u0430\n\u2502   \u2502   \u251c\u2500\u2500 db.py               # \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438 \u0441\u0435\u0441\u0441\u0438\u0438 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445.\n\u2502   \u2502   \u251c\u2500\u2500 cache.py            # \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043a\u0435\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\n\u2502   \u2502   \u2514\u2500\u2500 dependencies.py\n\u2502   \u2502\n\u2502   \u251c\u2500\u2500 api/                    # \u0421\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0432\u0441\u0435 API \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442\u044b, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u043f\u043e \u0432\u0435\u0440\u0441\u0438\u044f\u043c.\n\u2502   \u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u2502   \u2514\u2500\u2500 v1/\n\u2502   \u2502       \u251c\u2500\u2500 __init__.py\n\u2502   \u2502       \u251c\u2500\u2500 router.py       # \u0421\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0434\u043b\u044f \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 api\n\u2502   \u2502       \u2502\n\u2502   \u2502       \u251c\u2500\u2500 static/         # \u0421\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0444\u0430\u0439\u043b\u044b \u0441\u0442\u0430\u0442\u0438\u043a\u0438, \u0435\u0441\u043b\u0438 \u043e\u043d\u0438 \u043d\u0443\u0436\u043d\u044b\n\u2502   \u2502       \u2502   \u251c\u2500\u2500 js\n\u2502   \u2502       \u2502   \u251c\u2500\u2500 css\n\u2502   \u2502       \u2502   \u251c\u2500\u2500 img\n\u2502   \u2502       \u2502   \u2514\u2500\u2500 html\n\u2502   \u2502       \u2502\n\u2502   \u2502       \u251c\u2500\u2500 logic/          # \u0421\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0431\u0438\u0437\u043d\u0435\u0441 \u043b\u043e\u0433\u0438\u043a\u0443\n\u2502   \u2502       \u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u2502       \u2502   \u251c\u2500\u2500 users.py\n\u2502   \u2502       \u2502   \u2514\u2500\u2500 items.py\n\u2502   \u2502       \u2502\n\u2502   \u2502       \u251c\u2500\u2500 schemas/        # Pydantic \u043c\u043e\u0434\u0435\u043b\u0438 \u0434\u043b\u044f \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0438 \u043e\u0442\u0432\u0435\u0442\u043e\u0432.\n\u2502   \u2502       \u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u2502       \u2502   \u251c\u2500\u2500 user.py\n\u2502   \u2502       \u2502   \u2514\u2500\u2500 item.py\n\u2502   \u2502       \u2502\n\u2502   \u2502       \u251c\u2500\u2500 crud/           # \u0424\u0443\u043d\u043a\u0446\u0438\u0438 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0431\u0430\u0437\u043e\u0439 \u0434\u0430\u043d\u043d\u044b\u0445 (Create, Read, Update, Delete).\n\u2502   \u2502       \u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u2502       \u2502   \u251c\u2500\u2500 user.py\n\u2502   \u2502       \u2502   \u2514\u2500\u2500 item.py\n\u2502   \u2502       \u2502\n\u2502   \u2502       \u2514\u2500\u2500 tests/          # \u0414\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u044f \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432.\n\u2502   \u2502           \u251c\u2500\u2500 __init__.py\n\u2502   \u2502           \u251c\u2500\u2500 test_users.py\n\u2502   \u2502           \u2514\u2500\u2500 test_items.py\n\u2502   \u2502\n\u2502   \u251c\u2500\u2500 models/         # \u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u043c\u043e\u0434\u0435\u043b\u0435\u0439 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, SQLAlchemy \u043c\u043e\u0434\u0435\u043b\u0438).\n\u2502   \u2502    \u251c\u2500\u2500 __init__.py\n\u2502   \u2502    \u251c\u2500\u2500 user.py\n\u2502   \u2502    \u2514\u2500\u2500 item.py\n\u2502   \u2502\n\u2502   \u2514\u2500\u2500 fixture/          # \u0425\u0440\u0430\u043d\u0438\u0442 \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u044b \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0430\n\u2502       \u251c\u2500\u2500 __init__.py\n\u2502       \u251c\u2500\u2500 items_v1.py   # \u0422\u0435\u0441\u0442\u043e\u0432\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 \u0434\u043b\u044f \u0411\u0414\n\u2502       \u2514\u2500\u2500 utils.py      # \u041f\u0435\u0440\u0435\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0435 \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u044b \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432\n\u2502\n\u251c\u2500\u2500 common/         # Submodule \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\n\u2502\n\u251c\u2500\u2500 alembic/                # \u0414\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u044f \u0434\u043b\u044f \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0439 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445.\n\u2502   \u251c\u2500\u2500 versions/           # \u041f\u0430\u043f\u043a\u0430 \u0441 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u044f\u043c\u0438\n\u2502   \u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u2502   \u2514\u2500\u2500 0001_init.py    # \u0424\u0430\u0439\u043b \u0441 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0435\u0439\n\u2502   \u2514\u2500\u2500 env.py              # \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043b\u044f alembic\n\u2502\n\u251c\u2500 conf/                            # \u0424\u0430\u0439\u043b\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f prod\n\u2502   \u251c\u2500\u2500 settings_local.example.py   # \u041f\u0440\u0438\u043c\u0435\u0440 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f settings_local.py\n\u2502   \u2514\u2500\u2500 Dockerfile                  # \u0424\u0430\u0439\u043b \u0434\u043b\u044f prod\n\u2502\n\u251c\u2500\u2500 pytest.ini          # \u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0434\u043b\u044f pytest\n\u251c\u2500\u2500 conftest.py         # \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0442\u0435\u0441\u0442\u043e\u0432\n\u2502\n\u251c\u2500\u2500 .gitignore          # \u041a\u0430\u043a\u0438\u0435 \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0444\u0430\u0439\u043b\u044b \u0438 \u043f\u0430\u043f\u043a\u0438 \u0432 git\n\u251c\u2500\u2500 .gitlab-ci.yml      # \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 CI pipeline\n\u2502\n\u251c\u2500\u2500 pyproject.toml      # \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Poetry\n\u2502\n\u251c\u2500\u2500 Makefile            # \u041f\u0435\u0440\u0435\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0435 bash \u043a\u043e\u043c\u0430\u043d\u0434\u044b\n\u2502\n\u251c\u2500\u2500 README.md           # \u041e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430\n\u251c\u2500\u2500 CHANGELOG.md        # \u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0435\n\u251c\u2500\u2500 version.toml        # \u0412\u0435\u0440\u0441\u0438\u044f \u043f\u0440\u043e\u0435\u043a\u0442\u0430\n\u2502\n\u251c\u2500\u2500 alembic.ini         # \u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f alembic\n\u2502\n\u251c\u2500\u2500 DockerfileDev       # \u0424\u0430\u0439\u043b \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f dev \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 \u0441 APP\n\u251c\u2500\u2500 docker-compose.yml  # \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u0441\u0431\u043e\u0440\u043a\u0438 dev \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f\n\u2502\n\u251c\u2500\u2500 admin_panel.py      # \u0410\u0434\u043c\u0438\u043d \u043f\u0430\u043d\u0435\u043b\u044c\n\u2502\n\u2514\u2500\u2500 main.py             # \u0422\u043e\u0447\u043a\u0430 \u0432\u0445\u043e\u0434\u0430 \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0433\u0434\u0435 \u0441\u043e\u0437\u0434\u0430\u0435\u0442\u0441\u044f \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 FastAPI.\n```\n\n## Use Base Pattern\n\n\u0424\u0443\u043d\u043a\u0446\u0438\u044f `base_pattern` \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0435\u0437\u043d\u044b\u0445 \u0444\u0443\u043d\u043a\u0446\u0438\u0439 \u0432 `app`, \u0432\u043a\u043b\u044e\u0447\u0430\u044f:\n\n-   \u0417\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 `state` \u0438 \u0434\u0440\u0443\u0433\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0443 `app`.\n-   \u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u0435 `CORS`.\n-   \u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0440\u043e\u0443\u0442\u0435\u0440\u043e\u0432 \u0441 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u043e\u0439 `ViewSet`.\n-   \u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043c\u0435\u0442\u043e\u0434\u0430 `healthcheck`.\n-   `Middleware` \u0434\u043b\u044f \u043e\u0442\u043b\u0430\u0434\u043a\u0438 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f API-\u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432.\n-   \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u044b\u0439 \u0432\u044b\u0432\u043e\u0434 \u0434\u043b\u044f `HTTP` \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439.\n\n## Use DatabaseManager\n\n`DatabaseManager` - \u044d\u0442\u043e \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u044b\u0439 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0420\u0421\u0423\u0411\u0414, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u044e\u0449\u0438\u0439 \u043a\u0430\u043a \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0435, \u0442\u0430\u043a \u0438 \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0435(\u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442\u0441\u044f \u043d\u0430 `a`) \u043c\u0435\u0442\u043e\u0434\u044b.\n\n`DatabaseManager` \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043f\u0430\u0442\u0435\u0440 \u043e\u0434\u0438\u043d\u043e\u0447\u043a\u0430.\n\n\u041f\u0440\u0438\u043c\u0435\u0440 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430 \u0411\u0414 \u0432 \u0444\u0430\u0439\u043b\u0435 `app/db/base.py`:\n\n```python\n\"\"\"\u041c\u043e\u0434\u0443\u043b\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0420\u0421\u0423\u0411\u0414\"\"\"\n\nfrom app.core.config import DATABASE_URL, DEBUG, DEV_STATUS\nfrom fastapi_accelerator.dbsession import MainDatabaseManager\n\n# \u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430 \u0434\u043b\u044f \u0420\u0421\u0423\u0411\u0414\nDatabaseManager = MainDatabaseManager(DATABASE_URL, echo=DEBUG, DEV_STATUS=DEV_STATUS)\n```\n\n### \u0421\u043e\u0437\u0434\u0430\u0442\u044c \u043c\u043e\u0434\u0435\u043b\u044c \u0447\u0435\u0440\u0435\u0437 DatabaseManager\n\n```python\nfrom sqlalchemy import Column, Integer, String\n\nfrom app.db.base import DatabaseManager\n\n\nclass User(DatabaseManager.Base):\n    __tablename__ = \"users\"\n\n    id = Column(Integer, primary_key=True, index=True)\n    name = Column(String, index=True)\n    login = Column(String, index=True)\n    pthone = Column(String, index=True)\n    email = Column(String, unique=True, index=True)\n```\n\n### \u0412\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 CRUD \u0447\u0435\u0440\u0435\u0437 DatabaseManager\n\n```python\n# \u0410\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\nclass FileView:\n    @router.get(\"/file\")\n    async def get_files(\n        skip=Query(0),\n        limit=Query(100),\n        aorm: OrmAsync = Depends(DatabaseManager.aget_orm),\n    ) -> List[File]:\n        return await aorm.get_list(FileDb, select(FileDb).offset(skip).limit(limit))\n\n    @router.get(\"/file/{file_uid}\")\n    async def get_file(\n        file_uid: str = Path(),\n        aorm: OrmAsync = Depends(DatabaseManager.aget_orm),\n    ) -> File:\n        return await aorm.get(select(FileDb).filter(FileDb.uid == file_uid))\n\n    @router.post(\"/file\")\n    async def create_file(\n        aorm: OrmAsync = Depends(DatabaseManager.aget_orm),\n    ) -> File:\n        file_uid = uuid.uuid4()\n        new_user = FileDb(uid=file_uid)\n        return await aorm.create_item(new_user)\n\n    @router.put(\"/file/{file_uid}\")\n    async def update_file(\n        file_uid: str = Path(),\n        aorm: OrmAsync = Depends(DatabaseManager.aget_orm),\n    ) -> File:\n        update_data = {\"filename\": \"new\"}\n        return await aorm.update(\n            update(FileDb).filter(FileDb.uid == file_uid), update_data\n        )\n\n    @router.delete(\"/file/{file_uid}\")\n    async def delte_file(\n        file_uid: str = Path(),\n        aorm: OrmAsync = Depends(DatabaseManager.aget_orm),\n    ):\n        return await aorm.delete(delete(FileDb).filter(FileDb.uid == file_uid))\n\n# \u0421\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\n@router.get(\"/file-sync\")\nasync def get_file_sync(\n    session: Session = Depends(DatabaseManager.get_session),\n) -> List[File]:\n    skip = 0\n    limit = 100\n    res = session.query(FileDb).offset(skip).limit(limit).all()\n    return res\n```\n\n### \u0420\u0430\u0431\u043e\u0442\u0430 \u0441 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u044f\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 Alembic\n\n1.  \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430\n\n```bash\npoetry add alembic\n```\n\n2.  \u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0435\u043a\u0442\u0430\n\n```bash\nalembic init alembic\n```\n\n3.  \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c `alembic/env.py`\n\n```python\n# \u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u043c \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430 \u0411\u0414\nfrom app.core.db import DatabaseManager\n\n# > ! \u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043c\u043e\u0434\u0435\u043b\u0438 \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0443\u0436\u043d\u043e \u043e\u0442\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c\nfrom app.models import *  # noqa F401\n\nfrom fastapi_accelerator.pattern.pattern_alembic import AlembicEnv\n\n# \u041f\u0440\u0435\u0434\u043d\u0430\u0441\u0442\u043e\u0435\u043d\u043d\u0430\u044f \u043b\u043e\u0433\u0438\u043a\u0430 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0439 \u0447\u0440\u0435\u0437 Alembic\nAlembicEnv(DatabaseManager).run()\n```\n\n4. \u041c\u043e\u0436\u0435\u043c \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c `alembic.ini`\n\n```ini\n# \u0424\u043e\u0440\u043c\u0430\u0442 \u0434\u043b\u044f \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0444\u0430\u0439\u043b\u0430 \u0441 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0435\u0439\nfile_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s\n```\n\n> \u0412\u0430\u0436\u043d\u044b\u0439 \u0430\u0441\u043f\u0435\u043a\u0442 \u043f\u043e\u0438\u0441\u043a\u0430 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0439\n> \u041d\u0443\u0436\u043d\u043e \u0447\u0442\u043e\u0431\u044b \u043c\u043e\u0434\u0435\u043b\u0438 \u0431\u044b\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u044b \u0432 `alembic/env.py` \u0447\u0442\u043e\u0431\u044b \u044d\u0442\u0438 \u043c\u043e\u0434\u0435\u043b\u0438 \u0437\u0430\u043f\u0438\u0441\u0430\u043b\u0438 \u0441\u0432\u043e\u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 `Base.metadata`\n>\n> \u041f\u043e\u044d\u0442\u043e\u043c\u0443:\n>\n> 1. \u0412 `app.models.__init__.py` \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0441\u0435 \u043c\u043e\u0434\u0435\u043b\u0438\n>\n> ```python\n> from .files import *\n> from .users import *\n> ```\n>\n> 2. \u041d\u0443\u0436\u043d\u043e \u0432 `alembic/env.py` \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0441\u0435 \u043c\u043e\u0434\u0435\u043b\u0438\n>\n> ```python\n> from app.models import *\n> ```\n\n5. \u0421\u043e\u0437\u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0438 \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u0439\u0442\u0435 \u0438\u0445\n\n```bash\nalembic revision --autogenerate\nalembic upgrade head\n```\n\n## Use Cache\n\n-   \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043a\u0435\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 API \u043e\u0442\u0432\u0435\u0442\u0430 \u0447\u0435\u0440\u0435\u0437 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440 `@cache_redis()`\n\n```python\nfrom datetime import timedelta\nfrom fastapi_accelerator.cache import cache_redis\n\n@app.get(f\"files/{{item_id}}\")\n@cache_redis(cache_class=redis_client, cache_ttl=timedelta(minutes=10))\nasync def get_item(\n    request: Request,\n    item_uid: str = Path(...),\n    aorm: OrmAsync = Depends(DatabaseManager.aget_orm),\n) -> FilesSchema:\n    response = await aorm.get(\n        select(Files).filter(Files.id == item_uid)\n    )\n    return response\n```\n\n-   \u041f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430, \u0437\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0444\u0430\u0439\u043b `app/core/cache.py`:\n\n```python\nimport redis.asyncio as redis\n\nfrom app.core.config import REDIS_URL\n\n# \u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0433\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0439 \u043e\u0431\u044a\u0435\u043a\u0442 Redis\nredis_client = redis.from_url(REDIS_URL, encoding=\"utf-8\", decode_responses=True)\n```\n\n## Use ViewSet\n\n1. \u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 `app/api/v1/router.py`\n\n```python\nfrom datetime import timedelta\nfrom typing import List\nfrom uuid import UUID\n\nfrom fastapi import APIRouter, Depends, Query\nfrom pydantic import BaseModel\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\nfrom app.api.v1.schemas.file import File\nfrom app.api.v1.schemas.timemeasurement import TaskExecution\nfrom app.api.v1.schemas.user import User\nfrom app.core.cache import redis_client\nfrom app.core.db import DatabaseManager\nfrom app.models.file import File as FileDb\nfrom app.models.timemeasurement import TaskExecution as TaskExecutionDb\nfrom app.models.users import User as UserDb\nfrom fastapi_accelerator.auth_jwt import jwt_auth\nfrom fastapi_accelerator.db.dbsession import OrmAsync\nfrom fastapi_accelerator.paginator import DefaultPaginator\nfrom fastapi_accelerator.viewset import AppOrm, FullViewSet\n\nrouter = APIRouter(prefix=\"/api/v1\")\n\nclass FileViewSet(FullViewSet):\n    \"\"\"\n    \u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0444\u0430\u0439\u043b\u0430\u043c\u0438\n    \"\"\"\n\n    # \u041c\u043e\u0434\u0435\u043b\u044c \u0411\u0414\n    db_model = FileDb\n    # \u041c\u043e\u0434\u0435\u043b\u044c \u0421\u0445\u0435\u043c\u044b\n    pydantic_model = File\n    # \u041a\u0435\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\n    cache_class = redis_client\n    cache_ttl = timedelta(minutes=10)\n    # \u041f\u0430\u0433\u0438\u043d\u0430\u0446\u0438\u044f\n    paginator_class = DefaultPaginator\n\n    async def db_update(\n        self, item_id: str | int | UUID, item: type[BaseModel], aorm: OrmAsync\n    ) -> object:\n        \"\"\"\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u043c\u0435\u0442\u043e\u0434\u0430 db_update\"\"\"\n        return await super().db_update(item_id, item, aorm)\n\n\nclass UserViewSet(FullViewSet):\n    \"\"\"\n    \u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\u043c\u0438\n    \"\"\"\n\n    # \u041c\u043e\u0434\u0435\u043b\u044c \u0411\u0414\n    db_model = UserDb\n    # \u041c\u043e\u0434\u0435\u043b\u044c \u0421\u0445\u0435\u043c\u044b\n    pydantic_model = User\n\n    def list(self):\n        \"\"\"\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u043c\u0435\u0442\u043e\u0434\u0430 list\"\"\"\n\n        @self.router.get(f\"{self.prefix}\", tags=self.tags)\n        async def get_list_items(\n            skip: int = Query(0),\n            limit: int = Query(100),\n            aorm: OrmAsync = Depends(AppOrm.aget_orm),\n        ) -> List[self.pydantic_model]:\n            return await aorm.get_list(\n                select(self.db_model).offset(skip).limit(limit), deep=self.deep_schema\n            )\n        return get_list_items\n\nclass TaskExecutionViewSet(FullViewSet):\n    \"\"\"\n    \u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0442\u0440\u0443\u0434\u043e\u0437\u0430\u0442\u0440\u0430\u0442\u0430\u043c\u0438\n    \"\"\"\n\n    # \u041c\u043e\u0434\u0435\u043b\u044c \u0411\u0414\n    db_model = TaskExecutionDb\n    # \u041c\u043e\u0434\u0435\u043b\u044c \u0421\u0445\u0435\u043c\u044b\n    pydantic_model = TaskExecution\n\n    # \u041f\u0430\u0433\u0438\u043d\u0430\u0446\u0438\u044f\n    paginator_class = DefaultPaginator\n\n    # \u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 \u0432\u043b\u043e\u0436\u0435\u043d\u043d\u044b\u0445 \u0441\u0445\u0435\u043c pydantic\n    # \u044d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442 \u0447\u0442\u043e \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442\u044c \u0440\u0435\u043a\u0443\u0440\u0441\u0438\u0432\u043d\u043e\u0435\n    # \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435, \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435, \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0437\u0430\u043f\u0438\u0441\u0435\u0439\n    deep_schema = True\n\n    # \u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0437\u0430\u0449\u0438\u0442\u0443 \u0447\u0435\u0440\u0435\u0437 JWT\n    dependencies = [Depends(jwt_auth)]\n\nrouter.views = [\n    FileViewSet().as_view(router, prefix=\"/file\"),\n    UserViewSet().as_view(router, prefix=\"/user\"),\n    TaskExecutionViewSet().as_view(router, prefix=\"/taskexecution\"),\n]\n```\n\n## Use Time Zone\n\n\u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u0435\u043a\u0443\u0449\u0438\u0435 \u0432\u0440\u0435\u043c\u044f \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0441 \u0443\u0447\u0451\u0442\u043e\u043c \u0435\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u0437\u043e\u043d\u044b\n\n```python\nfrom fastapi_accelerator.timezone import get_datetime_now\n\n# \u0412\u0430\u0440\u0438\u0430\u043d\u0442 1\nget_datetime_now(request.app).isoformat()\n# \u0412\u0430\u0440\u0438\u0430\u043d\u0442 2\nget_datetime_now(app).isoformat()\n```\n\n## Use HTTPException\n\n-   \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435:\n\n```python\nfrom fastapi_accelerator.exception import HTTPException403\n\n@router.get(\"/\")\nasync def get_users():\n    if True:\n        raise HTTPException403()\n    return [{\"user_id\": \"user1\"}, {\"user_id\": \"user2\"}]\n```\n\n## Use AuthJWT\n\n\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0447\u0435\u0440\u0435\u0437 JWT\n\n-   \u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a FastAPI \u043f\u0440\u043e\u0435\u043a\u0442\u0443:\n\n```python\nfrom fastapi_accelerator.auth_jwt import BaseAuthJWT\n\nclass AuthJWT(BaseAuthJWT):\n    def check_auth(username: str, password: str) -> bool:\n        \"\"\"\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0432\u0432\u0435\u0434\u0435\u043d\u043d\u043e\u0433\u043e \u043b\u043e\u0433\u0438\u043d\u0430 \u0438 \u043f\u0430\u0440\u043e\u043b\u044f.\"\"\"\n        return username == \"admin\" and password == \"admin\"\n\n    def add_jwt_body(username: str) -> dict:\n        \"\"\"\u0424\u0443\u043d\u043a\u0446\u0438\u044f \u0434\u043b\u044f \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u0432 JWT \u0442\u043e\u043a\u0435\u043d \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\"\"\"\n        return {\"version\": username.title()}\n\n\n# \u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u043f\u043e JWT\nAuthJWT.mount_auth(app)\n```\n\n-   \u041f\u0440\u0438\u043c\u0435\u0440 \u0437\u0430\u0449\u0438\u0442\u044b API \u043c\u0435\u0442\u043e\u0434\u0430:\n\n```python\nfrom fastapi_accelerator.auth_jwt import jwt_auth\n\n@app.get(\"/check_protected\", summary=\"\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u043f\u043e JWT\")\nasync def protected_route(jwt: dict = Depends(jwt_auth)):\n    return {\"message\": \"This is a protected route\", \"user\": jwt}\n```\n\n## Use Admin Panel\n\n1.  \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430\n\n```bash\npoetry add flask-admin\n```\n\n2. \u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0444\u0430\u0439\u043b `admin_panel.py`\n\n```python\nfrom flask import Flask\n\nfrom app.core.config import ADMIN_PASSWORD, ADMIN_USERNAME, SECRET_KEY\nfrom app.db.base import DatabaseManager\nfrom app.models import File, User\nfrom fastapi_accelerator.pattern_flask_admin import base_pattern\n\napp = Flask(__name__)\n\nadmin = base_pattern(\n    app,\n    SECRET_KEY,\n    ADMIN_PASSWORD,\n    ADMIN_USERNAME,\n    # > \u041c\u043e\u0434\u0435\u043b\u0438 \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0443\u0436\u043d\u044b \u0432 \u0430\u0434\u043c\u0438\u043d \u043f\u0430\u043d\u0435\u043b\u0438\n    models=[User, File],\n    database_manager_sync=DatabaseManager,\n)\n\n\nif __name__ == \"__main__\":\n    app.run(\n        host=\"0.0.0.0\",\n        port=8001,\n        debug=True,\n    )\n```\n\n3. \u0417\u0430\u043f\u0443\u0441\u043a\u0430\u0442\u044c `python admin_panel.py`\n\n4. \u0412\u043e\u0439\u0442\u0438 \u0432 \u0430\u0434\u043c\u0438\u043d \u043f\u0430\u043d\u0435\u043b\u044c:\n\n-   `http://localhost:8233/admin`\n-   `http://localhost:8233/login`\n-   `http://localhost:8233/logout`\n\n# Common \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 FastAPI\n\n## \u0417\u0430\u0447\u0435\u043c \u0438 \u043a\u0430\u043a \u043f\u0438\u0441\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u044b\n\n\u0420\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435: REST API, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0435\u0433\u043e \u0440\u0435\u043b\u044f\u0446\u0438\u043e\u043d\u043d\u0443\u044e \u0421\u0423\u0411\u0414 \u0438 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u044e\u0449\u0435\u0433\u043e \u043e\u0442\u0432\u0435\u0442\u044b \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 JSON.\n\n\u042d\u0442\u043e \u043e\u0434\u0438\u043d \u0438\u0437 \u043d\u0430\u0438\u0431\u043e\u043b\u0435\u0435 \u0440\u0430\u0441\u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0435\u043d\u043d\u044b\u0445 \u043f\u043e\u0434\u0445\u043e\u0434\u043e\u0432 \u043a \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u043c\u0443 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044e, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u044d\u043c\u0443\u043b\u0438\u0440\u0443\u0435\u0442 \u0440\u0443\u0447\u043d\u0443\u044e \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 API \u043c\u0435\u0442\u043e\u0434\u043e\u0432. \u0422\u0430\u043a\u043e\u0439 \u043f\u043e\u0434\u0445\u043e\u0434 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u043a\u0440\u044b\u0442\u044c \u0437\u043d\u0430\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0438 \u0442\u0435\u0441\u0442\u0430\u043c\u0438, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f API \u2013 \u0445\u043e\u0440\u043e\u0448\u0438\u0439 \u0441\u0442\u0430\u0440\u0442. \u042d\u0442\u0438 \u0442\u0435\u0441\u0442\u044b \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u044f\u0442 \u0440\u0443\u0447\u043d\u043e\u0435 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435, \u043d\u043e \u0438 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u044e\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u043f\u043e\u0431\u043e\u0447\u043d\u044b\u0435 \u044d\u0444\u0444\u0435\u043a\u0442\u044b \u0432 \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445, \u0442\u0430\u043a\u0438\u0435 \u043a\u0430\u043a \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043d\u043e\u0432\u044b\u0445 \u0437\u0430\u043f\u0438\u0441\u0435\u0439 \u043f\u043e\u0441\u043b\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f POST-\u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432.\n\n\u0420\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c \u0442\u0430\u043a\u0438\u0435 \u0442\u0435\u0441\u0442\u044b \u0432 `Django` \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043f\u0440\u043e\u0441\u0442\u043e \u0431\u043b\u0430\u0433\u043e\u0434\u0430\u0440\u044f \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u043c \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u0430\u043c, \u043e\u0434\u043d\u0430\u043a\u043e \u0432 `FastAPI` \u044d\u0442\u0430 \u0437\u0430\u0434\u0430\u0447\u0430 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0431\u043e\u043b\u044c\u0448\u0435\u0433\u043e \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u044f. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0430\u043b \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u044e\u0442 \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0435 \u0442\u0435\u0441\u0442\u044b API \u043d\u0430 `FastAPI` \u0442\u0430\u043a \u0436\u0435 \u0443\u0434\u043e\u0431\u043d\u043e \u0438 \u0431\u044b\u0441\u0442\u0440\u043e, \u043a\u0430\u043a \u0438 \u0432 `Django`.\n\n## \u041f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\n\n1. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430\n\n```bash\npoetry add pytest pytest-asyncio httpx\n```\n\n2. \u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0444\u0430\u0439\u043b `app/pytest.ini`\n\n```ini\n[pytest]\n; \u0414\u043e\u043f \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u044b \u043a \u0437\u0430\u043f\u0443\u0441\u043a\u0443\naddopts = -v -l -x -s --lf --disable-warnings\n\n; \u041c\u0430\u0441\u043a\u0430 \u0434\u043b\u044f \u043f\u043e\u0438\u0441\u043a\u0430 \u0444\u0430\u0439\u043b\u043e\u0432 \u0441 \u0442\u0435\u0441\u0442\u0430\u043c\u0438\npython_files = tests.py test_*.py *_tests.py\n\n; \u041f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0432\u044b\u0432\u043e\u0434\u0438\u0442\u044c \u043b\u043e\u0433\u0438 (logs) \u0432 \u043a\u043e\u043d\u0441\u043e\u043b\u044c \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435 \u0442\u0435\u0441\u0442\u043e\u0432.\nlog_cli = true\n```\n\n3. \u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0444\u0430\u0439\u043b `app/conftest.py`\n\n```python\nfrom app.core.config import TEST_DATABASE_URL\nfrom fastapi_accelerator.db.dbsession import MainDatabaseManager\n\n# \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0442\u043e\u0447\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u043c\u043f\u043e\u0440\u0442\u0430, \u044d\u0442\u043e \u0434\u043b\u044f \u043f\u0440\u043e\u0441\u0442\u043e\u0442\u044b \u043c\u044b \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u043c \u0432\u0441\u0435\nfrom fastapi_accelerator.testutils import *  # noqa E402\n\n# \u041d\u0443\u0436\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0411\u0414 \u0434\u043e \u0438\u043c\u043f\u043e\u0440\u0442\u0430 APP\n# \u0447\u0442\u043e\u0431\u044b \u043f\u0430\u0442\u0442\u0435\u0440\u043d \u043e\u0434\u0438\u043d\u043e\u0447\u043a\u0430 \u0441\u043e\u0437\u0434\u0430\u043b \u0442\u043e\u043b\u044c\u043a\u043e \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0435 instance\n# \u0438 \u0432 APP \u0443\u0436\u0435 \u0432\u0437\u044f\u043b\u0441\u044f \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0439 instance\nTestDatabaseManager = MainDatabaseManager(\n    TEST_DATABASE_URL, echo=False, DEV_STATUS=True\n)\n\nfrom main import app  # noqa E402\n\n# \u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a\u0435\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0442\u0435\u0441\u0442\u043e\u0432\napp.state.CACHE_STATUS = False\n\nSettingTest(TestDatabaseManager, app, alembic_migrate=True, keepdb=True) # noqa F405\n```\n\n## \u041e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\n\n\u0414\u043b\u044f \u0443\u043f\u0440\u043e\u0449\u0435\u043d\u0438\u044f \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0442\u0435\u0441\u0442\u043e\u0432, \u0438\u0445 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u0438\u0437\u0430\u0446\u0438\u044e, \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b:\n\n-   \u0424\u0438\u043a\u0441\u0442\u0443\u0440\u044b:\n\n    -   `client` - \u041a\u043b\u0438\u0435\u043d\u0442 \u0434\u043b\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0445 API \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432\n    -   `test_app` - \u0422\u0435\u0441\u0442\u043e\u0432\u043e\u0435 FastAPI \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\n    -   `url_path_for` - \u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043f\u043e\u043b\u043d\u044b\u0439 URL \u043f\u0443\u0442\u044c, \u043f\u043e \u0438\u043c\u0435\u043d\u0438 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430\n    -   `engine` - \u0421\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0439 \u0434\u0432\u0438\u0433\u0430\u0442\u0435\u043b\u044c\n    -   `aengine` - \u0410\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0439 \u0434\u0432\u0438\u0433\u0430\u0442\u0435\u043b\u044c\n    -   `db_session` - \u0421\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0411\u0414\n    -   `db_manager` - \u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0441 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0411\u0414\n\n-   \u0424\u0443\u043d\u043a\u0446\u0438\u0438:\n\n    -   `check_response_json` - \u0424\u0443\u043d\u043a\u0446\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043e\u0431\u044a\u0435\u0434\u0438\u043d\u044f\u0435\u0442 \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u0434\u043b\u044f API \u043e\u0442\u0432\u0435\u0442\u0430\n    -   `rm_key_from_deep_dict` - \u0424\u0443\u043d\u043a\u0446\u0438\u044f \u0434\u043b\u044f \u043e\u0442\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u043d\u0435 \u043d\u0443\u0436\u043d\u044b\u0435 \u043a\u043b\u044e\u0447\u0438 \u0443 API \u043e\u0442\u0432\u0435\u0442\u0430\n\n-   \u041a\u043b\u0430\u0441\u0441\u044b:\n\n    -   `BasePytest` - \u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u043b\u0430\u0441\u0441 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0447\u0435\u0440\u0435\u0437 \u043a\u043b\u0430\u0441\u0441\u044b\n    -   `BaseAuthJwtPytest` - \u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u043e JWT(`@client_auth_jwt`) \u0434\u043b\u044f `BasePytest`\n\n-   \u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u044b\u0439 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440:\n\n    -   `track_queries` - \u041f\u0435\u0440\u0435\u0445\u0432\u0430\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u043c\u044b\u0445 SQL \u043a\u043e\u043c\u0430\u043d\u0434, \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430, \u0434\u043b\u044f \u043f\u043e\u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0433\u043e \u0430\u043d\u0430\u043b\u0438\u0437 - \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u043f\u043e\u0434\u0441\u0447\u0451\u0442\u0430 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u0430.\n\n-   \u0414\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u044b:\n\n    -   `@apply_fixture_db(\u0424\u0443\u043d\u043a\u0446\u0438\u044f\u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u044e\u0449\u0430\u044f\u0424\u0438\u043a\u0441\u0442\u0443\u0440\u044b)` - \u0414\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u044b \u0432 \u0411\u0414 \u043f\u0435\u0440\u0435\u0434 \u0442\u0435\u0441\u0442\u043e\u043c \u0438 \u0443\u0434\u0430\u043b\u044f\u0435\u0442 \u0438\u0445 \u043f\u043e\u0441\u043b\u0435 \u0442\u0435\u0441\u0442\u0430.\n    -   `@client_auth_jwt()` - \u0414\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u0442 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0433\u043e \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043f\u043e JWT.\n\n## \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u043f\u0440\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\n\n### \u0424\u0438\u043a\u0441\u0442\u0443\u0440\u0430 `client`\n\n\u041e\u0441\u043d\u043e\u0432\u043d\u0430\u044f \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u0430 \u0434\u043b\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0445 API \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432.\n\n\u041f\u043e\u0440\u044f\u0434\u043e\u043a \u0440\u0430\u0431\u043e\u0442\u044b \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u044b `client`:\n\n-   \u042d\u0442\u0430\u043f\u044b \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435\u0439 \u0432\u0441\u0435\u0439 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0441\u0435\u0441\u0441\u0438\u0438:\n\n    1. (before) \u0421\u043e\u0437\u0434\u0430\u0441\u0442\u0441\u044f \u0442\u0435\u0441\u0442\u043e\u0432\u0430\u044f \u0411\u0414 \u0435\u0441\u043b\u0438 \u0435\u0451 \u043d\u0435\u0442;\n    2. (before) \u0412 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 `SettingTest.alembic_migrate`;\n\n        - \u0415\u0441\u043b\u0438 `True` -> \u0421\u043e\u0437\u0434\u0430\u0441\u0442 \u0442\u0430\u0431\u043b\u0438\u0446\u044b \u0447\u0435\u0440\u0435\u0437 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 `alembic`\n        - \u0415\u0441\u043b\u0438 `False` -> \u0421\u043e\u0437\u0434\u0430\u0441\u0442 \u0442\u0430\u0431\u043b\u0438\u0446\u044b \u0447\u0435\u0440\u0435\u0437 `create_all()`\n\n    3. (after) \u041f\u043e\u0441\u043b\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0435 \u0432\u0441\u0435\u0445 \u0442\u0435\u0441\u0442\u043e\u0432 \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 `SettingTest.keepdb`;\n        - \u0415\u0441\u043b\u0438 `True` -> \u041d\u0438\u0447\u0435\u0433\u043e\n        - \u0415\u0441\u043b\u0438 `False` -> \u0423\u0434\u0430\u043b\u044f\u0442\u044c\u0441\u044f \u0432\u0441\u0435 \u0442\u0430\u0431\u043b\u0438\u0446\u044b \u0438\u0437 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0411\u0414\n\n-   \u042d\u0442\u0430\u043f\u044b \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435\u0439 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0433\u043e \u0444\u0443\u043d\u043a\u0446\u0438\u0438/\u043c\u0435\u0442\u043e\u0434\u0430:\n\n    3. \u0412 \u0442\u0435\u0441\u0442\u043e\u0432\u0443\u044e \u0444\u0443\u043d\u043a\u0446\u0438\u044e/\u043c\u0435\u0442\u043e\u0434\u0430 \u043f\u043e\u043f\u0430\u0434\u0430\u0435\u0442 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442 `client: TestClient`;\n    4. (after) \u041f\u043e\u0441\u043b\u0435 \u0432\u044b\u0445\u043e\u0434\u0430 \u0438\u0437 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u0438/\u043c\u0435\u0442\u043e\u0434\u0430, \u0432\u0441\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0432\u043e \u0432\u0441\u0435\u0445 \u0442\u0430\u0431\u043b\u0438\u0446\u0430\u0445 \u043e\u0442\u0447\u0438\u0449\u0430\u044e\u0442\u0441\u044f(\u043a\u0440\u043e\u043c\u0435 \u0442\u0430\u0431\u043b\u0438\u0446\u044b `alembic_version`, \u0442\u0430\u043a \u043a\u0430\u043a \u0441\u0430\u043c\u0443 \u0411\u0414 \u043c\u044b \u043d\u0435 \u0443\u0434\u0430\u043b\u044f\u0435\u043c);\n\n```python\nfrom fastapi.testclient import TestClient\n\ndef test_\u0438\u043c\u044f(client: TestClient):\n    response = client.get('url')\n```\n\n### \u0414\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440 `@client_auth_jwt`\n\n\u041d\u0430 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0435 \u043d\u0430\u043c \u0447\u0430\u0441\u0442\u043e \u043f\u0440\u0438\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c API \u043c\u0435\u0442\u043e\u0434\u044b \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0442\u0440\u0435\u0431\u0443\u044e\u0442 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e. \u0414\u0435\u043b\u0430\u0442\u044c \u043e\u0431\u0445\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0432 \u0442\u0435\u0441\u0442\u0430\u0445 \u043f\u043b\u043e\u0445\u043e\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442, \u0442\u0430\u043a \u043a\u0430\u043a \u043c\u043e\u0436\u043d\u043e \u0443\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f, \u0438\u043b\u0438 \u043b\u043e\u0433\u0438\u043a\u0443 API \u043c\u0435\u0442\u043e\u0434\u0430 \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0437\u0430\u0432\u044f\u0437\u0430\u043d\u0430 \u043d\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u0447\u0442\u043e\u0431\u044b \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0433\u043e \u043a\u043b\u0438\u0435\u043d\u0442\u0430, \u0443\u043a\u0430\u0436\u0438\u0442\u0435 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440 `@client_auth_jwt` \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u0438/\u043c\u0435\u0442\u043e\u0434\u0430\n\n-   \u041f\u0440\u0438\u043c\u0435\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u0430 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u0438:\n\n```python\nfrom fastapi.testclient import TestClient\nfrom fastapi_accelerator.testutils.fixture_auth import client_auth_jwt\n\n@client_auth_jwt(username='test')\ndef test_\u0438\u043c\u044f(client: TestClient):\n    print(client.headers['authorization']) # 'Bearer ...'\n```\n\n-   \u041f\u0440\u0438\u043c\u0435\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u0430 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430 \u0432 \u043a\u043b\u0430\u0441\u0441\u0435 `BasePytest`:\n\n```python\nfrom fastapi.testclient import TestClient\nfrom fastapi_accelerator.testutils.fixture_base import BasePytest\nfrom fastapi_accelerator.testutils.fixture_auth import client_auth_jwt\n\nclass Test\u0418\u043c\u044f\u041a\u043b\u0430\u0441\u0441\u0430(BasePytest):\n\n    @client_auth_jwt()\n    def test_\u0438\u043c\u044f(self, client: TestClient):\n        print(client.headers['authorization']) # 'Bearer ...'\n```\n\n> \u0415\u0441\u043b\u0438 \u0432\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440 `@client_auth_jwt` \u0432 \u043a\u043b\u0430\u0441\u0441\u0435 `BasePytest`, \u0442\u043e \u043e\u043d \u0432\u043e\u0437\u044c\u043c\u0435\u0442 `username` \u0438\u0437 `self.TEST_USER['username']`, \u044d\u0442\u043e\u0442 \u0430\u0442\u0440\u0438\u0431\u0443\u0442 \u0443\u0436\u0435 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d \u0432 `BasePytest` \u0438 \u0440\u0430\u0432\u0435\u043d \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e `test`.\n\n### \u0414\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440 `@apply_fixture_db`\n\n\u0418\u0434\u0435\u044f \u0432\u0437\u044f\u0442\u0430 \u0438\u0437 `Django` \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u043c\u043e\u0436\u043d\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0432 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0435 `fixtures` \u0441\u043f\u0438\u0441\u043e\u043a \u0444\u0430\u0439\u043b\u043e\u0432 \u0441 \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u0430\u043c\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d\u044b \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432, \u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u044b \u043f\u043e\u0441\u043b\u0435 \u043e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f. \u042d\u0442\u043e\u0442 \u043e\u0447\u0435\u043d\u044c \u0443\u0434\u043e\u0431\u043d\u043e \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445.\n\n\u041d\u043e \u044f \u0440\u0435\u0448\u0438\u043b \u043c\u043e\u0434\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0438 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u044b \u043d\u0435 \u0432 \u0432\u0438\u0434\u0435 `JSON` \u0430 \u0432\u0438\u0434\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 `SqlAlchemy`. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 `JSON` \u043b\u0443\u0447\u0448\u0435 \u043a\u043e\u0433\u0434\u0430 \u043d\u0443\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u043d\u043e\u0441\u0438\u0442\u044c \u044d\u0442\u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u043d\u0430 \u0434\u0440\u0443\u0433\u0438\u0435 \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u044b, \u043d\u043e \u0442\u0430\u043a\u043e\u0435 \u0432\u0441\u0442\u0440\u0435\u0447\u0430\u0435\u0442\u0441\u044f \u0440\u0435\u0434\u043a\u043e, \u0447\u0430\u0449\u0435 \u0432\u0441\u0435\u0433\u043e \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u044b \u0434\u043b\u044f backend \u0442\u0435\u0441\u0442\u043e\u0432 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043d\u0430 backend, \u0438 \u0433\u043e\u0440\u0430\u0437\u0434\u0430 \u0443\u0434\u043e\u0431\u043d\u0435\u0435 \u0438 \u0431\u044b\u0441\u0442\u0440\u0435\u0435 \u043f\u0438\u0441\u0430\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0411\u0414, \u0447\u0435\u043c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 `JSON`. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u0432\u044b\u0431\u0440\u0430\u043d \u0444\u043e\u0440\u043c\u0430\u0442 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432.\n\n\u041f\u043e\u0440\u044f\u0434\u043e\u043a \u0440\u0430\u0431\u043e\u0442\u044b \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u0430 `@apply_fixture_db`:\n\n1. \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u0437\u0430\u043f\u0438\u0441\u0438 \u0438\u0437 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u043e\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 `export_func`;\n2. \u0421\u043e\u0437\u0434\u0430\u0435\u0442 \u0437\u0430\u043f\u0438\u0441\u0438 \u0432 \u0411\u0414;\n3. \u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u0442\u0435\u0441\u0442\u043e\u0432\u0430\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u044f. \u0415\u0441\u043b\u0438 \u043e\u043d\u0430 \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442 `fixtures`, \u0442\u043e \u0432 \u043d\u0435\u0433\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0434\u0443\u0442\u0441\u044f \u0437\u0430\u043f\u0438\u0441\u0438 \u0438\u0437 `export_func`;\n4. \u0423\u0434\u0430\u043b\u044f\u0435\u0442 \u0437\u0430\u043f\u0438\u0441\u0438 \u0438\u0437 \u0411\u0414:\n    - \u0415\u0441\u043b\u0438 \u0432\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u0443 `client`, \u0442\u043e \u043e\u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u0442\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u0432\u0441\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u0442\u0430\u0431\u043b\u0438\u0446\u0430\u0445, \u043f\u043e\u0441\u043b\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u0438.\n    - \u0415\u0441\u043b\u0438 \u0432\u044b \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u0443 `client`, \u0442\u043e \u0434\u043b\u044f \u043e\u0442\u0447\u0438\u0441\u0442\u043a\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 \u0443\u043a\u0430\u0436\u0438\u0442\u0435 \u0432 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442 `flush=True`\n\n---\n\n-   \u041e\u0444\u043e\u0440\u043c\u043b\u0435\u043d\u0438\u0435 \u0444\u0430\u0439\u043b\u0430\u043c\u0438 \u0441 \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u043c\u0438 `app.fixture.items_v1.py`:\n\n```python\nfrom fastapi_accelerator.testutils.utils import to_namedtuple\nfrom app.models.timemeasurement import Task, TaskExecution, TaskUser\n\ndef export_fixture_task():\n    # \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0438 \u0437\u0430\u0434\u0430\u0447\n    user1 = TaskUser(id=0, name=\"Alice\")\n    user2 = TaskUser(id=1, name=\"Bob\")\n\n    task1 = Task(id=9, name=\"Admins\")\n    task2 = Task(id=8, name=\"Users\")\n\n    # \u0421\u0432\u044f\u0437\u044b\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0441 \u0437\u0430\u0434\u0430\u0447\u0430\u043c\u0438\n    user1.tasks.append(task1)\n    user2.tasks.append(task1)\n    user2.tasks.append(task2)\n\n    # \u0412\u0435\u0440\u043d\u0443\u0442\u044c \u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0439 \u043a\u0430\u0440\u0442\u0435\u0436\n    return to_namedtuple(\n        user1=user1,\n        user2=user2,\n        task1=task1,\n        task2=task2,\n        task_execution1=TaskExecution(\n            id=91,\n            task=task1,\n            start_time=\"2024-09-06T10:55:43\",\n            end_time=\"2024-09-06T10:59:43\",\n        ),\n    )\n```\n\n-   \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u0430 \u0432 \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0445 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u0445:\n\n```python\nfrom fastapi_accelerator.test_utils import apply_fixture_db\nfrom app.fixture.items_v1 import export_fixture_task\n\n@apply_fixture_db(export_fixture_task)\ndef test_\u0438\u043c\u044f(client: TestClient):\n    response = client.get('url')\n```\n\n-   \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u0430 \u0432 \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0445 \u043c\u0435\u0442\u043e\u0434\u0430\u0445, \u0432 \u044d\u0442\u043e\u043c \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0435 \u0432\u044b \u043c\u043e\u0436\u043d\u043e \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f `setUp`, \u0442\u043e\u0433\u0434\u0430 \u043e\u043d \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d \u0434\u043b\u044f \u0432\u0441\u0435\u0445 \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0445 \u043c\u0435\u0442\u043e\u0434\u043e\u0432:\n\n```python\nfrom fastapi.testclient import TestClient\nfrom fastapi_accelerator.testutils.fixture_base import BasePytest\nfrom fastapi_accelerator.test_utils import apply_fixture_db\nfrom app.fixture.items_v1 import export_fixture_task\n\nclass Test\u0418\u043c\u044f\u041a\u043b\u0430\u0441\u0441\u0430(BasePytest):\n\n    @apply_fixture_db(export_fixture_task)\n    def setUp(self, fixtures: NamedTuple):\n        self.fixtures = fixtures\n\n    def test_\u0438\u043c\u044f(self, client: TestClient):\n        response = client.get('url')\n        print(self.fixtures)\n```\n\n### \u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u044b\u0439 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 `track_queries`\n\n\u0418\u0434\u0435\u044f \u0432\u0437\u044f\u0442\u0430 \u0438\u0437 `Django` \u043c\u0435\u0442\u043e\u0434\u0430 `self.assertNumQueries`, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u043e\u0432\u0430\u043b\u044f\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u044b\u0445 SQL \u043a\u043e\u043c\u0430\u043d\u0434 \u0432 \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0435. \u042d\u0442\u043e \u043e\u0447\u0435\u043d\u044c \u043f\u043e\u043b\u0435\u0437\u043d\u043e \u043a\u043e\u0433\u0434\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f ORM, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u043e\u0436\u0435\u0442 \u0438\u0437 \u0437\u0430 \u043d\u0435\u0430\u043a\u043a\u0443\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f, \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0442\u043d\u0438 SQL \u043a\u043e\u043c\u0430\u043d\u0434. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u043b\u0443\u0447\u0448\u0435 \u0443 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0433\u043e API \u043c\u0435\u0442\u043e\u0434\u0430 \u043e\u0442\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u044b\u0445 SQL \u043a\u043e\u043c\u0430\u043d\u0434.\n\n-   \u041f\u0440\u0438\u043c\u0435\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0433\u043e \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430 `track_queries`:\n\n```python\nfrom fastapi_accelerator.testutils.fixture_db.trace_sql import track_queries\n\ndef test_\u0438\u043c\u044f(client: TestClient, db_manager: MainDatabaseManager):\n    with track_queries(db_manager, expected_count=3):\n        response = client.get('url')\n```\n\n-   \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043f\u043e\u043b\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u044b\u0445 SQL \u043a\u043e\u043c\u0430\u043d\u0434 \u0438\u0437 `tracker.queries`:\n\n```python\nfrom fastapi_accelerator.testutils.fixture_db.trace_sql import track_queries\n\ndef test_\u0438\u043c\u044f(client: TestClient, db_manager: MainDatabaseManager):\n    with track_queries(db_manager) as tracker:\n        response = client.get('url')\n\n    # \u0415\u0441\u043b\u0438 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c\u0441\u044f, \u0442\u043e \u0432\u044b\u0432\u0435\u0434\u0435\u0442\u0441\u044f \u0441\u043f\u0438\u0441\u043e\u043a \u0432\u0441\u0435\u0445 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u044b\u0445 SQL \u043a\u043e\u043c\u0430\u043d\u0434\n    assert tracker.count == 3, tracker.queries\n```\n\n### \u0424\u0443\u043d\u043a\u0446\u0438\u044f `check_response_json`\n\n\u041f\u043e \u043e\u043f\u044b\u0442\u0443 \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0442\u0435\u0441\u0442\u043e\u0432, \u043c\u043e\u0433\u0443 \u0432\u044b\u0434\u0435\u043b\u0438\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0445 \u043f\u0440\u043e\u0432\u0435\u0440\u043e\u043a \u0434\u043b\u044f \u043e\u0442\u0432\u0435\u0442\u043e\u0432 API JSON.\n\n1. \u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0441\u0442\u0430\u0442\u0443\u0441 \u043e\u0442\u0432\u0435\u0442\u0430\n2. \u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043e\u0442\u0432\u0435\u0442 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 JSON\n3. \u0415\u0441\u043b\u0438 \u043d\u0443\u0436\u043d\u043e, \u0442\u043e \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0434\u0438\u043d\u0430\u043c\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u043a\u043b\u044e\u0447\u0438 \u0438\u0437 \u043e\u0442\u0432\u0435\u0442\u0430, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0434\u0430\u0442\u0443 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f, \u0434\u0430\u0442\u0443 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f, \u043f\u0435\u0440\u0432\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043d\u043e\u0432\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438. \u0420\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0447\u0435\u0440\u0435\u0437 \u0444\u0443\u043d\u043a\u0446\u0438\u044e `rm_key_from_deep_dict`\n4. \u0421\u0440\u0430\u0432\u043d\u0438\u0442\u044c \u043e\u0442\u0432\u0435\u0442 \u0441 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u043c\n\n\u042d\u0442\u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044e\u0442\u0441\u044f \u0432 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 `check_response_json`\n\n\u041f\u0440\u0438\u043c\u0435\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f:\n\n```python\ndef test_\u0438\u043c\u044f(client: TestClient):\n    response = client.post('url', json={...})\n    check_response_json(\n        response,\n        200,\n        {\n            \"page\": 1,\n            \"size\": 10,\n            \"count\": 1,\n            \"items\": [\n                {\n                    \"end_time\": \"2024-09-06T10:59:43\",\n                    \"start_time\": \"2024-09-06T10:55:43\",\n                    \"task\": {\n                        \"description\": None,\n                        \"name\": \"Admins\",\n                    },\n                }\n            ],\n        },\n        exclude_list=['id','task_id']\n    )\n```\n\n### \u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0447\u0435\u0440\u0435\u0437 \u043a\u043b\u0430\u0441\u0441\u044b\n\n#### \u041a\u043b\u0430\u0441\u0441 `BasePytest`\n\n\u0423\u0434\u043e\u0431\u043d\u0435\u0435 \u0438 \u043f\u043e\u043d\u044f\u0442\u043d\u0435\u0435, \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0438 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0442\u0435\u0441\u0442\u044b \u0432 \u043e\u0434\u043d\u043e\u043c \u043a\u043b\u0430\u0441\u0441\u0435, \u0438 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0432 \u043c\u0435\u0442\u043e\u0434\u0435 `setUp` \u043e\u0431\u0449\u0443\u044e \u0434\u043b\u044f \u043d\u0438\u0445 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u043e\u0431\u0449\u0438\u0439 url, \u0438\u043b\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0445 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0432 \u0411\u0414, \u0438\u043b\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445 \u0445\u0440\u0430\u043d\u044f\u0449\u0438\u0445 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0439 JSON \u043e\u0442\u0432\u0435\u0442.\n\n-   \u041f\u0440\u0438\u043c\u0435\u0440 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043a\u043b\u0430\u0441\u0441\u0430 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u0435 `BasePytest`:\n\n```python\nfrom fastapi.testclient import TestClient\nfrom fastapi_accelerator.testutils.fixture_base import BasePytest\n\nclass Test\u0418\u043c\u044f\u041a\u043b\u0430\u0441\u0441\u0430(BasePytest):\n\n    def setUp(self):\n        # \u041c\u0435\u0442\u043e\u0434 \u0434\u043b\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0435\u0440\u0435\u0434 \u043a\u0430\u0436\u0434\u044b\u043c \u0442\u0435\u0441\u0442\u043e\u043c.\n        ...\n\n    def test_\u0438\u043c\u044f(self, client: TestClient):\n        ...\n```\n\n-   \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u044b \u0438 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u044b \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0445 \u043c\u0435\u0442\u043e\u0434\u0430\u0445, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0439 \u043f\u043e JWT:\n\n```python\nfrom fastapi.testclient import TestClient\nfrom fastapi_accelerator.testutils.fixture_base import BasePytest\nfrom fastapi_accelerator.testutils.fixture_auth import client_auth_jwt\n\nclass Test\u0418\u043c\u044f\u041a\u043b\u0430\u0441\u0441\u0430(BasePytest):\n\n    def setUp(self):\n        # \u041c\u0435\u0442\u043e\u0434 \u0434\u043b\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0435\u0440\u0435\u0434 \u043a\u0430\u0436\u0434\u044b\u043c \u0442\u0435\u0441\u0442\u043e\u043c.\n        ...\n\n    @client_auth_jwt()\n    def test_\u0438\u043c\u044f(self, client: TestClient):\n        print(client.headers['authorization']) # 'Bearer ...'\n        ...\n```\n\n#### \u041a\u043b\u0430\u0441\u0441 `BaseAuthJwtPytest`\n\n\u0427\u0442\u043e\u0431\u044b \u043d\u0435 \u043f\u0438\u0441\u0430\u0442\u044c \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430 \u0432 \u043a\u043b\u0430\u0441\u0441\u0435, \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440 `@client_auth_jwt` \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043e\u0442 `BaseAuthJwtPytest`, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u044d\u0442\u0430 \u043b\u043e\u0433\u0438\u043a\u0430 \u0443\u0436\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430.\n\n-   \u041f\u0440\u0438\u043c\u0435\u0440 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043a\u043b\u0430\u0441\u0441\u0430 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u0435 `BaseAuthJwtPytest`:\n\n```python\nfrom fastapi.testclient import TestClient\nfrom fastapi_accelerator.testutils.fixture_base import BaseAuthJwtPytest\n\nclass Test\u0418\u043c\u044f\u041a\u043b\u0430\u0441\u0441\u0430(BaseAuthJwtPytest):\n\n    def setUp(self):\n        # \u041c\u0435\u0442\u043e\u0434 \u0434\u043b\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0435\u0440\u0435\u0434 \u043a\u0430\u0436\u0434\u044b\u043c \u0442\u0435\u0441\u0442\u043e\u043c.\n        ...\n\n    def test_\u0438\u043c\u044f(self, client: TestClient):\n        print(client.headers['authorization']) # 'Bearer ...'\n        ...\n```\n\n## \u041f\u0440\u0438\u043c\u0435\u0440\u044b \u0442\u0435\u0441\u0442\u043e\u0432\n\n### \u041a\u043b\u0430\u0441\u0441\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u0438\n\n\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 REST API \u043c\u0435\u0442\u043e\u0434\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0420\u0421\u0423\u0411\u0414, \u0438 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0442\u0432\u0435\u0442 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 `JSON`:\n\n```python\nfrom typing import Callable, NamedTuple\n\nfrom fastapi.testclient import TestClient\n\nfrom app.fixture.items_v1 import export_fixture_file\nfrom fastapi_accelerator.db.dbsession import MainDatabaseManager\nfrom fastapi_accelerator.testutils import apply_fixture_db, client_auth_jwt, track_queries, check_response_json\n\n# \u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0433\u043e \u043a\u043b\u0438\u0435\u043d\u0442\u0430\n@client_auth_jwt(username=\"test\")\n# \u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0438\u0437 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u0441 \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u0430\u043c\u0438\n@apply_fixture_db(export_fixture_file)\ndef test_\u0438\u043c\u044f(\n    client: TestClient,  # \u0422\u0435\u0441\u0442\u043e\u0432\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442 \u0434\u043b\u044f API \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432\n    url_path_for: Callable,  # \u0424\u0443\u043d\u043a\u0446\u0438\u044f \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f url \u043f\u043e \u0438\u043c\u0435\u043d\u0438 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430\n    db_manager: MainDatabaseManager,  # \u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0411\u0414\n    fixtures: NamedTuple,  # \u0425\u0440\u0430\u043d\u0438\u0442 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0438\u043a\u0441\u0442\u0443\u0440\n):\n    # \u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u043c\u044b\u0445 SQL \u043a\u043e\u043c\u0430\u043d\u0434\n    with track_queries(db_manager, expected_count=3):\n        # \u0417\u0430\u043f\u0440\u043e\u0441 \u0432 API\n        response = client.get(url_path_for(\"\u0418\u043c\u044f\u0424\u0443\u043d\u043a\u0446\u0438\u0438\"))\n    # \u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 JSON API \u043e\u0442\u0432\u0435\u0442\u0430\n    check_response_json(\n        response,\n        200,\n        {\n            \"id\": fixtures.\u0418\u043c\u044f.id,\n        },\n    )\n    # TODO \u041c\u043e\u0436\u043d\u043e \u0434\u043b\u044f \u043c\u0435\u0442\u043e\u0434\u043e\u0432 POST, UPDATE, DELETE \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u043d\u0430 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0437\u0430\u043f\u0438\u0441\u0435\u0439 \u0432 \u0411\u0414.\n    ...\n```\n\n### \u041a\u043b\u0430\u0441\u0441\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0439 \u043a\u043b\u0430\u0441\u0441\n\n\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 REST API \u043c\u0435\u0442\u043e\u0434\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0420\u0421\u0423\u0411\u0414, \u0438 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0442\u0432\u0435\u0442 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 `JSON`:\n\n```python\nfrom typing import Callable, NamedTuple\n\nfrom fastapi.testclient import TestClient\n\nfrom app.fixture.items_v1 import export_fixture_file\nfrom fastapi_accelerator.db.dbsession import MainDatabaseManager\nfrom fastapi_accelerator.testutils import apply_fixture_db\nfrom fastapi_accelerator.testutils.fixture_auth import client_auth_jwt\nfrom fastapi_accelerator.testutils.fixture_db.trace_sql import track_queries\nfrom fastapi_accelerator.testutils.utils import BaseAuthJwtPytest, check_response_json\n\nBASE_URL_V1 = \"/api/v1/\"\n\nclass Test\u0418\u043c\u044f(BaseAuthJwtPytest):\n\n    # \u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0438\u0437 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u0441 \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u0430\u043c\u0438\n    @apply_fixture_db(export_fixture_file)\n    def setUp(self, fixtures: NamedTuple):\n        self.url = BASE_URL_V1 + \"taskexecution\"\n        self.fixtures = fixtures # \u0425\u0440\u0430\u043d\u0438\u0442 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0438\u043a\u0441\u0442\u0443\u0440\n\n    def test_\u0438\u043c\u044f(self, client: TestClient, db_manager: MainDatabaseManager):\n        # \u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u043c\u044b\u0445 SQL \u043a\u043e\u043c\u0430\u043d\u0434\n        with track_queries(db_manager, expected_count=3):\n            # \u0417\u0430\u043f\u0440\u043e\u0441 \u0432 API\n            response = client.get(self.url)\n        # \u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 JSON API \u043e\u0442\u0432\u0435\u0442\u0430\n        check_response_json(\n            response,\n            200,\n            {\n                \"id\": self.fixtures.\u0418\u043c\u044f.id,\n            },\n        )\n        # TODO \u041c\u043e\u0436\u043d\u043e \u0434\u043b\u044f \u043c\u0435\u0442\u043e\u0434\u043e\u0432 POST, UPDATE, DELETE \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u043d\u0430 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0437\u0430\u043f\u0438\u0441\u0435\u0439 \u0432 \u0411\u0414.\n        ...\n```\n\n### \u041f\u0440\u0438\u043c\u0435\u0440 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0433\u043e \u0444\u0430\u0439\u043b\u0430:\n\n```python\nfrom typing import Callable, NamedTuple\n\nfrom fastapi.testclient import TestClient\nfrom sqlalchemy.orm import Session\n\nfrom app.fixture.items_v1 import export_fixture_file, export_fixture_task\nfrom app.models.file import File as FileDb\nfrom fastapi_accelerator.db.dbsession import MainDatabaseManager\nfrom fastapi_accelerator.testutils import apply_fixture_db, rm_key_from_deep_dict\nfrom fastapi_accelerator.testutils.fixture_auth import client_auth_jwt\nfrom fastapi_accelerator.testutils.fixture_db.trace_sql import track_queries\nfrom fastapi_accelerator.testutils.utils import BaseAuthJwtPytest, BasePytest, check_response_json\n\n\ndef test_base(\n    client: TestClient,\n    url_path_for: Callable,\n    db_session: Session,\n):\n    # \u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0437\u0430\u043f\u0438\u0441\u044c \u0432 \u0442\u0435\u0441\u0442\u043e\u043c \u043c\u0435\u0442\u043e\u0434\u0435\n    file1 = FileDb(\n        uid=\"469d4176-98f3-48a2-8794-0e2472bc2b7e\",\n        filename=\"file1.txt\",\n        size=100,\n        format=\"text/plain\",\n        extension=\".txt\",\n    )\n    db_session.add(file1)\n    db_session.commit()\n\n    # \u0417\u0430\u043f\u0440\u043e\u0441 \u0432 API\n    response = client.get(url_path_for(\"get_file_sync\"))\n    assert response.status_code == 200\n    res = response.json()\n    rm_key_from_deep_dict(res, [\"updated_at\", \"created_at\"])\n    assert res == [\n        {\n            \"cloud_path\": None,\n            \"extension\": \".txt\",\n            \"filename\": \"file1.txt\",\n            \"format\": \"text/plain\",\n            \"local_path\": None,\n            \"size\": 100,\n            \"uid\": \"469d4176-98f3-48a2-8794-0e2472bc2b7e\",\n        },\n    ]\n\n\n@apply_fixture_db(export_fixture_file)\ndef test_base2(client: TestClient, url_path_for: Callable):\n    # \u0417\u0430\u043f\u0440\u043e\u0441 \u0432 API\n    response = client.get(url_path_for(\"get_file_sync\"))\n    assert response.status_code == 200\n    res = response.json()\n\n    rm_key_from_deep_dict(res, [\"updated_at\", \"created_at\"])\n\n    assert res == [\n        {\n            \"cloud_path\": None,\n            \"extension\": \".txt\",\n            \"filename\": \"file1.txt\",\n            \"format\": \"text/plain\",\n            \"local_path\": None,\n            \"size\": 100,\n            \"uid\": \"469d4176-98f3-48a2-8794-0e2472bc2b7e\",\n        },\n        {\n            \"cloud_path\": None,\n            \"extension\": \".txt\",\n            \"filename\": \"file1.txt\",\n            \"format\": \"text/plain\",\n            \"local_path\": None,\n            \"size\": 100,\n            \"uid\": \"569d4176-98f3-48a2-8794-0e2472bc2b7e\",\n        },\n    ]\n\n\nBASE_URL_V1 = \"/api/v1/\"\n\n\nclass TestTaskExecution(BaseAuthJwtPytest):\n\n    @apply_fixture_db(export_fixture_task)\n    def setUp(self, fixtures: NamedTuple):\n        self.url = BASE_URL_V1 + \"taskexecution\"\n        self.fixtures = fixtures\n\n    def test_get_list(self, client: TestClient, db_manager: MainDatabaseManager):\n        with track_queries(db_manager, expected_count=1) as tracker:\n            response = client.get(self.url)\n        check_response_json(\n            response,\n            200,\n            {\n                \"page\": 1,\n                \"size\": 10,\n                \"count\": 1,\n                \"items\": [\n                    {\n                        \"id\": self.fixtures.task_execution1.id,\n                        \"end_time\": \"2024-09-06T10:59:43\",\n                        \"start_time\": \"2024-09-06T10:55:43\",\n                        \"task_id\": self.fixtures.task1.id,\n                        \"task\": {\n                            \"description\": None,\n                            \"name\": \"Admins\",\n                            \"id\": self.fixtures.task1.id,\n                        },\n                    }\n                ],\n            },\n        )\n\n    def test_get_item(self, client: TestClient):\n        response = client.get(self.url + f\"/{self.fixtures.task_execution1.id}\")\n        check_response_json(\n            response,\n            200,\n            {\n                \"id\": self.fixtures.task_execution1.id,\n                \"start_time\": \"2024-09-06T10:55:43\",\n                \"end_time\": \"2024-09-06T10:59:43\",\n                \"task\": {\n                    \"id\": self.fixtures.task1.id,\n                    \"name\": \"Admins\",\n                    \"description\": None,\n                },\n            },\n        )\n\n\nclass TestTaskExecution2(BasePytest):\n\n    @apply_fixture_db(export_fixture_task)\n    def setUp(self, fixtures: NamedTuple):\n        self.url = BASE_URL_V1 + \"taskexecution\"\n        self.fixtures = fixtures\n\n    @client_auth_jwt()\n    def test_get_list(self, client: TestClient, db_manager: MainDatabaseManager):\n        with track_queries(db_manager, expected_count=1):\n            response = client.get(self.url)\n        check_response_json(\n            response,\n            200,\n            {\n                \"page\": 1,\n                \"size\": 10,\n                \"count\": 1,\n                \"items\": [\n                    {\n                        \"id\": self.fixtures.task_execution1.id,\n                        \"end_time\": \"2024-09-06T10:59:43\",\n                        \"start_time\": \"2024-09-06T10:55:43\",\n                        \"task_id\": self.fixtures.task1.id,\n                        \"task\": {\n                            \"description\": None,\n                            \"name\": \"Admins\",\n                            \"id\": self.fixtures.task1.id,\n                        },\n                    }\n                ],\n            },\n        )\n\n    @client_auth_jwt()\n    def test_get_item(self, client: TestClient):\n        response = client.get(self.url + f\"/{self.fixtures.task_execution1.id}\")\n        check_response_json(\n            response,\n            200,\n            {\n                \"id\": self.fixtures.task_execution1.id,\n                \"start_time\": \"2024-09-06T10:55:43\",\n                \"end_time\": \"2024-09-06T10:59:43\",\n                \"task\": {\n                    \"id\": self.fixtures.task1.id,\n                    \"name\": \"Admins\",\n                    \"description\": None,\n                },\n            },\n        )\n```\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": null,
    "version": "0.0.1",
    "project_urls": {
        "Homepage": "https://github.com/denisxab/fastapi-accelerator",
        "Repository": "https://github.com/denisxab/fastapi-accelerator"
    },
    "split_keywords": [
        "fastapi",
        " django"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "3ed47eca998a6b82cde175b191b441c72c72287e60fbcc779f5c2e66d7668559",
                "md5": "83fb12d3f80df62535f29146764f588e",
                "sha256": "e0f046765187764b249d9b1c363d6c0ef28397c6ed189b964dbc385310570d95"
            },
            "downloads": -1,
            "filename": "fastapi_accelerator-0.0.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "83fb12d3f80df62535f29146764f588e",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.9",
            "size": 56552,
            "upload_time": "2024-09-07T21:55:31",
            "upload_time_iso_8601": "2024-09-07T21:55:31.414998Z",
            "url": "https://files.pythonhosted.org/packages/3e/d4/7eca998a6b82cde175b191b441c72c72287e60fbcc779f5c2e66d7668559/fastapi_accelerator-0.0.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "853ca567c0ece414cd06cf6d29175ca9439bdcb6127372e82b57e07740dc97ae",
                "md5": "6838a752c8920df42f915cc19639dea4",
                "sha256": "36966ab027de035d30a740b7adfa7c101e4acf65edc7b025ac13b7ccb895d998"
            },
            "downloads": -1,
            "filename": "fastapi_accelerator-0.0.1.tar.gz",
            "has_sig": false,
            "md5_digest": "6838a752c8920df42f915cc19639dea4",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.9",
            "size": 45164,
            "upload_time": "2024-09-07T21:55:33",
            "upload_time_iso_8601": "2024-09-07T21:55:33.238913Z",
            "url": "https://files.pythonhosted.org/packages/85/3c/a567c0ece414cd06cf6d29175ca9439bdcb6127372e82b57e07740dc97ae/fastapi_accelerator-0.0.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-09-07 21:55:33",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "denisxab",
    "github_project": "fastapi-accelerator",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "fastapi-accelerator"
}
        
Elapsed time: 0.86049s