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 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 Exception:
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
- try:
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.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,,