Universal pagination toolkit for Python — one function, any backend, auto-detects sync/async
Project description
pypaginate
Universal pagination toolkit for Python -- one function, any backend, auto-detects sync/async.
pypaginate provides a single paginate() function that works with lists, SQLAlchemy queries (async and sync), and cursor-based pagination. The return type is automatically inferred from the params you pass in.
Features
- One function --
paginate()handles lists, SQLAlchemy queries, sync and async - Type-safe inference --
OffsetParamsreturnsOffsetPage,CursorParamsreturnsCursorPage - Filtering -- 20 operators (eq, gte, contains, between, regex, etc.)
- Sorting -- multi-column with direction and null placement control
- Search -- full-text with optional fuzzy matching (RapidFuzz)
- FastAPI --
Annotateddependencies for pagination, filtering, sorting, and search - Cursor pagination -- keyset/cursor-based pagination via sqlakeyset
- Pipeline -- compose filter + sort + search + paginate in one call
- 100% typed -- mypy strict mode, Pydantic v2 models
Installation
# Core (in-memory pagination only)
pip install pypaginate
# With SQLAlchemy support
pip install pypaginate[sqlalchemy]
# With FastAPI integration
pip install pypaginate[fastapi]
# With fuzzy search (RapidFuzz)
pip install pypaginate[search]
# Everything
pip install pypaginate[all]
Or with uv:
uv add pypaginate
uv add pypaginate[all]
Quick Start
Paginate a list in 3 lines:
from pypaginate import paginate, OffsetParams
page = paginate([1, 2, 3, 4, 5], OffsetParams(page=1, limit=2))
page.items # [1, 2]
page.total # 5
page.pages # 3
page.has_next # True
SQLAlchemy (Async)
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from pypaginate import paginate, OffsetParams
from pypaginate.adapters.sqlalchemy import SQLAlchemyBackend
async def list_users(session: AsyncSession):
stmt = select(User).order_by(User.created_at.desc())
backend = SQLAlchemyBackend(session)
page = await paginate(stmt, OffsetParams(page=1, limit=20), backend=backend)
page.items # list[User]
page.total # int
page.has_next # bool
For sync sessions, use SyncSQLAlchemyBackend:
from sqlalchemy.orm import Session
from pypaginate.adapters.sqlalchemy import SyncSQLAlchemyBackend
def list_users(session: Session):
backend = SyncSQLAlchemyBackend(session)
page = paginate(select(User), OffsetParams(page=1, limit=20), backend=backend)
Cursor Pagination
For large datasets where offset-based pagination is inefficient:
from pypaginate import paginate, CursorParams
from pypaginate.adapters.sqlalchemy import SQLAlchemyCursorBackend
async def scroll_users(session: AsyncSession, cursor: str | None = None):
stmt = select(User).order_by(User.id)
backend = SQLAlchemyCursorBackend(session)
page = await paginate(stmt, CursorParams(limit=20, after=cursor), backend=backend)
page.items # list[User]
page.next_cursor # str | None -- pass to next request
page.previous_cursor # str | None
page.has_next # bool
FastAPI Integration
pypaginate provides Annotated dependency types for clean FastAPI integration:
from fastapi import FastAPI
from pypaginate import paginate, OffsetPage
from pypaginate.adapters.fastapi import OffsetDep
app = FastAPI()
@app.get("/users")
async def list_users(params: OffsetDep) -> OffsetPage[dict]:
users = [{"name": "Alice"}, {"name": "Bob"}, {"name": "Charlie"}]
return paginate(users, params)
Available dependencies:
| Dependency | Query Params | Produces |
|---|---|---|
OffsetDep |
?page=1&limit=20 |
OffsetParams |
CursorDep |
?limit=20&after=abc |
CursorParams |
FilterDep |
(user-defined fields) | list[FilterSpec] |
SortDep |
?sort=name,-age |
list[SortSpec] |
SearchDep |
?q=alice&search_fields=name,email |
SearchSpec |
Declarative Filters
from typing import Annotated
from fastapi import Query
from pypaginate.adapters.fastapi import FilterDep, FilterField
class UserFilters(FilterDep):
name: str | None = FilterField(None, operator="contains")
age_min: int | None = FilterField(None, field="age", operator="gte")
status: str | None = FilterField(None, operator="eq")
@app.get("/users")
async def list_users(
params: OffsetDep,
filters: Annotated[UserFilters, Query()],
):
# filters.to_specs() returns list[FilterSpec] for non-None fields
...
Sorting and Search
from pypaginate.adapters.fastapi import OffsetDep, SortDep, SearchDep
@app.get("/users")
async def list_users(params: OffsetDep, sort: SortDep, search: SearchDep):
# sort: ?sort=name,-created_at (- prefix = descending)
# search: ?q=alice&search_fields=name,email
...
Filtering
Use FilterSpec to define filter conditions:
from pypaginate import FilterSpec
from pypaginate.filtering import FilterEngine, create_default_registry
engine = FilterEngine(create_default_registry())
users = [
{"name": "Alice", "age": 30, "status": "active"},
{"name": "Bob", "age": 25, "status": "inactive"},
{"name": "Charlie", "age": 35, "status": "active"},
]
# Simple equality
active = engine.apply(users, [FilterSpec(field="status", value="active")])
# [Alice, Charlie]
# Multiple filters (AND by default)
result = engine.apply(users, [
FilterSpec(field="age", operator="gte", value=30),
FilterSpec(field="status", value="active"),
])
# [Alice, Charlie]
Nested Filter Groups
from pypaginate import And, Or, FilterSpec
# (status = active) AND (age >= 30 OR name contains "bob")
group = And(
FilterSpec(field="status", value="active"),
Or(
FilterSpec(field="age", operator="gte", value=30),
FilterSpec(field="name", operator="contains", value="bob"),
),
)
result = engine.apply(users, group)
Available Filter Operators
| Operator | Description | Example |
|---|---|---|
eq, ne |
Equality / inequality | FilterSpec(field="status", value="active") |
gt, gte, lt, lte |
Comparisons | FilterSpec(field="age", operator="gte", value=18) |
in, not_in |
Membership | FilterSpec(field="role", operator="in", value=["admin", "user"]) |
contains, starts_with, ends_with |
Text matching | FilterSpec(field="name", operator="contains", value="ali") |
like, ilike |
SQL-style patterns | FilterSpec(field="email", operator="like", value="%@gmail.com") |
between |
Range | FilterSpec(field="price", operator="between", value=[10, 100]) |
is_null, is_not_null |
Null checks | FilterSpec(field="notes", operator="is_null") |
empty, not_empty |
Empty checks | FilterSpec(field="tags", operator="not_empty") |
exists |
Field existence | FilterSpec(field="id", operator="exists") |
regex |
Regex matching | FilterSpec(field="code", operator="regex", value="^A\\d+") |
Sorting
from pypaginate import SortSpec, SortDirection
from pypaginate.sorting import SortEngine
engine = SortEngine()
users = [
{"name": "Charlie", "age": 35},
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25},
]
sorted_users = engine.apply(users, [
SortSpec(field="age", direction=SortDirection.DESC),
])
# [Charlie (35), Alice (30), Bob (25)]
Search
from pypaginate import SearchSpec
from pypaginate.search import SearchEngine
engine = SearchEngine()
users = [
{"name": "Alice Smith", "email": "alice@example.com"},
{"name": "Bob Johnson", "email": "bob@example.com"},
]
results = engine.apply(users, SearchSpec(
query="alice",
fields=("name", "email"),
))
# [Alice Smith]
Fuzzy search (requires pypaginate[search]):
from pypaginate import SearchSpec, FuzzyMode
results = engine.apply(users, SearchSpec(
query="alce",
fields=("name",),
fuzzy=FuzzyMode.FUZZY,
threshold=75,
))
Pipeline (Filter + Sort + Search + Paginate)
Compose all operations in a single call:
from pypaginate import OffsetParams, FilterSpec, SortSpec, SortDirection
from pypaginate.engine.pipeline import SyncPipeline
from pypaginate.engine.paginator import Paginator
from pypaginate.adapters.memory import (
MemoryBackend,
MemoryFilterBackend,
MemorySortBackend,
)
pipeline = SyncPipeline(
Paginator(MemoryBackend()),
filter_backend=MemoryFilterBackend(),
sort_backend=MemorySortBackend(),
)
page = pipeline.execute(
users,
OffsetParams(page=1, limit=10),
filters=[FilterSpec(field="status", value="active")],
sorting=[SortSpec(field="name", direction=SortDirection.ASC)],
)
For async (e.g., SQLAlchemy), use AsyncPipeline with AsyncPaginator.
Architecture
pypaginate/
├── domain/ # Models, specs, enums, protocols (no deps)
├── engine/ # Paginator, cursor paginator, pipeline
├── filtering/ # In-memory filter engine + operators
├── sorting/ # In-memory sort engine
├── search/ # In-memory search engine
└── adapters/
├── memory/ # In-memory backends (filter, sort, search)
├── sqlalchemy/ # SA backends (offset, cursor, filter, sort, search)
└── fastapi/ # Annotated dependencies (OffsetDep, FilterDep, etc.)
Development
git clone https://github.com/CybLow/pypaginate.git
cd pypaginate
uv sync
# Run all checks
uv run ruff format . && uv run ruff check --fix . && uv run mypy src/ && uv run pytest
# Individual commands
uv run pytest # Tests
uv run pytest --cov # Coverage
uv run ruff format . # Format
uv run ruff check --fix . # Lint
uv run mypy src/ # Type check
Contributing
Contributions are welcome! See CONTRIBUTING.md for guidelines.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Run tests and quality checks (
uv run pytest && uv run ruff check .) - Commit with conventional commits (
git commit -m 'feat: add amazing feature') - Open a Pull Request
License
MIT -- see LICENSE for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pypaginate-0.2.0.tar.gz.
File metadata
- Download URL: pypaginate-0.2.0.tar.gz
- Upload date:
- Size: 237.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1aeb54465f3b4b8ad99f354e35cc218e36a76b0cd2b9f263a7bea6b7cb863916
|
|
| MD5 |
9fad036798b1fcac017b0d0f11b3c15f
|
|
| BLAKE2b-256 |
51c4eecb6499e8066ff776742f610c341850e2d6f762916acc8e896f44276ea1
|
Provenance
The following attestation bundles were made for pypaginate-0.2.0.tar.gz:
Publisher:
publish.yml on CybLow/pypaginate
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pypaginate-0.2.0.tar.gz -
Subject digest:
1aeb54465f3b4b8ad99f354e35cc218e36a76b0cd2b9f263a7bea6b7cb863916 - Sigstore transparency entry: 1113533866
- Sigstore integration time:
-
Permalink:
CybLow/pypaginate@a0a55a837f25adbfb3c6e2c2b8171850230cd446 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/CybLow
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a0a55a837f25adbfb3c6e2c2b8171850230cd446 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pypaginate-0.2.0-py3-none-any.whl.
File metadata
- Download URL: pypaginate-0.2.0-py3-none-any.whl
- Upload date:
- Size: 64.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
880a3af3e485016712ce628f669c9101ec36275be2c255732a81bdef2af701a9
|
|
| MD5 |
f79cc8fc85f2ef8b2b7dd95a73a5c66a
|
|
| BLAKE2b-256 |
b77f731bda7c0e5b646156c90f10a192070eba87a43c65fafc270933cb1c1ed9
|
Provenance
The following attestation bundles were made for pypaginate-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on CybLow/pypaginate
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pypaginate-0.2.0-py3-none-any.whl -
Subject digest:
880a3af3e485016712ce628f669c9101ec36275be2c255732a81bdef2af701a9 - Sigstore transparency entry: 1113533907
- Sigstore integration time:
-
Permalink:
CybLow/pypaginate@a0a55a837f25adbfb3c6e2c2b8171850230cd446 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/CybLow
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a0a55a837f25adbfb3c6e2c2b8171850230cd446 -
Trigger Event:
release
-
Statement type: