fastapi-fsp 0.2.1__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 +183 -65
- {fastapi_fsp-0.2.1.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.1.dist-info/RECORD +0 -7
- {fastapi_fsp-0.2.1.dist-info → fastapi_fsp-0.2.3.dist-info}/WHEEL +0 -0
- {fastapi_fsp-0.2.1.dist-info → fastapi_fsp-0.2.3.dist-info}/licenses/LICENSE +0 -0
fastapi_fsp/fsp.py
CHANGED
|
@@ -195,6 +195,7 @@ class FSPManager:
|
|
|
195
195
|
filters: Annotated[Optional[List[Filter]], Depends(_parse_filters)],
|
|
196
196
|
sorting: Annotated[Optional[SortingQuery], Depends(_parse_sort)],
|
|
197
197
|
pagination: Annotated[PaginationQuery, Depends(_parse_pagination)],
|
|
198
|
+
strict_mode: bool = False,
|
|
198
199
|
):
|
|
199
200
|
"""
|
|
200
201
|
Initialize FSPManager.
|
|
@@ -204,11 +205,76 @@ class FSPManager:
|
|
|
204
205
|
filters: Parsed filters
|
|
205
206
|
sorting: Sorting configuration
|
|
206
207
|
pagination: Pagination configuration
|
|
208
|
+
strict_mode: If True, raise errors for unknown fields instead of silently skipping
|
|
207
209
|
"""
|
|
208
210
|
self.request = request
|
|
209
211
|
self.filters = filters
|
|
210
212
|
self.sorting = sorting
|
|
211
213
|
self.pagination = pagination
|
|
214
|
+
self.strict_mode = strict_mode
|
|
215
|
+
self._type_cache: dict[int, Optional[type]] = {}
|
|
216
|
+
|
|
217
|
+
def _get_column_type(self, column: ColumnElement[Any]) -> Optional[type]:
|
|
218
|
+
"""
|
|
219
|
+
Get the Python type of a column with caching.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
column: SQLAlchemy column element
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Optional[type]: Python type of the column or None
|
|
226
|
+
"""
|
|
227
|
+
col_id = id(column)
|
|
228
|
+
if col_id not in self._type_cache:
|
|
229
|
+
try:
|
|
230
|
+
self._type_cache[col_id] = getattr(column.type, "python_type", None)
|
|
231
|
+
except (AttributeError, NotImplementedError):
|
|
232
|
+
# For computed fields (hybrid_property, etc.), type inference may fail
|
|
233
|
+
self._type_cache[col_id] = None
|
|
234
|
+
return self._type_cache[col_id]
|
|
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
|
|
212
278
|
|
|
213
279
|
def paginate(self, query: Select, session: Session) -> Any:
|
|
214
280
|
"""
|
|
@@ -257,8 +323,8 @@ class FSPManager:
|
|
|
257
323
|
PaginatedResponse: Complete paginated response
|
|
258
324
|
"""
|
|
259
325
|
columns_map = query.selected_columns
|
|
260
|
-
query =
|
|
261
|
-
query =
|
|
326
|
+
query = self._apply_filters(query, columns_map, self.filters)
|
|
327
|
+
query = self._apply_sort(query, columns_map, self.sorting)
|
|
262
328
|
|
|
263
329
|
total_items = self._count_total(query, session)
|
|
264
330
|
data_page = self.paginate(query, session)
|
|
@@ -278,8 +344,8 @@ class FSPManager:
|
|
|
278
344
|
PaginatedResponse: Complete paginated response
|
|
279
345
|
"""
|
|
280
346
|
columns_map = query.selected_columns
|
|
281
|
-
query =
|
|
282
|
-
query =
|
|
347
|
+
query = self._apply_filters(query, columns_map, self.filters)
|
|
348
|
+
query = self._apply_sort(query, columns_map, self.sorting)
|
|
283
349
|
|
|
284
350
|
total_items = await self._count_total_async(query, session)
|
|
285
351
|
data_page = await self.paginate_async(query, session)
|
|
@@ -338,22 +404,24 @@ class FSPManager:
|
|
|
338
404
|
)
|
|
339
405
|
|
|
340
406
|
@staticmethod
|
|
341
|
-
def _coerce_value(column: ColumnElement[Any], raw: str) -> Any:
|
|
407
|
+
def _coerce_value(column: ColumnElement[Any], raw: str, pytype: Optional[type] = None) -> Any:
|
|
342
408
|
"""
|
|
343
409
|
Coerce raw string value to column's Python type.
|
|
344
410
|
|
|
345
411
|
Args:
|
|
346
412
|
column: SQLAlchemy column element
|
|
347
413
|
raw: Raw string value
|
|
414
|
+
pytype: Optional pre-fetched python type (for performance)
|
|
348
415
|
|
|
349
416
|
Returns:
|
|
350
417
|
Any: Coerced value
|
|
351
418
|
"""
|
|
352
419
|
# Try to coerce raw (str) to the column's python type for proper comparisons
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
420
|
+
if pytype is None:
|
|
421
|
+
try:
|
|
422
|
+
pytype = getattr(column.type, "python_type", None)
|
|
423
|
+
except Exception:
|
|
424
|
+
pytype = None
|
|
357
425
|
if pytype is None or isinstance(raw, pytype):
|
|
358
426
|
return raw
|
|
359
427
|
# Handle booleans represented as strings
|
|
@@ -373,12 +441,17 @@ class FSPManager:
|
|
|
373
441
|
return int(float(raw))
|
|
374
442
|
except ValueError:
|
|
375
443
|
return raw
|
|
376
|
-
# Handle dates represented as strings
|
|
444
|
+
# Handle dates represented as strings - optimized with fast path for ISO 8601
|
|
377
445
|
if pytype is datetime:
|
|
378
446
|
try:
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
447
|
+
# Fast path for ISO 8601 format (most common)
|
|
448
|
+
return datetime.fromisoformat(raw)
|
|
449
|
+
except (ValueError, AttributeError):
|
|
450
|
+
# Fallback to flexible dateutil parser for other formats
|
|
451
|
+
try:
|
|
452
|
+
return parse(raw)
|
|
453
|
+
except ValueError:
|
|
454
|
+
return raw
|
|
382
455
|
# Generic cast with fallback
|
|
383
456
|
try:
|
|
384
457
|
return pytype(raw)
|
|
@@ -412,91 +485,96 @@ class FSPManager:
|
|
|
412
485
|
return hasattr(col, "ilike")
|
|
413
486
|
|
|
414
487
|
@staticmethod
|
|
415
|
-
def
|
|
488
|
+
def _build_filter_condition(
|
|
489
|
+
column: ColumnElement[Any], f: Filter, pytype: Optional[type] = None
|
|
490
|
+
) -> Optional[Any]:
|
|
416
491
|
"""
|
|
417
|
-
|
|
492
|
+
Build a filter condition for a query.
|
|
418
493
|
|
|
419
494
|
Args:
|
|
420
|
-
query: Base SQLAlchemy Select query
|
|
421
495
|
column: Column to apply filter to
|
|
422
496
|
f: Filter to apply
|
|
497
|
+
pytype: Optional pre-fetched python type (for performance)
|
|
423
498
|
|
|
424
499
|
Returns:
|
|
425
|
-
|
|
500
|
+
Optional[Any]: SQLAlchemy condition or None if invalid
|
|
426
501
|
"""
|
|
427
502
|
op = f.operator # type: FilterOperator
|
|
428
503
|
raw_value = f.value # type: str
|
|
429
504
|
|
|
430
505
|
# Build conditions based on operator
|
|
431
506
|
if op == FilterOperator.EQ:
|
|
432
|
-
|
|
507
|
+
return column == FSPManager._coerce_value(column, raw_value, pytype)
|
|
433
508
|
elif op == FilterOperator.NE:
|
|
434
|
-
|
|
509
|
+
return column != FSPManager._coerce_value(column, raw_value, pytype)
|
|
435
510
|
elif op == FilterOperator.GT:
|
|
436
|
-
|
|
511
|
+
return column > FSPManager._coerce_value(column, raw_value, pytype)
|
|
437
512
|
elif op == FilterOperator.GTE:
|
|
438
|
-
|
|
513
|
+
return column >= FSPManager._coerce_value(column, raw_value, pytype)
|
|
439
514
|
elif op == FilterOperator.LT:
|
|
440
|
-
|
|
515
|
+
return column < FSPManager._coerce_value(column, raw_value, pytype)
|
|
441
516
|
elif op == FilterOperator.LTE:
|
|
442
|
-
|
|
517
|
+
return column <= FSPManager._coerce_value(column, raw_value, pytype)
|
|
443
518
|
elif op == FilterOperator.LIKE:
|
|
444
|
-
|
|
519
|
+
return column.like(raw_value)
|
|
445
520
|
elif op == FilterOperator.NOT_LIKE:
|
|
446
|
-
|
|
521
|
+
return not_(column.like(raw_value))
|
|
447
522
|
elif op == FilterOperator.ILIKE:
|
|
448
523
|
pattern = raw_value
|
|
449
524
|
if FSPManager._ilike_supported(column):
|
|
450
|
-
|
|
525
|
+
return column.ilike(pattern)
|
|
451
526
|
else:
|
|
452
|
-
|
|
527
|
+
return func.lower(column).like(pattern.lower())
|
|
453
528
|
elif op == FilterOperator.NOT_ILIKE:
|
|
454
529
|
pattern = raw_value
|
|
455
530
|
if FSPManager._ilike_supported(column):
|
|
456
|
-
|
|
531
|
+
return not_(column.ilike(pattern))
|
|
457
532
|
else:
|
|
458
|
-
|
|
533
|
+
return not_(func.lower(column).like(pattern.lower()))
|
|
459
534
|
elif op == FilterOperator.IN:
|
|
460
535
|
vals = [
|
|
461
|
-
FSPManager._coerce_value(column, v
|
|
536
|
+
FSPManager._coerce_value(column, v, pytype)
|
|
537
|
+
for v in FSPManager._split_values(raw_value)
|
|
462
538
|
]
|
|
463
|
-
|
|
539
|
+
return column.in_(vals)
|
|
464
540
|
elif op == FilterOperator.NOT_IN:
|
|
465
541
|
vals = [
|
|
466
|
-
FSPManager._coerce_value(column, v
|
|
542
|
+
FSPManager._coerce_value(column, v, pytype)
|
|
543
|
+
for v in FSPManager._split_values(raw_value)
|
|
467
544
|
]
|
|
468
|
-
|
|
545
|
+
return not_(column.in_(vals))
|
|
469
546
|
elif op == FilterOperator.BETWEEN:
|
|
470
547
|
vals = FSPManager._split_values(raw_value)
|
|
471
548
|
if len(vals) == 2:
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
549
|
+
low = FSPManager._coerce_value(column, vals[0], pytype)
|
|
550
|
+
high = FSPManager._coerce_value(column, vals[1], pytype)
|
|
551
|
+
return column.between(low, high)
|
|
552
|
+
# Ignore malformed between
|
|
553
|
+
return None
|
|
476
554
|
elif op == FilterOperator.IS_NULL:
|
|
477
|
-
|
|
555
|
+
return column.is_(None)
|
|
478
556
|
elif op == FilterOperator.IS_NOT_NULL:
|
|
479
|
-
|
|
557
|
+
return column.is_not(None)
|
|
480
558
|
elif op == FilterOperator.STARTS_WITH:
|
|
481
559
|
pattern = f"{raw_value}%"
|
|
482
560
|
if FSPManager._ilike_supported(column):
|
|
483
|
-
|
|
561
|
+
return column.ilike(pattern)
|
|
484
562
|
else:
|
|
485
|
-
|
|
563
|
+
return func.lower(column).like(pattern.lower())
|
|
486
564
|
elif op == FilterOperator.ENDS_WITH:
|
|
487
565
|
pattern = f"%{raw_value}"
|
|
488
566
|
if FSPManager._ilike_supported(column):
|
|
489
|
-
|
|
567
|
+
return column.ilike(pattern)
|
|
490
568
|
else:
|
|
491
|
-
|
|
569
|
+
return func.lower(column).like(pattern.lower())
|
|
492
570
|
elif op == FilterOperator.CONTAINS:
|
|
493
571
|
pattern = f"%{raw_value}%"
|
|
494
572
|
if FSPManager._ilike_supported(column):
|
|
495
|
-
|
|
573
|
+
return column.ilike(pattern)
|
|
496
574
|
else:
|
|
497
|
-
|
|
498
|
-
# Unknown operator
|
|
499
|
-
return
|
|
575
|
+
return func.lower(column).like(pattern.lower())
|
|
576
|
+
# Unknown operator
|
|
577
|
+
return None
|
|
500
578
|
|
|
501
579
|
@staticmethod
|
|
502
580
|
def _count_total(query: Select, session: Session) -> int:
|
|
@@ -530,8 +608,8 @@ class FSPManager:
|
|
|
530
608
|
result = await session.exec(count_query)
|
|
531
609
|
return result.one()
|
|
532
610
|
|
|
533
|
-
@staticmethod
|
|
534
611
|
def _apply_filters(
|
|
612
|
+
self,
|
|
535
613
|
query: Select,
|
|
536
614
|
columns_map: ColumnCollection[str, ColumnElement[Any]],
|
|
537
615
|
filters: Optional[List[Filter]],
|
|
@@ -546,19 +624,46 @@ class FSPManager:
|
|
|
546
624
|
|
|
547
625
|
Returns:
|
|
548
626
|
Select: Query with filters applied
|
|
627
|
+
|
|
628
|
+
Raises:
|
|
629
|
+
HTTPException: If strict_mode is True and unknown field is encountered
|
|
549
630
|
"""
|
|
550
|
-
if filters:
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
631
|
+
if not filters:
|
|
632
|
+
return query
|
|
633
|
+
|
|
634
|
+
conditions = []
|
|
635
|
+
for f in filters:
|
|
636
|
+
# filter of `filters` has been validated in the `_parse_filters`
|
|
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
|
+
|
|
643
|
+
if column is None:
|
|
644
|
+
if self.strict_mode:
|
|
645
|
+
available = ", ".join(sorted(columns_map.keys()))
|
|
646
|
+
raise HTTPException(
|
|
647
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
648
|
+
detail=f"Unknown field '{f.field}'. Available fields: {available}",
|
|
649
|
+
)
|
|
650
|
+
# Skip unknown fields silently in non-strict mode
|
|
651
|
+
continue
|
|
652
|
+
|
|
653
|
+
# Get column type from cache for better performance
|
|
654
|
+
pytype = self._get_column_type(column)
|
|
655
|
+
condition = FSPManager._build_filter_condition(column, f, pytype)
|
|
656
|
+
if condition is not None:
|
|
657
|
+
conditions.append(condition)
|
|
658
|
+
|
|
659
|
+
# Apply all conditions in a single where() call for better SQL generation
|
|
660
|
+
if conditions:
|
|
661
|
+
query = query.where(*conditions)
|
|
557
662
|
|
|
558
663
|
return query
|
|
559
664
|
|
|
560
|
-
@staticmethod
|
|
561
665
|
def _apply_sort(
|
|
666
|
+
self,
|
|
562
667
|
query: Select,
|
|
563
668
|
columns_map: ColumnCollection[str, ColumnElement[Any]],
|
|
564
669
|
sorting: Optional[SortingQuery],
|
|
@@ -573,17 +678,30 @@ class FSPManager:
|
|
|
573
678
|
|
|
574
679
|
Returns:
|
|
575
680
|
Select: Query with sorting applied
|
|
681
|
+
|
|
682
|
+
Raises:
|
|
683
|
+
HTTPException: If strict_mode is True and unknown sort field is encountered
|
|
576
684
|
"""
|
|
577
685
|
if sorting and sorting.sort_by:
|
|
578
686
|
column = columns_map.get(sorting.sort_by)
|
|
687
|
+
|
|
688
|
+
# Fall back to computed fields (hybrid_property, etc.) if not in columns_map
|
|
579
689
|
if column is None:
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
690
|
+
column = FSPManager._get_entity_attribute(query, sorting.sort_by)
|
|
691
|
+
|
|
692
|
+
if column is None:
|
|
693
|
+
if self.strict_mode:
|
|
694
|
+
available = ", ".join(sorted(columns_map.keys()))
|
|
695
|
+
raise HTTPException(
|
|
696
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
697
|
+
detail=(
|
|
698
|
+
f"Unknown sort field '{sorting.sort_by}'. Available fields: {available}"
|
|
699
|
+
),
|
|
700
|
+
)
|
|
701
|
+
# Unknown sort column; skip sorting in non-strict mode
|
|
702
|
+
return query
|
|
703
|
+
|
|
704
|
+
query = query.order_by(
|
|
705
|
+
column.desc() if sorting.order == SortingOrder.DESC else column.asc()
|
|
706
|
+
)
|
|
589
707
|
return query
|
|
@@ -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=p7sGNRkYbiRtFPta0OvfZ12th69BjtD6h2GzSjbPoaU,19646
|
|
3
|
-
fastapi_fsp/models.py,sha256=1MwLBQFmUP8OwO3Gqby1u7s9ruimCR2XGfUzAqF4Tj4,2034
|
|
4
|
-
fastapi_fsp-0.2.1.dist-info/METADATA,sha256=LcnA3C98yai-Cw54cqw1t8XfDgb8YqiPhZTc1h0V3qk,6235
|
|
5
|
-
fastapi_fsp-0.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
-
fastapi_fsp-0.2.1.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
|
|
7
|
-
fastapi_fsp-0.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|