Skip to main content

ORM-agnostic pagination toolkit for Python

Project description

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_func must return an unevaluated Django QuerySet
  • 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 a RuntimeError
  • 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.paginator exposes 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 Session enables synchronous pagination
  • Passing an AsyncSession enables 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 > total should return empty items
  • Maximum limit - Test with limit=100 (the configured maximum)
  • Limit validation - limit=0 or limit > 100 should 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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pagi-0.3.0.tar.gz (7.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pagi-0.3.0-py3-none-any.whl (8.9 kB view details)

Uploaded Python 3

File details

Details for the file pagi-0.3.0.tar.gz.

File metadata

  • Download URL: pagi-0.3.0.tar.gz
  • Upload date:
  • Size: 7.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for pagi-0.3.0.tar.gz
Algorithm Hash digest
SHA256 7eb06b52c9d437c47346f67ee6c39cb10103baf2eb5579b0d12230da1ffa38c5
MD5 985e89ff61da84bd7f83cdb3420b48e0
BLAKE2b-256 03aa6f0f6f8b066b7ced5cd9d49934f181cc7e9e4b4aa6d1ff2a473679becf8e

See more details on using hashes here.

File details

Details for the file pagi-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: pagi-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 8.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for pagi-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 643f7b018fa53296692b97af0baec66f76eb2023f66c32d9f2eef1910a9fe16d
MD5 509f13d7ce43c91a0d01fcca2b38410e
BLAKE2b-256 7c925f06423298abc549ffed5b9a8e11a84c07e03032fab52d3b53fd10a26f84

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page