Skip to main content

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.

CI PyPI version Python Versions License: MIT codecov Ruff uv

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 -- OffsetParams returns OffsetPage, CursorParams returns CursorPage
  • 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 -- Annotated dependencies 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.

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Run tests and quality checks (uv run pytest && uv run ruff check .)
  4. Commit with conventional commits (git commit -m 'feat: add amazing feature')
  5. Open a Pull Request

License

MIT -- see LICENSE for details.

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

pypaginate-0.2.0.tar.gz (237.0 kB view details)

Uploaded Source

Built Distribution

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

pypaginate-0.2.0-py3-none-any.whl (64.6 kB view details)

Uploaded Python 3

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

Hashes for pypaginate-0.2.0.tar.gz
Algorithm Hash digest
SHA256 1aeb54465f3b4b8ad99f354e35cc218e36a76b0cd2b9f263a7bea6b7cb863916
MD5 9fad036798b1fcac017b0d0f11b3c15f
BLAKE2b-256 51c4eecb6499e8066ff776742f610c341850e2d6f762916acc8e896f44276ea1

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypaginate-0.2.0.tar.gz:

Publisher: publish.yml on CybLow/pypaginate

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for pypaginate-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 880a3af3e485016712ce628f669c9101ec36275be2c255732a81bdef2af701a9
MD5 f79cc8fc85f2ef8b2b7dd95a73a5c66a
BLAKE2b-256 b77f731bda7c0e5b646156c90f10a192070eba87a43c65fafc270933cb1c1ed9

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypaginate-0.2.0-py3-none-any.whl:

Publisher: publish.yml on CybLow/pypaginate

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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