Robyn¶
hare.contrib.frameworks.robyn подключает hare к приложению Robyn. Ставится вместе
с дополнительным пакетом robyn (Robyn 0.88, pydantic).
from pydantic import BaseModel, ConfigDict
from hare.contrib.frameworks import PageSchema
from hare.contrib.frameworks.robyn import HareRobyn
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 = HareRobyn(__file__, hare_config=HARE_CONFIG, atomic_requests=True)
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) -> Page[Book]:
return await books.page()
HareRobyn¶
class HareRobyn(Robyn):
def __init__(
self,
file_object: str,
*,
hare_config: Mapping[str, Any] | HareConfig | str,
atomic_requests: bool | Sequence[str] = False,
openapi: OpenAPI | None = None,
health_routes: HealthRoutes | None = None,
**kwargs: Any,
) -> None: ...
HareRobyn — это приложение Robyn; hare_config — то, что принимает Hare.init(config=...),
file_object и kwargs — то, что принимает Robyn().
HareContext. Модели привязываются при создании приложения; контекст открывается при запуске рабочего процесса — до собственногоstartup_handlerприложения — и закрывается при его остановке, после собственногоshutdown_handler. Контекст — глобальный запасной, поэтому его видит каждый запрос. При остановкеHare.close_connections()ещё и дожидается фоновых наблюдателей, которые ещё работают.- Запросы из параметров проверяются при запуске.
RequestQuery.check_declarations()выполняется, как только модели настроены: ошибочный запрос из параметров не даёт процессу запуститься. - Запросы из параметров для обработчиков и их параметры в схеме OpenAPI — см. ниже.
- Ответы, проверенные
response_modelмаршрута — см. ниже. - Ошибки ORM как ответы HTTP (
HareExceptionHandlers):
| Ошибка | Ответ |
|---|---|
InvalidRequestQuery |
422 Unprocessable Content, как Robyn сам отвечает на неверное тело pydantic: {"error": "Validation Error", "detail": [{"type", "loc": ["query", name], "msg", "input"}]}; у ошибки всего запроса (от проверки модели) "loc": ["query"], а в input — параметры запроса |
DoesNotExist |
404 Not Found, {"detail": "Not Found"} |
IntegrityError (уникальность, внешний ключ, защищённая связь) |
409 Conflict, {"detail": "Conflict"} — без сообщения базы, в котором названы таблицы и ограничения |
TransactionRetryError (сбой сериализации или взаимная блокировка — запрос можно отправить снова) |
409 Conflict, {"detail": "Conflict"} |
ValidationError (значение, которое поле не принимает: вне диапазона, слишком длинное, нулевой байт, который отвергает PostgreSQL) |
422 Unprocessable Content, сообщение поля в detail |
RequestQueryForbidden (параметр просит то, что запросу видеть нельзя: удалённые строки без разрешения may_see_deleted()) |
403 Forbidden, причина — в detail |
ApiGetterError (не загрузилось значение другого сервиса, которое требует Meta.api_getter запроса) |
502 Bad Gateway без текста ошибки — он называет сервис и запрошенные значения |
Любую другую ошибку обрабатывает Robyn (500). Robyn хранит один обработчик исключений на приложение и
передаёт его каждому маршруту при объявлении маршрута: приложение, которое отвечает на ошибки само,
задаёт свой обработчик через exception() до объявления маршрутов и первым делом вызывает в нём
HareExceptionHandlers.get_response(error).
- Собственное состояние у каждого запроса. Каждый запрос начинает с пустого счётчика
RepeatedQueryDetector (обработчик before_request). Только его собственные записи направляют его
чтения в подключение, через которое он писал (чтение своих
записей), — не записи запросов, которые его
задача обслужила раньше, и не записи при старте приложения.
- Транзакция на каждый запрос при atomic_requests — см. ниже.
Маршрутизатор приложения — это HareSubRouter, то есть SubRouter, маршруты которого получают то же
самое, а транзакции — когда приложение его подключит:
from hare.contrib.frameworks.robyn import HareSubRouter
chapters = HareSubRouter(prefix="/chapters")
@chapters.get("/:pk", response_model=ChapterSchema)
async def get_chapter(chapter: ChapterQuery) -> Chapter:
return await chapter.get()
app.include_router(chapters)
Запросы из параметров для обработчиков¶
Параметр обработчика с аннотацией класса запроса из параметров (books: BookQuery) получает запрос,
построенный из HTTP-запроса: из строки параметров — повторяемое имя для каждого значения списка,
?status__in=draft&status__in=published — и из параметров пути для параметров с пометкой InPath()
(pk: Annotated[KeyColumns[int, int], InPath()] с /chapters/:pk). Сам Robyn читал бы модель pydantic
из тела запроса. Запрос получает сам HTTP-запрос (query.request) и полный адрес запроса для next и
previous страницы. На значение, которое запрос отклоняет, отвечают 422.
Схема OpenAPI (HareOpenAPI, по умолчанию — схема приложения) перечисляет каждый параметр запроса —
параметр строки запроса или параметр пути для отмеченного InPath() — с типом, ограничениями,
перечислением и описанием.
Ответы¶
Ответ маршрута — строка, список, страница — проверяется по response_model по его атрибутам и отдаётся
как JSON со status_code маршрута (по умолчанию 200); сам Robyn проверяет только возвращённый словарь.
Возвращённый Response отправляется как есть. Схемы строки, списка и страницы — см.
Веб-фреймворки.
Транзакция на каждый запрос: atomic_requests¶
HareRobyn(..., atomic_requests=True) выполняет каждый обработчик в транзакции, которая фиксируется до
того, как Robyn отправит ответ; обработчик, помеченный RequestTransaction.skip, выполняется без неё,
как и константный маршрут:
from hare.contrib.frameworks import RequestTransaction
@app.post("/imports")
@RequestTransaction.skip
async def import_books(request: Request) -> None: ...
Когда транзакция фиксируется или откатывается — см. Веб-фреймворки.
Арендатор на каждый запрос¶
Robyn выполняет хуки before_request отдельно от обработчика запроса: область, которую задаёт синхронный
хук через Tenancy.set(), доходит до обработчика, а заданная async-хуком — нет. HareRobyn начинает
каждый запрос без области арендатора и без тегов запросов — раньше собственных хуков приложения, —
поэтому запрос без арендатора никогда не читает строки предыдущего запроса:
@app.before_request()
def set_tenant(request: Request) -> Request:
company = request.headers.get("x-company")
if company:
Tenancy.set(int(company))
return request
Запрос, которому хук не задал арендатора, обращается к модели с арендатором без области и получает
QueryError. Обработчик может открыть область и сам — with Tenancy.scope(company_id): ....
Маршруты здоровья: health_routes¶
from hare.contrib.frameworks.health_routes import HealthRoutes
from hare.health import HealthCheck
app = HareRobyn(__file__, hare_config=HARE_CONFIG, health_routes=HealthRoutes(HealthCheck()))
Какие маршруты готовности и работоспособности он добавляет — см. Веб-фреймворки.
Тесты¶
TestClient из Robyn запускает приложение в своём цикле событий — с pytest-плагином hare берите
hare_app_transactional_db: приложение берёт контекст теста, его записи фиксируются, а все таблицы
очищаются в конце теста, — см. Тесты приложения. Нужна
серверная база или файл SQLite.