Django
Translate pypaginate specs into Django Q objects and order_by arguments, then
offset- or keyset-paginate a QuerySet. Django is imported lazily inside the
helpers, so importing the adapter module does not require Django — only calling a
helper does. Keyset cursors are byte-compatible with the other adapters; see
cross-language parity.
Install
pip install "pypaginate[django]"
This pulls in Django>=4.2. The native engine ships inside the pypaginate
wheel.
Public API
from pypaginate.adapters.django import (
build_filter_q, # FilterSpec list | FilterGroup -> Q
apply_filters, # queryset + filters -> filtered queryset
build_order_by, # SortSpec list -> order_by() args
apply_sorting, # queryset + sorting -> ordered queryset
build_search_q, # SearchSpec -> case-insensitive OR-of-fields Q (or None)
apply_search, # queryset + SearchSpec -> filtered queryset
paginate_offset, # queryset + OffsetParams -> OffsetPage
paginate_keyset, # queryset + CursorParams -> CursorPage
)
Filtering
Each operator maps to a Django field lookup. build_filter_q accepts either a
flat FilterSpec list (each spec's logic selects AND/OR) or a nested And /
Or group; apply_filters calls .filter() for you (a no-op on an empty list).
from pypaginate import FilterSpec, And
from pypaginate.adapters.django import apply_filters
qs = apply_filters(User.objects.all(), [
FilterSpec(field="age", operator="gte", value=18),
FilterSpec(field="status", operator="eq", value="active"),
])
qs = apply_filters(User.objects.all(), And(
FilterSpec(field="age", operator="gte", value=18),
FilterSpec(field="name", operator="contains", value="a"),
))
Operator notes specific to the SQL backend:
ne/not_inrender as a negatedQ(~Q(...)).likemaps to__containsandiliketo__icontains— SQLLIKEwildcards are not interpreted; the value is matched as a substring.betweenuses__range;is_null/is_not_nulluse__isnull.- The in-memory-only operators
empty,not_empty, andexistshave no SQL equivalent and raiseFilterError.
Sorting
Without nulls, a key renders to a plain "field" / "-field" string; with
nulls set it renders to an F(...).asc(nulls_last=...) / .desc(nulls_first=...)
expression so Django emulates NULLS FIRST/LAST on backends lacking native
support.
from pypaginate import SortSpec
from pypaginate.adapters.django import apply_sorting
qs = apply_sorting(User.objects.all(), [
SortSpec(field="age", direction="desc"),
SortSpec(field="name", direction="asc", nulls="last"),
])
Search
apply_search keeps rows where any field matches the query via a
case-insensitive lookup per spec.mode (contains -> __icontains,
prefix -> __istartswith, exact -> __iexact). This is an unranked
match-filter — relevance ranking lives in the in-memory engine, not the database.
from pypaginate import SearchSpec
from pypaginate.adapters.django import apply_search
qs = apply_search(User.objects.all(), SearchSpec(query="alice", fields=["name", "email"]))
Offset pagination
paginate_offset counts via QuerySet.count() then slices
qs[offset:offset + limit], which Django renders to LIMIT/OFFSET.
from pypaginate import OffsetParams
from pypaginate.adapters.django import apply_filters, apply_sorting, paginate_offset
qs = apply_filters(User.objects.all(), [FilterSpec(field="age", operator="gte", value=18)])
qs = apply_sorting(qs, [SortSpec(field="age", direction="desc")])
page = paginate_offset(qs, OffsetParams(page=1, limit=20))
page.total # int
page.pages # int
list(page) # the page's model instances
Keyset (cursor) pagination
paginate_keyset reuses the cross-language cursor codec and the core's portable
keyset predicate, so cursors stay byte-compatible with the SQLAlchemy and TS
adapters. The QuerySet must carry an explicit order_by over simple string
field names (or a model Meta.ordering).
from pypaginate import CursorParams
from pypaginate.adapters.django import paginate_keyset
qs = Post.objects.order_by("-created_at", "-id")
first = paginate_keyset(qs, CursorParams(limit=20))
nxt = paginate_keyset(qs, CursorParams(limit=20, after=first.next_cursor))
prev = paginate_keyset(qs, CursorParams(limit=20, before=nxt.previous_cursor))
The page exposes next_cursor, previous_cursor, has_next, and has_previous.
A non-string order_by entry (e.g. an F expression) raises ConfigurationError.