Skip to content

Hawk-API/hawkapi-pagination

Repository files navigation

hawkapi-pagination

Pagination helpers for HawkAPI. Offset and cursor strategies. Page[T] / CursorPage[T] response envelopes. SQLAlchemy integration. In-memory iterable pagination.

Install

pip install hawkapi-pagination
pip install 'hawkapi-pagination[sqlalchemy]'   # adds SQLAlchemy + hawkapi-sqlalchemy helpers

Offset pagination

from 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_sizemax_size) or rejected (page < 1 raises 400).

Page[T] shape:

{ "items": [...], "total": 137, "page": 1, "size": 50, "pages": 3 }

Skipping the COUNT

For large tables where SELECT COUNT(*) is expensive:

page = await paginate_query(session, stmt, params, include_total=False)
# page.total == -1, page.pages == -1

Clients can detect "more pages" by comparing len(items) to size.

Cursor pagination

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.

In-memory iterables

from hawkapi_pagination import paginate_iterable


page = await paginate_iterable(some_list, params)
# Works with sync iterables AND async generators.

Security notes

  • Cursor signing — HMAC-SHA256 over the JSON payload + hmac.compare_digest for verification.
  • Endpoint binding — a cursor minted for /api/items cannot be used at /api/users. Always pass an endpoint string that is stable across requests for the same route.
  • TTL — default 1 hour. Override per route via ttl= to paginate_cursor.
  • max_size cap — every params class enforces it; clients cannot ask for an unbounded page. Default 200.
  • Negative-int guardpage < 1 and size < 1 raise ValueError (which HawkAPI surfaces as 400).

Development

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/

License

MIT.

About

Pagination helpers for HawkAPI — cursor + offset, Page[T] response model, SQLAlchemy integration

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages