fastapi-toolsets 3.0.0__tar.gz → 3.0.2__tar.gz
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_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/PKG-INFO +1 -1
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/pyproject.toml +1 -1
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/__init__.py +1 -1
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/crud/__init__.py +2 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/crud/factory.py +76 -11
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/crud/search.py +19 -6
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/schemas.py +1 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/types.py +3 -2
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/LICENSE +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/README.md +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/_imports.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/cli/__init__.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/cli/app.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/cli/commands/__init__.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/cli/commands/fixtures.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/cli/config.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/cli/pyproject.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/cli/utils.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/db.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/dependencies.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/exceptions/__init__.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/exceptions/exceptions.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/exceptions/handler.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/fixtures/__init__.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/fixtures/enum.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/fixtures/registry.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/fixtures/utils.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/logger.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/metrics/__init__.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/metrics/handler.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/metrics/registry.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/models/__init__.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/models/columns.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/models/watched.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/py.typed +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/pytest/__init__.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/pytest/plugin.py +0 -0
- {fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/pytest/utils.py +0 -0
|
@@ -12,6 +12,7 @@ from ..types import (
|
|
|
12
12
|
JoinType,
|
|
13
13
|
M2MFieldType,
|
|
14
14
|
OrderByClause,
|
|
15
|
+
OrderFieldType,
|
|
15
16
|
SearchFieldType,
|
|
16
17
|
)
|
|
17
18
|
from .factory import AsyncCrud, CrudFactory
|
|
@@ -28,6 +29,7 @@ __all__ = [
|
|
|
28
29
|
"M2MFieldType",
|
|
29
30
|
"NoSearchableFieldsError",
|
|
30
31
|
"OrderByClause",
|
|
32
|
+
"OrderFieldType",
|
|
31
33
|
"PaginationType",
|
|
32
34
|
"SearchConfig",
|
|
33
35
|
"SearchFieldType",
|
|
@@ -38,6 +38,7 @@ from ..types import (
|
|
|
38
38
|
M2MFieldType,
|
|
39
39
|
ModelType,
|
|
40
40
|
OrderByClause,
|
|
41
|
+
OrderFieldType,
|
|
41
42
|
SchemaType,
|
|
42
43
|
SearchFieldType,
|
|
43
44
|
)
|
|
@@ -134,7 +135,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
134
135
|
model: ClassVar[type[DeclarativeBase]]
|
|
135
136
|
searchable_fields: ClassVar[Sequence[SearchFieldType] | None] = None
|
|
136
137
|
facet_fields: ClassVar[Sequence[FacetFieldType] | None] = None
|
|
137
|
-
order_fields: ClassVar[Sequence[
|
|
138
|
+
order_fields: ClassVar[Sequence[OrderFieldType] | None] = None
|
|
138
139
|
m2m_fields: ClassVar[M2MFieldType | None] = None
|
|
139
140
|
default_load_options: ClassVar[Sequence[ExecutableOption] | None] = None
|
|
140
141
|
cursor_column: ClassVar[Any | None] = None
|
|
@@ -169,6 +170,18 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
169
170
|
return load_options
|
|
170
171
|
return cls.default_load_options
|
|
171
172
|
|
|
173
|
+
@classmethod
|
|
174
|
+
async def _reload_with_options(
|
|
175
|
+
cls: type[Self], session: AsyncSession, instance: ModelType
|
|
176
|
+
) -> ModelType:
|
|
177
|
+
"""Re-query instance by PK with default_load_options applied."""
|
|
178
|
+
mapper = cls.model.__mapper__
|
|
179
|
+
pk_filters = [
|
|
180
|
+
getattr(cls.model, col.key) == getattr(instance, col.key)
|
|
181
|
+
for col in mapper.primary_key
|
|
182
|
+
]
|
|
183
|
+
return await cls.get(session, filters=pk_filters)
|
|
184
|
+
|
|
172
185
|
@classmethod
|
|
173
186
|
async def _resolve_m2m(
|
|
174
187
|
cls: type[Self],
|
|
@@ -278,6 +291,17 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
278
291
|
return None
|
|
279
292
|
return search_field_keys(fields)
|
|
280
293
|
|
|
294
|
+
@classmethod
|
|
295
|
+
def _resolve_order_columns(
|
|
296
|
+
cls: type[Self],
|
|
297
|
+
order_fields: Sequence[OrderFieldType] | None,
|
|
298
|
+
) -> list[str] | None:
|
|
299
|
+
"""Return sort column keys, or None if no order fields configured."""
|
|
300
|
+
fields = order_fields if order_fields is not None else cls.order_fields
|
|
301
|
+
if not fields:
|
|
302
|
+
return None
|
|
303
|
+
return sorted(facet_keys(fields))
|
|
304
|
+
|
|
281
305
|
@classmethod
|
|
282
306
|
def _build_paginate_params(
|
|
283
307
|
cls: type[Self],
|
|
@@ -290,7 +314,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
290
314
|
order: bool,
|
|
291
315
|
search_fields: Sequence[SearchFieldType] | None,
|
|
292
316
|
facet_fields: Sequence[FacetFieldType] | None,
|
|
293
|
-
order_fields: Sequence[
|
|
317
|
+
order_fields: Sequence[OrderFieldType] | None,
|
|
294
318
|
default_order_field: QueryableAttribute[Any] | None,
|
|
295
319
|
default_order: Literal["asc", "desc"],
|
|
296
320
|
) -> Callable[..., Awaitable[dict[str, Any]]]:
|
|
@@ -349,14 +373,15 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
349
373
|
)
|
|
350
374
|
reserved_names.update(filter_keys)
|
|
351
375
|
|
|
352
|
-
order_field_map: dict[str,
|
|
376
|
+
order_field_map: dict[str, OrderFieldType] | None = None
|
|
353
377
|
order_valid_keys: list[str] | None = None
|
|
354
378
|
if order:
|
|
355
379
|
resolved_order = (
|
|
356
380
|
order_fields if order_fields is not None else cls.order_fields
|
|
357
381
|
)
|
|
358
382
|
if resolved_order:
|
|
359
|
-
|
|
383
|
+
keys = facet_keys(resolved_order)
|
|
384
|
+
order_field_map = dict(zip(keys, resolved_order))
|
|
360
385
|
order_valid_keys = sorted(order_field_map.keys())
|
|
361
386
|
all_params.extend(
|
|
362
387
|
[
|
|
@@ -408,9 +433,16 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
408
433
|
else:
|
|
409
434
|
field = order_field_map[order_by_val]
|
|
410
435
|
if field is not None:
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
436
|
+
if isinstance(field, tuple):
|
|
437
|
+
col = field[-1]
|
|
438
|
+
result["order_by"] = (
|
|
439
|
+
col.asc() if order_dir == "asc" else col.desc()
|
|
440
|
+
)
|
|
441
|
+
result["order_joins"] = list(field[:-1])
|
|
442
|
+
else:
|
|
443
|
+
result["order_by"] = (
|
|
444
|
+
field.asc() if order_dir == "asc" else field.desc()
|
|
445
|
+
)
|
|
414
446
|
else:
|
|
415
447
|
result["order_by"] = None
|
|
416
448
|
|
|
@@ -434,7 +466,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
434
466
|
order: bool = True,
|
|
435
467
|
search_fields: Sequence[SearchFieldType] | None = None,
|
|
436
468
|
facet_fields: Sequence[FacetFieldType] | None = None,
|
|
437
|
-
order_fields: Sequence[
|
|
469
|
+
order_fields: Sequence[OrderFieldType] | None = None,
|
|
438
470
|
default_order_field: QueryableAttribute[Any] | None = None,
|
|
439
471
|
default_order: Literal["asc", "desc"] = "asc",
|
|
440
472
|
) -> Callable[..., Awaitable[dict[str, Any]]]:
|
|
@@ -496,7 +528,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
496
528
|
order: bool = True,
|
|
497
529
|
search_fields: Sequence[SearchFieldType] | None = None,
|
|
498
530
|
facet_fields: Sequence[FacetFieldType] | None = None,
|
|
499
|
-
order_fields: Sequence[
|
|
531
|
+
order_fields: Sequence[OrderFieldType] | None = None,
|
|
500
532
|
default_order_field: QueryableAttribute[Any] | None = None,
|
|
501
533
|
default_order: Literal["asc", "desc"] = "asc",
|
|
502
534
|
) -> Callable[..., Awaitable[dict[str, Any]]]:
|
|
@@ -561,7 +593,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
561
593
|
order: bool = True,
|
|
562
594
|
search_fields: Sequence[SearchFieldType] | None = None,
|
|
563
595
|
facet_fields: Sequence[FacetFieldType] | None = None,
|
|
564
|
-
order_fields: Sequence[
|
|
596
|
+
order_fields: Sequence[OrderFieldType] | None = None,
|
|
565
597
|
default_order_field: QueryableAttribute[Any] | None = None,
|
|
566
598
|
default_order: Literal["asc", "desc"] = "asc",
|
|
567
599
|
) -> Callable[..., Awaitable[dict[str, Any]]]:
|
|
@@ -685,6 +717,8 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
685
717
|
|
|
686
718
|
session.add(db_model)
|
|
687
719
|
await session.refresh(db_model)
|
|
720
|
+
if cls.default_load_options:
|
|
721
|
+
db_model = await cls._reload_with_options(session, db_model)
|
|
688
722
|
result = cast(ModelType, db_model)
|
|
689
723
|
if schema:
|
|
690
724
|
return Response(data=schema.model_validate(result))
|
|
@@ -1040,6 +1074,8 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1040
1074
|
for rel_attr, related_instances in m2m_resolved.items():
|
|
1041
1075
|
setattr(db_model, rel_attr, related_instances)
|
|
1042
1076
|
await session.refresh(db_model)
|
|
1077
|
+
if cls.default_load_options:
|
|
1078
|
+
db_model = await cls._reload_with_options(session, db_model)
|
|
1043
1079
|
if schema:
|
|
1044
1080
|
return Response(data=schema.model_validate(db_model))
|
|
1045
1081
|
return db_model
|
|
@@ -1202,12 +1238,14 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1202
1238
|
outer_join: bool = False,
|
|
1203
1239
|
load_options: Sequence[ExecutableOption] | None = None,
|
|
1204
1240
|
order_by: OrderByClause | None = None,
|
|
1241
|
+
order_joins: list[Any] | None = None,
|
|
1205
1242
|
page: int = 1,
|
|
1206
1243
|
items_per_page: int = 20,
|
|
1207
1244
|
include_total: bool = True,
|
|
1208
1245
|
search: str | SearchConfig | None = None,
|
|
1209
1246
|
search_fields: Sequence[SearchFieldType] | None = None,
|
|
1210
1247
|
search_column: str | None = None,
|
|
1248
|
+
order_fields: Sequence[OrderFieldType] | None = None,
|
|
1211
1249
|
facet_fields: Sequence[FacetFieldType] | None = None,
|
|
1212
1250
|
filter_by: dict[str, Any] | BaseModel | None = None,
|
|
1213
1251
|
schema: type[BaseModel],
|
|
@@ -1228,6 +1266,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1228
1266
|
search: Search query string or SearchConfig object
|
|
1229
1267
|
search_fields: Fields to search in (overrides class default)
|
|
1230
1268
|
search_column: Restrict search to a single column key.
|
|
1269
|
+
order_fields: Fields allowed for sorting (overrides class default).
|
|
1231
1270
|
facet_fields: Columns to compute distinct values for (overrides class default)
|
|
1232
1271
|
filter_by: Dict of {column_key: value} to filter by declared facet fields.
|
|
1233
1272
|
Keys must match the column.key of a facet field. Scalar → equality,
|
|
@@ -1264,6 +1303,10 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1264
1303
|
# Apply search joins (always outer joins for search)
|
|
1265
1304
|
q = _apply_search_joins(q, search_joins)
|
|
1266
1305
|
|
|
1306
|
+
# Apply order joins (relation joins required for order_by field)
|
|
1307
|
+
if order_joins:
|
|
1308
|
+
q = _apply_search_joins(q, order_joins)
|
|
1309
|
+
|
|
1267
1310
|
if filters:
|
|
1268
1311
|
q = q.where(and_(*filters))
|
|
1269
1312
|
if resolved := cls._resolve_load_options(load_options):
|
|
@@ -1308,6 +1351,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1308
1351
|
session, facet_fields, filters, search_joins
|
|
1309
1352
|
)
|
|
1310
1353
|
search_columns = cls._resolve_search_columns(search_fields)
|
|
1354
|
+
order_columns = cls._resolve_order_columns(order_fields)
|
|
1311
1355
|
|
|
1312
1356
|
return OffsetPaginatedResponse(
|
|
1313
1357
|
data=items,
|
|
@@ -1319,6 +1363,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1319
1363
|
),
|
|
1320
1364
|
filter_attributes=filter_attributes,
|
|
1321
1365
|
search_columns=search_columns,
|
|
1366
|
+
order_columns=order_columns,
|
|
1322
1367
|
)
|
|
1323
1368
|
|
|
1324
1369
|
@classmethod
|
|
@@ -1332,10 +1377,12 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1332
1377
|
outer_join: bool = False,
|
|
1333
1378
|
load_options: Sequence[ExecutableOption] | None = None,
|
|
1334
1379
|
order_by: OrderByClause | None = None,
|
|
1380
|
+
order_joins: list[Any] | None = None,
|
|
1335
1381
|
items_per_page: int = 20,
|
|
1336
1382
|
search: str | SearchConfig | None = None,
|
|
1337
1383
|
search_fields: Sequence[SearchFieldType] | None = None,
|
|
1338
1384
|
search_column: str | None = None,
|
|
1385
|
+
order_fields: Sequence[OrderFieldType] | None = None,
|
|
1339
1386
|
facet_fields: Sequence[FacetFieldType] | None = None,
|
|
1340
1387
|
filter_by: dict[str, Any] | BaseModel | None = None,
|
|
1341
1388
|
schema: type[BaseModel],
|
|
@@ -1357,6 +1404,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1357
1404
|
search: Search query string or SearchConfig object.
|
|
1358
1405
|
search_fields: Fields to search in (overrides class default).
|
|
1359
1406
|
search_column: Restrict search to a single column key.
|
|
1407
|
+
order_fields: Fields allowed for sorting (overrides class default).
|
|
1360
1408
|
facet_fields: Columns to compute distinct values for (overrides class default).
|
|
1361
1409
|
filter_by: Dict of {column_key: value} to filter by declared facet fields.
|
|
1362
1410
|
Keys must match the column.key of a facet field. Scalar → equality,
|
|
@@ -1410,6 +1458,10 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1410
1458
|
# Apply search joins (always outer joins)
|
|
1411
1459
|
q = _apply_search_joins(q, search_joins)
|
|
1412
1460
|
|
|
1461
|
+
# Apply order joins (relation joins required for order_by field)
|
|
1462
|
+
if order_joins:
|
|
1463
|
+
q = _apply_search_joins(q, order_joins)
|
|
1464
|
+
|
|
1413
1465
|
if filters:
|
|
1414
1466
|
q = q.where(and_(*filters))
|
|
1415
1467
|
if resolved := cls._resolve_load_options(load_options):
|
|
@@ -1468,6 +1520,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1468
1520
|
session, facet_fields, filters, search_joins
|
|
1469
1521
|
)
|
|
1470
1522
|
search_columns = cls._resolve_search_columns(search_fields)
|
|
1523
|
+
order_columns = cls._resolve_order_columns(order_fields)
|
|
1471
1524
|
|
|
1472
1525
|
return CursorPaginatedResponse(
|
|
1473
1526
|
data=items,
|
|
@@ -1479,6 +1532,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1479
1532
|
),
|
|
1480
1533
|
filter_attributes=filter_attributes,
|
|
1481
1534
|
search_columns=search_columns,
|
|
1535
|
+
order_columns=order_columns,
|
|
1482
1536
|
)
|
|
1483
1537
|
|
|
1484
1538
|
@overload
|
|
@@ -1493,6 +1547,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1493
1547
|
outer_join: bool = ...,
|
|
1494
1548
|
load_options: Sequence[ExecutableOption] | None = ...,
|
|
1495
1549
|
order_by: OrderByClause | None = ...,
|
|
1550
|
+
order_joins: list[Any] | None = ...,
|
|
1496
1551
|
page: int = ...,
|
|
1497
1552
|
cursor: str | None = ...,
|
|
1498
1553
|
items_per_page: int = ...,
|
|
@@ -1500,6 +1555,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1500
1555
|
search: str | SearchConfig | None = ...,
|
|
1501
1556
|
search_fields: Sequence[SearchFieldType] | None = ...,
|
|
1502
1557
|
search_column: str | None = ...,
|
|
1558
|
+
order_fields: Sequence[OrderFieldType] | None = ...,
|
|
1503
1559
|
facet_fields: Sequence[FacetFieldType] | None = ...,
|
|
1504
1560
|
filter_by: dict[str, Any] | BaseModel | None = ...,
|
|
1505
1561
|
schema: type[BaseModel],
|
|
@@ -1517,6 +1573,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1517
1573
|
outer_join: bool = ...,
|
|
1518
1574
|
load_options: Sequence[ExecutableOption] | None = ...,
|
|
1519
1575
|
order_by: OrderByClause | None = ...,
|
|
1576
|
+
order_joins: list[Any] | None = ...,
|
|
1520
1577
|
page: int = ...,
|
|
1521
1578
|
cursor: str | None = ...,
|
|
1522
1579
|
items_per_page: int = ...,
|
|
@@ -1524,6 +1581,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1524
1581
|
search: str | SearchConfig | None = ...,
|
|
1525
1582
|
search_fields: Sequence[SearchFieldType] | None = ...,
|
|
1526
1583
|
search_column: str | None = ...,
|
|
1584
|
+
order_fields: Sequence[OrderFieldType] | None = ...,
|
|
1527
1585
|
facet_fields: Sequence[FacetFieldType] | None = ...,
|
|
1528
1586
|
filter_by: dict[str, Any] | BaseModel | None = ...,
|
|
1529
1587
|
schema: type[BaseModel],
|
|
@@ -1540,6 +1598,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1540
1598
|
outer_join: bool = False,
|
|
1541
1599
|
load_options: Sequence[ExecutableOption] | None = None,
|
|
1542
1600
|
order_by: OrderByClause | None = None,
|
|
1601
|
+
order_joins: list[Any] | None = None,
|
|
1543
1602
|
page: int = 1,
|
|
1544
1603
|
cursor: str | None = None,
|
|
1545
1604
|
items_per_page: int = 20,
|
|
@@ -1547,6 +1606,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1547
1606
|
search: str | SearchConfig | None = None,
|
|
1548
1607
|
search_fields: Sequence[SearchFieldType] | None = None,
|
|
1549
1608
|
search_column: str | None = None,
|
|
1609
|
+
order_fields: Sequence[OrderFieldType] | None = None,
|
|
1550
1610
|
facet_fields: Sequence[FacetFieldType] | None = None,
|
|
1551
1611
|
filter_by: dict[str, Any] | BaseModel | None = None,
|
|
1552
1612
|
schema: type[BaseModel],
|
|
@@ -1575,6 +1635,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1575
1635
|
search: Search query string or :class:`.SearchConfig` object.
|
|
1576
1636
|
search_fields: Fields to search in (overrides class default).
|
|
1577
1637
|
search_column: Restrict search to a single column key.
|
|
1638
|
+
order_fields: Fields allowed for sorting (overrides class default).
|
|
1578
1639
|
facet_fields: Columns to compute distinct values for (overrides
|
|
1579
1640
|
class default).
|
|
1580
1641
|
filter_by: Dict of ``{column_key: value}`` to filter by declared
|
|
@@ -1600,10 +1661,12 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1600
1661
|
outer_join=outer_join,
|
|
1601
1662
|
load_options=load_options,
|
|
1602
1663
|
order_by=order_by,
|
|
1664
|
+
order_joins=order_joins,
|
|
1603
1665
|
items_per_page=items_per_page,
|
|
1604
1666
|
search=search,
|
|
1605
1667
|
search_fields=search_fields,
|
|
1606
1668
|
search_column=search_column,
|
|
1669
|
+
order_fields=order_fields,
|
|
1607
1670
|
facet_fields=facet_fields,
|
|
1608
1671
|
filter_by=filter_by,
|
|
1609
1672
|
schema=schema,
|
|
@@ -1618,12 +1681,14 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1618
1681
|
outer_join=outer_join,
|
|
1619
1682
|
load_options=load_options,
|
|
1620
1683
|
order_by=order_by,
|
|
1684
|
+
order_joins=order_joins,
|
|
1621
1685
|
page=page,
|
|
1622
1686
|
items_per_page=items_per_page,
|
|
1623
1687
|
include_total=include_total,
|
|
1624
1688
|
search=search,
|
|
1625
1689
|
search_fields=search_fields,
|
|
1626
1690
|
search_column=search_column,
|
|
1691
|
+
order_fields=order_fields,
|
|
1627
1692
|
facet_fields=facet_fields,
|
|
1628
1693
|
filter_by=filter_by,
|
|
1629
1694
|
schema=schema,
|
|
@@ -1638,7 +1703,7 @@ def CrudFactory(
|
|
|
1638
1703
|
base_class: type[AsyncCrud[Any]] = AsyncCrud,
|
|
1639
1704
|
searchable_fields: Sequence[SearchFieldType] | None = None,
|
|
1640
1705
|
facet_fields: Sequence[FacetFieldType] | None = None,
|
|
1641
|
-
order_fields: Sequence[
|
|
1706
|
+
order_fields: Sequence[OrderFieldType] | None = None,
|
|
1642
1707
|
m2m_fields: M2MFieldType | None = None,
|
|
1643
1708
|
default_load_options: Sequence[ExecutableOption] | None = None,
|
|
1644
1709
|
cursor_column: Any | None = None,
|
|
@@ -278,6 +278,18 @@ _EQUALITY_TYPES = (String, Integer, Numeric, Date, DateTime, Time, Enum, Uuid)
|
|
|
278
278
|
"""Column types that support equality / IN filtering in build_filter_by."""
|
|
279
279
|
|
|
280
280
|
|
|
281
|
+
def _coerce_bool(value: Any) -> bool:
|
|
282
|
+
"""Coerce a string value to a Python bool for Boolean column filtering."""
|
|
283
|
+
if isinstance(value, bool):
|
|
284
|
+
return value
|
|
285
|
+
if isinstance(value, str):
|
|
286
|
+
if value.lower() == "true":
|
|
287
|
+
return True
|
|
288
|
+
if value.lower() == "false":
|
|
289
|
+
return False
|
|
290
|
+
raise ValueError(f"Cannot coerce {value!r} to bool")
|
|
291
|
+
|
|
292
|
+
|
|
281
293
|
def build_filter_by(
|
|
282
294
|
filter_by: dict[str, Any],
|
|
283
295
|
facet_fields: Sequence[FacetFieldType],
|
|
@@ -324,16 +336,17 @@ def build_filter_by(
|
|
|
324
336
|
added_join_keys.add(rel_key)
|
|
325
337
|
|
|
326
338
|
col_type = column.property.columns[0].type
|
|
327
|
-
if isinstance(col_type,
|
|
339
|
+
if isinstance(col_type, Boolean):
|
|
340
|
+
coerce = _coerce_bool
|
|
328
341
|
if isinstance(value, list):
|
|
329
|
-
filters.append(column.
|
|
342
|
+
filters.append(column.in_([coerce(v) for v in value]))
|
|
330
343
|
else:
|
|
331
|
-
filters.append(column
|
|
332
|
-
elif isinstance(col_type,
|
|
344
|
+
filters.append(column == coerce(value))
|
|
345
|
+
elif isinstance(col_type, ARRAY):
|
|
333
346
|
if isinstance(value, list):
|
|
334
|
-
filters.append(column.
|
|
347
|
+
filters.append(column.overlap(value))
|
|
335
348
|
else:
|
|
336
|
-
filters.append(column.
|
|
349
|
+
filters.append(column.any(value))
|
|
337
350
|
elif isinstance(col_type, _EQUALITY_TYPES):
|
|
338
351
|
if isinstance(value, list):
|
|
339
352
|
filters.append(column.in_(value))
|
|
@@ -163,6 +163,7 @@ class PaginatedResponse(BaseResponse, Generic[DataT]):
|
|
|
163
163
|
pagination_type: PaginationType | None = None
|
|
164
164
|
filter_attributes: dict[str, list[Any]] | None = None
|
|
165
165
|
search_columns: list[str] | None = None
|
|
166
|
+
order_columns: list[str] | None = None
|
|
166
167
|
|
|
167
168
|
_discriminated_union_cache: ClassVar[dict[Any, Any]] = {}
|
|
168
169
|
|
|
@@ -15,13 +15,14 @@ ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
|
|
15
15
|
SchemaType = TypeVar("SchemaType", bound=BaseModel)
|
|
16
16
|
|
|
17
17
|
# CRUD type aliases
|
|
18
|
-
JoinType = list[tuple[type[DeclarativeBase], Any]]
|
|
18
|
+
JoinType = list[tuple[type[DeclarativeBase] | Any, Any]]
|
|
19
19
|
M2MFieldType = Mapping[str, QueryableAttribute[Any]]
|
|
20
20
|
OrderByClause = ColumnElement[Any] | QueryableAttribute[Any]
|
|
21
21
|
|
|
22
|
-
# Search / facet type aliases
|
|
22
|
+
# Search / facet / order type aliases
|
|
23
23
|
SearchFieldType = InstrumentedAttribute[Any] | tuple[InstrumentedAttribute[Any], ...]
|
|
24
24
|
FacetFieldType = SearchFieldType
|
|
25
|
+
OrderFieldType = SearchFieldType
|
|
25
26
|
|
|
26
27
|
# Dependency type aliases
|
|
27
28
|
SessionDependency = Callable[[], AsyncGenerator[AsyncSession, None]] | Any
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/cli/commands/__init__.py
RENAMED
|
File without changes
|
{fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/cli/commands/fixtures.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/exceptions/__init__.py
RENAMED
|
File without changes
|
{fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/exceptions/exceptions.py
RENAMED
|
File without changes
|
{fastapi_toolsets-3.0.0 → fastapi_toolsets-3.0.2}/src/fastapi_toolsets/exceptions/handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|