Pagination helpers for HawkAPI. Offset and cursor strategies. Page[T] / CursorPage[T] response envelopes. SQLAlchemy integration. In-memory iterable pagination.
pip install hawkapi-pagination
pip install 'hawkapi-pagination[sqlalchemy]' # adds SQLAlchemy + hawkapi-sqlalchemy helpersfrom hawkapi import Depends, HawkAPI
from hawkapi_sqlalchemy import get_session
from hawkapi_pagination import Page, OffsetParams, pagination_params, paginate_query
from sqlalchemy import select
@app.get("/items")
async def list_items(
params: OffsetParams = Depends(pagination_params),
session = Depends(get_session),
) -> Page[Item]:
return await paginate_query(session, select(Item), params)The route accepts ?page=N&size=M. Defaults: page=1, size=50, max_size=200. Out-of-range values are clamped (size > max_size → max_size) or rejected (page < 1 raises 400).
Page[T] shape:
{ "items": [...], "total": 137, "page": 1, "size": 50, "pages": 3 }For large tables where SELECT COUNT(*) is expensive:
page = await paginate_query(session, stmt, params, include_total=False)
# page.total == -1, page.pages == -1Clients can detect "more pages" by comparing len(items) to size.
Cursor pagination is the right choice for large tables that change under you (offset pages skip/duplicate rows when items are inserted/deleted between requests). The cursor is an HMAC-signed opaque token bound to:
- the endpoint path (replay across routes rejected),
- a TTL (default 1 hour),
- a direction (
asc/desc).
from hawkapi_pagination import CursorPage, CursorParams, cursor_params, paginate_cursor
@app.get("/items")
async def list_items(
params: CursorParams = Depends(cursor_params),
session = Depends(get_session),
) -> CursorPage[Item]:
return await paginate_cursor(
session,
select(Item),
order_by=Item.id, # MUST be unique + sortable (PK is the usual choice)
params=params,
cursor_secret="stable secret, ≥32 chars",
endpoint="/items",
direction="asc",
)CursorPage[T] shape:
{ "items": [...], "next_cursor": "eyJrI...", "prev_cursor": "" }When next_cursor is "", there are no more pages. Send the cursor back as ?cursor=... for the next call.
from hawkapi_pagination import paginate_iterable
page = await paginate_iterable(some_list, params)
# Works with sync iterables AND async generators.- Cursor signing — HMAC-SHA256 over the JSON payload +
hmac.compare_digestfor verification. - Endpoint binding — a cursor minted for
/api/itemscannot be used at/api/users. Always pass anendpointstring that is stable across requests for the same route. - TTL — default 1 hour. Override per route via
ttl=topaginate_cursor. max_sizecap — every params class enforces it; clients cannot ask for an unbounded page. Default 200.- Negative-int guard —
page < 1andsize < 1raiseValueError(which HawkAPI surfaces as 400).
git clone https://github.com/Hawk-API/hawkapi-pagination.git
cd hawkapi-pagination
uv sync --extra dev
uv run pytest -q
uv run ruff check . && uv run ruff format --check .
uv run pyright src/MIT.