# Fastapi Accelerator бизнес логика
## Зачем нужен, какие проблемы решает ?
Основная цель - ускорить и упростить разработку проектов на FastAPI. Это достигается путем:
1. Предоставления переиспользуемого кода для типовых задач.
2. Внедрения универсального менеджера для работы с РСУБД.
3. Реализации ViewSet для быстрого создания представлений с базовой бизнес-логикой.
4. Интеграции аутентификации по JWT.
5. Добавления удобной админ-панели.
6. Упрощения написания и выполнения интеграционных тестов для API.
7. Оптимизации работы с Alembic для управления миграциями в production и test окружениях.
8. Стандартизация архитектуры для интеграций по HTTP.
## Описание файлов в составе fastapi_accelerator
```bash
fastapi_accelerator/
│
├── db/ # Логика взаимодействия с РСУБД
│ ├── __init__.py
│ ├── dborm.py
│ └── dbsession.py
│
├── pattern/ # Шаблоны для проектов
│ ├── __init__.py
│ ├── pattern_fastapi.py # Шаблоны для создания проекта на FastAPI
│ ├── pattern_alembic.py # Шаблоны для создания Alembic
│ └── pattern_flask_admin.py # Шаблоны для создания проекта Flask админ панели
│
├── integration/ # Утилиты интеграций с внешними системами
│ ├── __init__.py
│ ├── base_integration.py # Базовый класс для всех типов интеграций
│ ├── http_integration.py # Интеграции по HTTP
│ └── stability_patterns.py # Реализация паттернов стабильности
│
├── commands/ # CLI команды
│ ├── __init__.py
│ └── py2dantic # Генерация схемы pydantic из python dict
│
├── 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 # Работа с временными зонами
├── appstate.py # Получить один раз настройки проекта во время Runtime
├── 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 # Настройки кеширования
│ │ ├── useintegration.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
│ │
│ ├── integration/ # Интеграции с внешними сервисами
│ │ ├── __init__.py
│ │ └── google_translate/ # Пример пакета интеграции с Google переводчик(он может быть подключен как git submodule)
│ │ ├── __init__.py
│ │ ├── schema.py # Сдержит схемы запросов и ответов
│ │ └── view.py # Сдержит логику интеграций
│ │
│ └── fixture/ # Хранит фикстуры для тестирования этого проекта
│ ├── __init__.py
│ ├── items_v1.py # Тестовые записи для БД
│ └── utils.py # Переиспользуемые фикстуры для тестов
│
├── fastapi_accelerator/ # 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)
```
### Основанные компоненты `MainDatabaseManager`
- Общие характеристики
- `DEV_STATUS` - Индикатор режима разработки. При `DEV_STATUS=False` блокирует выполнение критических операций (`create_all`, `drop_all`, `clear_all`). Это мера безопасности для производственной среды.
- Синхронные компоненты
- `database_url` - Адрес для подключения к синхронной базе данных.
- `engine` - Механизм синхронного взаимодействия с БД.
- `session` - Генератор синхронных сессий.
- `Base` - Базовый класс для моделей данных.
- Функциональность:
- `get_session` - Инжектор сессии БД.
- `get_session_transaction` - Инжектор сессии БД с поддержкой транзакций.
- `create_all` - Инициализация всех таблиц в БД.
- `drop_all` - Удаление всей структуры БД.
- `clear_all` - Очистка содержимого таблиц. Параметр `exclude_tables_name` позволяет исключить определенные таблицы из процесса очистки.
- Асинхронные компоненты
- `adatabase_url` - Адрес для подключения к асинхронной БД.
- `aengine` - Асинхронный механизм работы с БД, включая пул соединений.
- `asession` - Генератор асинхронных сессий.
- Функциональность:
- `aget_session` - Асинхронный инжектор сессии БД.
- `aget_session_transaction` - Асинхронный инжектор сессии БД с поддержкой транзакций.
### Use OrmAsync
Этот класс оптимизирует асинхронное взаимодействие с БД:
- `get` - Извлечение объекта по заданным критериям.
- `get_list` - Получение набора объектов по запросу. (С возможностью глубокой выборки)
- `update` - Модификация объектов согласно запросу.
- `delete` - Удаление объектов по заданным параметрам.
- `get_item` - Извлечение объекта по первичному ключу. (С возможностью глубокой выборки)
- `create_item` - Создание нового объекта. (С возможностью каскадного создания)
- `update_item` - Обновление объекта по первичному ключу. (С возможностью каскадного обновления)
- `delete_item` - Удаление объекта по первичному ключу. (С возможностью каскадного удаления)
- `eager_refresh` - Полная загрузка всех связанных данных для объекта.
> Глубокая выборка/каскадные операции - это возможность работы со связанными данными.
> Активируется параметром `deep=True`
>
> Примеры:
>
> - get_list, get_item - Возвращают объекты со всеми связанными данными, готовые для использования в Pydantic
> - create_item - Создает записи в связанных таблицах
> - update_item - Обновляет данные в связанных таблицах
> - delete_item - Удаляет записи из связанных таблиц
### Создать модель через 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(
self.db_model, 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)]
# Подключить ViewSet
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.state.TIMEZONE).isoformat()
# Вариант 2
get_datetime_now(app.state.TIMEZONE).isoformat()
# Вариант 3
import pytz
get_datetime_now(pytz.timezone("Europe/Moscow")).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 Integration
Большинство API-сервисов взаимодействуют с другими API или gRPC/RPC сервисами. Такие интеграции могут быть сложными и часто оказываются не полностью понятно разработчикам. Из-за этого они легко превращаются в легаси-код, который сложно поддерживать, а тестирование интеграций локально зачастую невозможно.
Важно, чтобы в проекте была библиотека, следящая за качеством написания интеграций и заставляющая документировать их для упрощения дальнейшей поддержки. Именно для этого я разработал специальные модули:
- `IntegrationHTTP`: Класс для создания интеграций по HTTP.
- `Stability Patterns`: Паттернов стабильности для применения к методам интеграции.
- `py2dantic`: Утилита для перевода Python dict в Pydantic схему.
- `docintegration`: Авто генерация документации, для используемых интеграций.
### Use Integration HTTP
`IntegrationHTTP` - Класс для создания методов интеграции по HTTP, централизует логику вызовов к внешним системам, проводя валидацию исходящих данных. Также в классе указывается версия и документация внешнего API.
Преимущества использования этого подхода:
- Явная спецификация форматов запроса и ответа.
- Легкая переносимость кода между проектами — достаточно импортировать классы, основанные на `IntegrationHTTP`.
- Консолидация логики внешних запросов в одном месте, что упрощает поддержку.
- Возможность легко заменять реальные методы на `mock` для тестирования.
- Легкое внедрение `Stability Patterns` для методов интеграции.
Для создания интеграции следуйте следующим шагам:
1. Рекомендуется располагать код интеграций в директории `app/integration/ИмяПакетаИнтеграции`.
2. Создать класса интеграций `app/integration/ИмяПакетаИнтеграции/endpoint.py`:
```python
import httpx
from pydantic import BaseModel
from fastapi_accelerator.integration.http_integration import (
ApiHTTP,
EndpointsDeclaration,
HTTPMethod,
IntegrationHTTP,
)
from fastapi_accelerator.integration.stability_patterns import sp
class ИмяIntegration(EndpointsDeclaration):
integration = IntegrationHTTP(
"Имя Интеграции",
doc="Интеграция с ... API",
)
class Schema:
"""Схемы Pydantic для успешных ответов"""
class Successful(BaseModel)
body: str
class SchemaError:
"""Схемы Pydantic для не успешных ответов"""
class http400(BaseModel)
error: str
@integration.endpoint(
HTTPMethod.post,
"/путь",
version="...",
docurl="https://..."
)
@sp.RetryPattern()
async def имя_метода(api: ApiHTTP, аргумент_1: str) -> Schema.Successful | SchemaError.http400:
try:
response: httpx.Response = await api.client.post(api.url.geturl(), json=...)
return response.json()
except httpx.RequestError as e:
raise e
```
3. Настроить и подключить интеграции к проекту `app/core/useintegration.py`:
```python
"""Интеграции используемые в проекте"""
from app.integration.ИмяПакетаИнтеграции.endpoint import ИмяIntegration
# Создание экземпляра интеграции
имя_api = ИмяIntegration(
# Начало для url пути
base_url="https://путь...",
# Доступы, которые можем использовать в методах интеграции
credentials={...},
)
```
4. Пример использования класса интеграции в `FastAPI`:
```python
from app.core.useintegration import имя_api
from app.integration.ИмяПакетаИнтеграции.schema import ИмяSchema
@router.get("/имя")
async def имя(аргумент_1: str) -> ИмяIntegration.Schema.Successful:
# Вызвать метод интеграции
return await имя_api.имя_метода(аргумент_1)
```
---
Вам необходимо указывать тип возвращаемого объекта из метода интеграции.
Ответ может быть:
- `dict`: который можно конвертировать в одну схему `Pydantic`.
- `list[dict]`: который можно конвертировать в список схем `Pydantic`.
- Несколько типов ответа: Это необходимо для указания типа корректного ответа и для обработки ошибок. Например, `-> УспешныйОтвет | НеУспешныйОтвет` или `-> list[УспешныйОтвет] | НеУспешныйОтвет`.
- В худшем случае можете указать `Any`.
#### Пример интеграции с Google Translate
- Класса интеграции `app/integration/google_translate/endpoint.py`:
```python
import httpx
from pydantic import BaseModel
from fastapi_accelerator.integration.http_integration import (
ApiHTTP,
EndpointsDeclaration,
HTTPMethod,
IntegrationHTTP,
)
from fastapi_accelerator.integration.stability_patterns import sp
class GoogleTranslateEndpoints(EndpointsDeclaration):
integration = IntegrationHTTP(
"Google Translate",
doc="Интеграция с Google Translate API",
)
class Schema:
"""Схемы для успешных ответов"""
class TranslateV2(BaseModel):
text: str
class SchemaError:
"""Схемы для не успешных ответов"""
class http400Error(BaseModel):
code: int
message: str
errors: list[dict]
status: str
details: list[dict]
class http400(BaseModel):
error: dict
@integration.endpoint(
HTTPMethod.post,
"/v1/translateHtml",
version="v2",
docurl="https://cloud.google.com/translate/docs/reference/rest",
)
# Применяем паттерны стабильности
@sp.Timeout()
# Автоматически повторяет запрос при возникновении ошибки.
@sp.RetryPattern()
async def translate(
api: ApiHTTP,
text: str,
from_lang: str,
to_lang: str,
) -> Schema.TranslateV2 | SchemaError.http400: # Указать типа ответа
"""Перевод текста с помощью Google Translate"""
try:
# Выполнить запрос к внешней системе
response: httpx.Response = await api.client.post(
api.url.geturl(),
json=[[text.split("\n"), from_lang, to_lang], "te_lib"],
headers={
"content-type": "application/json+protobuf",
"x-goog-api-key": api.credentials["API_TOKEN"],
},
)
# Обработка ответа
print(f"Processed {api.url}: Status {response.status_code}")
return {"text": "\n".join(x[0] for x in response.json())}
except httpx.RequestError as e:
print(f"Error processing {api.url}: {e}")
raise e
```
- Подключение в `app/core/useintegration.py`:
```python
"""Интеграции используемые в проекте"""
from app.integration.google_translate.endpoint import GoogleTranslateIntegration
# Создание экземпляра интеграции
gtapi = GoogleTranslateIntegration(
base_url="https://translate-pa.googleapis.com",
# Сохраняем в класс доступы, которые можем использовать в методах интеграции
credentials={"API_TOKEN": "..."},
)
```
- Пример использования в эндпоинте `FastAPI`:
```python
from datetime import timedelta
from fastapi_accelerator.cache import cache_redis
from app.core.cache import redis_client
from app.core.useintegration import gtapi
from app.integration.google_translate.schema import GoogleTranslateSchema
@router.get("/translate")
# Можем легко кешировать ответы от интеграций
@cache_redis(cache_class=redis_client, cache_ttl=timedelta(minutes=10))
async def translate(
text: str, from_lang: str = "en", to_lang: str = "ru"
) -> GoogleTranslateEndpoints.Schema.TranslateV2:
# Вызвать метод интеграции
return await gtapi.translate(text, from_lang, to_lang)
```
### Use Stability Patterns
Модуль поддерживает паттерны стабильности (Stability Patterns), которые помогают избежать ошибок и перегрузок при работе с внешними сервисами.
> Основным критерием не успешного выполнения является возникновение исключения (raise) в методе интеграции. Если вы получили ответ с кодом 400 (ошибка клиента) или 500 (ошибка сервера), но не вызвали исключение, Stability Patterns будет считать это успешным выполнением и не применит свою логику для обработки ошибок.
Ниже приведено описание основных декораторов:
- `@sp.Fallback` (Резервный вариант) - Предоставляет альтернативный путь выполнения в случае сбоя основного. Позволяет системе деградировать контролируемо, а не падать с ошибкой.
- `@sp.Timeout` (Тайм-аут) - Ограничивает время ожидания ответа от внешнего сервиса. Предотвращает блокировку ресурсов при зависании вызова.
- `@sp.CircuitBreaker` (Предохранитель) - Отслеживает количество ошибок при вызове внешнего сервиса. При превышении лимита временно блокирует вызов, предотвращая каскадные сбои.
- `@sp.RetryPattern` (Паттерн повторения) - Автоматически повторяет запрос при возникновении ошибки.
- `@sp.Throttling` (Регулирование) - Ограничивает количество запросов к ресурсу для предотвращения его перегрузки. Защищает систему от шторма запросов.
Эти паттерны делают систему более устойчивой, минимизируя риск сбоев и обеспечивая плавную деградацию при возникновении проблем.
### Use py2dantic
`py2dantic` — это удобная утилита, которая позволяет быстро создавать схемы Pydantic из словарей Python. Эти схемы можно эффективно использовать для типизации в вашем проекте.
- Пример использования:
```python
from fastapi_accelerator.commands.py2dantic import generate_pydantic_models
sample_data = {
"items": [
{
"id": "0000",
"premium": False,
"name": "Python Developer",
"department": None,
"has_test": True,
"response_letter_required": False,
"salary": None,
"type": {"id": "open", "name": "Открытая"},
"address": None,
"response_url": None,
"sort_point_distance": None,
"published_at": "2024-09-04T08:27:02+0300",
"created_at": "2024-09-04T08:27:02+0300",
"archived": False,
"apply_alternate_url": "https://hh.ru/applicant/vacancy_response?vacancyId=0000",
"branding": {"type": "MAKEUP", "tariff": None},
"show_logo_in_search": True
},
],
"found": 187,
"pages": 2,
"page": 1,
"per_page": 100,
"clusters": None,
"arguments": None,
"fixes": None,
"suggests": None,
"alternate_url": "https://hh.ru/search/vacancy?enable_snippets=true&items_on_page=100&order_by=publication_time&page=1&salary=200000&schedule=remote&text=Python+FastAPI",
}
assert (
generate_pydantic_models(sample_data, depth=2, prfix_class_name="Job").strip()
== """
class Job_ItemsItem(BaseModel):
id: str = None
premium: int = None
name: str = None
department: Any = None
has_test: int = None
response_letter_required: int = None
area: Dict = None
salary: Any = None
type: Dict = None
address: Any = None
response_url: Any = None
sort_point_distance: Any = None
published_at: str = None
created_at: str = None
archived: int = None
apply_alternate_url: str = None
branding: Dict = None
show_logo_in_search: int = None
class Job(BaseModel):
items: List[Job_ItemsItem]
found: int = None
pages: int = None
page: int = None
per_page: int = None
clusters: Any = None
arguments: Any = None
fixes: Any = None
suggests: Any = None
alternate_url: str = None
""".strip()
)
```
### Use docintegration
Данный функционал позволяет узнать, какие интеграции используются в проекте, аналогично тому, как это реализовано в OpenAPI Swagger для стандартного FastAPI.
Документация доступна по адресу: `http://host:port/docintegration`.
Чтобы активировать этот путь, необходимо в файле `main.py` в параметре `base_pattern` указать список интеграций в аргументе `useintegration`:
```python
from app.core.useintegration import интеграция_1, интеграция_2
from fastapi_accelerator.pattern.pattern_fastapi import base_pattern
# Паттерн для проекта
base_pattern(
app,
...
useintegration=[интеграция_1, интеграция_2],
)
```
![Внешний вид документации](./__attach__/Screenshot_20240914_153330.png)
## 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=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`
# Fastapi Accelerator тестирование
## Зачем и как писать тесты
Рассмотрим тестирование: 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.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 ...'
...
```
## Тестирование интеграций с внешними API
Самым сложным аспектом тестирования являются интеграции с внешними API, поскольку во время тестов необходимо избегать выполнения реальных запросов к этим API. Поэтому нам приходится самостоятельно разрабатывать логику для имитации работы внешнего API. Хотя наша имитация может не полностью отражать реальную работу API, это все же лучше, чем игнорировать интеграцию.
В командах часто каждый разработчик создает свои собственные моки для интеграций, что приводит к путанице и отсутствию единого стандарта. Существует высокая вероятность ошибок, когда мок может не сработать, и произойдет отправка запроса в реальный API.
Для решения этой проблемы мы используем классы интеграции `EndpointsDeclaration` с декоратором `@integration.endpoint`, что позволяет создать единую точку входа, которую можно легко заменить во время тестирования и исключить возможность выполнения реального метода интеграции.
Пример тестирования метода `FastAPI`, который вызывает метод интеграции:
- Обработчик FastAPI:
```python
@router.get("/translate")
async def translate_api(
text: str, from_lang: str = "en", to_lang: str = "ru"
) -> GoogleTranslateEndpoints.Schema.TranslateV2:
# Вызвать метод интеграции
return await gtapi.translate(text, from_lang, to_lang)
```
- `test_имя.py` пример интеграции с `google` переводчик:
```python
from fastapi_accelerator.testutils.fixture_integration import patch_integration
from app.integration.google_translate.mock import google_translate_mock_rules
# Правила подмены методов интеграции на mock.
# Если в коде вызывается интеграция, которая не указана в mock_rules, возникает исключение.
# Это предотвращает случайные реальные запросы, если вы забыли указать mock.
@patch_integration(mock_rules=google_translate_mock_rules)
def test_integration_google_translate(client: TestClient, url_path_for: Callable):
# Выполнение тестового запроса
response = client.get(
url_path_for("translate_api"),
params=dict(text="Hello", from_lang="en", to_lang="ru"),
)
# Проверка ответа
assert response.json() == {"text": "Привет"}
```
> Значение для `mock_rules` можно использовать откуда угодно, но рекомендую хранить и брать из `app/integration/ПакетИнтеграции/mock.py`
- Рекомендуется хранить подменные функции в одном пакете с интеграцией в `app/integration/ПакетИнтеграции/mock.py`, чтобы при импорте этого пакета в другой проект также можно было использовать функции из `mock.py`, не создавая свои имитации.
```python
from app.integration.google_translate.endpoint import GoogleTranslateEndpoints
from fastapi_accelerator.integration.http_integration import ApiHTTP
from fastapi_accelerator.testutils.fixture_integration import MockRules
async def overwrite_translate(api: ApiHTTP, *args, **kwargs):
# Удобный вариант имитации, когда через match аргументов, возвращаем определенный ответ.
match args:
case ("hello", "en", "ru"):
return {"text": "Привет"}
return None
# Правила замены методов интеграции на mock
google_translate_mock_rules = MockRules(
# Реальный метод интеграции: замена на mock функцию
{GoogleTranslateEndpoints.translate: overwrite_translate}
)
```
> К мок-функциям применяются те же требования к формату ответа, что и к реальному методу интеграции.
## Примеры тестов
### Классическая тестовой функции
Проверки 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, web-framework, api, rest, async, sqlalchemy, admin-panel",
"author": "Denis Kustov",
"author_email": "pro-progerkustov@yandex.ru",
"download_url": "https://files.pythonhosted.org/packages/c1/66/8b25799c962336811c310380adef3a69add211c46dc4ce3a38dedc80ddf9/fastapi_accelerator-0.0.5.tar.gz",
"platform": null,
"description": "# Fastapi Accelerator \u0431\u0438\u0437\u043d\u0435\u0441 \u043b\u043e\u0433\u0438\u043a\u0430\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.\n8. \u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u044b \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0439 \u043f\u043e HTTP.\n\n## \u041e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u0444\u0430\u0439\u043b\u043e\u0432 \u0432 \u0441\u043e\u0441\u0442\u0430\u0432\u0435 fastapi_accelerator\n\n```bash\nfastapi_accelerator/\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 integration/ # \u0423\u0442\u0438\u043b\u0438\u0442\u044b \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0439 \u0441 \u0432\u043d\u0435\u0448\u043d\u0438\u043c\u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u043c\u0438\n\u2502 \u251c\u2500\u2500 __init__.py\n\u2502 \u251c\u2500\u2500 base_integration.py # \u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u043b\u0430\u0441\u0441 \u0434\u043b\u044f \u0432\u0441\u0435\u0445 \u0442\u0438\u043f\u043e\u0432 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0439\n\u2502 \u251c\u2500\u2500 http_integration.py # \u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u043f\u043e HTTP\n\u2502 \u2514\u2500\u2500 stability_patterns.py # \u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043f\u0430\u0442\u0442\u0435\u0440\u043d\u043e\u0432 \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u043e\u0441\u0442\u0438\n\u2502\n\u251c\u2500\u2500 commands/ # CLI \u043a\u043e\u043c\u0430\u043d\u0434\u044b\n\u2502 \u251c\u2500\u2500 __init__.py\n\u2502 \u2514\u2500\u2500 py2dantic # \u0413\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u044f \u0441\u0445\u0435\u043c\u044b pydantic \u0438\u0437 python dict\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 appstate.py # \u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0432\u043e \u0432\u0440\u0435\u043c\u044f Runtime\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\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# \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 \u251c\u2500\u2500 useintegration.py # \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0435\n\u2502 \u2502 \u2514\u2500\u2500 dependencies.py # \u041e\u0431\u0449\u0438\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u0434\u043b\u044f \u043f\u0440\u043e\u0435\u043a\u0442\u0430\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 \u251c\u2500\u2500 integration/ # \u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 \u0432\u043d\u0435\u0448\u043d\u0438\u043c\u0438 \u0441\u0435\u0440\u0432\u0438\u0441\u0430\u043c\u0438\n\u2502 \u2502 \u251c\u2500\u2500 __init__.py\n\u2502 \u2502 \u2514\u2500\u2500 google_translate/ # \u041f\u0440\u0438\u043c\u0435\u0440 \u043f\u0430\u043a\u0435\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Google \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u0447\u0438\u043a(\u043e\u043d \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a\u0430\u043a git submodule)\n\u2502 \u2502 \u251c\u2500\u2500 __init__.py\n\u2502 \u2502 \u251c\u2500\u2500 schema.py # \u0421\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0445\u0435\u043c\u044b \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0438 \u043e\u0442\u0432\u0435\u0442\u043e\u0432\n\u2502 \u2502 \u2514\u2500\u2500 view.py # \u0421\u0434\u0435\u0440\u0436\u0438\u0442 \u043b\u043e\u0433\u0438\u043a\u0443 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0439\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 fastapi_accelerator/ # 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. `DatabaseManager` \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043f\u0430\u0442\u0435\u0440 \u043e\u0434\u0438\u043d\u043e\u0447\u043a\u0430, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043b\u0435\u0433\u043a\u043e \u043f\u043e\u0434\u043c\u0435\u043d\u0435\u043d \u0432 \u0442\u0435\u0441\u0442\u0430\u0445.\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### \u041e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b `MainDatabaseManager`\n\n- \u041e\u0431\u0449\u0438\u0435 \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0438\n\n - `DEV_STATUS` - \u0418\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440 \u0440\u0435\u0436\u0438\u043c\u0430 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438. \u041f\u0440\u0438 `DEV_STATUS=False` \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u043a\u0440\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0439 (`create_all`, `drop_all`, `clear_all`). \u042d\u0442\u043e \u043c\u0435\u0440\u0430 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 \u0434\u043b\u044f \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0439 \u0441\u0440\u0435\u0434\u044b.\n\n- \u0421\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b\n\n - `database_url` - \u0410\u0434\u0440\u0435\u0441 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u043e\u0439 \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445.\n - `engine` - \u041c\u0435\u0445\u0430\u043d\u0438\u0437\u043c \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u043e\u0433\u043e \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441 \u0411\u0414.\n - `session` - \u0413\u0435\u043d\u0435\u0440\u0430\u0442\u043e\u0440 \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0445 \u0441\u0435\u0441\u0441\u0438\u0439.\n - `Base` - \u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u043b\u0430\u0441\u0441 \u0434\u043b\u044f \u043c\u043e\u0434\u0435\u043b\u0435\u0439 \u0434\u0430\u043d\u043d\u044b\u0445.\n\n - \u0424\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c:\n\n - `get_session` - \u0418\u043d\u0436\u0435\u043a\u0442\u043e\u0440 \u0441\u0435\u0441\u0441\u0438\u0438 \u0411\u0414.\n - `get_session_transaction` - \u0418\u043d\u0436\u0435\u043a\u0442\u043e\u0440 \u0441\u0435\u0441\u0441\u0438\u0438 \u0411\u0414 \u0441 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u043e\u0439 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0439.\n - `create_all` - \u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0432\u0441\u0435\u0445 \u0442\u0430\u0431\u043b\u0438\u0446 \u0432 \u0411\u0414.\n - `drop_all` - \u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u0432\u0441\u0435\u0439 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u044b \u0411\u0414.\n - `clear_all` - \u041e\u0447\u0438\u0441\u0442\u043a\u0430 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e \u0442\u0430\u0431\u043b\u0438\u0446. \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 `exclude_tables_name` \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u0442\u0430\u0431\u043b\u0438\u0446\u044b \u0438\u0437 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u043e\u0447\u0438\u0441\u0442\u043a\u0438.\n\n- \u0410\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b\n\n - `adatabase_url` - \u0410\u0434\u0440\u0435\u0441 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u043e\u0439 \u0411\u0414.\n - `aengine` - \u0410\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0439 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0411\u0414, \u0432\u043a\u043b\u044e\u0447\u0430\u044f \u043f\u0443\u043b \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0439.\n - `asession` - \u0413\u0435\u043d\u0435\u0440\u0430\u0442\u043e\u0440 \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0445 \u0441\u0435\u0441\u0441\u0438\u0439.\n\n - \u0424\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c:\n\n - `aget_session` - \u0410\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0439 \u0438\u043d\u0436\u0435\u043a\u0442\u043e\u0440 \u0441\u0435\u0441\u0441\u0438\u0438 \u0411\u0414.\n - `aget_session_transaction` - \u0410\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0439 \u0438\u043d\u0436\u0435\u043a\u0442\u043e\u0440 \u0441\u0435\u0441\u0441\u0438\u0438 \u0411\u0414 \u0441 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u043e\u0439 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0439.\n\n### Use OrmAsync\n\n\u042d\u0442\u043e\u0442 \u043a\u043b\u0430\u0441\u0441 \u043e\u043f\u0442\u0438\u043c\u0438\u0437\u0438\u0440\u0443\u0435\u0442 \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u043e\u0435 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u0441 \u0411\u0414:\n\n- `get` - \u0418\u0437\u0432\u043b\u0435\u0447\u0435\u043d\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u043f\u043e \u0437\u0430\u0434\u0430\u043d\u043d\u044b\u043c \u043a\u0440\u0438\u0442\u0435\u0440\u0438\u044f\u043c.\n- `get_list` - \u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u043d\u0430\u0431\u043e\u0440\u0430 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u043f\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0443. (\u0421 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c\u044e \u0433\u043b\u0443\u0431\u043e\u043a\u043e\u0439 \u0432\u044b\u0431\u043e\u0440\u043a\u0438)\n- `update` - \u041c\u043e\u0434\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0441\u043e\u0433\u043b\u0430\u0441\u043d\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0443.\n- `delete` - \u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u043f\u043e \u0437\u0430\u0434\u0430\u043d\u043d\u044b\u043c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u043c.\n- `get_item` - \u0418\u0437\u0432\u043b\u0435\u0447\u0435\u043d\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u043f\u043e \u043f\u0435\u0440\u0432\u0438\u0447\u043d\u043e\u043c\u0443 \u043a\u043b\u044e\u0447\u0443. (\u0421 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c\u044e \u0433\u043b\u0443\u0431\u043e\u043a\u043e\u0439 \u0432\u044b\u0431\u043e\u0440\u043a\u0438)\n- `create_item` - \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043d\u043e\u0432\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430. (\u0421 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c\u044e \u043a\u0430\u0441\u043a\u0430\u0434\u043d\u043e\u0433\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f)\n- `update_item` - \u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u043f\u043e \u043f\u0435\u0440\u0432\u0438\u0447\u043d\u043e\u043c\u0443 \u043a\u043b\u044e\u0447\u0443. (\u0421 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c\u044e \u043a\u0430\u0441\u043a\u0430\u0434\u043d\u043e\u0433\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f)\n- `delete_item` - \u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u043f\u043e \u043f\u0435\u0440\u0432\u0438\u0447\u043d\u043e\u043c\u0443 \u043a\u043b\u044e\u0447\u0443. (\u0421 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c\u044e \u043a\u0430\u0441\u043a\u0430\u0434\u043d\u043e\u0433\u043e \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f)\n- `eager_refresh` - \u041f\u043e\u043b\u043d\u0430\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u0432\u0441\u0435\u0445 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043b\u044f \u043e\u0431\u044a\u0435\u043a\u0442\u0430.\n\n> \u0413\u043b\u0443\u0431\u043e\u043a\u0430\u044f \u0432\u044b\u0431\u043e\u0440\u043a\u0430/\u043a\u0430\u0441\u043a\u0430\u0434\u043d\u044b\u0435 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0438 - \u044d\u0442\u043e \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0440\u0430\u0431\u043e\u0442\u044b \u0441\u043e \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u043c\u0438.\n> \u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u043c `deep=True`\n>\n> \u041f\u0440\u0438\u043c\u0435\u0440\u044b:\n>\n> - get_list, get_item - \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u044e\u0442 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0441\u043e \u0432\u0441\u0435\u043c\u0438 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u043c\u0438, \u0433\u043e\u0442\u043e\u0432\u044b\u0435 \u0434\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0432 Pydantic\n> - create_item - \u0421\u043e\u0437\u0434\u0430\u0435\u0442 \u0437\u0430\u043f\u0438\u0441\u0438 \u0432 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0442\u0430\u0431\u043b\u0438\u0446\u0430\u0445\n> - update_item - \u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0442\u0430\u0431\u043b\u0438\u0446\u0430\u0445\n> - delete_item - \u0423\u0434\u0430\u043b\u044f\u0435\u0442 \u0437\u0430\u043f\u0438\u0441\u0438 \u0438\u0437 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0442\u0430\u0431\u043b\u0438\u0446\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>\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 \u043d\u0443\u0436\u043d\u043e:\n>\n> 1. \u0412 `app.models.__init__.py` \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u043c \u0432\u0441\u0435 \u043c\u043e\u0434\u0435\u043b\u0438\n>\n> ```python\n> from .files import *\n> from .users import *\n> ```\n>\n> 2. \u0412 `alembic/env.py` \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0441\u0435(\u0438\u043b\u0438 \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\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\n# \u0421\u043e\u0437\u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u044e\nalembic revision --autogenerate\n# \u041f\u0440\u0438\u043c\u0435\u043d\u0438\u0442\u044c \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u044e \u043a \u0411\u0414\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 self.db_model, 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\n# \u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c ViewSet\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.state.TIMEZONE).isoformat()\n# \u0412\u0430\u0440\u0438\u0430\u043d\u0442 2\nget_datetime_now(app.state.TIMEZONE).isoformat()\n# \u0412\u0430\u0440\u0438\u0430\u043d\u0442 3\nimport pytz\nget_datetime_now(pytz.timezone(\"Europe/Moscow\")).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 Integration\n\n\u0411\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u043e API-\u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0443\u044e\u0442 \u0441 \u0434\u0440\u0443\u0433\u0438\u043c\u0438 API \u0438\u043b\u0438 gRPC/RPC \u0441\u0435\u0440\u0432\u0438\u0441\u0430\u043c\u0438. \u0422\u0430\u043a\u0438\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u0441\u043b\u043e\u0436\u043d\u044b\u043c\u0438 \u0438 \u0447\u0430\u0441\u0442\u043e \u043e\u043a\u0430\u0437\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u043d\u0435 \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u043f\u043e\u043d\u044f\u0442\u043d\u043e \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430\u043c. \u0418\u0437-\u0437\u0430 \u044d\u0442\u043e\u0433\u043e \u043e\u043d\u0438 \u043b\u0435\u0433\u043a\u043e \u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u044e\u0442\u0441\u044f \u0432 \u043b\u0435\u0433\u0430\u0441\u0438-\u043a\u043e\u0434, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043b\u043e\u0436\u043d\u043e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0442\u044c, \u0430 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0439 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e \u0437\u0430\u0447\u0430\u0441\u0442\u0443\u044e \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e.\n\n\u0412\u0430\u0436\u043d\u043e, \u0447\u0442\u043e\u0431\u044b \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u0431\u044b\u043b\u0430 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430, \u0441\u043b\u0435\u0434\u044f\u0449\u0430\u044f \u0437\u0430 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u043e\u043c \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0439 \u0438 \u0437\u0430\u0441\u0442\u0430\u0432\u043b\u044f\u044e\u0449\u0430\u044f \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0445 \u0434\u043b\u044f \u0443\u043f\u0440\u043e\u0449\u0435\u043d\u0438\u044f \u0434\u0430\u043b\u044c\u043d\u0435\u0439\u0448\u0435\u0439 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0438. \u0418\u043c\u0435\u043d\u043d\u043e \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0430\u043b \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0435 \u043c\u043e\u0434\u0443\u043b\u0438:\n\n- `IntegrationHTTP`: \u041a\u043b\u0430\u0441\u0441 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0439 \u043f\u043e HTTP.\n- `Stability Patterns`: \u041f\u0430\u0442\u0442\u0435\u0440\u043d\u043e\u0432 \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0434\u043b\u044f \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u043a \u043c\u0435\u0442\u043e\u0434\u0430\u043c \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.\n- `py2dantic`: \u0423\u0442\u0438\u043b\u0438\u0442\u0430 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u0430 Python dict \u0432 Pydantic \u0441\u0445\u0435\u043c\u0443.\n- `docintegration`: \u0410\u0432\u0442\u043e \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u044f \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438, \u0434\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0445 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0439.\n\n### Use Integration HTTP\n\n`IntegrationHTTP` - \u041a\u043b\u0430\u0441\u0441 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043c\u0435\u0442\u043e\u0434\u043e\u0432 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u043f\u043e HTTP, \u0446\u0435\u043d\u0442\u0440\u0430\u043b\u0438\u0437\u0443\u0435\u0442 \u043b\u043e\u0433\u0438\u043a\u0443 \u0432\u044b\u0437\u043e\u0432\u043e\u0432 \u043a \u0432\u043d\u0435\u0448\u043d\u0438\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u043c, \u043f\u0440\u043e\u0432\u043e\u0434\u044f \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044e \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445. \u0422\u0430\u043a\u0436\u0435 \u0432 \u043a\u043b\u0430\u0441\u0441\u0435 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u0438 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f \u0432\u043d\u0435\u0448\u043d\u0435\u0433\u043e API.\n\n\u041f\u0440\u0435\u0438\u043c\u0443\u0449\u0435\u0441\u0442\u0432\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u0434\u0445\u043e\u0434\u0430:\n\n- \u042f\u0432\u043d\u0430\u044f \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0444\u043e\u0440\u043c\u0430\u0442\u043e\u0432 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0438 \u043e\u0442\u0432\u0435\u0442\u0430.\n- \u041b\u0435\u0433\u043a\u0430\u044f \u043f\u0435\u0440\u0435\u043d\u043e\u0441\u0438\u043c\u043e\u0441\u0442\u044c \u043a\u043e\u0434\u0430 \u043c\u0435\u0436\u0434\u0443 \u043f\u0440\u043e\u0435\u043a\u0442\u0430\u043c\u0438 \u2014 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u0430\u0441\u0441\u044b, \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043d\u0430 `IntegrationHTTP`.\n- \u041a\u043e\u043d\u0441\u043e\u043b\u0438\u0434\u0430\u0446\u0438\u044f \u043b\u043e\u0433\u0438\u043a\u0438 \u0432\u043d\u0435\u0448\u043d\u0438\u0445 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0432 \u043e\u0434\u043d\u043e\u043c \u043c\u0435\u0441\u0442\u0435, \u0447\u0442\u043e \u0443\u043f\u0440\u043e\u0449\u0430\u0435\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443.\n- \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043b\u0435\u0433\u043a\u043e \u0437\u0430\u043c\u0435\u043d\u044f\u0442\u044c \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0435 \u043c\u0435\u0442\u043e\u0434\u044b \u043d\u0430 `mock` \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f.\n- \u041b\u0435\u0433\u043a\u043e\u0435 \u0432\u043d\u0435\u0434\u0440\u0435\u043d\u0438\u0435 `Stability Patterns` \u0434\u043b\u044f \u043c\u0435\u0442\u043e\u0434\u043e\u0432 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.\n\n\u0414\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u0448\u0430\u0433\u0430\u043c:\n\n1. \u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0440\u0430\u0441\u043f\u043e\u043b\u0430\u0433\u0430\u0442\u044c \u043a\u043e\u0434 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0439 \u0432 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438 `app/integration/\u0418\u043c\u044f\u041f\u0430\u043a\u0435\u0442\u0430\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438`.\n2. \u0421\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u0430\u0441\u0441\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0439 `app/integration/\u0418\u043c\u044f\u041f\u0430\u043a\u0435\u0442\u0430\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438/endpoint.py`:\n\n ```python\n import httpx\n from pydantic import BaseModel\n\n from fastapi_accelerator.integration.http_integration import (\n ApiHTTP,\n EndpointsDeclaration,\n HTTPMethod,\n IntegrationHTTP,\n )\n from fastapi_accelerator.integration.stability_patterns import sp\n\n class \u0418\u043c\u044fIntegration(EndpointsDeclaration):\n\n integration = IntegrationHTTP(\n \"\u0418\u043c\u044f \u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438\",\n doc=\"\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441 ... API\",\n )\n\n class Schema:\n \"\"\"\u0421\u0445\u0435\u043c\u044b Pydantic \u0434\u043b\u044f \u0443\u0441\u043f\u0435\u0448\u043d\u044b\u0445 \u043e\u0442\u0432\u0435\u0442\u043e\u0432\"\"\"\n\n class Successful(BaseModel)\n body: str\n\n class SchemaError:\n \"\"\"\u0421\u0445\u0435\u043c\u044b Pydantic \u0434\u043b\u044f \u043d\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u044b\u0445 \u043e\u0442\u0432\u0435\u0442\u043e\u0432\"\"\"\n\n class http400(BaseModel)\n error: str\n\n @integration.endpoint(\n HTTPMethod.post,\n \"/\u043f\u0443\u0442\u044c\",\n version=\"...\",\n docurl=\"https://...\"\n )\n @sp.RetryPattern()\n async def \u0438\u043c\u044f_\u043c\u0435\u0442\u043e\u0434\u0430(api: ApiHTTP, \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442_1: str) -> Schema.Successful | SchemaError.http400:\n try:\n response: httpx.Response = await api.client.post(api.url.geturl(), json=...)\n return response.json()\n except httpx.RequestError as e:\n raise e\n ```\n\n3. \u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u043a \u043f\u0440\u043e\u0435\u043a\u0442\u0443 `app/core/useintegration.py`:\n\n ```python\n \"\"\"\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0435 \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0435\"\"\"\n\n from app.integration.\u0418\u043c\u044f\u041f\u0430\u043a\u0435\u0442\u0430\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.endpoint import \u0418\u043c\u044fIntegration\n\n # \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438\n \u0438\u043c\u044f_api = \u0418\u043c\u044fIntegration(\n # \u041d\u0430\u0447\u0430\u043b\u043e \u0434\u043b\u044f url \u043f\u0443\u0442\u0438\n base_url=\"https://\u043f\u0443\u0442\u044c...\",\n # \u0414\u043e\u0441\u0442\u0443\u043f\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u043e\u0436\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0432 \u043c\u0435\u0442\u043e\u0434\u0430\u0445 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438\n credentials={...},\n )\n ```\n\n4. \u041f\u0440\u0438\u043c\u0435\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043a\u043b\u0430\u0441\u0441\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432 `FastAPI`:\n\n ```python\n from app.core.useintegration import \u0438\u043c\u044f_api\n from app.integration.\u0418\u043c\u044f\u041f\u0430\u043a\u0435\u0442\u0430\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.schema import \u0418\u043c\u044fSchema\n\n @router.get(\"/\u0438\u043c\u044f\")\n async def \u0438\u043c\u044f(\u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442_1: str) -> \u0418\u043c\u044fIntegration.Schema.Successful:\n # \u0412\u044b\u0437\u0432\u0430\u0442\u044c \u043c\u0435\u0442\u043e\u0434 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438\n return await \u0438\u043c\u044f_api.\u0438\u043c\u044f_\u043c\u0435\u0442\u043e\u0434\u0430(\u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442_1)\n ```\n\n---\n\n\u0412\u0430\u043c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0442\u0438\u043f \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0438\u0437 \u043c\u0435\u0442\u043e\u0434\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.\n\n\u041e\u0442\u0432\u0435\u0442 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c:\n\n- `dict`: \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u043e\u0436\u043d\u043e \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432 \u043e\u0434\u043d\u0443 \u0441\u0445\u0435\u043c\u0443 `Pydantic`.\n- `list[dict]`: \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u043e\u0436\u043d\u043e \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432 \u0441\u043f\u0438\u0441\u043e\u043a \u0441\u0445\u0435\u043c `Pydantic`.\n- \u041d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0442\u0438\u043f\u043e\u0432 \u043e\u0442\u0432\u0435\u0442\u0430: \u042d\u0442\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u043b\u044f \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u044f \u0442\u0438\u043f\u0430 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0433\u043e \u043e\u0442\u0432\u0435\u0442\u0430 \u0438 \u0434\u043b\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u043e\u0448\u0438\u0431\u043e\u043a. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, `-> \u0423\u0441\u043f\u0435\u0448\u043d\u044b\u0439\u041e\u0442\u0432\u0435\u0442 | \u041d\u0435\u0423\u0441\u043f\u0435\u0448\u043d\u044b\u0439\u041e\u0442\u0432\u0435\u0442` \u0438\u043b\u0438 `-> list[\u0423\u0441\u043f\u0435\u0448\u043d\u044b\u0439\u041e\u0442\u0432\u0435\u0442] | \u041d\u0435\u0423\u0441\u043f\u0435\u0448\u043d\u044b\u0439\u041e\u0442\u0432\u0435\u0442`.\n- \u0412 \u0445\u0443\u0434\u0448\u0435\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c `Any`.\n\n#### \u041f\u0440\u0438\u043c\u0435\u0440 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Google Translate\n\n- \u041a\u043b\u0430\u0441\u0441\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 `app/integration/google_translate/endpoint.py`:\n\n```python\nimport httpx\nfrom pydantic import BaseModel\n\nfrom fastapi_accelerator.integration.http_integration import (\n ApiHTTP,\n EndpointsDeclaration,\n HTTPMethod,\n IntegrationHTTP,\n)\nfrom fastapi_accelerator.integration.stability_patterns import sp\n\n\nclass GoogleTranslateEndpoints(EndpointsDeclaration):\n integration = IntegrationHTTP(\n \"Google Translate\",\n doc=\"\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441 Google Translate API\",\n )\n\n class Schema:\n \"\"\"\u0421\u0445\u0435\u043c\u044b \u0434\u043b\u044f \u0443\u0441\u043f\u0435\u0448\u043d\u044b\u0445 \u043e\u0442\u0432\u0435\u0442\u043e\u0432\"\"\"\n\n class TranslateV2(BaseModel):\n text: str\n\n class SchemaError:\n \"\"\"\u0421\u0445\u0435\u043c\u044b \u0434\u043b\u044f \u043d\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u044b\u0445 \u043e\u0442\u0432\u0435\u0442\u043e\u0432\"\"\"\n\n class http400Error(BaseModel):\n code: int\n message: str\n errors: list[dict]\n status: str\n details: list[dict]\n\n class http400(BaseModel):\n error: dict\n\n @integration.endpoint(\n HTTPMethod.post,\n \"/v1/translateHtml\",\n version=\"v2\",\n docurl=\"https://cloud.google.com/translate/docs/reference/rest\",\n )\n # \u041f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u043c \u043f\u0430\u0442\u0442\u0435\u0440\u043d\u044b \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u043e\u0441\u0442\u0438\n @sp.Timeout()\n # \u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441 \u043f\u0440\u0438 \u0432\u043e\u0437\u043d\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u0438\u0438 \u043e\u0448\u0438\u0431\u043a\u0438.\n @sp.RetryPattern()\n async def translate(\n api: ApiHTTP,\n text: str,\n from_lang: str,\n to_lang: str,\n ) -> Schema.TranslateV2 | SchemaError.http400: # \u0423\u043a\u0430\u0437\u0430\u0442\u044c \u0442\u0438\u043f\u0430 \u043e\u0442\u0432\u0435\u0442\u0430\n \"\"\"\u041f\u0435\u0440\u0435\u0432\u043e\u0434 \u0442\u0435\u043a\u0441\u0442\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e Google Translate\"\"\"\n try:\n # \u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0437\u0430\u043f\u0440\u043e\u0441 \u043a \u0432\u043d\u0435\u0448\u043d\u0435\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435\n response: httpx.Response = await api.client.post(\n api.url.geturl(),\n json=[[text.split(\"\\n\"), from_lang, to_lang], \"te_lib\"],\n headers={\n \"content-type\": \"application/json+protobuf\",\n \"x-goog-api-key\": api.credentials[\"API_TOKEN\"],\n },\n )\n # \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u043e\u0442\u0432\u0435\u0442\u0430\n print(f\"Processed {api.url}: Status {response.status_code}\")\n return {\"text\": \"\\n\".join(x[0] for x in response.json())}\n except httpx.RequestError as e:\n print(f\"Error processing {api.url}: {e}\")\n raise e\n```\n\n- \u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0432 `app/core/useintegration.py`:\n\n```python\n\"\"\"\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0435 \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0435\"\"\"\n\nfrom app.integration.google_translate.endpoint import GoogleTranslateIntegration\n\n# \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438\ngtapi = GoogleTranslateIntegration(\n base_url=\"https://translate-pa.googleapis.com\",\n # \u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u043c \u0432 \u043a\u043b\u0430\u0441\u0441 \u0434\u043e\u0441\u0442\u0443\u043f\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u043e\u0436\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0432 \u043c\u0435\u0442\u043e\u0434\u0430\u0445 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438\n credentials={\"API_TOKEN\": \"...\"},\n)\n```\n\n- \u041f\u0440\u0438\u043c\u0435\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0432 \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442\u0435 `FastAPI`:\n\n```python\nfrom datetime import timedelta\nfrom fastapi_accelerator.cache import cache_redis\nfrom app.core.cache import redis_client\nfrom app.core.useintegration import gtapi\nfrom app.integration.google_translate.schema import GoogleTranslateSchema\n\n@router.get(\"/translate\")\n# \u041c\u043e\u0436\u0435\u043c \u043b\u0435\u0433\u043a\u043e \u043a\u0435\u0448\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u0432\u0435\u0442\u044b \u043e\u0442 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0439\n@cache_redis(cache_class=redis_client, cache_ttl=timedelta(minutes=10))\nasync def translate(\n text: str, from_lang: str = \"en\", to_lang: str = \"ru\"\n) -> GoogleTranslateEndpoints.Schema.TranslateV2:\n # \u0412\u044b\u0437\u0432\u0430\u0442\u044c \u043c\u0435\u0442\u043e\u0434 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438\n return await gtapi.translate(text, from_lang, to_lang)\n```\n\n### Use Stability Patterns\n\n\u041c\u043e\u0434\u0443\u043b\u044c \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0430\u0442\u0442\u0435\u0440\u043d\u044b \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u043e\u0441\u0442\u0438 (Stability Patterns), \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e\u043c\u043e\u0433\u0430\u044e\u0442 \u0438\u0437\u0431\u0435\u0436\u0430\u0442\u044c \u043e\u0448\u0438\u0431\u043e\u043a \u0438 \u043f\u0435\u0440\u0435\u0433\u0440\u0443\u0437\u043e\u043a \u043f\u0440\u0438 \u0440\u0430\u0431\u043e\u0442\u0435 \u0441 \u0432\u043d\u0435\u0448\u043d\u0438\u043c\u0438 \u0441\u0435\u0440\u0432\u0438\u0441\u0430\u043c\u0438.\n\n> \u041e\u0441\u043d\u043e\u0432\u043d\u044b\u043c \u043a\u0440\u0438\u0442\u0435\u0440\u0438\u0435\u043c \u043d\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e\u0433\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432\u043e\u0437\u043d\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u0438\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f (raise) \u0432 \u043c\u0435\u0442\u043e\u0434\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438. \u0415\u0441\u043b\u0438 \u0432\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 \u043e\u0442\u0432\u0435\u0442 \u0441 \u043a\u043e\u0434\u043e\u043c 400 (\u043e\u0448\u0438\u0431\u043a\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0430) \u0438\u043b\u0438 500 (\u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430), \u043d\u043e \u043d\u0435 \u0432\u044b\u0437\u0432\u0430\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435, Stability Patterns \u0431\u0443\u0434\u0435\u0442 \u0441\u0447\u0438\u0442\u0430\u0442\u044c \u044d\u0442\u043e \u0443\u0441\u043f\u0435\u0448\u043d\u044b\u043c \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435\u043c \u0438 \u043d\u0435 \u043f\u0440\u0438\u043c\u0435\u043d\u0438\u0442 \u0441\u0432\u043e\u044e \u043b\u043e\u0433\u0438\u043a\u0443 \u0434\u043b\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u043e\u0448\u0438\u0431\u043e\u043a.\n\n\u041d\u0438\u0436\u0435 \u043f\u0440\u0438\u0432\u0435\u0434\u0435\u043d\u043e \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0445 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u043e\u0432:\n\n- `@sp.Fallback` (\u0420\u0435\u0437\u0435\u0440\u0432\u043d\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442) - \u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u043f\u0443\u0442\u044c \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u0441\u0431\u043e\u044f \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e. \u041f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 \u0434\u0435\u0433\u0440\u0430\u0434\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u0443\u0435\u043c\u043e, \u0430 \u043d\u0435 \u043f\u0430\u0434\u0430\u0442\u044c \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439.\n- `@sp.Timeout` (\u0422\u0430\u0439\u043c-\u0430\u0443\u0442) - \u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u0432\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u043e\u0442\u0432\u0435\u0442\u0430 \u043e\u0442 \u0432\u043d\u0435\u0448\u043d\u0435\u0433\u043e \u0441\u0435\u0440\u0432\u0438\u0441\u0430. \u041f\u0440\u0435\u0434\u043e\u0442\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0443 \u0440\u0435\u0441\u0443\u0440\u0441\u043e\u0432 \u043f\u0440\u0438 \u0437\u0430\u0432\u0438\u0441\u0430\u043d\u0438\u0438 \u0432\u044b\u0437\u043e\u0432\u0430.\n- `@sp.CircuitBreaker` (\u041f\u0440\u0435\u0434\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u0435\u043b\u044c) - \u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043e\u0448\u0438\u0431\u043e\u043a \u043f\u0440\u0438 \u0432\u044b\u0437\u043e\u0432\u0435 \u0432\u043d\u0435\u0448\u043d\u0435\u0433\u043e \u0441\u0435\u0440\u0432\u0438\u0441\u0430. \u041f\u0440\u0438 \u043f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u0438\u0438 \u043b\u0438\u043c\u0438\u0442\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442 \u0432\u044b\u0437\u043e\u0432, \u043f\u0440\u0435\u0434\u043e\u0442\u0432\u0440\u0430\u0449\u0430\u044f \u043a\u0430\u0441\u043a\u0430\u0434\u043d\u044b\u0435 \u0441\u0431\u043e\u0438.\n- `@sp.RetryPattern` (\u041f\u0430\u0442\u0442\u0435\u0440\u043d \u043f\u043e\u0432\u0442\u043e\u0440\u0435\u043d\u0438\u044f) - \u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441 \u043f\u0440\u0438 \u0432\u043e\u0437\u043d\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u0438\u0438 \u043e\u0448\u0438\u0431\u043a\u0438.\n- `@sp.Throttling` (\u0420\u0435\u0433\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435) - \u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043a \u0440\u0435\u0441\u0443\u0440\u0441\u0443 \u0434\u043b\u044f \u043f\u0440\u0435\u0434\u043e\u0442\u0432\u0440\u0430\u0449\u0435\u043d\u0438\u044f \u0435\u0433\u043e \u043f\u0435\u0440\u0435\u0433\u0440\u0443\u0437\u043a\u0438. \u0417\u0430\u0449\u0438\u0449\u0430\u0435\u0442 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u043e\u0442 \u0448\u0442\u043e\u0440\u043c\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432.\n\n\u042d\u0442\u0438 \u043f\u0430\u0442\u0442\u0435\u0440\u043d\u044b \u0434\u0435\u043b\u0430\u044e\u0442 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0431\u043e\u043b\u0435\u0435 \u0443\u0441\u0442\u043e\u0439\u0447\u0438\u0432\u043e\u0439, \u043c\u0438\u043d\u0438\u043c\u0438\u0437\u0438\u0440\u0443\u044f \u0440\u0438\u0441\u043a \u0441\u0431\u043e\u0435\u0432 \u0438 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u044f \u043f\u043b\u0430\u0432\u043d\u0443\u044e \u0434\u0435\u0433\u0440\u0430\u0434\u0430\u0446\u0438\u044e \u043f\u0440\u0438 \u0432\u043e\u0437\u043d\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.\n\n### Use py2dantic\n\n`py2dantic` \u2014 \u044d\u0442\u043e \u0443\u0434\u043e\u0431\u043d\u0430\u044f \u0443\u0442\u0438\u043b\u0438\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0431\u044b\u0441\u0442\u0440\u043e \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0441\u0445\u0435\u043c\u044b Pydantic \u0438\u0437 \u0441\u043b\u043e\u0432\u0430\u0440\u0435\u0439 Python. \u042d\u0442\u0438 \u0441\u0445\u0435\u043c\u044b \u043c\u043e\u0436\u043d\u043e \u044d\u0444\u0444\u0435\u043a\u0442\u0438\u0432\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043b\u044f \u0442\u0438\u043f\u0438\u0437\u0430\u0446\u0438\u0438 \u0432 \u0432\u0430\u0448\u0435\u043c \u043f\u0440\u043e\u0435\u043a\u0442\u0435.\n\n- \u041f\u0440\u0438\u043c\u0435\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f:\n\n```python\nfrom fastapi_accelerator.commands.py2dantic import generate_pydantic_models\n\nsample_data = {\n \"items\": [\n {\n \"id\": \"0000\",\n \"premium\": False,\n \"name\": \"Python Developer\",\n \"department\": None,\n \"has_test\": True,\n \"response_letter_required\": False,\n \"salary\": None,\n \"type\": {\"id\": \"open\", \"name\": \"\u041e\u0442\u043a\u0440\u044b\u0442\u0430\u044f\"},\n \"address\": None,\n \"response_url\": None,\n \"sort_point_distance\": None,\n \"published_at\": \"2024-09-04T08:27:02+0300\",\n \"created_at\": \"2024-09-04T08:27:02+0300\",\n \"archived\": False,\n \"apply_alternate_url\": \"https://hh.ru/applicant/vacancy_response?vacancyId=0000\",\n \"branding\": {\"type\": \"MAKEUP\", \"tariff\": None},\n \"show_logo_in_search\": True\n },\n ],\n \"found\": 187,\n \"pages\": 2,\n \"page\": 1,\n \"per_page\": 100,\n \"clusters\": None,\n \"arguments\": None,\n \"fixes\": None,\n \"suggests\": None,\n \"alternate_url\": \"https://hh.ru/search/vacancy?enable_snippets=true&items_on_page=100&order_by=publication_time&page=1&salary=200000&schedule=remote&text=Python+FastAPI\",\n}\n\nassert (\n generate_pydantic_models(sample_data, depth=2, prfix_class_name=\"Job\").strip()\n == \"\"\"\nclass Job_ItemsItem(BaseModel):\n id: str = None\n premium: int = None\n name: str = None\n department: Any = None\n has_test: int = None\n response_letter_required: int = None\n area: Dict = None\n salary: Any = None\n type: Dict = None\n address: Any = None\n response_url: Any = None\n sort_point_distance: Any = None\n published_at: str = None\n created_at: str = None\n archived: int = None\n apply_alternate_url: str = None\n branding: Dict = None\n show_logo_in_search: int = None\n\nclass Job(BaseModel):\n items: List[Job_ItemsItem]\n found: int = None\n pages: int = None\n page: int = None\n per_page: int = None\n clusters: Any = None\n arguments: Any = None\n fixes: Any = None\n suggests: Any = None\n alternate_url: str = None\n \"\"\".strip()\n )\n```\n\n### Use docintegration\n\n\u0414\u0430\u043d\u043d\u044b\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0443\u0437\u043d\u0430\u0442\u044c, \u043a\u0430\u043a\u0438\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0435, \u0430\u043d\u0430\u043b\u043e\u0433\u0438\u0447\u043d\u043e \u0442\u043e\u043c\u0443, \u043a\u0430\u043a \u044d\u0442\u043e \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043e \u0432 OpenAPI Swagger \u0434\u043b\u044f \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0433\u043e FastAPI.\n\n\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443: `http://host:port/docintegration`.\n\n\u0427\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u043f\u0443\u0442\u044c, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432 \u0444\u0430\u0439\u043b\u0435 `main.py` \u0432 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0435 `base_pattern` \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0439 \u0432 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u0435 `useintegration`:\n\n```python\nfrom app.core.useintegration import \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f_1, \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f_2\nfrom fastapi_accelerator.pattern.pattern_fastapi import base_pattern\n\n# \u041f\u0430\u0442\u0442\u0435\u0440\u043d \u0434\u043b\u044f \u043f\u0440\u043e\u0435\u043a\u0442\u0430\nbase_pattern(\n app,\n ...\n useintegration=[\u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f_1, \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f_2],\n)\n```\n\n![\u0412\u043d\u0435\u0448\u043d\u0438\u0439 \u0432\u0438\u0434 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438](./__attach__/Screenshot_20240914_153330.png)\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=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# Fastapi Accelerator \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\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 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f `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.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 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f `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## \u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0439 \u0441 \u0432\u043d\u0435\u0448\u043d\u0438\u043c\u0438 API\n\n\u0421\u0430\u043c\u044b\u043c \u0441\u043b\u043e\u0436\u043d\u044b\u043c \u0430\u0441\u043f\u0435\u043a\u0442\u043e\u043c \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u044f\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 \u0432\u043d\u0435\u0448\u043d\u0438\u043c\u0438 API, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0442\u0435\u0441\u0442\u043e\u0432 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0438\u0437\u0431\u0435\u0433\u0430\u0442\u044c \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0445 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043a \u044d\u0442\u0438\u043c API. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u043d\u0430\u043c \u043f\u0440\u0438\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0441\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0442\u044c \u043b\u043e\u0433\u0438\u043a\u0443 \u0434\u043b\u044f \u0438\u043c\u0438\u0442\u0430\u0446\u0438\u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u0432\u043d\u0435\u0448\u043d\u0435\u0433\u043e API. \u0425\u043e\u0442\u044f \u043d\u0430\u0448\u0430 \u0438\u043c\u0438\u0442\u0430\u0446\u0438\u044f \u043c\u043e\u0436\u0435\u0442 \u043d\u0435 \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u043e\u0442\u0440\u0430\u0436\u0430\u0442\u044c \u0440\u0435\u0430\u043b\u044c\u043d\u0443\u044e \u0440\u0430\u0431\u043e\u0442\u0443 API, \u044d\u0442\u043e \u0432\u0441\u0435 \u0436\u0435 \u043b\u0443\u0447\u0448\u0435, \u0447\u0435\u043c \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e.\n\n\u0412 \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0445 \u0447\u0430\u0441\u0442\u043e \u043a\u0430\u0436\u0434\u044b\u0439 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u0441\u043e\u0437\u0434\u0430\u0435\u0442 \u0441\u0432\u043e\u0438 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 \u043c\u043e\u043a\u0438 \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0439, \u0447\u0442\u043e \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442 \u043a \u043f\u0443\u0442\u0430\u043d\u0438\u0446\u0435 \u0438 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u044e \u0435\u0434\u0438\u043d\u043e\u0433\u043e \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u0430. \u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u0432\u044b\u0441\u043e\u043a\u0430\u044f \u0432\u0435\u0440\u043e\u044f\u0442\u043d\u043e\u0441\u0442\u044c \u043e\u0448\u0438\u0431\u043e\u043a, \u043a\u043e\u0433\u0434\u0430 \u043c\u043e\u043a \u043c\u043e\u0436\u0435\u0442 \u043d\u0435 \u0441\u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c, \u0438 \u043f\u0440\u043e\u0438\u0437\u043e\u0439\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0439 API.\n\n\u0414\u043b\u044f \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u043c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u043a\u043b\u0430\u0441\u0441\u044b \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 `EndpointsDeclaration` \u0441 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u043e\u043c `@integration.endpoint`, \u0447\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0435\u0434\u0438\u043d\u0443\u044e \u0442\u043e\u0447\u043a\u0443 \u0432\u0445\u043e\u0434\u0430, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043c\u043e\u0436\u043d\u043e \u043b\u0435\u0433\u043a\u043e \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.\n\n\u041f\u0440\u0438\u043c\u0435\u0440 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043c\u0435\u0442\u043e\u0434\u0430 `FastAPI`, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 \u043c\u0435\u0442\u043e\u0434 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438:\n\n- \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a FastAPI:\n\n```python\n@router.get(\"/translate\")\nasync def translate_api(\n text: str, from_lang: str = \"en\", to_lang: str = \"ru\"\n) -> GoogleTranslateEndpoints.Schema.TranslateV2:\n # \u0412\u044b\u0437\u0432\u0430\u0442\u044c \u043c\u0435\u0442\u043e\u0434 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438\n return await gtapi.translate(text, from_lang, to_lang)\n```\n\n- `test_\u0438\u043c\u044f.py` \u043f\u0440\u0438\u043c\u0435\u0440 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 `google` \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u0447\u0438\u043a:\n\n```python\nfrom fastapi_accelerator.testutils.fixture_integration import patch_integration\nfrom app.integration.google_translate.mock import google_translate_mock_rules\n\n# \u041f\u0440\u0430\u0432\u0438\u043b\u0430 \u043f\u043e\u0434\u043c\u0435\u043d\u044b \u043c\u0435\u0442\u043e\u0434\u043e\u0432 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u043d\u0430 mock.\n# \u0415\u0441\u043b\u0438 \u0432 \u043a\u043e\u0434\u0435 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u0430 \u0432 mock_rules, \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u0435\u0442 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435.\n# \u042d\u0442\u043e \u043f\u0440\u0435\u0434\u043e\u0442\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u0435 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0435 \u0437\u0430\u043f\u0440\u043e\u0441\u044b, \u0435\u0441\u043b\u0438 \u0432\u044b \u0437\u0430\u0431\u044b\u043b\u0438 \u0443\u043a\u0430\u0437\u0430\u0442\u044c mock.\n@patch_integration(mock_rules=google_translate_mock_rules)\ndef test_integration_google_translate(client: TestClient, url_path_for: Callable):\n # \u0412\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430\n response = client.get(\n url_path_for(\"translate_api\"),\n params=dict(text=\"Hello\", from_lang=\"en\", to_lang=\"ru\"),\n )\n # \u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043e\u0442\u0432\u0435\u0442\u0430\n assert response.json() == {\"text\": \"\u041f\u0440\u0438\u0432\u0435\u0442\"}\n```\n\n> \u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u043b\u044f `mock_rules` \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u043a\u0443\u0434\u0430 \u0443\u0433\u043e\u0434\u043d\u043e, \u043d\u043e \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u044e \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0438 \u0431\u0440\u0430\u0442\u044c \u0438\u0437 `app/integration/\u041f\u0430\u043a\u0435\u0442\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438/mock.py`\n\n- \u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043f\u043e\u0434\u043c\u0435\u043d\u043d\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u0432 \u043e\u0434\u043d\u043e\u043c \u043f\u0430\u043a\u0435\u0442\u0435 \u0441 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439 \u0432 `app/integration/\u041f\u0430\u043a\u0435\u0442\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438/mock.py`, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0435 \u044d\u0442\u043e\u0433\u043e \u043f\u0430\u043a\u0435\u0442\u0430 \u0432 \u0434\u0440\u0443\u0433\u043e\u0439 \u043f\u0440\u043e\u0435\u043a\u0442 \u0442\u0430\u043a\u0436\u0435 \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u0438\u0437 `mock.py`, \u043d\u0435 \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u044f \u0441\u0432\u043e\u0438 \u0438\u043c\u0438\u0442\u0430\u0446\u0438\u0438.\n\n```python\nfrom app.integration.google_translate.endpoint import GoogleTranslateEndpoints\nfrom fastapi_accelerator.integration.http_integration import ApiHTTP\nfrom fastapi_accelerator.testutils.fixture_integration import MockRules\n\n\nasync def overwrite_translate(api: ApiHTTP, *args, **kwargs):\n # \u0423\u0434\u043e\u0431\u043d\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0438\u043c\u0438\u0442\u0430\u0446\u0438\u0438, \u043a\u043e\u0433\u0434\u0430 \u0447\u0435\u0440\u0435\u0437 match \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u043e\u0432, \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0439 \u043e\u0442\u0432\u0435\u0442.\n match args:\n case (\"hello\", \"en\", \"ru\"):\n return {\"text\": \"\u041f\u0440\u0438\u0432\u0435\u0442\"}\n return None\n\n\n# \u041f\u0440\u0430\u0432\u0438\u043b\u0430 \u0437\u0430\u043c\u0435\u043d\u044b \u043c\u0435\u0442\u043e\u0434\u043e\u0432 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u043d\u0430 mock\ngoogle_translate_mock_rules = MockRules(\n # \u0420\u0435\u0430\u043b\u044c\u043d\u044b\u0439 \u043c\u0435\u0442\u043e\u0434 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438: \u0437\u0430\u043c\u0435\u043d\u0430 \u043d\u0430 mock \u0444\u0443\u043d\u043a\u0446\u0438\u044e\n {GoogleTranslateEndpoints.translate: overwrite_translate}\n)\n```\n\n> \u041a \u043c\u043e\u043a-\u0444\u0443\u043d\u043a\u0446\u0438\u044f\u043c \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u044e\u0442\u0441\u044f \u0442\u0435 \u0436\u0435 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f \u043a \u0444\u043e\u0440\u043c\u0430\u0442\u0443 \u043e\u0442\u0432\u0435\u0442\u0430, \u0447\u0442\u043e \u0438 \u043a \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c\u0443 \u043c\u0435\u0442\u043e\u0434\u0443 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.\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": "\u0423\u0441\u043a\u043e\u0440\u0438\u0442\u0435\u043b\u044c \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0434\u043b\u044f FastAPI, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u044e\u0449\u0438\u0439 \u0433\u043e\u0442\u043e\u0432\u044b\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u0438 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u044b \u0434\u043b\u044f \u0431\u044b\u0441\u0442\u0440\u043e\u0433\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439",
"version": "0.0.5",
"project_urls": {
"Homepage": "https://github.com/denisxab/fastapi-accelerator",
"Repository": "https://github.com/denisxab/fastapi-accelerator"
},
"split_keywords": [
"fastapi",
" django",
" web-framework",
" api",
" rest",
" async",
" sqlalchemy",
" admin-panel"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "05ece29fd3fd8dd96cbae633fc1bc991ba4e1b4579b3e17a09c6d03bd78a3502",
"md5": "b45d2289d3a35dfbf719e93221ff9dd1",
"sha256": "2813dd779a0bb11da3b88ce5f9af4c5f443271bdae09a5026ecfce487ab02f5a"
},
"downloads": -1,
"filename": "fastapi_accelerator-0.0.5-py3-none-any.whl",
"has_sig": false,
"md5_digest": "b45d2289d3a35dfbf719e93221ff9dd1",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.9",
"size": 88021,
"upload_time": "2024-09-14T13:28:05",
"upload_time_iso_8601": "2024-09-14T13:28:05.186822Z",
"url": "https://files.pythonhosted.org/packages/05/ec/e29fd3fd8dd96cbae633fc1bc991ba4e1b4579b3e17a09c6d03bd78a3502/fastapi_accelerator-0.0.5-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "c1668b25799c962336811c310380adef3a69add211c46dc4ce3a38dedc80ddf9",
"md5": "7ab5aff2649d94e6a4ff72d3e3379e6d",
"sha256": "dbb08e0c3e834fc1d79c7588b9bda62803b86312311d1d97479ec5e062176898"
},
"downloads": -1,
"filename": "fastapi_accelerator-0.0.5.tar.gz",
"has_sig": false,
"md5_digest": "7ab5aff2649d94e6a4ff72d3e3379e6d",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.9",
"size": 72325,
"upload_time": "2024-09-14T13:28:07",
"upload_time_iso_8601": "2024-09-14T13:28:07.749820Z",
"url": "https://files.pythonhosted.org/packages/c1/66/8b25799c962336811c310380adef3a69add211c46dc4ce3a38dedc80ddf9/fastapi_accelerator-0.0.5.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-09-14 13:28:07",
"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"
}