fastapi-fsp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_fsp-0.1.0/.github/workflows/ci.yml +53 -0
- fastapi_fsp-0.1.0/.github/workflows/release.yml +42 -0
- fastapi_fsp-0.1.0/.gitignore +56 -0
- fastapi_fsp-0.1.0/LICENSE +21 -0
- fastapi_fsp-0.1.0/PKG-INFO +203 -0
- fastapi_fsp-0.1.0/PROJECT.md +124 -0
- fastapi_fsp-0.1.0/README.md +178 -0
- fastapi_fsp-0.1.0/fastapi_fsp/__init__.py +9 -0
- fastapi_fsp-0.1.0/fastapi_fsp/fsp.py +269 -0
- fastapi_fsp-0.1.0/fastapi_fsp/models.py +81 -0
- fastapi_fsp-0.1.0/main.py +6 -0
- fastapi_fsp-0.1.0/pyproject.toml +59 -0
- fastapi_fsp-0.1.0/pytest.ini +3 -0
- fastapi_fsp-0.1.0/tests/__init__.py +0 -0
- fastapi_fsp-0.1.0/tests/conftest.py +28 -0
- fastapi_fsp-0.1.0/tests/main.py +59 -0
- fastapi_fsp-0.1.0/tests/test_fsp.py +53 -0
- fastapi_fsp-0.1.0/uv.lock +533 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main, master ]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build-test-lint:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Checkout
|
|
13
|
+
uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Set up Python
|
|
16
|
+
uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: '3.12'
|
|
19
|
+
|
|
20
|
+
- name: Set up uv
|
|
21
|
+
uses: astral-sh/setup-uv@v4
|
|
22
|
+
|
|
23
|
+
- name: Sync dependencies (with dev)
|
|
24
|
+
run: uv sync --dev
|
|
25
|
+
|
|
26
|
+
- name: Lint (ruff)
|
|
27
|
+
run: uv run ruff check .
|
|
28
|
+
|
|
29
|
+
- name: Format check (ruff)
|
|
30
|
+
run: uv run ruff format --check .
|
|
31
|
+
|
|
32
|
+
- name: Run tests
|
|
33
|
+
env:
|
|
34
|
+
PYTHONWARNINGS: default
|
|
35
|
+
run: uv run pytest -q
|
|
36
|
+
|
|
37
|
+
- name: Upload coverage artifacts
|
|
38
|
+
if: always()
|
|
39
|
+
uses: actions/upload-artifact@v4
|
|
40
|
+
with:
|
|
41
|
+
name: coverage-reports
|
|
42
|
+
path: |
|
|
43
|
+
coverage.xml
|
|
44
|
+
htmlcov
|
|
45
|
+
|
|
46
|
+
- name: Upload coverage to Codecov
|
|
47
|
+
if: always()
|
|
48
|
+
uses: codecov/codecov-action@v4
|
|
49
|
+
with:
|
|
50
|
+
files: ./coverage.xml
|
|
51
|
+
fail_ci_if_error: false
|
|
52
|
+
env:
|
|
53
|
+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*.*.*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build-and-publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
contents: read
|
|
14
|
+
steps:
|
|
15
|
+
- name: Checkout
|
|
16
|
+
uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: '3.12'
|
|
22
|
+
|
|
23
|
+
- name: Set up uv
|
|
24
|
+
uses: astral-sh/setup-uv@v4
|
|
25
|
+
|
|
26
|
+
- name: Sync (no dev)
|
|
27
|
+
run: uv sync
|
|
28
|
+
|
|
29
|
+
- name: Run tests
|
|
30
|
+
run: uv run pytest -q --maxfail=1
|
|
31
|
+
|
|
32
|
+
- name: Build package
|
|
33
|
+
run: uv build
|
|
34
|
+
|
|
35
|
+
- name: Publish to PyPI
|
|
36
|
+
env:
|
|
37
|
+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
|
38
|
+
run: |
|
|
39
|
+
if [ -z "$UV_PUBLISH_TOKEN" ]; then
|
|
40
|
+
echo "PYPI_API_TOKEN secret is not set." && exit 1
|
|
41
|
+
fi
|
|
42
|
+
uv publish --token "$UV_PUBLISH_TOKEN"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# -------- Python --------
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
ENV/
|
|
13
|
+
.env/
|
|
14
|
+
.python-version
|
|
15
|
+
|
|
16
|
+
# Packaging / build
|
|
17
|
+
build/
|
|
18
|
+
dist/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.eggs/
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Unit test / coverage reports
|
|
24
|
+
.coverage
|
|
25
|
+
.coverage.*
|
|
26
|
+
.cache/
|
|
27
|
+
.pytest_cache/
|
|
28
|
+
htmlcov/
|
|
29
|
+
coverage.xml
|
|
30
|
+
|
|
31
|
+
# Logs
|
|
32
|
+
*.log
|
|
33
|
+
|
|
34
|
+
# IDEs and editors
|
|
35
|
+
.vscode/
|
|
36
|
+
.idea/
|
|
37
|
+
*.iml
|
|
38
|
+
|
|
39
|
+
# OS-specific
|
|
40
|
+
.DS_Store
|
|
41
|
+
Thumbs.db
|
|
42
|
+
|
|
43
|
+
# Databases (local dev/testing)
|
|
44
|
+
*.db
|
|
45
|
+
*.sqlite3
|
|
46
|
+
|
|
47
|
+
# FastAPI/UVicorn
|
|
48
|
+
debug.log
|
|
49
|
+
|
|
50
|
+
# uv
|
|
51
|
+
# Keep uv.lock tracked (do not ignore)
|
|
52
|
+
# uv creates virtual envs typically under .venv which is already ignored
|
|
53
|
+
|
|
54
|
+
# Misc
|
|
55
|
+
.swp
|
|
56
|
+
.swo
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-fsp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Filter, Sort, and Paginate (FSP) utilities for FastAPI + SQLModel
|
|
5
|
+
Project-URL: Homepage, https://github.com/your-org/fastapi-fsp
|
|
6
|
+
Project-URL: Repository, https://github.com/your-org/fastapi-fsp
|
|
7
|
+
Project-URL: Issues, https://github.com/your-org/fastapi-fsp/issues
|
|
8
|
+
Author-email: Evert Jan Stamhuis <ej@fromejdevelopment.nl>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: api,fastapi,filtering,pagination,sorting,sqlmodel
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: FastAPI
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Requires-Dist: fastapi>=0.111
|
|
23
|
+
Requires-Dist: sqlmodel>=0.0.24
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# fastapi-fsp
|
|
27
|
+
|
|
28
|
+
Filter, Sort, and Paginate (FSP) utilities for FastAPI + SQLModel.
|
|
29
|
+
|
|
30
|
+
fastapi-fsp helps you build standardized list endpoints that support:
|
|
31
|
+
- Filtering on arbitrary fields with rich operators (eq, ne, lt, lte, gt, gte, in, between, like/ilike, null checks, contains/starts_with/ends_with)
|
|
32
|
+
- Sorting by field (asc/desc)
|
|
33
|
+
- Pagination with page/per_page and convenient HATEOAS links
|
|
34
|
+
|
|
35
|
+
It is framework-friendly: you declare it as a FastAPI dependency and feed it a SQLModel/SQLAlchemy Select query and a Session.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
Using uv (recommended):
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
# create & activate virtual env with uv
|
|
43
|
+
uv venv
|
|
44
|
+
. .venv/bin/activate
|
|
45
|
+
|
|
46
|
+
# add runtime dependency
|
|
47
|
+
uv add fastapi-fsp
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Using pip:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
pip install fastapi-fsp
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
Below is a minimal example using FastAPI and SQLModel.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from typing import Optional
|
|
62
|
+
from fastapi import Depends, FastAPI
|
|
63
|
+
from sqlmodel import Field, SQLModel, Session, create_engine, select
|
|
64
|
+
|
|
65
|
+
from fastapi_fsp.fsp import FSPManager
|
|
66
|
+
from fastapi_fsp.models import PaginatedResponse
|
|
67
|
+
|
|
68
|
+
class HeroBase(SQLModel):
|
|
69
|
+
name: str = Field(index=True)
|
|
70
|
+
secret_name: str
|
|
71
|
+
age: Optional[int] = Field(default=None, index=True)
|
|
72
|
+
|
|
73
|
+
class Hero(HeroBase, table=True):
|
|
74
|
+
id: Optional[int] = Field(default=None, primary_key=True)
|
|
75
|
+
|
|
76
|
+
class HeroPublic(HeroBase):
|
|
77
|
+
id: int
|
|
78
|
+
|
|
79
|
+
engine = create_engine("sqlite:///database.db", connect_args={"check_same_thread": False})
|
|
80
|
+
SQLModel.metadata.create_all(engine)
|
|
81
|
+
|
|
82
|
+
app = FastAPI()
|
|
83
|
+
|
|
84
|
+
def get_session():
|
|
85
|
+
with Session(engine) as session:
|
|
86
|
+
yield session
|
|
87
|
+
|
|
88
|
+
@app.get("/heroes/", response_model=PaginatedResponse[HeroPublic])
|
|
89
|
+
def read_heroes(*, session: Session = Depends(get_session), fsp: FSPManager = Depends(FSPManager)):
|
|
90
|
+
query = select(Hero)
|
|
91
|
+
return fsp.generate_response(query, session)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Run the app and query:
|
|
95
|
+
|
|
96
|
+
- Pagination: `GET /heroes/?page=1&per_page=10`
|
|
97
|
+
- Sorting: `GET /heroes/?sort_by=name&order=asc`
|
|
98
|
+
- Filtering: `GET /heroes/?field=age&operator=gte&value=21`
|
|
99
|
+
|
|
100
|
+
The response includes data, meta (pagination, filters, sorting), and links (self, first, next, prev, last).
|
|
101
|
+
|
|
102
|
+
## Query parameters
|
|
103
|
+
|
|
104
|
+
Pagination:
|
|
105
|
+
- page: integer (>=1), default 1
|
|
106
|
+
- per_page: integer (1..100), default 10
|
|
107
|
+
|
|
108
|
+
Sorting:
|
|
109
|
+
- sort_by: the field name, e.g., `name`
|
|
110
|
+
- order: `asc` or `desc`
|
|
111
|
+
|
|
112
|
+
Filtering (repeatable sets; arrays are supported by sending multiple parameters):
|
|
113
|
+
- field: the field/column name, e.g., `name`
|
|
114
|
+
- operator: one of
|
|
115
|
+
- eq, ne
|
|
116
|
+
- lt, lte, gt, gte
|
|
117
|
+
- in, not_in (comma-separated values)
|
|
118
|
+
- between (two comma-separated values)
|
|
119
|
+
- like, not_like
|
|
120
|
+
- ilike, not_ilike (if backend supports ILIKE)
|
|
121
|
+
- is_null, is_not_null
|
|
122
|
+
- contains, starts_with, ends_with (translated to LIKE patterns)
|
|
123
|
+
- value: raw string value (or list-like comma-separated depending on operator)
|
|
124
|
+
|
|
125
|
+
Examples:
|
|
126
|
+
- `?field=name&operator=eq&value=Deadpond`
|
|
127
|
+
- `?field=age&operator=between&value=18,30`
|
|
128
|
+
- `?field=name&operator=in&value=Deadpond,Rusty-Man`
|
|
129
|
+
- `?field=name&operator=contains&value=man`
|
|
130
|
+
|
|
131
|
+
You can chain multiple filters by repeating the triplet:
|
|
132
|
+
```
|
|
133
|
+
?field=age&operator=gte&value=18&field=name&operator=ilike&value=rust
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Response model
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
{
|
|
140
|
+
"data": [ ... ],
|
|
141
|
+
"meta": {
|
|
142
|
+
"pagination": {
|
|
143
|
+
"total_items": 42,
|
|
144
|
+
"per_page": 10,
|
|
145
|
+
"current_page": 1,
|
|
146
|
+
"total_pages": 5
|
|
147
|
+
},
|
|
148
|
+
"filters": [
|
|
149
|
+
{"field": "name", "operator": "eq", "value": "Deadpond"}
|
|
150
|
+
],
|
|
151
|
+
"sort": {"sort_by": "name", "order": "asc"}
|
|
152
|
+
},
|
|
153
|
+
"links": {
|
|
154
|
+
"self": "/heroes/?page=1&per_page=10",
|
|
155
|
+
"first": "/heroes/?page=1&per_page=10",
|
|
156
|
+
"next": "/heroes/?page=2&per_page=10",
|
|
157
|
+
"prev": null,
|
|
158
|
+
"last": "/heroes/?page=5&per_page=10"
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Development
|
|
164
|
+
|
|
165
|
+
This project uses uv as the package manager.
|
|
166
|
+
|
|
167
|
+
- Create env and sync deps:
|
|
168
|
+
```
|
|
169
|
+
uv venv
|
|
170
|
+
. .venv/bin/activate
|
|
171
|
+
uv sync --dev
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
- Run lint and format checks:
|
|
175
|
+
```
|
|
176
|
+
uv run ruff check .
|
|
177
|
+
uv run ruff format --check .
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
- Run tests:
|
|
181
|
+
```
|
|
182
|
+
uv run pytest -q
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- Build the package:
|
|
186
|
+
```
|
|
187
|
+
uv build
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## CI/CD and Releases
|
|
191
|
+
|
|
192
|
+
GitHub Actions workflows are included:
|
|
193
|
+
- CI (lint + tests) runs on pushes and PRs.
|
|
194
|
+
- Release: pushing a tag matching `v*.*.*` runs tests, builds, and publishes to PyPI using `PYPI_API_TOKEN` secret.
|
|
195
|
+
|
|
196
|
+
To release:
|
|
197
|
+
1. Update the version in `pyproject.toml`.
|
|
198
|
+
2. Push a tag, e.g. `git tag v0.1.1 && git push origin v0.1.1`.
|
|
199
|
+
3. Ensure the repository has `PYPI_API_TOKEN` secret set (an API token from PyPI).
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT License. See LICENSE.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# FastAPI Filtering, Sorting and Pagination
|
|
2
|
+
|
|
3
|
+
## pypi package name
|
|
4
|
+
fastapi-fsp
|
|
5
|
+
|
|
6
|
+
## pypi package version
|
|
7
|
+
0.1.0
|
|
8
|
+
|
|
9
|
+
## pypi package description
|
|
10
|
+
Package to implement filtering, sorting and pagination in FastAPI endpoints
|
|
11
|
+
using SQLModel
|
|
12
|
+
|
|
13
|
+
Endpoint Depends class that parses query parameters for filtering,
|
|
14
|
+
sorting and pagination. It performs all necessary checks.
|
|
15
|
+
Filtering is with query params: field, operator, value (support multiple)
|
|
16
|
+
Sorting is with query params: sort, order.
|
|
17
|
+
Pagination is with query params: page, per_page.
|
|
18
|
+
|
|
19
|
+
a request with query params:
|
|
20
|
+
http://localhost:8000/items?field=name&operator=eq&value=Deadpond&sort=name&order=asc&page=1&per_page=10
|
|
21
|
+
|
|
22
|
+
A response with data:
|
|
23
|
+
|
|
24
|
+
data: list of objects
|
|
25
|
+
meta: pagination info, filters, sort, etc.
|
|
26
|
+
links: pagination links
|
|
27
|
+
|
|
28
|
+
Example response:
|
|
29
|
+
```
|
|
30
|
+
{
|
|
31
|
+
"data": [
|
|
32
|
+
{
|
|
33
|
+
"id": 1,
|
|
34
|
+
"name": "Deadpond",
|
|
35
|
+
"secret_name": "Dive Wilson",
|
|
36
|
+
"age": 28
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
"meta": {
|
|
40
|
+
"pagination": {
|
|
41
|
+
"total_items": 1,
|
|
42
|
+
"per_page": 10,
|
|
43
|
+
"current_page": 1,
|
|
44
|
+
"total_pages": 1
|
|
45
|
+
},
|
|
46
|
+
"filters": [
|
|
47
|
+
{
|
|
48
|
+
"field": "name",
|
|
49
|
+
"operator": "eq",
|
|
50
|
+
"value": "Deadpond"
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"sort": {
|
|
54
|
+
"sort": "name",
|
|
55
|
+
"order": "asc"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"links": {
|
|
59
|
+
"self": "http://127.0.0.1:8000/heroes/?field=age&operator=gt&value=20&page=1&limit=2",
|
|
60
|
+
"first": "http://127.0.0.1:8000/heroes/?field=age&operator=gt&value=20&page=1&limit=2",
|
|
61
|
+
"next": "http://127.0.0.1:8000/heroes/?field=age&operator=gt&value=20&page=2&limit=2",
|
|
62
|
+
"prev": null
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
Example usage in endpoint:
|
|
69
|
+
|
|
70
|
+
class Item(SQLModel, table=True):
|
|
71
|
+
id: int = Field(default=None, primary_key=True)
|
|
72
|
+
name: str
|
|
73
|
+
secret_name: str | None = None
|
|
74
|
+
age: int | None = None
|
|
75
|
+
|
|
76
|
+
@app.get("/items/", response_model=FSPResponse[Item])
|
|
77
|
+
def read_items(
|
|
78
|
+
fsp: Depends(FSPManager(Item)),
|
|
79
|
+
):
|
|
80
|
+
query = Item.select()
|
|
81
|
+
return fsp.make_response(query)
|
|
82
|
+
|
|
83
|
+
also support async endpoints:
|
|
84
|
+
|
|
85
|
+
@app.get("/items/", response_model=FSPResponse[Item])
|
|
86
|
+
async def read_items(
|
|
87
|
+
fsp: Depends(FSPManager(Item)),
|
|
88
|
+
):
|
|
89
|
+
query = Item.select()
|
|
90
|
+
return await fsp.make_response(query)
|
|
91
|
+
|
|
92
|
+
## pypi package dependencies
|
|
93
|
+
use the following dependencies:
|
|
94
|
+
- uv as package manager
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
- black as code formatter
|
|
98
|
+
- isort as import formatter
|
|
99
|
+
- flake8 as linter
|
|
100
|
+
- pytest as test runner
|
|
101
|
+
- pytest-cov as test coverage runner
|
|
102
|
+
- pytest-asyncio as async test runner
|
|
103
|
+
- pytest-mock as mock runner
|
|
104
|
+
- FastAPI
|
|
105
|
+
- SQLModel
|
|
106
|
+
- pydantic
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
## pypi package keywords
|
|
110
|
+
fastapi, SQLModel, orm, filtering, sorting, pagination
|
|
111
|
+
|
|
112
|
+
## open source on github
|
|
113
|
+
Add a license file
|
|
114
|
+
|
|
115
|
+
## Documentations
|
|
116
|
+
Add a README.md file with documentation
|
|
117
|
+
Add mkdocs documentation with mkdocs material theme
|
|
118
|
+
Add an implementation example
|
|
119
|
+
|
|
120
|
+
## full test coverage
|
|
121
|
+
Implement unit tests using pytest
|
|
122
|
+
|
|
123
|
+
## workflow to deploy to run all tests and package and publish to pypi
|
|
124
|
+
Github Actions workflow file to deploy to run all tests and package and publish to pypi
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# fastapi-fsp
|
|
2
|
+
|
|
3
|
+
Filter, Sort, and Paginate (FSP) utilities for FastAPI + SQLModel.
|
|
4
|
+
|
|
5
|
+
fastapi-fsp helps you build standardized list endpoints that support:
|
|
6
|
+
- Filtering on arbitrary fields with rich operators (eq, ne, lt, lte, gt, gte, in, between, like/ilike, null checks, contains/starts_with/ends_with)
|
|
7
|
+
- Sorting by field (asc/desc)
|
|
8
|
+
- Pagination with page/per_page and convenient HATEOAS links
|
|
9
|
+
|
|
10
|
+
It is framework-friendly: you declare it as a FastAPI dependency and feed it a SQLModel/SQLAlchemy Select query and a Session.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Using uv (recommended):
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
# create & activate virtual env with uv
|
|
18
|
+
uv venv
|
|
19
|
+
. .venv/bin/activate
|
|
20
|
+
|
|
21
|
+
# add runtime dependency
|
|
22
|
+
uv add fastapi-fsp
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Using pip:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
pip install fastapi-fsp
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
Below is a minimal example using FastAPI and SQLModel.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from typing import Optional
|
|
37
|
+
from fastapi import Depends, FastAPI
|
|
38
|
+
from sqlmodel import Field, SQLModel, Session, create_engine, select
|
|
39
|
+
|
|
40
|
+
from fastapi_fsp.fsp import FSPManager
|
|
41
|
+
from fastapi_fsp.models import PaginatedResponse
|
|
42
|
+
|
|
43
|
+
class HeroBase(SQLModel):
|
|
44
|
+
name: str = Field(index=True)
|
|
45
|
+
secret_name: str
|
|
46
|
+
age: Optional[int] = Field(default=None, index=True)
|
|
47
|
+
|
|
48
|
+
class Hero(HeroBase, table=True):
|
|
49
|
+
id: Optional[int] = Field(default=None, primary_key=True)
|
|
50
|
+
|
|
51
|
+
class HeroPublic(HeroBase):
|
|
52
|
+
id: int
|
|
53
|
+
|
|
54
|
+
engine = create_engine("sqlite:///database.db", connect_args={"check_same_thread": False})
|
|
55
|
+
SQLModel.metadata.create_all(engine)
|
|
56
|
+
|
|
57
|
+
app = FastAPI()
|
|
58
|
+
|
|
59
|
+
def get_session():
|
|
60
|
+
with Session(engine) as session:
|
|
61
|
+
yield session
|
|
62
|
+
|
|
63
|
+
@app.get("/heroes/", response_model=PaginatedResponse[HeroPublic])
|
|
64
|
+
def read_heroes(*, session: Session = Depends(get_session), fsp: FSPManager = Depends(FSPManager)):
|
|
65
|
+
query = select(Hero)
|
|
66
|
+
return fsp.generate_response(query, session)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Run the app and query:
|
|
70
|
+
|
|
71
|
+
- Pagination: `GET /heroes/?page=1&per_page=10`
|
|
72
|
+
- Sorting: `GET /heroes/?sort_by=name&order=asc`
|
|
73
|
+
- Filtering: `GET /heroes/?field=age&operator=gte&value=21`
|
|
74
|
+
|
|
75
|
+
The response includes data, meta (pagination, filters, sorting), and links (self, first, next, prev, last).
|
|
76
|
+
|
|
77
|
+
## Query parameters
|
|
78
|
+
|
|
79
|
+
Pagination:
|
|
80
|
+
- page: integer (>=1), default 1
|
|
81
|
+
- per_page: integer (1..100), default 10
|
|
82
|
+
|
|
83
|
+
Sorting:
|
|
84
|
+
- sort_by: the field name, e.g., `name`
|
|
85
|
+
- order: `asc` or `desc`
|
|
86
|
+
|
|
87
|
+
Filtering (repeatable sets; arrays are supported by sending multiple parameters):
|
|
88
|
+
- field: the field/column name, e.g., `name`
|
|
89
|
+
- operator: one of
|
|
90
|
+
- eq, ne
|
|
91
|
+
- lt, lte, gt, gte
|
|
92
|
+
- in, not_in (comma-separated values)
|
|
93
|
+
- between (two comma-separated values)
|
|
94
|
+
- like, not_like
|
|
95
|
+
- ilike, not_ilike (if backend supports ILIKE)
|
|
96
|
+
- is_null, is_not_null
|
|
97
|
+
- contains, starts_with, ends_with (translated to LIKE patterns)
|
|
98
|
+
- value: raw string value (or list-like comma-separated depending on operator)
|
|
99
|
+
|
|
100
|
+
Examples:
|
|
101
|
+
- `?field=name&operator=eq&value=Deadpond`
|
|
102
|
+
- `?field=age&operator=between&value=18,30`
|
|
103
|
+
- `?field=name&operator=in&value=Deadpond,Rusty-Man`
|
|
104
|
+
- `?field=name&operator=contains&value=man`
|
|
105
|
+
|
|
106
|
+
You can chain multiple filters by repeating the triplet:
|
|
107
|
+
```
|
|
108
|
+
?field=age&operator=gte&value=18&field=name&operator=ilike&value=rust
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Response model
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
{
|
|
115
|
+
"data": [ ... ],
|
|
116
|
+
"meta": {
|
|
117
|
+
"pagination": {
|
|
118
|
+
"total_items": 42,
|
|
119
|
+
"per_page": 10,
|
|
120
|
+
"current_page": 1,
|
|
121
|
+
"total_pages": 5
|
|
122
|
+
},
|
|
123
|
+
"filters": [
|
|
124
|
+
{"field": "name", "operator": "eq", "value": "Deadpond"}
|
|
125
|
+
],
|
|
126
|
+
"sort": {"sort_by": "name", "order": "asc"}
|
|
127
|
+
},
|
|
128
|
+
"links": {
|
|
129
|
+
"self": "/heroes/?page=1&per_page=10",
|
|
130
|
+
"first": "/heroes/?page=1&per_page=10",
|
|
131
|
+
"next": "/heroes/?page=2&per_page=10",
|
|
132
|
+
"prev": null,
|
|
133
|
+
"last": "/heroes/?page=5&per_page=10"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
This project uses uv as the package manager.
|
|
141
|
+
|
|
142
|
+
- Create env and sync deps:
|
|
143
|
+
```
|
|
144
|
+
uv venv
|
|
145
|
+
. .venv/bin/activate
|
|
146
|
+
uv sync --dev
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
- Run lint and format checks:
|
|
150
|
+
```
|
|
151
|
+
uv run ruff check .
|
|
152
|
+
uv run ruff format --check .
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
- Run tests:
|
|
156
|
+
```
|
|
157
|
+
uv run pytest -q
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- Build the package:
|
|
161
|
+
```
|
|
162
|
+
uv build
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## CI/CD and Releases
|
|
166
|
+
|
|
167
|
+
GitHub Actions workflows are included:
|
|
168
|
+
- CI (lint + tests) runs on pushes and PRs.
|
|
169
|
+
- Release: pushing a tag matching `v*.*.*` runs tests, builds, and publishes to PyPI using `PYPI_API_TOKEN` secret.
|
|
170
|
+
|
|
171
|
+
To release:
|
|
172
|
+
1. Update the version in `pyproject.toml`.
|
|
173
|
+
2. Push a tag, e.g. `git tag v0.1.1 && git push origin v0.1.1`.
|
|
174
|
+
3. Ensure the repository has `PYPI_API_TOKEN` secret set (an API token from PyPI).
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT License. See LICENSE.
|