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.
@@ -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,,