fastapi-fsp 0.2.2__py3-none-any.whl → 0.2.3__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/fsp.py +53 -5
- {fastapi_fsp-0.2.2.dist-info → fastapi_fsp-0.2.3.dist-info}/METADATA +60 -1
- fastapi_fsp-0.2.3.dist-info/RECORD +7 -0
- fastapi_fsp-0.2.2.dist-info/RECORD +0 -7
- {fastapi_fsp-0.2.2.dist-info → fastapi_fsp-0.2.3.dist-info}/WHEEL +0 -0
- {fastapi_fsp-0.2.2.dist-info → fastapi_fsp-0.2.3.dist-info}/licenses/LICENSE +0 -0
fastapi_fsp/fsp.py
CHANGED
|
@@ -228,10 +228,54 @@ class FSPManager:
|
|
|
228
228
|
if col_id not in self._type_cache:
|
|
229
229
|
try:
|
|
230
230
|
self._type_cache[col_id] = getattr(column.type, "python_type", None)
|
|
231
|
-
except
|
|
231
|
+
except (AttributeError, NotImplementedError):
|
|
232
|
+
# For computed fields (hybrid_property, etc.), type inference may fail
|
|
232
233
|
self._type_cache[col_id] = None
|
|
233
234
|
return self._type_cache[col_id]
|
|
234
235
|
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _get_entity_attribute(query: Select, field: str) -> Optional[ColumnElement[Any]]:
|
|
238
|
+
"""
|
|
239
|
+
Try to get a column-like attribute from the query's entity.
|
|
240
|
+
|
|
241
|
+
This enables filtering/sorting on computed fields like hybrid_property
|
|
242
|
+
that have SQL expressions defined.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
query: SQLAlchemy Select query
|
|
246
|
+
field: Name of the field/attribute to get
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Optional[ColumnElement]: The SQL expression if available, None otherwise
|
|
250
|
+
"""
|
|
251
|
+
try:
|
|
252
|
+
# Get the entity class from the query
|
|
253
|
+
column_descriptions = query.column_descriptions
|
|
254
|
+
if not column_descriptions:
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
entity = column_descriptions[0].get("entity")
|
|
258
|
+
if entity is None:
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
# Get the attribute from the entity class
|
|
262
|
+
attr = getattr(entity, field, None)
|
|
263
|
+
if attr is None:
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
# Check if it's directly usable as a ColumnElement (hybrid_property with expression)
|
|
267
|
+
# When accessing a hybrid_property on the class, it returns the SQL expression
|
|
268
|
+
if isinstance(attr, ColumnElement):
|
|
269
|
+
return attr
|
|
270
|
+
|
|
271
|
+
# Some expressions may need to call __clause_element__
|
|
272
|
+
if hasattr(attr, "__clause_element__"):
|
|
273
|
+
return attr.__clause_element__()
|
|
274
|
+
|
|
275
|
+
return None
|
|
276
|
+
except Exception:
|
|
277
|
+
return None
|
|
278
|
+
|
|
235
279
|
def paginate(self, query: Select, session: Session) -> Any:
|
|
236
280
|
"""
|
|
237
281
|
Execute pagination on a query.
|
|
@@ -591,6 +635,11 @@ class FSPManager:
|
|
|
591
635
|
for f in filters:
|
|
592
636
|
# filter of `filters` has been validated in the `_parse_filters`
|
|
593
637
|
column = columns_map.get(f.field)
|
|
638
|
+
|
|
639
|
+
# Fall back to computed fields (hybrid_property, etc.) if not in columns_map
|
|
640
|
+
if column is None:
|
|
641
|
+
column = FSPManager._get_entity_attribute(query, f.field)
|
|
642
|
+
|
|
594
643
|
if column is None:
|
|
595
644
|
if self.strict_mode:
|
|
596
645
|
available = ", ".join(sorted(columns_map.keys()))
|
|
@@ -635,11 +684,10 @@ class FSPManager:
|
|
|
635
684
|
"""
|
|
636
685
|
if sorting and sorting.sort_by:
|
|
637
686
|
column = columns_map.get(sorting.sort_by)
|
|
687
|
+
|
|
688
|
+
# Fall back to computed fields (hybrid_property, etc.) if not in columns_map
|
|
638
689
|
if column is None:
|
|
639
|
-
|
|
640
|
-
column = getattr(query.column_descriptions[0]["entity"], sorting.sort_by, None)
|
|
641
|
-
except Exception:
|
|
642
|
-
pass
|
|
690
|
+
column = FSPManager._get_entity_attribute(query, sorting.sort_by)
|
|
643
691
|
|
|
644
692
|
if column is None:
|
|
645
693
|
if self.strict_mode:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-fsp
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Filter, Sort, and Paginate (FSP) utilities for FastAPI + SQLModel
|
|
5
5
|
Project-URL: Homepage, https://github.com/fromej-dev/fastapi-fsp
|
|
6
6
|
Project-URL: Repository, https://github.com/fromej-dev/fastapi-fsp
|
|
@@ -146,6 +146,65 @@ Notes:
|
|
|
146
146
|
- Both formats are equivalent; the indexed format takes precedence if present.
|
|
147
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
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
|
+
|
|
149
208
|
## Response model
|
|
150
209
|
|
|
151
210
|
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
fastapi_fsp/__init__.py,sha256=0I_XN_ptNu1NyNRpjdyVm6nwvrilAB8FyT2EfgVF_QA,215
|
|
2
|
+
fastapi_fsp/fsp.py,sha256=b6OuHgbTpWrFtxGKm7fcPpjHtD3Cdjrs22m3IYOBhp4,23986
|
|
3
|
+
fastapi_fsp/models.py,sha256=1MwLBQFmUP8OwO3Gqby1u7s9ruimCR2XGfUzAqF4Tj4,2034
|
|
4
|
+
fastapi_fsp-0.2.3.dist-info/METADATA,sha256=w4wfXs3hye-UI0-h2a81OjkG3yzzZWUq91n1HhmyUi4,8224
|
|
5
|
+
fastapi_fsp-0.2.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
+
fastapi_fsp-0.2.3.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
|
|
7
|
+
fastapi_fsp-0.2.3.dist-info/RECORD,,
|
|
@@ -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
|