pagi
A minimal, ORM-agnostic pagination toolkit for Python.
pagi lets you define pagination logic once and reuse it across different ORMs (SQLAlchemy, Django, etc.), returning consistent, typed responses powered by Pydantic.
Features
- Offset/limit pagination with validation via Pydantic
- Unified response model (
PaginatedResponse) - SQLAlchemy support (sync and async)
- Django ORM support
- Tortoise ORM support
- Strategy-based internal design for easy extensibility
- ORM-agnostic public API
Installation
pip install pagi
Or with development dependencies:
pip install -e .[dev]
or if you are using uv
uv pip install -e .[dev]
Basic Usage
Importing
The installable package name is pagi, but the Python module is paginator.
Recommended import:
from paginator.paginator import paginate, paginate_sync
SQLAlchemy (Synchronous)
from sqlalchemy import select
from sqlalchemy.orm import Session
from paginator import paginate_sync
def get_users(session: Session):
return paginate_sync(
session,
lambda: select(User),
offset=10,
limit=5,
backend="sqlalchemy",
)
SQLAlchemy (Asynchronous)
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from paginator import paginate
async def get_users(session: AsyncSession):
return await paginate(
session,
lambda: select(User),
offset=10,
limit=5,
backend="sqlalchemy",
)
The correct strategy (sync vs async) is selected automatically based on the session type.
Django ORM
from paginator import paginate_sync
from myapp.models import User
result = paginate_sync(
connection=None,
query_func=lambda: User.objects.all(),
offset=20,
limit=10,
backend="django",
)
Notes:
query_funcmust return an unevaluated DjangoQuerySet- Django pagination is synchronous (async execution is not supported)
Tortoise ORM
from paginator import paginate
from myapp.models import User
result = await paginate(
connection=None,
query_func=lambda: User.all().order_by("id"),
offset=20,
limit=10,
backend="tortoise",
)
Notes:
- Tortoise ORM is async-first, so only
paginate()(async) is supported paginate_sync()will raise aRuntimeError- Make sure Tortoise is initialized before calling pagination functions
Design and Architecture
pagi is built around the Strategy pattern, allowing multiple ORMs to be supported while keeping a single, simple public API.
paginator.paginatorexposes the public functions (paginate,paginate_sync)- Each ORM implements its own pagination strategy
- A small factory selects the appropriate strategy at runtime based on the backend and connection type
- Pagination logic is decoupled from data access, making new backends easy to add
SQLAlchemy Strategy Selection
For SQLAlchemy, pagi uses a factory-based approach:
- Passing a
Sessionenables synchronous pagination - Passing an
AsyncSessionenables asynchronous pagination - The correct strategy is chosen automatically without extra configuration
Roadmap
- Cursor-based pagination (cursor tokens instead of offset/limit)
- Optional total count for performance-sensitive queries
Testing and Edge Cases
The following edge cases should be considered when testing pagination across all backends:
Common Edge Cases
- Empty result set - Query returns 0 records
- First page -
offset=0, limit=N - Last page (partial) - Requested limit exceeds remaining records
- Exact page boundary -
offset + limit == total - Offset beyond total -
offset > totalshould return empty items - Maximum limit - Test with
limit=100(the configured maximum) - Limit validation -
limit=0orlimit > 100should raise validation errors - Negative offset - Should raise validation errors
Backend-Specific Considerations
| Backend | Sync | Async | Notes |
|---|---|---|---|
| SQLAlchemy | yes | yes | Strategy auto-selected by Session type |
| Django | yes | no | Wrap with sync_to_async if needed |
| Tortoise | no | yes | Async-first ORM |
Development
Run tests with:
pytest
The test suite covers:
- SQLAlchemy (sync)
- Django ORM
- Tortoise ORM
License
MIT
Metadata
Release files for pagi 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pagi-0.3.0.tar.gz | 7.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pagi-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 16.8 kB
Release files / pagi-0.3.0.tar.gz
| Download URL | pagi-0.3.0.tar.gz |
|---|---|
| Size | 7.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7eb06b52c9d437c47346f67ee6c39cb10103baf2eb5579b0d12230da1ffa38c5
|
|
BLAKE2b-256 checksum How to use checksums |
03aa6f0f6f8b066b7ced5cd9d49934f181cc7e9e4b4aa6d1ff2a473679becf8e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|
Release files / pagi-0.3.0-py3-none-any.whl
| Download URL | pagi-0.3.0-py3-none-any.whl |
|---|---|
| Size | 8.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
643f7b018fa53296692b97af0baec66f76eb2023f66c32d9f2eef1910a9fe16d
|
|
BLAKE2b-256 checksum How to use checksums |
7c925f06423298abc549ffed5b9a8e11a84c07e03032fab52d3b53fd10a26f84
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|