fastapi-toolsets 2.1.0__tar.gz → 2.2.1__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-2.1.0 → fastapi_toolsets-2.2.1}/PKG-INFO +1 -1
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/pyproject.toml +1 -1
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/__init__.py +1 -1
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/crud/factory.py +157 -24
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/LICENSE +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/README.md +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/_imports.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/cli/__init__.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/cli/app.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/cli/commands/__init__.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/cli/commands/fixtures.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/cli/config.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/cli/pyproject.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/cli/utils.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/crud/__init__.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/crud/search.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/db.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/dependencies.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/exceptions/__init__.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/exceptions/exceptions.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/exceptions/handler.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/fixtures/__init__.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/fixtures/enum.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/fixtures/registry.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/fixtures/utils.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/logger.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/metrics/__init__.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/metrics/handler.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/metrics/registry.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/models.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/py.typed +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/pytest/__init__.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/pytest/plugin.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/pytest/utils.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/schemas.py +0 -0
- {fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/types.py +0 -0
|
@@ -14,7 +14,6 @@ from typing import Any, ClassVar, Generic, Literal, Self, cast, overload
|
|
|
14
14
|
from fastapi import Query
|
|
15
15
|
from pydantic import BaseModel
|
|
16
16
|
from sqlalchemy import Date, DateTime, Float, Integer, Numeric, Uuid, and_, func, select
|
|
17
|
-
from sqlalchemy import delete as sql_delete
|
|
18
17
|
from sqlalchemy.dialects.postgresql import insert
|
|
19
18
|
from sqlalchemy.exc import NoResultFound
|
|
20
19
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
@@ -80,13 +79,13 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
80
79
|
facet_fields: ClassVar[Sequence[FacetFieldType] | None] = None
|
|
81
80
|
order_fields: ClassVar[Sequence[QueryableAttribute[Any]] | None] = None
|
|
82
81
|
m2m_fields: ClassVar[M2MFieldType | None] = None
|
|
83
|
-
default_load_options: ClassVar[
|
|
82
|
+
default_load_options: ClassVar[Sequence[ExecutableOption] | None] = None
|
|
84
83
|
cursor_column: ClassVar[Any | None] = None
|
|
85
84
|
|
|
86
85
|
@classmethod
|
|
87
86
|
def _resolve_load_options(
|
|
88
|
-
cls, load_options:
|
|
89
|
-
) ->
|
|
87
|
+
cls, load_options: Sequence[ExecutableOption] | None
|
|
88
|
+
) -> Sequence[ExecutableOption] | None:
|
|
90
89
|
"""Return load_options if provided, else fall back to default_load_options."""
|
|
91
90
|
if load_options is not None:
|
|
92
91
|
return load_options
|
|
@@ -361,7 +360,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
361
360
|
joins: JoinType | None = None,
|
|
362
361
|
outer_join: bool = False,
|
|
363
362
|
with_for_update: bool = False,
|
|
364
|
-
load_options:
|
|
363
|
+
load_options: Sequence[ExecutableOption] | None = None,
|
|
365
364
|
schema: type[SchemaType],
|
|
366
365
|
) -> Response[SchemaType]: ...
|
|
367
366
|
|
|
@@ -375,7 +374,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
375
374
|
joins: JoinType | None = None,
|
|
376
375
|
outer_join: bool = False,
|
|
377
376
|
with_for_update: bool = False,
|
|
378
|
-
load_options:
|
|
377
|
+
load_options: Sequence[ExecutableOption] | None = None,
|
|
379
378
|
schema: None = ...,
|
|
380
379
|
) -> ModelType: ...
|
|
381
380
|
|
|
@@ -388,7 +387,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
388
387
|
joins: JoinType | None = None,
|
|
389
388
|
outer_join: bool = False,
|
|
390
389
|
with_for_update: bool = False,
|
|
391
|
-
load_options:
|
|
390
|
+
load_options: Sequence[ExecutableOption] | None = None,
|
|
392
391
|
schema: type[BaseModel] | None = None,
|
|
393
392
|
) -> ModelType | Response[Any]:
|
|
394
393
|
"""Get exactly one record. Raises NotFoundError if not found.
|
|
@@ -410,6 +409,82 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
410
409
|
NotFoundError: If no record found
|
|
411
410
|
MultipleResultsFound: If more than one record found
|
|
412
411
|
"""
|
|
412
|
+
result = await cls.get_or_none(
|
|
413
|
+
session,
|
|
414
|
+
filters,
|
|
415
|
+
joins=joins,
|
|
416
|
+
outer_join=outer_join,
|
|
417
|
+
with_for_update=with_for_update,
|
|
418
|
+
load_options=load_options,
|
|
419
|
+
schema=schema,
|
|
420
|
+
)
|
|
421
|
+
if result is None:
|
|
422
|
+
raise NotFoundError()
|
|
423
|
+
return result
|
|
424
|
+
|
|
425
|
+
@overload
|
|
426
|
+
@classmethod
|
|
427
|
+
async def get_or_none( # pragma: no cover
|
|
428
|
+
cls: type[Self],
|
|
429
|
+
session: AsyncSession,
|
|
430
|
+
filters: list[Any],
|
|
431
|
+
*,
|
|
432
|
+
joins: JoinType | None = None,
|
|
433
|
+
outer_join: bool = False,
|
|
434
|
+
with_for_update: bool = False,
|
|
435
|
+
load_options: Sequence[ExecutableOption] | None = None,
|
|
436
|
+
schema: type[SchemaType],
|
|
437
|
+
) -> Response[SchemaType] | None: ...
|
|
438
|
+
|
|
439
|
+
@overload
|
|
440
|
+
@classmethod
|
|
441
|
+
async def get_or_none( # pragma: no cover
|
|
442
|
+
cls: type[Self],
|
|
443
|
+
session: AsyncSession,
|
|
444
|
+
filters: list[Any],
|
|
445
|
+
*,
|
|
446
|
+
joins: JoinType | None = None,
|
|
447
|
+
outer_join: bool = False,
|
|
448
|
+
with_for_update: bool = False,
|
|
449
|
+
load_options: Sequence[ExecutableOption] | None = None,
|
|
450
|
+
schema: None = ...,
|
|
451
|
+
) -> ModelType | None: ...
|
|
452
|
+
|
|
453
|
+
@classmethod
|
|
454
|
+
async def get_or_none(
|
|
455
|
+
cls: type[Self],
|
|
456
|
+
session: AsyncSession,
|
|
457
|
+
filters: list[Any],
|
|
458
|
+
*,
|
|
459
|
+
joins: JoinType | None = None,
|
|
460
|
+
outer_join: bool = False,
|
|
461
|
+
with_for_update: bool = False,
|
|
462
|
+
load_options: Sequence[ExecutableOption] | None = None,
|
|
463
|
+
schema: type[BaseModel] | None = None,
|
|
464
|
+
) -> ModelType | Response[Any] | None:
|
|
465
|
+
"""Get exactly one record, or ``None`` if not found.
|
|
466
|
+
|
|
467
|
+
Like :meth:`get` but returns ``None`` instead of raising
|
|
468
|
+
:class:`~fastapi_toolsets.exceptions.NotFoundError` when no record
|
|
469
|
+
matches the filters.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
session: DB async session
|
|
473
|
+
filters: List of SQLAlchemy filter conditions
|
|
474
|
+
joins: List of (model, condition) tuples for joining related tables
|
|
475
|
+
outer_join: Use LEFT OUTER JOIN instead of INNER JOIN
|
|
476
|
+
with_for_update: Lock the row for update
|
|
477
|
+
load_options: SQLAlchemy loader options (e.g., selectinload)
|
|
478
|
+
schema: Pydantic schema to serialize the result into. When provided,
|
|
479
|
+
the result is automatically wrapped in a ``Response[schema]``.
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
Model instance, ``Response[schema]`` when ``schema`` is given,
|
|
483
|
+
or ``None`` when no record matches.
|
|
484
|
+
|
|
485
|
+
Raises:
|
|
486
|
+
MultipleResultsFound: If more than one record found
|
|
487
|
+
"""
|
|
413
488
|
q = select(cls.model)
|
|
414
489
|
q = _apply_joins(q, joins, outer_join)
|
|
415
490
|
q = q.where(and_(*filters))
|
|
@@ -419,12 +494,40 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
419
494
|
q = q.with_for_update()
|
|
420
495
|
result = await session.execute(q)
|
|
421
496
|
item = result.unique().scalar_one_or_none()
|
|
422
|
-
if
|
|
423
|
-
|
|
424
|
-
|
|
497
|
+
if item is None:
|
|
498
|
+
return None
|
|
499
|
+
db_model = cast(ModelType, item)
|
|
425
500
|
if schema:
|
|
426
|
-
return Response(data=schema.model_validate(
|
|
427
|
-
return
|
|
501
|
+
return Response(data=schema.model_validate(db_model))
|
|
502
|
+
return db_model
|
|
503
|
+
|
|
504
|
+
@overload
|
|
505
|
+
@classmethod
|
|
506
|
+
async def first( # pragma: no cover
|
|
507
|
+
cls: type[Self],
|
|
508
|
+
session: AsyncSession,
|
|
509
|
+
filters: list[Any] | None = None,
|
|
510
|
+
*,
|
|
511
|
+
joins: JoinType | None = None,
|
|
512
|
+
outer_join: bool = False,
|
|
513
|
+
with_for_update: bool = False,
|
|
514
|
+
load_options: Sequence[ExecutableOption] | None = None,
|
|
515
|
+
schema: type[SchemaType],
|
|
516
|
+
) -> Response[SchemaType] | None: ...
|
|
517
|
+
|
|
518
|
+
@overload
|
|
519
|
+
@classmethod
|
|
520
|
+
async def first( # pragma: no cover
|
|
521
|
+
cls: type[Self],
|
|
522
|
+
session: AsyncSession,
|
|
523
|
+
filters: list[Any] | None = None,
|
|
524
|
+
*,
|
|
525
|
+
joins: JoinType | None = None,
|
|
526
|
+
outer_join: bool = False,
|
|
527
|
+
with_for_update: bool = False,
|
|
528
|
+
load_options: Sequence[ExecutableOption] | None = None,
|
|
529
|
+
schema: None = ...,
|
|
530
|
+
) -> ModelType | None: ...
|
|
428
531
|
|
|
429
532
|
@classmethod
|
|
430
533
|
async def first(
|
|
@@ -434,8 +537,10 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
434
537
|
*,
|
|
435
538
|
joins: JoinType | None = None,
|
|
436
539
|
outer_join: bool = False,
|
|
437
|
-
|
|
438
|
-
|
|
540
|
+
with_for_update: bool = False,
|
|
541
|
+
load_options: Sequence[ExecutableOption] | None = None,
|
|
542
|
+
schema: type[BaseModel] | None = None,
|
|
543
|
+
) -> ModelType | Response[Any] | None:
|
|
439
544
|
"""Get the first matching record, or None.
|
|
440
545
|
|
|
441
546
|
Args:
|
|
@@ -443,10 +548,14 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
443
548
|
filters: List of SQLAlchemy filter conditions
|
|
444
549
|
joins: List of (model, condition) tuples for joining related tables
|
|
445
550
|
outer_join: Use LEFT OUTER JOIN instead of INNER JOIN
|
|
446
|
-
|
|
551
|
+
with_for_update: Lock the row for update
|
|
552
|
+
load_options: SQLAlchemy loader options (e.g., selectinload)
|
|
553
|
+
schema: Pydantic schema to serialize the result into. When provided,
|
|
554
|
+
the result is automatically wrapped in a ``Response[schema]``.
|
|
447
555
|
|
|
448
556
|
Returns:
|
|
449
|
-
Model instance
|
|
557
|
+
Model instance, ``Response[schema]`` when ``schema`` is given,
|
|
558
|
+
or ``None`` when no record matches.
|
|
450
559
|
"""
|
|
451
560
|
q = select(cls.model)
|
|
452
561
|
q = _apply_joins(q, joins, outer_join)
|
|
@@ -454,8 +563,16 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
454
563
|
q = q.where(and_(*filters))
|
|
455
564
|
if resolved := cls._resolve_load_options(load_options):
|
|
456
565
|
q = q.options(*resolved)
|
|
566
|
+
if with_for_update:
|
|
567
|
+
q = q.with_for_update()
|
|
457
568
|
result = await session.execute(q)
|
|
458
|
-
|
|
569
|
+
item = result.unique().scalars().first()
|
|
570
|
+
if item is None:
|
|
571
|
+
return None
|
|
572
|
+
db_model = cast(ModelType, item)
|
|
573
|
+
if schema:
|
|
574
|
+
return Response(data=schema.model_validate(db_model))
|
|
575
|
+
return db_model
|
|
459
576
|
|
|
460
577
|
@classmethod
|
|
461
578
|
async def get_multi(
|
|
@@ -465,7 +582,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
465
582
|
filters: list[Any] | None = None,
|
|
466
583
|
joins: JoinType | None = None,
|
|
467
584
|
outer_join: bool = False,
|
|
468
|
-
load_options:
|
|
585
|
+
load_options: Sequence[ExecutableOption] | None = None,
|
|
469
586
|
order_by: OrderByClause | None = None,
|
|
470
587
|
limit: int | None = None,
|
|
471
588
|
offset: int | None = None,
|
|
@@ -674,8 +791,10 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
674
791
|
``None``, or ``Response[None]`` when ``return_response=True``.
|
|
675
792
|
"""
|
|
676
793
|
async with get_transaction(session):
|
|
677
|
-
|
|
678
|
-
|
|
794
|
+
result = await session.execute(select(cls.model).where(and_(*filters)))
|
|
795
|
+
objects = result.scalars().all()
|
|
796
|
+
for obj in objects:
|
|
797
|
+
await session.delete(obj)
|
|
679
798
|
if return_response:
|
|
680
799
|
return Response(data=None)
|
|
681
800
|
return None
|
|
@@ -741,7 +860,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
741
860
|
filters: list[Any] | None = None,
|
|
742
861
|
joins: JoinType | None = None,
|
|
743
862
|
outer_join: bool = False,
|
|
744
|
-
load_options:
|
|
863
|
+
load_options: Sequence[ExecutableOption] | None = None,
|
|
745
864
|
order_by: OrderByClause | None = None,
|
|
746
865
|
page: int = 1,
|
|
747
866
|
items_per_page: int = 20,
|
|
@@ -852,7 +971,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
852
971
|
filters: list[Any] | None = None,
|
|
853
972
|
joins: JoinType | None = None,
|
|
854
973
|
outer_join: bool = False,
|
|
855
|
-
load_options:
|
|
974
|
+
load_options: Sequence[ExecutableOption] | None = None,
|
|
856
975
|
order_by: OrderByClause | None = None,
|
|
857
976
|
items_per_page: int = 20,
|
|
858
977
|
search: str | SearchConfig | None = None,
|
|
@@ -993,7 +1112,7 @@ def CrudFactory(
|
|
|
993
1112
|
facet_fields: Sequence[FacetFieldType] | None = None,
|
|
994
1113
|
order_fields: Sequence[QueryableAttribute[Any]] | None = None,
|
|
995
1114
|
m2m_fields: M2MFieldType | None = None,
|
|
996
|
-
default_load_options:
|
|
1115
|
+
default_load_options: Sequence[ExecutableOption] | None = None,
|
|
997
1116
|
cursor_column: Any | None = None,
|
|
998
1117
|
) -> type[AsyncCrud[ModelType]]:
|
|
999
1118
|
"""Create a CRUD class for a specific model.
|
|
@@ -1092,12 +1211,26 @@ def CrudFactory(
|
|
|
1092
1211
|
)
|
|
1093
1212
|
```
|
|
1094
1213
|
"""
|
|
1214
|
+
pk_key = model.__mapper__.primary_key[0].key
|
|
1215
|
+
assert pk_key is not None
|
|
1216
|
+
pk_col = getattr(model, pk_key)
|
|
1217
|
+
|
|
1218
|
+
if searchable_fields is None:
|
|
1219
|
+
effective_searchable = [pk_col]
|
|
1220
|
+
else:
|
|
1221
|
+
existing_keys = {f.key for f in searchable_fields if not isinstance(f, tuple)}
|
|
1222
|
+
effective_searchable = (
|
|
1223
|
+
[pk_col, *searchable_fields]
|
|
1224
|
+
if pk_key not in existing_keys
|
|
1225
|
+
else list(searchable_fields)
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1095
1228
|
cls = type(
|
|
1096
1229
|
f"Async{model.__name__}Crud",
|
|
1097
1230
|
(AsyncCrud,),
|
|
1098
1231
|
{
|
|
1099
1232
|
"model": model,
|
|
1100
|
-
"searchable_fields":
|
|
1233
|
+
"searchable_fields": effective_searchable,
|
|
1101
1234
|
"facet_fields": facet_fields,
|
|
1102
1235
|
"order_fields": order_fields,
|
|
1103
1236
|
"m2m_fields": m2m_fields,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/cli/commands/__init__.py
RENAMED
|
File without changes
|
{fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/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
|
|
File without changes
|
|
File without changes
|
{fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/exceptions/__init__.py
RENAMED
|
File without changes
|
{fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/src/fastapi_toolsets/exceptions/exceptions.py
RENAMED
|
File without changes
|
{fastapi_toolsets-2.1.0 → fastapi_toolsets-2.2.1}/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
|