Pagination utilities for FastAPI with SQLModel ORM
Project description
FastAPI RestKit
A complete REST toolkit for FastAPI with SQLModel ORM - featuring pagination, filtering, and sorting.
Features
- ๐ Easy pagination with customizable page sizes
- ๐ Powerful filtering with Django-style filter lookups
- ๐ Flexible sorting with multi-field support
- ๐ฆ Generic response models with full type hints
- โก FastAPI dependencies ready to use
- ๐ Async support out of the box
- ๐ Auto-generated OpenAPI docs for all parameters
Installation
pip install fastapi-restkit
Or with uv:
uv add fastapi-restkit
Table of Contents
Quick Start
from fastapi import FastAPI, Depends
from sqlmodel import Session, select
from fastapi_restkit import PaginationParams, paginate, PaginatedResponse
app = FastAPI()
@app.get("/items", response_model=PaginatedResponse[Item])
async def list_items(
session: Session = Depends(get_session),
pagination: PaginationParams = Depends(),
):
return await paginate(session, select(Item), pagination)
Pagination
Simple pagination with configurable page size:
from fastapi_restkit import PaginationParams, paginate, PaginatedResponse
@app.get("/products", response_model=PaginatedResponse[Product])
async def list_products(
session: Session = Depends(get_session),
pagination: PaginationParams = Depends(),
):
query = select(Product)
return await paginate(session, query, pagination)
URL Examples:
GET /products?page=1&page_size=20
GET /products?page=2&page_size=50
Filtering
Available Filter Types
| Filter | Description | Value Type | URL Example |
|---|---|---|---|
SearchFilter |
Text search (case-insensitive) | str |
?name=laptop |
BooleanFilter |
Boolean filter | bool |
?is_active=true |
NumberFilter |
Numeric with comparisons | float |
?price=99.99 |
ListFilter[T] |
List of values (IN clause) | list |
?status=active&status=pending |
DateFilter |
Date filter | date |
?created_at=2024-01-15 |
DateRangeFilter |
Date range (min/max) | date |
?created_at[min]=2024-01-01 |
DateTimeFilter |
DateTime filter | datetime |
?updated_at=2024-01-15T10:30:00Z |
NumericRangeFilter |
Numeric range | float |
?price[min]=10&price[max]=100 |
TimeRangeFilter |
Time range | time |
?hours[min]=08:00&hours[max]=17:00 |
Lookup Operators
Filters support Django-style lookup operators:
| Lookup | Description | SQL Example |
|---|---|---|
exact |
Exact match (default) | column = 'value' |
iexact |
Case-insensitive equal | column ILIKE 'value' |
contains |
Contains | column LIKE '%value%' |
icontains |
Case-insensitive contains | column ILIKE '%value%' |
startswith |
Starts with | column LIKE 'value%' |
endswith |
Ends with | column LIKE '%value' |
gt |
Greater than | column > value |
gte |
Greater or equal | column >= value |
lt |
Less than | column < value |
lte |
Less or equal | column <= value |
isnull |
Is null check | column IS NULL |
Creating a FilterSet
from typing import Optional
from pydantic import Field
from fastapi_restkit.filterset import FilterSet, filter_as_query
from fastapi_restkit.filters import (
SearchFilter,
BooleanFilter,
ListFilter,
NumericRangeFilter,
DateRangeFilter,
)
class ProductFilterSet(FilterSet):
"""Filters for products."""
# Text search (auto-mapped to 'name' column)
name: Optional[SearchFilter] = Field(
default_factory=SearchFilter,
description="Search by product name"
)
# Boolean filter
is_active: Optional[BooleanFilter] = Field(
default_factory=BooleanFilter,
description="Filter by active status"
)
# List filter (IN clause)
category: Optional[ListFilter[str]] = Field(
default_factory=ListFilter,
description="Filter by categories"
)
# Numeric range
price: Optional[NumericRangeFilter] = Field(
default_factory=NumericRangeFilter,
description="Filter by price range"
)
# Date range
created_at: Optional[DateRangeFilter] = Field(
default_factory=DateRangeFilter,
description="Filter by creation date"
)
# Multi-column search
search: Optional[SearchFilter] = Field(
default_factory=SearchFilter,
description="Search in name and description"
)
class Config:
# Custom column mapping
field_columns = {
"search": ["name", "description"], # OR search across columns
}
# Use in endpoint
@app.get("/products")
async def list_products(
session: Session = Depends(get_session),
filters: ProductFilterSet = Depends(filter_as_query(ProductFilterSet)),
):
query = select(Product)
query = filters.apply_to_query(query, Product)
return session.exec(query).all()
Filter URL Examples
# Simple text search
GET /products?name=laptop
# With lookup operator
GET /products?name=lap&name[lookup]=startswith
# Boolean filter
GET /products?is_active=true
# List filter (IN clause)
GET /products?category=electronics&category=computers
# Numeric range
GET /products?price[min]=100&price[max]=500
# Date range
GET /products?created_at[min]=2024-01-01&created_at[max]=2024-12-31
# Combined filters
GET /products?is_active=true&category=electronics&price[min]=100
Relationship Expansion (expand / omit)
You can configure relationship expansion directly in a FilterSet using expand and omit query params.
By default, expansions use selectinload. Use default_joined to allow joinedload for specific
relationships.
from typing import Optional
from sqlmodel import select
from fastapi_restkit.filterset import FilterSet, filter_as_query
class ProductFilterSet(FilterSet):
name: Optional[SearchFilter] = Field(default_factory=SearchFilter)
class Config:
# api_name -> relationship path
expandable = {
"owner": "owner", # Product.owner
"reviews": "reviews", # Product.reviews
}
# Always expand owner by default
default_expand = {"owner"}
# Only owner uses joinedload; reviews uses selectinload
default_joined = {"owner"}
@app.get("/products")
async def list_products(
session: Session = Depends(get_session),
filters: ProductFilterSet = Depends(filter_as_query(ProductFilterSet)),
):
query = select(Product)
query = filters.apply_to_query(query, Product)
query = filters.apply_expands_to_query(query, Product)
return session.exec(query).all()
URL Examples:
# Default expansion (owner) is applied
GET /products
# Expand reviews (selectinload)
GET /products?expand=reviews
# Omit default expansion
GET /products?omit=owner
Notes:
expandandomitonly accept fields defined inConfig.expandable.selectinloadis always the default strategy.joinedloadis only used for relationships inConfig.default_joined.onlyacts as a whitelist and has precedence overexpandandomit.
Using projection in detail endpoints
You can also use expand / omit / only in detail endpoints (e.g. GET /users/{id}) without
defining any filter fields.
Create a projection-only FilterSet (only Config) and apply it with
apply_expands_to_query():
from fastapi_restkit.filterset import FilterSet, filter_as_query
class UserDetailProjection(FilterSet):
class Config:
expandable = {"orders": "orders", "company": "company"}
@app.get("/users/{user_id}")
async def get_user(
user_id: int,
session: Session = Depends(get_session),
projection: UserDetailProjection = Depends(filter_as_query(UserDetailProjection)),
):
query = select(User).where(User.id == user_id)
query = projection.apply_expands_to_query(query, User)
return session.exec(query).one()
Omitting columns from the main model
omit in FilterSet can also omit columns from the main model when you define
Config.column_fields. The default omission strategy is defer, but you can
switch to load_only with Config.column_omit_mode.
class ProductFilterSet(FilterSet):
class Config:
column_fields = {
"description": "description",
"internal_notes": "internal_notes",
}
# "defer" (default) or "load_only"
column_omit_mode = "defer"
@app.get(
"/products",
response_model=PaginatedResponse[ProductRead],
)
async def list_products(
session: Session = Depends(get_session),
filters: ProductFilterSet = Depends(filter_as_query(ProductFilterSet)),
pagination: PaginationParams = Depends(),
):
query = select(Product)
query = filters.apply_to_query(query, Product)
query = filters.apply_expands_to_query(query, Product)
return await paginate(session, query, pagination)
URL Examples:
GET /products?omit=description
GET /products?omit=description,internal_notes
Only include specific fields
Use only to return only specific relationships or columns. only has
precedence over expand and omit.
# Only the owner relationship and name column
GET /products?only=owner,name
For strict contracts, define a dedicated response schema without those columns, or
use response_model_exclude when appropriate.
Sorting
Creating a SortingSet
from fastapi_restkit.sortingset import SortingSet, SortableField, sorting_as_query
class ProductSortingSet(SortingSet):
"""Sorting options for products."""
id: SortableField = SortableField(description="Product ID")
name: SortableField = SortableField(description="Product name")
price: SortableField = SortableField(description="Price")
created_at: SortableField = SortableField(description="Creation date")
# Map to different column name
updated: SortableField = SortableField(
description="Update date",
column="updated_at",
)
class Config:
default_sorting = ["created_at:desc"]
# Use in endpoint
@app.get("/products")
async def list_products(
session: Session = Depends(get_session),
sorting: ProductSortingSet = Depends(sorting_as_query(ProductSortingSet)),
):
query = select(Product)
query = sorting.apply_to_query(query, Product)
return session.exec(query).all()
Sorting URL Examples
# Ascending (default)
GET /products?sort_by=name
# Descending
GET /products?sort_by=price:desc
# Multiple fields
GET /products?sort_by=category:asc&sort_by=price:desc
# Results in: ORDER BY category ASC, price DESC
Complete Example
Combining pagination, filtering, and sorting:
from fastapi import APIRouter, Depends
from sqlmodel import Session, select
from fastapi_restkit import PaginationParams, paginate, PaginatedResponse
from fastapi_restkit.filterset import FilterSet, filter_as_query
from fastapi_restkit.sortingset import SortingSet, SortableField, sorting_as_query
from fastapi_restkit.filters import SearchFilter, BooleanFilter, NumericRangeFilter
class ProductFilterSet(FilterSet):
name: Optional[SearchFilter] = Field(default_factory=SearchFilter)
is_active: Optional[BooleanFilter] = Field(default_factory=BooleanFilter)
price: Optional[NumericRangeFilter] = Field(default_factory=NumericRangeFilter)
class ProductSortingSet(SortingSet):
name: SortableField = SortableField(description="Name")
price: SortableField = SortableField(description="Price")
created_at: SortableField = SortableField(description="Created")
class Config:
default_sorting = ["created_at:desc"]
router = APIRouter(prefix="/products", tags=["Products"])
@router.get("", response_model=PaginatedResponse[ProductRead])
async def list_products(
session: Session = Depends(get_session),
filters: ProductFilterSet = Depends(filter_as_query(ProductFilterSet)),
sorting: ProductSortingSet = Depends(sorting_as_query(ProductSortingSet)),
pagination: PaginationParams = Depends(),
):
"""
List products with filtering, sorting, and pagination.
**Filters:** name, is_active, price
**Sorting:** name, price, created_at
**Pagination:** page, page_size
"""
query = select(Product)
query = filters.apply_to_query(query, Product)
query = sorting.apply_to_query(query, Product)
return await paginate(session, query, pagination)
Combined URL Example:
GET /products?is_active=true&price[min]=100&price[max]=500&sort_by=price:asc&page=1&page_size=20
Development
Setup
# Clone the repository
git clone https://github.com/cacenot/fastapi-restkit.git
cd fastapi-restkit
# Install dependencies with uv
uv sync --dev
# Run tests
uv run pytest
# Run linter
uv run ruff check .
# Run formatter
uv run ruff format .
Project Structure
fastapi-restkit/
โโโ src/
โ โโโ fastapi_restkit/
โ โโโ __init__.py
โ โโโ pagination.py # Pagination logic
โ โโโ filters.py # Filter types
โ โโโ filterset.py # FilterSet base class
โ โโโ sorting.py # Sorting utilities
โ โโโ sortingset.py # SortingSet base class
โ โโโ models.py # Response models
โ โโโ dependencies.py # FastAPI dependencies
โโโ tests/
โโโ docs/
โโโ pyproject.toml
License
MIT License - 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
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 fastapi_restkit-0.2.0.tar.gz.
File metadata
- Download URL: fastapi_restkit-0.2.0.tar.gz
- Upload date:
- Size: 25.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0fbfb1400bd06e72cbefb13ef111952fa05ff153301d89e7a49eec8d6a902825
|
|
| MD5 |
24c4cf68a1e2bfab967fffdf0bc5726d
|
|
| BLAKE2b-256 |
a01f1f8a23a1a04b5581b9a4df0a1a26eb0b7e8cf19f57fd57475a3d7a648b85
|
File details
Details for the file fastapi_restkit-0.2.0-py3-none-any.whl.
File metadata
- Download URL: fastapi_restkit-0.2.0-py3-none-any.whl
- Upload date:
- Size: 30.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2a16f6ffbba70c294af498224adebef7654886d078da31f90a22070b74b28f87
|
|
| MD5 |
45f73e7b03b1d963a9738cad87d8e7c2
|
|
| BLAKE2b-256 |
8b1a407994ec9652cb6f9535d16f7f5e1b50d65b3bfcf5876b9fa170f103a13e
|