fastapi-fsp 0.2.2__py3-none-any.whl → 0.3.0__py3-none-any.whl
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/__init__.py +34 -0
- fastapi_fsp/builder.py +339 -0
- fastapi_fsp/config.py +158 -0
- fastapi_fsp/fsp.py +159 -7
- fastapi_fsp/presets.py +267 -0
- fastapi_fsp-0.3.0.dist-info/METADATA +433 -0
- fastapi_fsp-0.3.0.dist-info/RECORD +10 -0
- fastapi_fsp-0.2.2.dist-info/METADATA +0 -216
- fastapi_fsp-0.2.2.dist-info/RECORD +0 -7
- {fastapi_fsp-0.2.2.dist-info → fastapi_fsp-0.3.0.dist-info}/WHEEL +0 -0
- {fastapi_fsp-0.2.2.dist-info → fastapi_fsp-0.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-fsp
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Filter, Sort, and Paginate (FSP) utilities for FastAPI + SQLModel
|
|
5
|
+
Project-URL: Homepage, https://github.com/fromej-dev/fastapi-fsp
|
|
6
|
+
Project-URL: Repository, https://github.com/fromej-dev/fastapi-fsp
|
|
7
|
+
Project-URL: Issues, https://github.com/fromej-dev/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: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Requires-Dist: fastapi>=0.121.1
|
|
25
|
+
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
26
|
+
Requires-Dist: sqlmodel>=0.0.27
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# fastapi-fsp
|
|
30
|
+
|
|
31
|
+
Filter, Sort, and Paginate (FSP) utilities for FastAPI + SQLModel.
|
|
32
|
+
|
|
33
|
+
fastapi-fsp helps you build standardized list endpoints that support:
|
|
34
|
+
- Filtering on arbitrary fields with rich operators (eq, ne, lt, lte, gt, gte, in, between, like/ilike, null checks, contains/starts_with/ends_with)
|
|
35
|
+
- Sorting by field (asc/desc)
|
|
36
|
+
- Pagination with page/per_page and convenient HATEOAS links
|
|
37
|
+
|
|
38
|
+
It is framework-friendly: you declare it as a FastAPI dependency and feed it a SQLModel/SQLAlchemy Select query and a Session.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
Using uv (recommended):
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
# create & activate virtual env with uv
|
|
46
|
+
uv venv
|
|
47
|
+
. .venv/bin/activate
|
|
48
|
+
|
|
49
|
+
# add runtime dependency
|
|
50
|
+
uv add fastapi-fsp
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Using pip:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
pip install fastapi-fsp
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
Below is a minimal example using FastAPI and SQLModel.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from typing import Optional
|
|
65
|
+
from fastapi import Depends, FastAPI
|
|
66
|
+
from sqlmodel import Field, SQLModel, Session, create_engine, select
|
|
67
|
+
|
|
68
|
+
from fastapi_fsp.fsp import FSPManager
|
|
69
|
+
from fastapi_fsp.models import PaginatedResponse
|
|
70
|
+
|
|
71
|
+
class HeroBase(SQLModel):
|
|
72
|
+
name: str = Field(index=True)
|
|
73
|
+
secret_name: str
|
|
74
|
+
age: Optional[int] = Field(default=None, index=True)
|
|
75
|
+
|
|
76
|
+
class Hero(HeroBase, table=True):
|
|
77
|
+
id: Optional[int] = Field(default=None, primary_key=True)
|
|
78
|
+
|
|
79
|
+
class HeroPublic(HeroBase):
|
|
80
|
+
id: int
|
|
81
|
+
|
|
82
|
+
engine = create_engine("sqlite:///database.db", connect_args={"check_same_thread": False})
|
|
83
|
+
SQLModel.metadata.create_all(engine)
|
|
84
|
+
|
|
85
|
+
app = FastAPI()
|
|
86
|
+
|
|
87
|
+
def get_session():
|
|
88
|
+
with Session(engine) as session:
|
|
89
|
+
yield session
|
|
90
|
+
|
|
91
|
+
@app.get("/heroes/", response_model=PaginatedResponse[HeroPublic])
|
|
92
|
+
def read_heroes(*, session: Session = Depends(get_session), fsp: FSPManager = Depends(FSPManager)):
|
|
93
|
+
query = select(Hero)
|
|
94
|
+
return fsp.generate_response(query, session)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Run the app and query:
|
|
98
|
+
|
|
99
|
+
- Pagination: `GET /heroes/?page=1&per_page=10`
|
|
100
|
+
- Sorting: `GET /heroes/?sort_by=name&order=asc`
|
|
101
|
+
- Filtering: `GET /heroes/?field=age&operator=gte&value=21`
|
|
102
|
+
|
|
103
|
+
The response includes data, meta (pagination, filters, sorting), and links (self, first, next, prev, last).
|
|
104
|
+
|
|
105
|
+
## Query parameters
|
|
106
|
+
|
|
107
|
+
Pagination:
|
|
108
|
+
- page: integer (>=1), default 1
|
|
109
|
+
- per_page: integer (1..100), default 10
|
|
110
|
+
|
|
111
|
+
Sorting:
|
|
112
|
+
- sort_by: the field name, e.g., `name`
|
|
113
|
+
- order: `asc` or `desc`
|
|
114
|
+
|
|
115
|
+
Filtering (two supported formats):
|
|
116
|
+
|
|
117
|
+
1) Simple (triplets repeated in the query string):
|
|
118
|
+
- field: the field/column name, e.g., `name`
|
|
119
|
+
- operator: one of
|
|
120
|
+
- eq, ne
|
|
121
|
+
- lt, lte, gt, gte
|
|
122
|
+
- in, not_in (comma-separated values)
|
|
123
|
+
- between (two comma-separated values)
|
|
124
|
+
- like, not_like
|
|
125
|
+
- ilike, not_ilike (if backend supports ILIKE)
|
|
126
|
+
- is_null, is_not_null
|
|
127
|
+
- contains, starts_with, ends_with (translated to LIKE patterns)
|
|
128
|
+
- value: raw string value (or list-like comma-separated depending on operator)
|
|
129
|
+
|
|
130
|
+
Examples (simple format):
|
|
131
|
+
- `?field=name&operator=eq&value=Deadpond`
|
|
132
|
+
- `?field=age&operator=between&value=18,30`
|
|
133
|
+
- `?field=name&operator=in&value=Deadpond,Rusty-Man`
|
|
134
|
+
- `?field=name&operator=contains&value=man`
|
|
135
|
+
- Chain multiple filters by repeating the triplet: `?field=age&operator=gte&value=18&field=name&operator=ilike&value=rust`
|
|
136
|
+
|
|
137
|
+
2) Indexed format (useful for clients that handle arrays of objects):
|
|
138
|
+
- Use keys like `filters[0][field]`, `filters[0][operator]`, `filters[0][value]`, then increment the index for additional filters (`filters[1][...]`, etc.).
|
|
139
|
+
|
|
140
|
+
Example (indexed format):
|
|
141
|
+
```
|
|
142
|
+
?filters[0][field]=age&filters[0][operator]=gte&filters[0][value]=18&filters[1][field]=name&filters[1][operator]=ilike&filters[1][value]=joy
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Notes:
|
|
146
|
+
- Both formats are equivalent; the indexed format takes precedence if present.
|
|
147
|
+
- If any filter is incomplete (missing operator or value in the indexed form, or mismatched counts of simple triplets), the API responds with HTTP 400.
|
|
148
|
+
|
|
149
|
+
## Filtering on Computed Fields
|
|
150
|
+
|
|
151
|
+
You can filter (and sort) on SQLAlchemy `hybrid_property` fields that have a SQL expression defined. This enables filtering on calculated or derived values at the database level.
|
|
152
|
+
|
|
153
|
+
### Defining a Computed Field
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from typing import ClassVar, Optional
|
|
157
|
+
from sqlalchemy import func
|
|
158
|
+
from sqlalchemy.ext.hybrid import hybrid_property
|
|
159
|
+
from sqlmodel import Field, SQLModel
|
|
160
|
+
|
|
161
|
+
class HeroBase(SQLModel):
|
|
162
|
+
name: str = Field(index=True)
|
|
163
|
+
secret_name: str
|
|
164
|
+
age: Optional[int] = Field(default=None)
|
|
165
|
+
full_name: ClassVar[str] # Required: declare as ClassVar for Pydantic
|
|
166
|
+
|
|
167
|
+
@hybrid_property
|
|
168
|
+
def full_name(self) -> str:
|
|
169
|
+
"""Python-level implementation (used on instances)."""
|
|
170
|
+
return f"{self.name}-{self.secret_name}"
|
|
171
|
+
|
|
172
|
+
@full_name.expression
|
|
173
|
+
def full_name(cls):
|
|
174
|
+
"""SQL-level implementation (used in queries)."""
|
|
175
|
+
return func.concat(cls.name, "-", cls.secret_name)
|
|
176
|
+
|
|
177
|
+
class Hero(HeroBase, table=True):
|
|
178
|
+
id: Optional[int] = Field(default=None, primary_key=True)
|
|
179
|
+
|
|
180
|
+
class HeroPublic(HeroBase):
|
|
181
|
+
id: int
|
|
182
|
+
full_name: str # Include in response model
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Querying Computed Fields
|
|
186
|
+
|
|
187
|
+
Once defined, you can filter and sort on the computed field like any regular field:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
# Filter by computed field
|
|
191
|
+
GET /heroes/?field=full_name&operator=eq&value=Spider-Man
|
|
192
|
+
GET /heroes/?field=full_name&operator=ilike&value=%man
|
|
193
|
+
GET /heroes/?field=full_name&operator=contains&value=Spider
|
|
194
|
+
|
|
195
|
+
# Sort by computed field
|
|
196
|
+
GET /heroes/?sort_by=full_name&order=asc
|
|
197
|
+
|
|
198
|
+
# Combine with other filters
|
|
199
|
+
GET /heroes/?field=full_name&operator=starts_with&value=Spider&field=age&operator=gte&value=21
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Requirements
|
|
203
|
+
|
|
204
|
+
- The `hybrid_property` must have an `.expression` decorator that returns a valid SQL expression
|
|
205
|
+
- The field should be declared as `ClassVar[type]` in the SQLModel base class to work with Pydantic
|
|
206
|
+
- Only computed fields with SQL expressions are supported; Python-only properties cannot be filtered at the database level
|
|
207
|
+
|
|
208
|
+
## FilterBuilder API
|
|
209
|
+
|
|
210
|
+
For programmatic filter creation, use the fluent `FilterBuilder` API:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from fastapi_fsp import FilterBuilder
|
|
214
|
+
|
|
215
|
+
# Instead of manually creating Filter objects:
|
|
216
|
+
# filters = [
|
|
217
|
+
# Filter(field="age", operator=FilterOperator.GTE, value="30"),
|
|
218
|
+
# Filter(field="city", operator=FilterOperator.EQ, value="Chicago"),
|
|
219
|
+
# ]
|
|
220
|
+
|
|
221
|
+
# Use the builder pattern:
|
|
222
|
+
filters = (
|
|
223
|
+
FilterBuilder()
|
|
224
|
+
.where("age").gte(30)
|
|
225
|
+
.where("city").eq("Chicago")
|
|
226
|
+
.where("active").eq(True)
|
|
227
|
+
.where("tags").in_(["python", "fastapi"])
|
|
228
|
+
.where("created_at").between("2024-01-01", "2024-12-31")
|
|
229
|
+
.build()
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Use with FSPManager
|
|
233
|
+
@app.get("/heroes/")
|
|
234
|
+
def read_heroes(session: Session = Depends(get_session), fsp: FSPManager = Depends(FSPManager)):
|
|
235
|
+
additional_filters = FilterBuilder().where("deleted").eq(False).build()
|
|
236
|
+
fsp.with_filters(additional_filters)
|
|
237
|
+
return fsp.generate_response(select(Hero), session)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Available FilterBuilder Methods
|
|
241
|
+
|
|
242
|
+
| Method | Description |
|
|
243
|
+
|--------|-------------|
|
|
244
|
+
| `.eq(value)` | Equal to |
|
|
245
|
+
| `.ne(value)` | Not equal to |
|
|
246
|
+
| `.gt(value)` | Greater than |
|
|
247
|
+
| `.gte(value)` | Greater than or equal |
|
|
248
|
+
| `.lt(value)` | Less than |
|
|
249
|
+
| `.lte(value)` | Less than or equal |
|
|
250
|
+
| `.like(pattern)` | Case-sensitive LIKE |
|
|
251
|
+
| `.ilike(pattern)` | Case-insensitive LIKE |
|
|
252
|
+
| `.in_(values)` | IN list |
|
|
253
|
+
| `.not_in(values)` | NOT IN list |
|
|
254
|
+
| `.between(low, high)` | BETWEEN range |
|
|
255
|
+
| `.is_null()` | IS NULL |
|
|
256
|
+
| `.is_not_null()` | IS NOT NULL |
|
|
257
|
+
| `.starts_with(prefix)` | Starts with (case-insensitive) |
|
|
258
|
+
| `.ends_with(suffix)` | Ends with (case-insensitive) |
|
|
259
|
+
| `.contains(substring)` | Contains (case-insensitive) |
|
|
260
|
+
|
|
261
|
+
## Common Filter Presets
|
|
262
|
+
|
|
263
|
+
For frequently used filter patterns, use `CommonFilters`:
|
|
264
|
+
|
|
265
|
+
```python
|
|
266
|
+
from fastapi_fsp import CommonFilters
|
|
267
|
+
|
|
268
|
+
# Active (non-deleted) records
|
|
269
|
+
filters = CommonFilters.active() # deleted=false
|
|
270
|
+
|
|
271
|
+
# Recent records (last 7 days)
|
|
272
|
+
filters = CommonFilters.recent(days=7)
|
|
273
|
+
|
|
274
|
+
# Date range
|
|
275
|
+
filters = CommonFilters.date_range(start=datetime(2024, 1, 1), end=datetime(2024, 12, 31))
|
|
276
|
+
|
|
277
|
+
# Records created today
|
|
278
|
+
filters = CommonFilters.today()
|
|
279
|
+
|
|
280
|
+
# Null checks
|
|
281
|
+
filters = CommonFilters.not_null("email")
|
|
282
|
+
filters = CommonFilters.is_null("deleted_at")
|
|
283
|
+
|
|
284
|
+
# Search
|
|
285
|
+
filters = CommonFilters.search("name", "john", match_type="contains")
|
|
286
|
+
|
|
287
|
+
# Combine presets
|
|
288
|
+
filters = CommonFilters.active() + CommonFilters.recent(days=30)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Configuration
|
|
292
|
+
|
|
293
|
+
Customize FSPManager behavior with `FSPConfig`:
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
from fastapi_fsp import FSPConfig, FSPPresets
|
|
297
|
+
|
|
298
|
+
# Custom configuration
|
|
299
|
+
config = FSPConfig(
|
|
300
|
+
max_per_page=50,
|
|
301
|
+
default_per_page=20,
|
|
302
|
+
strict_mode=True, # Raise errors for unknown fields
|
|
303
|
+
max_page=100,
|
|
304
|
+
allow_deep_pagination=False,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Or use presets
|
|
308
|
+
config = FSPPresets.strict() # strict_mode=True
|
|
309
|
+
config = FSPPresets.limited_pagination(max_page=50) # Limit deep pagination
|
|
310
|
+
config = FSPPresets.high_volume(max_per_page=500) # High-volume APIs
|
|
311
|
+
|
|
312
|
+
# Apply configuration
|
|
313
|
+
@app.get("/heroes/")
|
|
314
|
+
def read_heroes(session: Session = Depends(get_session), fsp: FSPManager = Depends(FSPManager)):
|
|
315
|
+
fsp.apply_config(config)
|
|
316
|
+
return fsp.generate_response(select(Hero), session)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Strict Mode
|
|
320
|
+
|
|
321
|
+
When `strict_mode=True`, FSPManager raises HTTP 400 errors for unknown filter/sort fields:
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
# With strict_mode=True, this raises HTTP 400:
|
|
325
|
+
# GET /heroes/?field=unknown_field&operator=eq&value=test
|
|
326
|
+
# Error: "Unknown field 'unknown_field'. Available fields: age, id, name, secret_name"
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Convenience Methods
|
|
330
|
+
|
|
331
|
+
### from_model()
|
|
332
|
+
|
|
333
|
+
Simplify common queries with `from_model()`:
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
@app.get("/heroes/")
|
|
337
|
+
def read_heroes(session: Session = Depends(get_session), fsp: FSPManager = Depends(FSPManager)):
|
|
338
|
+
# Instead of:
|
|
339
|
+
# query = select(Hero)
|
|
340
|
+
# return fsp.generate_response(query, session)
|
|
341
|
+
|
|
342
|
+
# Use:
|
|
343
|
+
return fsp.from_model(Hero, session)
|
|
344
|
+
|
|
345
|
+
# Async version
|
|
346
|
+
@app.get("/heroes/")
|
|
347
|
+
async def read_heroes(session: AsyncSession = Depends(get_session), fsp: FSPManager = Depends(FSPManager)):
|
|
348
|
+
return await fsp.from_model_async(Hero, session)
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Method Chaining
|
|
352
|
+
|
|
353
|
+
Chain configuration methods:
|
|
354
|
+
|
|
355
|
+
```python
|
|
356
|
+
@app.get("/heroes/")
|
|
357
|
+
def read_heroes(session: Session = Depends(get_session), fsp: FSPManager = Depends(FSPManager)):
|
|
358
|
+
return (
|
|
359
|
+
fsp
|
|
360
|
+
.with_filters(CommonFilters.active())
|
|
361
|
+
.apply_config(FSPPresets.strict())
|
|
362
|
+
.generate_response(select(Hero), session)
|
|
363
|
+
)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Response model
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
{
|
|
370
|
+
"data": [ ... ],
|
|
371
|
+
"meta": {
|
|
372
|
+
"pagination": {
|
|
373
|
+
"total_items": 42,
|
|
374
|
+
"per_page": 10,
|
|
375
|
+
"current_page": 1,
|
|
376
|
+
"total_pages": 5
|
|
377
|
+
},
|
|
378
|
+
"filters": [
|
|
379
|
+
{"field": "name", "operator": "eq", "value": "Deadpond"}
|
|
380
|
+
],
|
|
381
|
+
"sort": {"sort_by": "name", "order": "asc"}
|
|
382
|
+
},
|
|
383
|
+
"links": {
|
|
384
|
+
"self": "/heroes/?page=1&per_page=10",
|
|
385
|
+
"first": "/heroes/?page=1&per_page=10",
|
|
386
|
+
"next": "/heroes/?page=2&per_page=10",
|
|
387
|
+
"prev": null,
|
|
388
|
+
"last": "/heroes/?page=5&per_page=10"
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## Development
|
|
394
|
+
|
|
395
|
+
This project uses uv as the package manager.
|
|
396
|
+
|
|
397
|
+
- Create env and sync deps:
|
|
398
|
+
```
|
|
399
|
+
uv venv
|
|
400
|
+
. .venv/bin/activate
|
|
401
|
+
uv sync --dev
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
- Run lint and format checks:
|
|
405
|
+
```
|
|
406
|
+
uv run ruff check .
|
|
407
|
+
uv run ruff format --check .
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
- Run tests:
|
|
411
|
+
```
|
|
412
|
+
uv run pytest -q
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
- Build the package:
|
|
416
|
+
```
|
|
417
|
+
uv build
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## CI/CD and Releases
|
|
421
|
+
|
|
422
|
+
GitHub Actions workflows are included:
|
|
423
|
+
- CI (lint + tests) runs on pushes and PRs.
|
|
424
|
+
- Release: pushing a tag matching `v*.*.*` runs tests, builds, and publishes to PyPI using `PYPI_API_TOKEN` secret.
|
|
425
|
+
|
|
426
|
+
To release:
|
|
427
|
+
1. Update the version in `pyproject.toml`.
|
|
428
|
+
2. Push a tag, e.g. `git tag v0.1.1 && git push origin v0.1.1`.
|
|
429
|
+
3. Ensure the repository has `PYPI_API_TOKEN` secret set (an API token from PyPI).
|
|
430
|
+
|
|
431
|
+
## License
|
|
432
|
+
|
|
433
|
+
MIT License. See LICENSE.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
fastapi_fsp/__init__.py,sha256=vQtASadi_DWCs14VpPO60upWbJxqxSotA7thQ7dgYb0,925
|
|
2
|
+
fastapi_fsp/builder.py,sha256=due8kTVApNXWZJGVDhqe6Ch9jCqdjnNe0rvWNUOgVMw,9660
|
|
3
|
+
fastapi_fsp/config.py,sha256=GcrSyv_wOvNgLFq00fhsZznZH4Fnl0dCBJa73n2QwIs,4977
|
|
4
|
+
fastapi_fsp/fsp.py,sha256=WNcarSmffEDs86VE7xerFC1-JC89mqusM_fx1k4v3P8,27022
|
|
5
|
+
fastapi_fsp/models.py,sha256=1MwLBQFmUP8OwO3Gqby1u7s9ruimCR2XGfUzAqF4Tj4,2034
|
|
6
|
+
fastapi_fsp/presets.py,sha256=hpfUmCaeqoCeb1PimpUoGEW5S0Ycc-yBEZQq6vJWv50,8500
|
|
7
|
+
fastapi_fsp-0.3.0.dist-info/METADATA,sha256=tGLU0lGCCYPVBGV55c8I8gjf66pdLInb_wSKG9jBlq4,12610
|
|
8
|
+
fastapi_fsp-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
9
|
+
fastapi_fsp-0.3.0.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
|
|
10
|
+
fastapi_fsp-0.3.0.dist-info/RECORD,,
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: fastapi-fsp
|
|
3
|
-
Version: 0.2.2
|
|
4
|
-
Summary: Filter, Sort, and Paginate (FSP) utilities for FastAPI + SQLModel
|
|
5
|
-
Project-URL: Homepage, https://github.com/fromej-dev/fastapi-fsp
|
|
6
|
-
Project-URL: Repository, https://github.com/fromej-dev/fastapi-fsp
|
|
7
|
-
Project-URL: Issues, https://github.com/fromej-dev/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: Programming Language :: Python :: 3.13
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
-
Classifier: Topic :: Internet :: WWW/HTTP
|
|
22
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
-
Requires-Python: >=3.12
|
|
24
|
-
Requires-Dist: fastapi>=0.121.1
|
|
25
|
-
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
26
|
-
Requires-Dist: sqlmodel>=0.0.27
|
|
27
|
-
Description-Content-Type: text/markdown
|
|
28
|
-
|
|
29
|
-
# fastapi-fsp
|
|
30
|
-
|
|
31
|
-
Filter, Sort, and Paginate (FSP) utilities for FastAPI + SQLModel.
|
|
32
|
-
|
|
33
|
-
fastapi-fsp helps you build standardized list endpoints that support:
|
|
34
|
-
- Filtering on arbitrary fields with rich operators (eq, ne, lt, lte, gt, gte, in, between, like/ilike, null checks, contains/starts_with/ends_with)
|
|
35
|
-
- Sorting by field (asc/desc)
|
|
36
|
-
- Pagination with page/per_page and convenient HATEOAS links
|
|
37
|
-
|
|
38
|
-
It is framework-friendly: you declare it as a FastAPI dependency and feed it a SQLModel/SQLAlchemy Select query and a Session.
|
|
39
|
-
|
|
40
|
-
## Installation
|
|
41
|
-
|
|
42
|
-
Using uv (recommended):
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
# create & activate virtual env with uv
|
|
46
|
-
uv venv
|
|
47
|
-
. .venv/bin/activate
|
|
48
|
-
|
|
49
|
-
# add runtime dependency
|
|
50
|
-
uv add fastapi-fsp
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
Using pip:
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
pip install fastapi-fsp
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
## Quick start
|
|
60
|
-
|
|
61
|
-
Below is a minimal example using FastAPI and SQLModel.
|
|
62
|
-
|
|
63
|
-
```python
|
|
64
|
-
from typing import Optional
|
|
65
|
-
from fastapi import Depends, FastAPI
|
|
66
|
-
from sqlmodel import Field, SQLModel, Session, create_engine, select
|
|
67
|
-
|
|
68
|
-
from fastapi_fsp.fsp import FSPManager
|
|
69
|
-
from fastapi_fsp.models import PaginatedResponse
|
|
70
|
-
|
|
71
|
-
class HeroBase(SQLModel):
|
|
72
|
-
name: str = Field(index=True)
|
|
73
|
-
secret_name: str
|
|
74
|
-
age: Optional[int] = Field(default=None, index=True)
|
|
75
|
-
|
|
76
|
-
class Hero(HeroBase, table=True):
|
|
77
|
-
id: Optional[int] = Field(default=None, primary_key=True)
|
|
78
|
-
|
|
79
|
-
class HeroPublic(HeroBase):
|
|
80
|
-
id: int
|
|
81
|
-
|
|
82
|
-
engine = create_engine("sqlite:///database.db", connect_args={"check_same_thread": False})
|
|
83
|
-
SQLModel.metadata.create_all(engine)
|
|
84
|
-
|
|
85
|
-
app = FastAPI()
|
|
86
|
-
|
|
87
|
-
def get_session():
|
|
88
|
-
with Session(engine) as session:
|
|
89
|
-
yield session
|
|
90
|
-
|
|
91
|
-
@app.get("/heroes/", response_model=PaginatedResponse[HeroPublic])
|
|
92
|
-
def read_heroes(*, session: Session = Depends(get_session), fsp: FSPManager = Depends(FSPManager)):
|
|
93
|
-
query = select(Hero)
|
|
94
|
-
return fsp.generate_response(query, session)
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
Run the app and query:
|
|
98
|
-
|
|
99
|
-
- Pagination: `GET /heroes/?page=1&per_page=10`
|
|
100
|
-
- Sorting: `GET /heroes/?sort_by=name&order=asc`
|
|
101
|
-
- Filtering: `GET /heroes/?field=age&operator=gte&value=21`
|
|
102
|
-
|
|
103
|
-
The response includes data, meta (pagination, filters, sorting), and links (self, first, next, prev, last).
|
|
104
|
-
|
|
105
|
-
## Query parameters
|
|
106
|
-
|
|
107
|
-
Pagination:
|
|
108
|
-
- page: integer (>=1), default 1
|
|
109
|
-
- per_page: integer (1..100), default 10
|
|
110
|
-
|
|
111
|
-
Sorting:
|
|
112
|
-
- sort_by: the field name, e.g., `name`
|
|
113
|
-
- order: `asc` or `desc`
|
|
114
|
-
|
|
115
|
-
Filtering (two supported formats):
|
|
116
|
-
|
|
117
|
-
1) Simple (triplets repeated in the query string):
|
|
118
|
-
- field: the field/column name, e.g., `name`
|
|
119
|
-
- operator: one of
|
|
120
|
-
- eq, ne
|
|
121
|
-
- lt, lte, gt, gte
|
|
122
|
-
- in, not_in (comma-separated values)
|
|
123
|
-
- between (two comma-separated values)
|
|
124
|
-
- like, not_like
|
|
125
|
-
- ilike, not_ilike (if backend supports ILIKE)
|
|
126
|
-
- is_null, is_not_null
|
|
127
|
-
- contains, starts_with, ends_with (translated to LIKE patterns)
|
|
128
|
-
- value: raw string value (or list-like comma-separated depending on operator)
|
|
129
|
-
|
|
130
|
-
Examples (simple format):
|
|
131
|
-
- `?field=name&operator=eq&value=Deadpond`
|
|
132
|
-
- `?field=age&operator=between&value=18,30`
|
|
133
|
-
- `?field=name&operator=in&value=Deadpond,Rusty-Man`
|
|
134
|
-
- `?field=name&operator=contains&value=man`
|
|
135
|
-
- Chain multiple filters by repeating the triplet: `?field=age&operator=gte&value=18&field=name&operator=ilike&value=rust`
|
|
136
|
-
|
|
137
|
-
2) Indexed format (useful for clients that handle arrays of objects):
|
|
138
|
-
- Use keys like `filters[0][field]`, `filters[0][operator]`, `filters[0][value]`, then increment the index for additional filters (`filters[1][...]`, etc.).
|
|
139
|
-
|
|
140
|
-
Example (indexed format):
|
|
141
|
-
```
|
|
142
|
-
?filters[0][field]=age&filters[0][operator]=gte&filters[0][value]=18&filters[1][field]=name&filters[1][operator]=ilike&filters[1][value]=joy
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
Notes:
|
|
146
|
-
- Both formats are equivalent; the indexed format takes precedence if present.
|
|
147
|
-
- If any filter is incomplete (missing operator or value in the indexed form, or mismatched counts of simple triplets), the API responds with HTTP 400.
|
|
148
|
-
|
|
149
|
-
## Response model
|
|
150
|
-
|
|
151
|
-
```
|
|
152
|
-
{
|
|
153
|
-
"data": [ ... ],
|
|
154
|
-
"meta": {
|
|
155
|
-
"pagination": {
|
|
156
|
-
"total_items": 42,
|
|
157
|
-
"per_page": 10,
|
|
158
|
-
"current_page": 1,
|
|
159
|
-
"total_pages": 5
|
|
160
|
-
},
|
|
161
|
-
"filters": [
|
|
162
|
-
{"field": "name", "operator": "eq", "value": "Deadpond"}
|
|
163
|
-
],
|
|
164
|
-
"sort": {"sort_by": "name", "order": "asc"}
|
|
165
|
-
},
|
|
166
|
-
"links": {
|
|
167
|
-
"self": "/heroes/?page=1&per_page=10",
|
|
168
|
-
"first": "/heroes/?page=1&per_page=10",
|
|
169
|
-
"next": "/heroes/?page=2&per_page=10",
|
|
170
|
-
"prev": null,
|
|
171
|
-
"last": "/heroes/?page=5&per_page=10"
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
## Development
|
|
177
|
-
|
|
178
|
-
This project uses uv as the package manager.
|
|
179
|
-
|
|
180
|
-
- Create env and sync deps:
|
|
181
|
-
```
|
|
182
|
-
uv venv
|
|
183
|
-
. .venv/bin/activate
|
|
184
|
-
uv sync --dev
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
- Run lint and format checks:
|
|
188
|
-
```
|
|
189
|
-
uv run ruff check .
|
|
190
|
-
uv run ruff format --check .
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
- Run tests:
|
|
194
|
-
```
|
|
195
|
-
uv run pytest -q
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
- Build the package:
|
|
199
|
-
```
|
|
200
|
-
uv build
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
## CI/CD and Releases
|
|
204
|
-
|
|
205
|
-
GitHub Actions workflows are included:
|
|
206
|
-
- CI (lint + tests) runs on pushes and PRs.
|
|
207
|
-
- Release: pushing a tag matching `v*.*.*` runs tests, builds, and publishes to PyPI using `PYPI_API_TOKEN` secret.
|
|
208
|
-
|
|
209
|
-
To release:
|
|
210
|
-
1. Update the version in `pyproject.toml`.
|
|
211
|
-
2. Push a tag, e.g. `git tag v0.1.1 && git push origin v0.1.1`.
|
|
212
|
-
3. Ensure the repository has `PYPI_API_TOKEN` secret set (an API token from PyPI).
|
|
213
|
-
|
|
214
|
-
## License
|
|
215
|
-
|
|
216
|
-
MIT License. See LICENSE.
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
fastapi_fsp/__init__.py,sha256=0I_XN_ptNu1NyNRpjdyVm6nwvrilAB8FyT2EfgVF_QA,215
|
|
2
|
-
fastapi_fsp/fsp.py,sha256=nPubvF7IStCWG0hdCeZX-1JgcHDyGkZa_XoAM7lq1Xw,22166
|
|
3
|
-
fastapi_fsp/models.py,sha256=1MwLBQFmUP8OwO3Gqby1u7s9ruimCR2XGfUzAqF4Tj4,2034
|
|
4
|
-
fastapi_fsp-0.2.2.dist-info/METADATA,sha256=zrje4Au8BOM4ids9JnX61jrP52ierjYuJxLGqvtCxFU,6235
|
|
5
|
-
fastapi_fsp-0.2.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
-
fastapi_fsp-0.2.2.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
|
|
7
|
-
fastapi_fsp-0.2.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|