# fastapi-fsp
Filter, Sort, and Paginate (FSP) utilities for FastAPI + SQLModel.
fastapi-fsp helps you build standardized list endpoints that support:
- Filtering on arbitrary fields with rich operators (eq, ne, lt, lte, gt, gte, in, between, like/ilike, null checks, contains/starts_with/ends_with)
- Sorting by field (asc/desc)
- Pagination with page/per_page and convenient HATEOAS links
It is framework-friendly: you declare it as a FastAPI dependency and feed it a SQLModel/SQLAlchemy Select query and a Session.
## Installation
Using uv (recommended):
```
# create & activate virtual env with uv
uv venv
. .venv/bin/activate
# add runtime dependency
uv add fastapi-fsp
```
Using pip:
```
pip install fastapi-fsp
```
## Quick start
Below is a minimal example using FastAPI and SQLModel.
```python
from typing import Optional
from fastapi import Depends, FastAPI
from sqlmodel import Field, SQLModel, Session, create_engine, select
from fastapi_fsp.fsp import FSPManager
from fastapi_fsp.models import PaginatedResponse
class HeroBase(SQLModel):
name: str = Field(index=True)
secret_name: str
age: Optional[int] = Field(default=None, index=True)
class Hero(HeroBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
class HeroPublic(HeroBase):
id: int
engine = create_engine("sqlite:///database.db", connect_args={"check_same_thread": False})
SQLModel.metadata.create_all(engine)
app = FastAPI()
def get_session():
with Session(engine) as session:
yield session
@app.get("/heroes/", response_model=PaginatedResponse[HeroPublic])
def read_heroes(*, session: Session = Depends(get_session), fsp: FSPManager = Depends(FSPManager)):
query = select(Hero)
return fsp.generate_response(query, session)
```
Run the app and query:
- Pagination: `GET /heroes/?page=1&per_page=10`
- Sorting: `GET /heroes/?sort_by=name&order=asc`
- Filtering: `GET /heroes/?field=age&operator=gte&value=21`
The response includes data, meta (pagination, filters, sorting), and links (self, first, next, prev, last).
## Query parameters
Pagination:
- page: integer (>=1), default 1
- per_page: integer (1..100), default 10
Sorting:
- sort_by: the field name, e.g., `name`
- order: `asc` or `desc`
Filtering (repeatable sets; arrays are supported by sending multiple parameters):
- field: the field/column name, e.g., `name`
- operator: one of
- eq, ne
- lt, lte, gt, gte
- in, not_in (comma-separated values)
- between (two comma-separated values)
- like, not_like
- ilike, not_ilike (if backend supports ILIKE)
- is_null, is_not_null
- contains, starts_with, ends_with (translated to LIKE patterns)
- value: raw string value (or list-like comma-separated depending on operator)
Examples:
- `?field=name&operator=eq&value=Deadpond`
- `?field=age&operator=between&value=18,30`
- `?field=name&operator=in&value=Deadpond,Rusty-Man`
- `?field=name&operator=contains&value=man`
You can chain multiple filters by repeating the triplet:
```
?field=age&operator=gte&value=18&field=name&operator=ilike&value=rust
```
## Response model
```
{
"data": [ ... ],
"meta": {
"pagination": {
"total_items": 42,
"per_page": 10,
"current_page": 1,
"total_pages": 5
},
"filters": [
{"field": "name", "operator": "eq", "value": "Deadpond"}
],
"sort": {"sort_by": "name", "order": "asc"}
},
"links": {
"self": "/heroes/?page=1&per_page=10",
"first": "/heroes/?page=1&per_page=10",
"next": "/heroes/?page=2&per_page=10",
"prev": null,
"last": "/heroes/?page=5&per_page=10"
}
}
```
## Development
This project uses uv as the package manager.
- Create env and sync deps:
```
uv venv
. .venv/bin/activate
uv sync --dev
```
- Run lint and format checks:
```
uv run ruff check .
uv run ruff format --check .
```
- Run tests:
```
uv run pytest -q
```
- Build the package:
```
uv build
```
## CI/CD and Releases
GitHub Actions workflows are included:
- CI (lint + tests) runs on pushes and PRs.
- Release: pushing a tag matching `v*.*.*` runs tests, builds, and publishes to PyPI using `PYPI_API_TOKEN` secret.
To release:
1. Update the version in `pyproject.toml`.
2. Push a tag, e.g. `git tag v0.1.1 && git push origin v0.1.1`.
3. Ensure the repository has `PYPI_API_TOKEN` secret set (an API token from PyPI).
## License
MIT License. See LICENSE.
Raw data
{
"_id": null,
"home_page": null,
"name": "fastapi-fsp",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.12",
"maintainer_email": null,
"keywords": "api, fastapi, filtering, pagination, sorting, sqlmodel",
"author": null,
"author_email": "Evert Jan Stamhuis <ej@fromejdevelopment.nl>",
"download_url": "https://files.pythonhosted.org/packages/8a/60/bdb75c0007716da0005b4bd6cd55df7c05c64467e55f82bf29e6a859d1ac/fastapi_fsp-0.1.1.tar.gz",
"platform": null,
"description": "# fastapi-fsp\n\nFilter, Sort, and Paginate (FSP) utilities for FastAPI + SQLModel.\n\nfastapi-fsp helps you build standardized list endpoints that support:\n- Filtering on arbitrary fields with rich operators (eq, ne, lt, lte, gt, gte, in, between, like/ilike, null checks, contains/starts_with/ends_with)\n- Sorting by field (asc/desc)\n- Pagination with page/per_page and convenient HATEOAS links\n\nIt is framework-friendly: you declare it as a FastAPI dependency and feed it a SQLModel/SQLAlchemy Select query and a Session.\n\n## Installation\n\nUsing uv (recommended):\n\n```\n# create & activate virtual env with uv\nuv venv\n. .venv/bin/activate\n\n# add runtime dependency\nuv add fastapi-fsp\n```\n\nUsing pip:\n\n```\npip install fastapi-fsp\n```\n\n## Quick start\n\nBelow is a minimal example using FastAPI and SQLModel.\n\n```python\nfrom typing import Optional\nfrom fastapi import Depends, FastAPI\nfrom sqlmodel import Field, SQLModel, Session, create_engine, select\n\nfrom fastapi_fsp.fsp import FSPManager\nfrom fastapi_fsp.models import PaginatedResponse\n\nclass HeroBase(SQLModel):\n name: str = Field(index=True)\n secret_name: str\n age: Optional[int] = Field(default=None, index=True)\n\nclass Hero(HeroBase, table=True):\n id: Optional[int] = Field(default=None, primary_key=True)\n\nclass HeroPublic(HeroBase):\n id: int\n\nengine = create_engine(\"sqlite:///database.db\", connect_args={\"check_same_thread\": False})\nSQLModel.metadata.create_all(engine)\n\napp = FastAPI()\n\ndef get_session():\n with Session(engine) as session:\n yield session\n\n@app.get(\"/heroes/\", response_model=PaginatedResponse[HeroPublic])\ndef read_heroes(*, session: Session = Depends(get_session), fsp: FSPManager = Depends(FSPManager)):\n query = select(Hero)\n return fsp.generate_response(query, session)\n```\n\nRun the app and query:\n\n- Pagination: `GET /heroes/?page=1&per_page=10`\n- Sorting: `GET /heroes/?sort_by=name&order=asc`\n- Filtering: `GET /heroes/?field=age&operator=gte&value=21`\n\nThe response includes data, meta (pagination, filters, sorting), and links (self, first, next, prev, last).\n\n## Query parameters\n\nPagination:\n- page: integer (>=1), default 1\n- per_page: integer (1..100), default 10\n\nSorting:\n- sort_by: the field name, e.g., `name`\n- order: `asc` or `desc`\n\nFiltering (repeatable sets; arrays are supported by sending multiple parameters):\n- field: the field/column name, e.g., `name`\n- operator: one of\n - eq, ne\n - lt, lte, gt, gte\n - in, not_in (comma-separated values)\n - between (two comma-separated values)\n - like, not_like\n - ilike, not_ilike (if backend supports ILIKE)\n - is_null, is_not_null\n - contains, starts_with, ends_with (translated to LIKE patterns)\n- value: raw string value (or list-like comma-separated depending on operator)\n\nExamples:\n- `?field=name&operator=eq&value=Deadpond`\n- `?field=age&operator=between&value=18,30`\n- `?field=name&operator=in&value=Deadpond,Rusty-Man`\n- `?field=name&operator=contains&value=man`\n\nYou can chain multiple filters by repeating the triplet:\n```\n?field=age&operator=gte&value=18&field=name&operator=ilike&value=rust\n```\n\n## Response model\n\n```\n{\n \"data\": [ ... ],\n \"meta\": {\n \"pagination\": {\n \"total_items\": 42,\n \"per_page\": 10,\n \"current_page\": 1,\n \"total_pages\": 5\n },\n \"filters\": [\n {\"field\": \"name\", \"operator\": \"eq\", \"value\": \"Deadpond\"}\n ],\n \"sort\": {\"sort_by\": \"name\", \"order\": \"asc\"}\n },\n \"links\": {\n \"self\": \"/heroes/?page=1&per_page=10\",\n \"first\": \"/heroes/?page=1&per_page=10\",\n \"next\": \"/heroes/?page=2&per_page=10\",\n \"prev\": null,\n \"last\": \"/heroes/?page=5&per_page=10\"\n }\n}\n```\n\n## Development\n\nThis project uses uv as the package manager.\n\n- Create env and sync deps:\n```\nuv venv\n. .venv/bin/activate\nuv sync --dev\n```\n\n- Run lint and format checks:\n```\nuv run ruff check .\nuv run ruff format --check .\n```\n\n- Run tests:\n```\nuv run pytest -q\n```\n\n- Build the package:\n```\nuv build\n```\n\n## CI/CD and Releases\n\nGitHub Actions workflows are included:\n- CI (lint + tests) runs on pushes and PRs.\n- Release: pushing a tag matching `v*.*.*` runs tests, builds, and publishes to PyPI using `PYPI_API_TOKEN` secret.\n\nTo release:\n1. Update the version in `pyproject.toml`.\n2. Push a tag, e.g. `git tag v0.1.1 && git push origin v0.1.1`.\n3. Ensure the repository has `PYPI_API_TOKEN` secret set (an API token from PyPI).\n\n## License\n\nMIT License. See LICENSE.\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Filter, Sort, and Paginate (FSP) utilities for FastAPI + SQLModel",
"version": "0.1.1",
"project_urls": {
"Homepage": "https://github.com/fromej-dev/fastapi-fsp",
"Issues": "https://github.com/fromej-dev/fastapi-fsp/issues",
"Repository": "https://github.com/fromej-dev/fastapi-fsp"
},
"split_keywords": [
"api",
" fastapi",
" filtering",
" pagination",
" sorting",
" sqlmodel"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "b43f5981f1ac27f2bd23f792edcf4bddbe5f36c9c98c7fa84765292ee626c76a",
"md5": "676c73bbd6687acf44ae83cadb8c316b",
"sha256": "0c673977433af3054fea1838735843eeedf00fe96c456fb0c5da48baa917b1c7"
},
"downloads": -1,
"filename": "fastapi_fsp-0.1.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "676c73bbd6687acf44ae83cadb8c316b",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.12",
"size": 7842,
"upload_time": "2025-08-20T13:31:05",
"upload_time_iso_8601": "2025-08-20T13:31:05.728275Z",
"url": "https://files.pythonhosted.org/packages/b4/3f/5981f1ac27f2bd23f792edcf4bddbe5f36c9c98c7fa84765292ee626c76a/fastapi_fsp-0.1.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "8a60bdb75c0007716da0005b4bd6cd55df7c05c64467e55f82bf29e6a859d1ac",
"md5": "22086d790fc9f5517d5b947f824b1d7f",
"sha256": "f216b5d65c14a8d16014b6dddedfd14998cdb557d3aacdf9964ae4ba0c73f76c"
},
"downloads": -1,
"filename": "fastapi_fsp-0.1.1.tar.gz",
"has_sig": false,
"md5_digest": "22086d790fc9f5517d5b947f824b1d7f",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.12",
"size": 39139,
"upload_time": "2025-08-20T13:31:06",
"upload_time_iso_8601": "2025-08-20T13:31:06.495466Z",
"url": "https://files.pythonhosted.org/packages/8a/60/bdb75c0007716da0005b4bd6cd55df7c05c64467e55f82bf29e6a859d1ac/fastapi_fsp-0.1.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-08-20 13:31:06",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "fromej-dev",
"github_project": "fastapi-fsp",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "fastapi-fsp"
}