FastAPI¶
hare.contrib.frameworks.fastapi connects hare to a FastAPI application.
Install it with the fastapi extra (FastAPI 0.135 or newer, 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 is a FastAPI application; hare_config is what Hare.init(config=...) takes, kwargs
what FastAPI() takes.
- The models are bound when the application is created (
Hare.bind_models(), no connection). FastAPI reads a route's parameters when the route is declared, and a request query's parameters —Meta.filters, the descriptions of its filters — are read from the models. A router declared in a module imported before the application is created needsHare.bind_models(HARE_CONFIG)called before that import. - The
HareContext. Opened when the application starts, before its ownlifespan— which already has the database — and closed when it stops. The context is the global fallback, so every request sees it. On shutdownHare.close_connections()also waits for the background observers still running. - Request queries are checked at startup.
RequestQuery.check_declarations()runs once the models are set up: a wrong request query stops the application from starting. - ORM errors as HTTP answers, in FastAPI's own error format, unless the application answers the error
itself (
exception_handlers=):
| Error | Answer |
|---|---|
DoesNotExist |
404 Not Found, {"detail": "Not Found"} |
IntegrityError (unique, foreign key, protected relation) |
409 Conflict, {"detail": "Conflict"} — without the database's message, which names tables and constraints |
TransactionRetryError (a serialization failure or deadlock — the request may be sent again) |
409 Conflict, {"detail": "Conflict"} |
ValidationError (a value a field refuses: out of its range, too long, a null byte PostgreSQL refuses) |
422 Unprocessable Content, the field's message in detail |
InvalidRequestQuery |
422 Unprocessable Content as FastAPI answers invalid parameters itself: {"detail": [{"type", "loc": ["query", name], "msg", "input"}]}; an error of the whole query (a model validator's) has "loc": ["query"] and the query's parameters as input |
RequestQueryForbidden (a parameter asks for what the request may not see: deleted rows without may_see_deleted()) |
403 Forbidden, the reason in detail |
ApiGetterError (a value of another service a request query's Meta.api_getter requires couldn't be loaded) |
502 Bad Gateway, without the error's message, which names the service and the values asked |
An application mounted on it (app.mount("/v1", sub_app)) answers its own errors: give it the same
answers with FastAPI(exception_handlers=HareExceptionHandlers.get_handlers()).
- A request's own state. Each request starts with an empty count of
RepeatedQueryDetector. Only its own writes send its reads to the connection written through (reading your own writes) — not the writes of the requests its task served before or of the application's startup. - A transaction per request with
atomic_requests— see below.
Request queries as dependencies¶
RequestQueryDependency.provide(QueryClass) is the Depends() building a request query from the
request. Each parameter of the class is a parameter of the route, validated by FastAPI and listed in the
OpenAPI schema with its type, default, limits, enum and description:
- a query parameter — a list one repeated,
?status__in=draft&status__in=published; - a path parameter for one marked
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 and CommaSeparated reach the query as the text of the parameter, which the query parses.
The query gets the request: query.request, and the addresses of a page's next and previous. A value
the query refuses — an ordering it doesn't allow, a malformed cursor — is answered as FastAPI answers an
invalid parameter.
Responses¶
FastAPI validates what a route returns with its response_model from its attributes and documents it —
the schemas of a row, a list and a page — see Web frameworks.
A transaction per request: atomic_requests¶
HareFastAPI(..., atomic_requests=True) runs each HTTP request in a transaction; a route whose endpoint
is marked RequestTransaction.skip runs without it:
from hare.contrib.frameworks import RequestTransaction
@app.post("/imports")
@RequestTransaction.skip
async def import_books(request: Request) -> None: ...
When the transaction commits or rolls back, and what else runs without it — see Web frameworks.
Health routes: 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()))
The readiness and liveness routes it adds — see Web frameworks.
Tests¶
With hare's pytest plugin the application under test runs on the test databases: started in the test's
event loop (its lifespan, with an httpx ASGITransport client), under hare_app_db it takes the test's
context and its writes are rolled back with the test's — see
Application tests. Starlette's TestClient runs the application
in a thread of its own — take hare_app_transactional_db with it.