fastapi-toolsets 5.0.0b2__tar.gz → 5.1.0__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-5.0.0b2 → fastapi_toolsets-5.1.0}/PKG-INFO +2 -7
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/README.md +0 -2
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/pyproject.toml +14 -6
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/__init__.py +1 -1
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/cli/commands/fixtures.py +1 -1
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/crud/factory.py +109 -46
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/crud/search.py +87 -60
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/db/core.py +2 -3
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/db/m2m.py +1 -3
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/dependencies.py +25 -33
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/exceptions/handler.py +25 -26
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/fixtures/__init__.py +6 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/fixtures/utils.py +9 -7
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/models/watched.py +46 -37
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/schemas.py +4 -4
- fastapi_toolsets-5.0.0b2/src/fastapi_toolsets/security/__init__.py +0 -26
- fastapi_toolsets-5.0.0b2/src/fastapi_toolsets/security/abc.py +0 -55
- fastapi_toolsets-5.0.0b2/src/fastapi_toolsets/security/oauth.py +0 -197
- fastapi_toolsets-5.0.0b2/src/fastapi_toolsets/security/sources/__init__.py +0 -8
- fastapi_toolsets-5.0.0b2/src/fastapi_toolsets/security/sources/bearer.py +0 -120
- fastapi_toolsets-5.0.0b2/src/fastapi_toolsets/security/sources/cookie.py +0 -148
- fastapi_toolsets-5.0.0b2/src/fastapi_toolsets/security/sources/header.py +0 -67
- fastapi_toolsets-5.0.0b2/src/fastapi_toolsets/security/sources/multi.py +0 -71
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/LICENSE +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/_imports.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/cli/__init__.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/cli/app.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/cli/commands/__init__.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/cli/config.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/cli/pyproject.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/cli/utils.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/crud/__init__.py +1 -1
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/db/__init__.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/db/locks.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/db/testing.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/db/watch.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/exceptions/__init__.py +2 -2
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/exceptions/exceptions.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/fixtures/enum.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/fixtures/registry.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/logger.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/metrics/__init__.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/metrics/handler.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/metrics/registry.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/models/__init__.py +3 -3
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/models/columns.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/py.typed +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/pytest/__init__.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/pytest/plugin.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/pytest/utils.py +0 -0
- {fastapi_toolsets-5.0.0b2 → fastapi_toolsets-5.1.0}/src/fastapi_toolsets/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-toolsets
|
|
3
|
-
Version: 5.0
|
|
3
|
+
Version: 5.1.0
|
|
4
4
|
Summary: Production-ready utilities for FastAPI applications
|
|
5
5
|
Keywords: fastapi,sqlalchemy,postgresql
|
|
6
6
|
Author: d3vyce
|
|
@@ -29,14 +29,12 @@ Requires-Dist: asyncpg>=0.29.0
|
|
|
29
29
|
Requires-Dist: fastapi>=0.100.0
|
|
30
30
|
Requires-Dist: pydantic>=2.0
|
|
31
31
|
Requires-Dist: sqlalchemy[asyncio]>=2.0
|
|
32
|
-
Requires-Dist: fastapi-toolsets[cli,metrics,pytest
|
|
32
|
+
Requires-Dist: fastapi-toolsets[cli,metrics,pytest] ; extra == 'all'
|
|
33
33
|
Requires-Dist: typer>=0.9.0 ; extra == 'cli'
|
|
34
34
|
Requires-Dist: prometheus-client>=0.20.0 ; extra == 'metrics'
|
|
35
35
|
Requires-Dist: httpx>=0.25.0 ; extra == 'pytest'
|
|
36
36
|
Requires-Dist: pytest-xdist>=3.0.0 ; extra == 'pytest'
|
|
37
37
|
Requires-Dist: pytest>=8.0.0 ; extra == 'pytest'
|
|
38
|
-
Requires-Dist: async-lru>=1.0 ; extra == 'security'
|
|
39
|
-
Requires-Dist: httpx>=0.25.0 ; extra == 'security'
|
|
40
38
|
Requires-Python: >=3.11
|
|
41
39
|
Project-URL: Homepage, https://github.com/d3vyce/fastapi-toolsets
|
|
42
40
|
Project-URL: Documentation, https://fastapi-toolsets.d3vyce.fr/
|
|
@@ -46,7 +44,6 @@ Provides-Extra: all
|
|
|
46
44
|
Provides-Extra: cli
|
|
47
45
|
Provides-Extra: metrics
|
|
48
46
|
Provides-Extra: pytest
|
|
49
|
-
Provides-Extra: security
|
|
50
47
|
Description-Content-Type: text/markdown
|
|
51
48
|
|
|
52
49
|
# FastAPI Toolsets
|
|
@@ -82,7 +79,6 @@ Install only the extras you need:
|
|
|
82
79
|
```bash
|
|
83
80
|
uv add "fastapi-toolsets[cli]"
|
|
84
81
|
uv add "fastapi-toolsets[metrics]"
|
|
85
|
-
uv add "fastapi-toolsets[security]"
|
|
86
82
|
uv add "fastapi-toolsets[pytest]"
|
|
87
83
|
```
|
|
88
84
|
|
|
@@ -108,7 +104,6 @@ uv add "fastapi-toolsets[all]"
|
|
|
108
104
|
|
|
109
105
|
### Optional
|
|
110
106
|
|
|
111
|
-
- **Security**: Composable authentication sources (`BearerTokenAuth`, `CookieAuth`, `APIKeyHeaderAuth`, `MultiAuth`) with HMAC-signed cookies and OAuth 2.0 / OIDC helpers
|
|
112
107
|
- **CLI**: Django-like command-line interface with fixture management and custom commands support
|
|
113
108
|
- **Metrics**: Prometheus metrics endpoint with provider/collector registry
|
|
114
109
|
- **Pytest Helpers**: Async test client, database session management, `pytest-xdist` support, and table cleanup utilities
|
|
@@ -31,7 +31,6 @@ Install only the extras you need:
|
|
|
31
31
|
```bash
|
|
32
32
|
uv add "fastapi-toolsets[cli]"
|
|
33
33
|
uv add "fastapi-toolsets[metrics]"
|
|
34
|
-
uv add "fastapi-toolsets[security]"
|
|
35
34
|
uv add "fastapi-toolsets[pytest]"
|
|
36
35
|
```
|
|
37
36
|
|
|
@@ -57,7 +56,6 @@ uv add "fastapi-toolsets[all]"
|
|
|
57
56
|
|
|
58
57
|
### Optional
|
|
59
58
|
|
|
60
|
-
- **Security**: Composable authentication sources (`BearerTokenAuth`, `CookieAuth`, `APIKeyHeaderAuth`, `MultiAuth`) with HMAC-signed cookies and OAuth 2.0 / OIDC helpers
|
|
61
59
|
- **CLI**: Django-like command-line interface with fixture management and custom commands support
|
|
62
60
|
- **Metrics**: Prometheus metrics endpoint with provider/collector registry
|
|
63
61
|
- **Pytest Helpers**: Async test client, database session management, `pytest-xdist` support, and table cleanup utilities
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fastapi-toolsets"
|
|
3
|
-
version = "5.0
|
|
3
|
+
version = "5.1.0"
|
|
4
4
|
description = "Production-ready utilities for FastAPI applications"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -50,17 +50,13 @@ cli = [
|
|
|
50
50
|
metrics = [
|
|
51
51
|
"prometheus_client>=0.20.0",
|
|
52
52
|
]
|
|
53
|
-
security = [
|
|
54
|
-
"async-lru>=1.0",
|
|
55
|
-
"httpx>=0.25.0",
|
|
56
|
-
]
|
|
57
53
|
pytest = [
|
|
58
54
|
"httpx>=0.25.0",
|
|
59
55
|
"pytest-xdist>=3.0.0",
|
|
60
56
|
"pytest>=8.0.0",
|
|
61
57
|
]
|
|
62
58
|
all = [
|
|
63
|
-
"fastapi-toolsets[cli,metrics,pytest
|
|
59
|
+
"fastapi-toolsets[cli,metrics,pytest]",
|
|
64
60
|
]
|
|
65
61
|
|
|
66
62
|
[project.scripts]
|
|
@@ -98,6 +94,18 @@ docs-src = [
|
|
|
98
94
|
requires = ["uv_build>=0.10,<0.12.0"]
|
|
99
95
|
build-backend = "uv_build"
|
|
100
96
|
|
|
97
|
+
[tool.ruff.format]
|
|
98
|
+
exclude = ["*.md"]
|
|
99
|
+
|
|
100
|
+
[tool.ruff.lint]
|
|
101
|
+
extend-select = ["E712"]
|
|
102
|
+
|
|
103
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
104
|
+
extend-immutable-calls = ["fastapi.Depends"]
|
|
105
|
+
|
|
106
|
+
[tool.ruff.lint.per-file-ignores]
|
|
107
|
+
"tests/**" = ["RUF012", "RUF059", "SIM117", "DTZ001", "S110", "BLE001"]
|
|
108
|
+
|
|
101
109
|
[tool.pytest.ini_options]
|
|
102
110
|
testpaths = ["tests"]
|
|
103
111
|
filterwarnings = [
|
|
@@ -44,6 +44,7 @@ from ..types import (
|
|
|
44
44
|
)
|
|
45
45
|
from .search import (
|
|
46
46
|
SearchConfig,
|
|
47
|
+
apply_search_joins,
|
|
47
48
|
build_facets,
|
|
48
49
|
build_filter_by,
|
|
49
50
|
build_search_filters,
|
|
@@ -51,7 +52,6 @@ from .search import (
|
|
|
51
52
|
search_field_keys,
|
|
52
53
|
)
|
|
53
54
|
|
|
54
|
-
|
|
55
55
|
_ForUpdateMode: TypeAlias = bool | Literal["nowait", "skip_locked"]
|
|
56
56
|
|
|
57
57
|
|
|
@@ -128,17 +128,6 @@ def _apply_joins(q: Any, joins: JoinType | None, outer_join: bool) -> Any:
|
|
|
128
128
|
return q
|
|
129
129
|
|
|
130
130
|
|
|
131
|
-
def _apply_search_joins(q: Any, search_joins: list[Any]) -> Any:
|
|
132
|
-
"""Apply relationship-based outer joins (from search/filter_by) to a query."""
|
|
133
|
-
seen: set[str] = set()
|
|
134
|
-
for join_rel in search_joins:
|
|
135
|
-
key = str(join_rel)
|
|
136
|
-
if key not in seen:
|
|
137
|
-
seen.add(key)
|
|
138
|
-
q = q.outerjoin(join_rel)
|
|
139
|
-
return q
|
|
140
|
-
|
|
141
|
-
|
|
142
131
|
class AsyncCrud(Generic[ModelType]):
|
|
143
132
|
"""Generic async CRUD operations for SQLAlchemy models.
|
|
144
133
|
|
|
@@ -184,16 +173,30 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
184
173
|
return cls.default_load_options
|
|
185
174
|
|
|
186
175
|
@classmethod
|
|
187
|
-
|
|
188
|
-
|
|
176
|
+
def _capture_pk_values(cls: type[Self], instance: ModelType) -> dict[str, Any]:
|
|
177
|
+
"""Capture PK values off instance — call before commit expires attributes."""
|
|
178
|
+
return {
|
|
179
|
+
cast(str, col.key): getattr(instance, cast(str, col.key))
|
|
180
|
+
for col in cls.model.__mapper__.primary_key
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
async def _reload_with_options_by_pk(
|
|
185
|
+
cls: type[Self], session: AsyncSession, pk_values: dict[str, Any]
|
|
189
186
|
) -> ModelType:
|
|
190
|
-
"""Re-query
|
|
191
|
-
|
|
187
|
+
"""Re-query by previously captured PK values, with default_load_options applied."""
|
|
188
|
+
# Only called when cls.default_load_options is set (see call sites).
|
|
192
189
|
pk_filters = [
|
|
193
|
-
getattr(cls.model,
|
|
194
|
-
for col in mapper.primary_key
|
|
190
|
+
getattr(cls.model, key) == value for key, value in pk_values.items()
|
|
195
191
|
]
|
|
196
|
-
|
|
192
|
+
q = select(cls.model).where(and_(*pk_filters))
|
|
193
|
+
q = q.execution_options(populate_existing=True)
|
|
194
|
+
q = q.options(*cast(Sequence[ExecutableOption], cls.default_load_options))
|
|
195
|
+
result = await session.execute(q)
|
|
196
|
+
item = result.unique().scalar_one_or_none()
|
|
197
|
+
if item is None: # pragma: no cover — row was just flushed in this transaction
|
|
198
|
+
raise NotFoundError()
|
|
199
|
+
return cast(ModelType, item)
|
|
197
200
|
|
|
198
201
|
@classmethod
|
|
199
202
|
async def _resolve_m2m(
|
|
@@ -264,12 +267,12 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
264
267
|
cls: type[Self],
|
|
265
268
|
filter_by: dict[str, Any] | BaseModel | None,
|
|
266
269
|
facet_fields: Sequence[FacetFieldType] | None,
|
|
267
|
-
) -> tuple[
|
|
268
|
-
"""Normalize filter_by and return (
|
|
270
|
+
) -> tuple[dict[str, Any], list[Any]]:
|
|
271
|
+
"""Normalize filter_by and return ({facet_key: filter}, joins) to apply to the query."""
|
|
269
272
|
if isinstance(filter_by, BaseModel):
|
|
270
273
|
filter_by = filter_by.model_dump(exclude_none=True)
|
|
271
274
|
if not filter_by:
|
|
272
|
-
return
|
|
275
|
+
return {}, []
|
|
273
276
|
resolved = cls._resolve_facet_fields(facet_fields)
|
|
274
277
|
return build_filter_by(filter_by, resolved or [])
|
|
275
278
|
|
|
@@ -280,8 +283,13 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
280
283
|
facet_fields: Sequence[FacetFieldType] | None,
|
|
281
284
|
filters: list[Any],
|
|
282
285
|
search_joins: list[Any],
|
|
286
|
+
*,
|
|
287
|
+
include_facets: bool = True,
|
|
288
|
+
own_filters: dict[str, Any] | None = None,
|
|
283
289
|
) -> dict[str, list[Any]] | None:
|
|
284
|
-
"""Build facet filter_attributes, or
|
|
290
|
+
"""Build facet filter_attributes, or None if disabled/no facet fields configured."""
|
|
291
|
+
if not include_facets:
|
|
292
|
+
return None
|
|
285
293
|
resolved = cls._resolve_facet_fields(facet_fields)
|
|
286
294
|
if not resolved:
|
|
287
295
|
return None
|
|
@@ -291,6 +299,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
291
299
|
resolved,
|
|
292
300
|
base_filters=filters,
|
|
293
301
|
base_joins=search_joins,
|
|
302
|
+
own_filters=own_filters,
|
|
294
303
|
)
|
|
295
304
|
|
|
296
305
|
@classmethod
|
|
@@ -474,6 +483,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
474
483
|
default_page_size: int = 20,
|
|
475
484
|
max_page_size: int = 100,
|
|
476
485
|
include_total: bool = True,
|
|
486
|
+
include_facets: bool = True,
|
|
477
487
|
search: bool = True,
|
|
478
488
|
filter: bool = True,
|
|
479
489
|
order: bool = True,
|
|
@@ -489,6 +499,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
489
499
|
default_page_size: Default ``items_per_page`` value.
|
|
490
500
|
max_page_size: Maximum ``items_per_page`` value.
|
|
491
501
|
include_total: Whether to include total count (not a query param).
|
|
502
|
+
include_facets: Whether to run facet queries (not a query param).
|
|
492
503
|
search: Enable search query parameters.
|
|
493
504
|
filter: Enable facet filter query parameters.
|
|
494
505
|
order: Enable order query parameters.
|
|
@@ -518,7 +529,10 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
518
529
|
]
|
|
519
530
|
return cls._build_paginate_params(
|
|
520
531
|
pagination_params=pagination_params,
|
|
521
|
-
pagination_fixed={
|
|
532
|
+
pagination_fixed={
|
|
533
|
+
"include_total": include_total,
|
|
534
|
+
"include_facets": include_facets,
|
|
535
|
+
},
|
|
522
536
|
dep_name=f"{cls.model.__name__}OffsetPaginateParams",
|
|
523
537
|
search=search,
|
|
524
538
|
filter=filter,
|
|
@@ -536,6 +550,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
536
550
|
*,
|
|
537
551
|
default_page_size: int = 20,
|
|
538
552
|
max_page_size: int = 100,
|
|
553
|
+
include_facets: bool = True,
|
|
539
554
|
search: bool = True,
|
|
540
555
|
filter: bool = True,
|
|
541
556
|
order: bool = True,
|
|
@@ -550,6 +565,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
550
565
|
Args:
|
|
551
566
|
default_page_size: Default ``items_per_page`` value.
|
|
552
567
|
max_page_size: Maximum ``items_per_page`` value.
|
|
568
|
+
include_facets: Whether to run facet queries (not a query param).
|
|
553
569
|
search: Enable search query parameters.
|
|
554
570
|
filter: Enable facet filter query parameters.
|
|
555
571
|
order: Enable order query parameters.
|
|
@@ -581,7 +597,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
581
597
|
]
|
|
582
598
|
return cls._build_paginate_params(
|
|
583
599
|
pagination_params=pagination_params,
|
|
584
|
-
pagination_fixed={},
|
|
600
|
+
pagination_fixed={"include_facets": include_facets},
|
|
585
601
|
dep_name=f"{cls.model.__name__}CursorPaginateParams",
|
|
586
602
|
search=search,
|
|
587
603
|
filter=filter,
|
|
@@ -601,6 +617,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
601
617
|
max_page_size: int = 100,
|
|
602
618
|
default_pagination_type: PaginationType = PaginationType.OFFSET,
|
|
603
619
|
include_total: bool = True,
|
|
620
|
+
include_facets: bool = True,
|
|
604
621
|
search: bool = True,
|
|
605
622
|
filter: bool = True,
|
|
606
623
|
order: bool = True,
|
|
@@ -617,6 +634,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
617
634
|
max_page_size: Maximum ``items_per_page`` value.
|
|
618
635
|
default_pagination_type: Default pagination strategy.
|
|
619
636
|
include_total: Whether to include total count (not a query param).
|
|
637
|
+
include_facets: Whether to run facet queries (not a query param).
|
|
620
638
|
search: Enable search query parameters.
|
|
621
639
|
filter: Enable facet filter query parameters.
|
|
622
640
|
order: Enable order query parameters.
|
|
@@ -665,7 +683,10 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
665
683
|
]
|
|
666
684
|
return cls._build_paginate_params(
|
|
667
685
|
pagination_params=pagination_params,
|
|
668
|
-
pagination_fixed={
|
|
686
|
+
pagination_fixed={
|
|
687
|
+
"include_total": include_total,
|
|
688
|
+
"include_facets": include_facets,
|
|
689
|
+
},
|
|
669
690
|
dep_name=f"{cls.model.__name__}PaginateParams",
|
|
670
691
|
search=search,
|
|
671
692
|
filter=filter,
|
|
@@ -729,9 +750,14 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
729
750
|
setattr(db_model, rel_attr, related_instances)
|
|
730
751
|
|
|
731
752
|
session.add(db_model)
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
753
|
+
pk_values: dict[str, Any] | None = None
|
|
754
|
+
if cls.default_load_options:
|
|
755
|
+
await session.flush()
|
|
756
|
+
pk_values = cls._capture_pk_values(db_model)
|
|
757
|
+
if pk_values is not None:
|
|
758
|
+
db_model = await cls._reload_with_options_by_pk(session, pk_values)
|
|
759
|
+
else:
|
|
760
|
+
await session.refresh(db_model)
|
|
735
761
|
result = cast(ModelType, db_model)
|
|
736
762
|
if schema:
|
|
737
763
|
return Response(data=schema.model_validate(result))
|
|
@@ -1096,9 +1122,15 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1096
1122
|
m2m_resolved = await cls._resolve_m2m(session, obj, only_set=True)
|
|
1097
1123
|
for rel_attr, related_instances in m2m_resolved.items():
|
|
1098
1124
|
setattr(db_model, rel_attr, related_instances)
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1125
|
+
|
|
1126
|
+
pk_values: dict[str, Any] | None = None
|
|
1127
|
+
if cls.default_load_options:
|
|
1128
|
+
await session.flush()
|
|
1129
|
+
pk_values = cls._capture_pk_values(db_model)
|
|
1130
|
+
if pk_values is not None:
|
|
1131
|
+
db_model = await cls._reload_with_options_by_pk(session, pk_values)
|
|
1132
|
+
else:
|
|
1133
|
+
await session.refresh(db_model)
|
|
1102
1134
|
if schema:
|
|
1103
1135
|
return Response(data=schema.model_validate(db_model))
|
|
1104
1136
|
return db_model
|
|
@@ -1270,6 +1302,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1270
1302
|
search_column: str | None = None,
|
|
1271
1303
|
order_fields: Sequence[OrderFieldType] | None = None,
|
|
1272
1304
|
facet_fields: Sequence[FacetFieldType] | None = None,
|
|
1305
|
+
include_facets: bool = True,
|
|
1273
1306
|
filter_by: dict[str, Any] | BaseModel | None = None,
|
|
1274
1307
|
schema: type[BaseModel],
|
|
1275
1308
|
) -> OffsetPaginatedResponse[Any]:
|
|
@@ -1291,6 +1324,9 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1291
1324
|
search_column: Restrict search to a single column key.
|
|
1292
1325
|
order_fields: Fields allowed for sorting (overrides class default).
|
|
1293
1326
|
facet_fields: Columns to compute distinct values for (overrides class default)
|
|
1327
|
+
include_facets: When ``False``, skip facet queries entirely;
|
|
1328
|
+
``filter_attributes`` will be ``None``. Useful on pages 2..N
|
|
1329
|
+
where the facet counts were already fetched on page 1.
|
|
1294
1330
|
filter_by: Dict of {column_key: value} to filter by declared facet fields.
|
|
1295
1331
|
Keys must match the column.key of a facet field. Scalar → equality,
|
|
1296
1332
|
list → IN clause. Raises InvalidFacetFilterError for unknown keys.
|
|
@@ -1303,7 +1339,6 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1303
1339
|
offset = (page - 1) * items_per_page
|
|
1304
1340
|
|
|
1305
1341
|
fb_filters, search_joins = cls._prepare_filter_by(filter_by, facet_fields)
|
|
1306
|
-
filters.extend(fb_filters)
|
|
1307
1342
|
|
|
1308
1343
|
# Build search filters
|
|
1309
1344
|
if search:
|
|
@@ -1317,6 +1352,11 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1317
1352
|
filters.extend(search_filters)
|
|
1318
1353
|
search_joins.extend(new_search_joins)
|
|
1319
1354
|
|
|
1355
|
+
# Facets combine these with each facet's own filter individually, so
|
|
1356
|
+
# fb_filters is applied to the query below but excluded here.
|
|
1357
|
+
facet_base_filters = list(filters)
|
|
1358
|
+
filters.extend(fb_filters.values())
|
|
1359
|
+
|
|
1320
1360
|
# Build query with joins
|
|
1321
1361
|
q = select(cls.model)
|
|
1322
1362
|
|
|
@@ -1324,11 +1364,11 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1324
1364
|
q = _apply_joins(q, joins, outer_join)
|
|
1325
1365
|
|
|
1326
1366
|
# Apply search joins (always outer joins for search)
|
|
1327
|
-
q =
|
|
1367
|
+
q = apply_search_joins(q, search_joins)
|
|
1328
1368
|
|
|
1329
1369
|
# Apply order joins (relation joins required for order_by field)
|
|
1330
1370
|
if order_joins:
|
|
1331
|
-
q =
|
|
1371
|
+
q = apply_search_joins(q, order_joins)
|
|
1332
1372
|
|
|
1333
1373
|
if filters:
|
|
1334
1374
|
q = q.where(and_(*filters))
|
|
@@ -1351,7 +1391,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1351
1391
|
count_q = _apply_joins(count_q, joins, outer_join)
|
|
1352
1392
|
|
|
1353
1393
|
# Apply search joins to count query
|
|
1354
|
-
count_q =
|
|
1394
|
+
count_q = apply_search_joins(count_q, search_joins)
|
|
1355
1395
|
|
|
1356
1396
|
if filters:
|
|
1357
1397
|
count_q = count_q.where(and_(*filters))
|
|
@@ -1371,7 +1411,12 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1371
1411
|
items: list[Any] = [schema.model_validate(item) for item in raw_items]
|
|
1372
1412
|
|
|
1373
1413
|
filter_attributes = await cls._build_filter_attributes(
|
|
1374
|
-
session,
|
|
1414
|
+
session,
|
|
1415
|
+
facet_fields,
|
|
1416
|
+
facet_base_filters,
|
|
1417
|
+
search_joins,
|
|
1418
|
+
include_facets=include_facets,
|
|
1419
|
+
own_filters=fb_filters,
|
|
1375
1420
|
)
|
|
1376
1421
|
search_columns = cls._resolve_search_columns(search_fields)
|
|
1377
1422
|
order_columns = cls._resolve_order_columns(order_fields)
|
|
@@ -1407,6 +1452,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1407
1452
|
search_column: str | None = None,
|
|
1408
1453
|
order_fields: Sequence[OrderFieldType] | None = None,
|
|
1409
1454
|
facet_fields: Sequence[FacetFieldType] | None = None,
|
|
1455
|
+
include_facets: bool = True,
|
|
1410
1456
|
filter_by: dict[str, Any] | BaseModel | None = None,
|
|
1411
1457
|
schema: type[BaseModel],
|
|
1412
1458
|
) -> CursorPaginatedResponse[Any]:
|
|
@@ -1429,6 +1475,8 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1429
1475
|
search_column: Restrict search to a single column key.
|
|
1430
1476
|
order_fields: Fields allowed for sorting (overrides class default).
|
|
1431
1477
|
facet_fields: Columns to compute distinct values for (overrides class default).
|
|
1478
|
+
include_facets: When ``False``, skip facet queries entirely;
|
|
1479
|
+
``filter_attributes`` will be ``None``.
|
|
1432
1480
|
filter_by: Dict of {column_key: value} to filter by declared facet fields.
|
|
1433
1481
|
Keys must match the column.key of a facet field. Scalar → equality,
|
|
1434
1482
|
list → IN clause. Raises InvalidFacetFilterError for unknown keys.
|
|
@@ -1440,7 +1488,6 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1440
1488
|
filters = list(filters) if filters else []
|
|
1441
1489
|
|
|
1442
1490
|
fb_filters, search_joins = cls._prepare_filter_by(filter_by, facet_fields)
|
|
1443
|
-
filters.extend(fb_filters)
|
|
1444
1491
|
|
|
1445
1492
|
if cls.cursor_column is None:
|
|
1446
1493
|
raise ValueError(
|
|
@@ -1472,6 +1519,11 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1472
1519
|
filters.extend(search_filters)
|
|
1473
1520
|
search_joins.extend(new_search_joins)
|
|
1474
1521
|
|
|
1522
|
+
# Facets combine these with each facet's own filter individually, so
|
|
1523
|
+
# fb_filters is applied to the query below but excluded here.
|
|
1524
|
+
facet_base_filters = list(filters)
|
|
1525
|
+
filters.extend(fb_filters.values())
|
|
1526
|
+
|
|
1475
1527
|
# Build query
|
|
1476
1528
|
q = select(cls.model)
|
|
1477
1529
|
|
|
@@ -1479,11 +1531,11 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1479
1531
|
q = _apply_joins(q, joins, outer_join)
|
|
1480
1532
|
|
|
1481
1533
|
# Apply search joins (always outer joins)
|
|
1482
|
-
q =
|
|
1534
|
+
q = apply_search_joins(q, search_joins)
|
|
1483
1535
|
|
|
1484
1536
|
# Apply order joins (relation joins required for order_by field)
|
|
1485
1537
|
if order_joins:
|
|
1486
|
-
q =
|
|
1538
|
+
q = apply_search_joins(q, order_joins)
|
|
1487
1539
|
|
|
1488
1540
|
if filters:
|
|
1489
1541
|
q = q.where(and_(*filters))
|
|
@@ -1528,11 +1580,10 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1528
1580
|
|
|
1529
1581
|
# prev_cursor: points before the first item in ascending order
|
|
1530
1582
|
prev_cursor: str | None = None
|
|
1531
|
-
if
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
elif direction is _CursorDirection.PREV and has_more and items_page:
|
|
1583
|
+
if items_page and (
|
|
1584
|
+
(direction is _CursorDirection.NEXT and cursor is not None)
|
|
1585
|
+
or (direction is _CursorDirection.PREV and has_more)
|
|
1586
|
+
):
|
|
1536
1587
|
prev_cursor = _encode_cursor(
|
|
1537
1588
|
getattr(items_page[0], cursor_col_name), direction=_CursorDirection.PREV
|
|
1538
1589
|
)
|
|
@@ -1540,7 +1591,12 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1540
1591
|
items: list[Any] = [schema.model_validate(item) for item in items_page]
|
|
1541
1592
|
|
|
1542
1593
|
filter_attributes = await cls._build_filter_attributes(
|
|
1543
|
-
session,
|
|
1594
|
+
session,
|
|
1595
|
+
facet_fields,
|
|
1596
|
+
facet_base_filters,
|
|
1597
|
+
search_joins,
|
|
1598
|
+
include_facets=include_facets,
|
|
1599
|
+
own_filters=fb_filters,
|
|
1544
1600
|
)
|
|
1545
1601
|
search_columns = cls._resolve_search_columns(search_fields)
|
|
1546
1602
|
order_columns = cls._resolve_order_columns(order_fields)
|
|
@@ -1580,6 +1636,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1580
1636
|
search_column: str | None = ...,
|
|
1581
1637
|
order_fields: Sequence[OrderFieldType] | None = ...,
|
|
1582
1638
|
facet_fields: Sequence[FacetFieldType] | None = ...,
|
|
1639
|
+
include_facets: bool = ...,
|
|
1583
1640
|
filter_by: dict[str, Any] | BaseModel | None = ...,
|
|
1584
1641
|
schema: type[BaseModel],
|
|
1585
1642
|
) -> OffsetPaginatedResponse[Any]: ...
|
|
@@ -1606,6 +1663,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1606
1663
|
search_column: str | None = ...,
|
|
1607
1664
|
order_fields: Sequence[OrderFieldType] | None = ...,
|
|
1608
1665
|
facet_fields: Sequence[FacetFieldType] | None = ...,
|
|
1666
|
+
include_facets: bool = ...,
|
|
1609
1667
|
filter_by: dict[str, Any] | BaseModel | None = ...,
|
|
1610
1668
|
schema: type[BaseModel],
|
|
1611
1669
|
) -> CursorPaginatedResponse[Any]: ...
|
|
@@ -1631,6 +1689,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1631
1689
|
search_column: str | None = None,
|
|
1632
1690
|
order_fields: Sequence[OrderFieldType] | None = None,
|
|
1633
1691
|
facet_fields: Sequence[FacetFieldType] | None = None,
|
|
1692
|
+
include_facets: bool = True,
|
|
1634
1693
|
filter_by: dict[str, Any] | BaseModel | None = None,
|
|
1635
1694
|
schema: type[BaseModel],
|
|
1636
1695
|
) -> OffsetPaginatedResponse[Any] | CursorPaginatedResponse[Any]:
|
|
@@ -1661,6 +1720,8 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1661
1720
|
order_fields: Fields allowed for sorting (overrides class default).
|
|
1662
1721
|
facet_fields: Columns to compute distinct values for (overrides
|
|
1663
1722
|
class default).
|
|
1723
|
+
include_facets: When ``False``, skip facet queries entirely;
|
|
1724
|
+
``filter_attributes`` will be ``None``.
|
|
1664
1725
|
filter_by: Dict of ``{column_key: value}`` to filter by declared
|
|
1665
1726
|
facet fields. Keys must match the ``column.key`` of a facet
|
|
1666
1727
|
field. Scalar → equality, list → IN clause. Raises
|
|
@@ -1691,6 +1752,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1691
1752
|
search_column=search_column,
|
|
1692
1753
|
order_fields=order_fields,
|
|
1693
1754
|
facet_fields=facet_fields,
|
|
1755
|
+
include_facets=include_facets,
|
|
1694
1756
|
filter_by=filter_by,
|
|
1695
1757
|
schema=schema,
|
|
1696
1758
|
)
|
|
@@ -1713,6 +1775,7 @@ class AsyncCrud(Generic[ModelType]):
|
|
|
1713
1775
|
search_column=search_column,
|
|
1714
1776
|
order_fields=order_fields,
|
|
1715
1777
|
facet_fields=facet_fields,
|
|
1778
|
+
include_facets=include_facets,
|
|
1716
1779
|
filter_by=filter_by,
|
|
1717
1780
|
schema=schema,
|
|
1718
1781
|
)
|