PaginationCursor¶
Opaque, signed cursors, both ways, bound to the query: ?cursor=...&limit=20.
In an app: the example. How strategies work: Pagination. Every argument: the API reference.
A cursor points at the item at the edge of a page by its sort key, not by its position: rows
added or deleted meanwhile don't shift the pages (no item skipped or seen twice), and a database
finds any page through an index, from either side, as fast as the first. Cursors are the way to
the next and previous pages and nothing else: opaque, signed, bound to the query they were
issued for and, with a ttl, short-lived. For feeds, infinite scroll, lists with "Previous" and
"Next", and public APIs whose pagination may change behind the cursor without breaking anybody:
from fastapi_alcazaba.pagination.cursor import CursorPage, CursorParams, PaginationCursor
cursors = PaginationCursor(settings.cursor_secret) # secrets.token_urlsafe(32)
@app.get("/events")
@cache(expire=60)
async def events(
params: Annotated[CursorParams, Depends(cursors)],
kind: Kind | None = None,
) -> CursorPage[Event]:
key = tuple_(Event.created_at, Event.id)
if params.before is not None: # the page before: the nearest rows first
query = select(Event).where(key < params.before)
query = query.order_by(Event.created_at.desc(), Event.id.desc())
else:
query = select(Event).order_by(Event.created_at, Event.id)
if params.after is not None:
query = query.where(key > params.after)
if kind is not None:
query = query.where(Event.kind == kind)
rows = (await session.scalars(query.limit(params.fetch))).all()
return cursors.paginate(rows, params, key=lambda e: (e.created_at, e.id), queried=True)
GET /events?kind=login&limit=20 answers {"items": [...], "limit": 20, "prev_cursor": null,
"next_cursor": "WyJhIixb..."}, and ?kind=login&cursor=<next_cursor> the next page:
next_cursor is null on the last page, prev_cursor on the first. The Link header has the
first page and, when there's one, the previous and the next.
- The sort key (
key) must be unique:(created_at, id), notcreated_atalone, which two items may share. Its values can be strings, integers (up to 64 bits), floats, booleans, datetimes, dates, UUIDs and decimals, andparams.afterandparams.beforehave them back as such: one of them is set, by the cursor's direction, neither on the first page. - Queried or sliced: with
queried=True, the rows are those the database returned, up toparams.fetch(the limit and one more, which tells whether there's another page that way): pastparams.afterin order, or withparams.before, before it in the reverse order, nearest first, whichpaginateputs back in order. The other way there's always a page, the one the cursor came from. Without it,paginatefinds the page in the whole sorted list.descending=Truefor newest first, whereafterreadskey < :afterandbeforekey > :before ORDER BY key. - What a cursor holds: its direction, the sort key, a digest of the request's other query
parameters (all but
cursorandlimit, in any order) and, with attl, when it was issued, signed with HMAC-SHA256 (secretis required; a list rotates secrets). A cursor that a client made or changed, that comes with other query parameters than its own (the next page ofkind=loginasked withkind=logout), or older thanttlis a 400 before the endpoint runs; one longer than 1024 characters or malformed is a 422. Cursors are base64, not encrypted: clients can read the sort key, so sort by what they may see. - Cached apart: the query's digest is part of
CursorParams, so@cachekeeps the pages of each query apart, even for parameters the endpoint doesn't read. With attl, a page served from the cache hands out the cursors issued when it was stored: keepexpirewell underttl. - Links of your own:
cursors.encode(position, before=False, query=[...])makes the cursor of the page after (or before) any position, such as the events after a date. - Its limits: no total and no page numbers, only the first, previous and next pages. Going
back from a page that didn't start where the first page's walk would have, the first page can
be shorter than the limit. For a key clients read and write themselves, use
PaginationKeyset.