Перейти к содержанию

FastAPI

hare.contrib.frameworks.fastapi подключает hare к приложению FastAPI. Ставится вместе с дополнительным пакетом fastapi (FastAPI 0.135 или новее, pydantic).

from fastapi import Depends
from pydantic import BaseModel, ConfigDict

from hare.contrib.frameworks import PageSchema
from hare.contrib.frameworks.fastapi import HareFastAPI, RequestQueryDependency
from hare.contrib.request_query import Page

HARE_CONFIG = {
    "connections": {"default": "postgresql://library:secret@localhost:5432/library"},
    "apps": {"library": {"models": ["library.models"], "default_connection": "default"}},
}

app = HareFastAPI(hare_config=HARE_CONFIG, atomic_requests=True, title="Books")


class BookSchema(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    title: str


@app.get("/books", response_model=PageSchema[BookSchema])
async def list_books(books: BookQuery = RequestQueryDependency.provide(BookQuery)) -> Page[Book]:
    return await books.page()

HareFastAPI

class HareFastAPI(FastAPI):
    def __init__(
        self,
        *,
        hare_config: Mapping[str, Any] | HareConfig | str,
        atomic_requests: bool | Sequence[str] = False,
        lifespan: Lifespan | None = None,
        health_routes: HealthRoutes | None = None,
        **kwargs: Any,
    ) -> None: ...

HareFastAPI — это приложение FastAPI; hare_config — то, что принимает Hare.init(config=...), kwargs — то, что принимает FastAPI().

  • Модели привязываются при создании приложения (Hare.bind_models(), без подключения). FastAPI читает параметры маршрута при его объявлении, а параметры запроса из параметров — Meta.filters, описания его фильтров — читаются из моделей. Маршрутизатору, объявленному в модуле, который импортируется до создания приложения, нужен вызов Hare.bind_models(HARE_CONFIG) до этого импорта.
  • HareContext. Открывается при запуске приложения, до его собственного lifespan — у которого база уже есть, — и закрывается при остановке. Контекст — глобальный запасной, поэтому его видит каждый запрос. При остановке Hare.close_connections() ещё и дожидается фоновых наблюдателей, которые ещё работают.
  • Запросы из параметров проверяются при запуске. RequestQuery.check_declarations() выполняется, как только модели настроены: ошибочный запрос из параметров не даёт приложению запуститься.
  • Ошибки ORM как ответы HTTP в собственном формате ошибок FastAPI, если приложение не отвечает на ошибку само (exception_handlers=):
Ошибка Ответ
DoesNotExist 404 Not Found, {"detail": "Not Found"}
IntegrityError (уникальность, внешний ключ, защищённая связь) 409 Conflict, {"detail": "Conflict"} — без сообщения базы, в котором названы таблицы и ограничения
TransactionRetryError (сбой сериализации или взаимная блокировка — запрос можно отправить снова) 409 Conflict, {"detail": "Conflict"}
ValidationError (значение, которое поле не принимает: вне диапазона, слишком длинное, нулевой байт, который отвергает PostgreSQL) 422 Unprocessable Content, сообщение поля в detail
InvalidRequestQuery 422 Unprocessable Content, как FastAPI сам отвечает на неверные параметры: {"detail": [{"type", "loc": ["query", name], "msg", "input"}]}; у ошибки всего запроса (от проверки модели) "loc": ["query"], а в input — параметры запроса
RequestQueryForbidden (параметр просит то, что запросу видеть нельзя: удалённые строки без разрешения may_see_deleted()) 403 Forbidden, причина — в detail
ApiGetterError (не загрузилось значение другого сервиса, которое требует Meta.api_getter запроса) 502 Bad Gateway без текста ошибки — он называет сервис и запрошенные значения

Смонтированное в него приложение (app.mount("/v1", sub_app)) отвечает на свои ошибки само: те же ответы ему дают FastAPI(exception_handlers=HareExceptionHandlers.get_handlers()).

  • Собственное состояние у каждого запроса. Каждый запрос начинает с пустого счётчика RepeatedQueryDetector. Только его собственные записи направляют его чтения в подключение, через которое он писал (чтение своих записей), — не записи запросов, которые его задача обслужила раньше, и не записи при старте приложения.
  • Транзакция на каждый запрос при atomic_requests — см. ниже.

Запросы из параметров как зависимости

RequestQueryDependency.provide(QueryClass) — это Depends(), который строит запрос из параметров HTTP-запроса. Каждый параметр класса становится параметром маршрута, проверяется FastAPI и перечисляется в схеме OpenAPI с типом, значением по умолчанию, ограничениями, перечислением и описанием:

  • параметр строки запроса — повторяемый для списка, ?status__in=draft&status__in=published;
  • параметр пути — для параметра с пометкой InPath():
class ChapterQuery(RequestQuery[Chapter]):
    pk: Annotated[KeyColumns[int, int], InPath()]

    class Meta:
        queryset = Chapter.objects.all()
        pagination = None


@app.get("/chapters/{pk}", response_model=ChapterSchema)
async def get_chapter(chapter: ChapterQuery = RequestQueryDependency.provide(ChapterQuery)) -> Chapter:
    return await chapter.get()

KeyColumns и CommaSeparated приходят в запрос текстом параметра, который запрос разбирает сам. Запрос получает сам HTTP-запрос: query.request, а также адреса next и previous для страницы. На значение, которое запрос отклоняет, — сортировку, которую он не разрешает, испорченный курсор, — отвечают так же, как FastAPI отвечает на неверный параметр.

Ответы

FastAPI проверяет то, что маршрут возвращает со своим response_model, по его атрибутам и описывает это в схеме; схемы строки, списка и страницы — см. Веб-фреймворки.

Транзакция на каждый запрос: atomic_requests

HareFastAPI(..., atomic_requests=True) выполняет каждый HTTP-запрос в транзакции; маршрут, обработчик которого помечен RequestTransaction.skip, выполняется без неё:

from hare.contrib.frameworks import RequestTransaction


@app.post("/imports")
@RequestTransaction.skip
async def import_books(request: Request) -> None: ...

Когда транзакция фиксируется или откатывается и что ещё выполняется без неё — см. Веб-фреймворки.

Маршруты здоровья: health_routes

from hare.contrib.frameworks.health_routes import HealthRoutes
from hare.health import HealthCheck

app = HareFastAPI(hare_config=HARE_CONFIG, health_routes=HealthRoutes(HealthCheck()))

Какие маршруты готовности и работоспособности он добавляет — см. Веб-фреймворки.

Тесты

С pytest-плагином hare тестируемое приложение работает на тестовых базах: запущенное в цикле событий теста (его lifespan, с клиентом httpx и ASGITransport), под hare_app_db оно берёт контекст теста, и его записи откатываются вместе с записями теста, — см. Тесты приложения. TestClient из Starlette запускает приложение в своём потоке — с ним берите hare_app_transactional_db.