dynamicforms-fastapi-viewsets 0.5.1__tar.gz → 0.5.4__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.
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/.gitignore +1 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/PKG-INFO +1 -1
- dynamicforms_fastapi_viewsets-0.5.4/fastapi_viewsets/__init__.py +1 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/backends/django_orm.py +8 -7
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/collection_viewset.py +1 -3
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/context/auth/jwt.py +1 -1
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/cursor.py +5 -4
- dynamicforms_fastapi_viewsets-0.5.4/fastapi_viewsets/decorators/__init__.py +19 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/decorators/build_schema.py +11 -10
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/decorators/celery_viewset/__init__.py +6 -3
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/decorators/celery_viewset/client.py +24 -3
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/decorators/celery_viewset/server.py +41 -9
- dynamicforms_fastapi_viewsets-0.5.4/fastapi_viewsets/decorators/conftest.py +14 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/decorators/lifecycle_runner.py +1 -1
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/decorators/primary_key_model_helper.py +7 -2
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/decorators/route_viewset.py +41 -38
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/endpoint_docs.py +1 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/filters/declaration.py +1 -5
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/filters/registry.py +1 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/list_shapes.py +1 -2
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/mixins.py +16 -12
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/mux_ws/protocol.py +1 -5
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/mux_ws/registry.py +1 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/response_classes.py +3 -1
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/save_state/save_state.py +1 -0
- dynamicforms_fastapi_viewsets-0.5.1/fastapi_viewsets/__init__.py +0 -1
- dynamicforms_fastapi_viewsets-0.5.1/fastapi_viewsets/decorators/__init__.py +0 -10
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/LICENSE +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/README.md +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/action_configuration.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/backends/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/conf.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/context/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/context/auth/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/context/auth/django.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/context/auth/static.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/decorators/celery_viewset/result_reader.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/filters/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/filters/base.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/filters/operators.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/list_query.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/middleware/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/middleware/auth/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/middleware/auth/authorization.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/middleware/rate_limiter.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/mux_ws/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/mux_ws/asgi.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/mux_ws/server.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/mux_ws/transports.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/py.typed +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/save_state/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/save_state/serialize_state.py +1 -1
- {dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: dynamicforms-fastapi-viewsets
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
4
4
|
Summary: Django REST Framework-style viewsets for FastAPI, with optional Celery-backed async execution and a Vue/TypeScript client counterpart.
|
|
5
5
|
Project-URL: Homepage, https://github.com/dynamicforms/fastapi-viewsets
|
|
6
6
|
Project-URL: Repository, https://github.com/dynamicforms/fastapi-viewsets
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.4"
|
|
@@ -180,9 +180,7 @@ class DjangoORMViewSet(ImplMixin[K, T], Generic[K, T]):
|
|
|
180
180
|
return await super().apply_filter(context, query, records)
|
|
181
181
|
|
|
182
182
|
concrete = self._concrete_fields()
|
|
183
|
-
translatable = all(
|
|
184
|
-
all(name in concrete for name in fltr.fields()) for fltr in filter_set
|
|
185
|
-
)
|
|
183
|
+
translatable = all(all(name in concrete for name in fltr.fields()) for fltr in filter_set)
|
|
186
184
|
if not translatable or not can_compile_all(type(self), filter_set):
|
|
187
185
|
return await super().apply_filter(context, query, records)
|
|
188
186
|
|
|
@@ -372,6 +370,7 @@ for _filter_type in _LOOKUPS:
|
|
|
372
370
|
# Cursor predicate
|
|
373
371
|
# ---------------------------------------------------------------------------
|
|
374
372
|
|
|
373
|
+
|
|
375
374
|
class RowValues(Func):
|
|
376
375
|
"""`(a, b, c)` - a SQL row constructor."""
|
|
377
376
|
|
|
@@ -414,14 +413,16 @@ def _cursor_condition(viewset: "DjangoORMViewSet", fltr: CursorPredicate) -> Q:
|
|
|
414
413
|
directly as a condition.
|
|
415
414
|
"""
|
|
416
415
|
keys = fltr.keys
|
|
417
|
-
nullable = {
|
|
418
|
-
field.name for field in viewset.model._meta.get_fields() if getattr(field, "null", False)
|
|
419
|
-
}
|
|
416
|
+
nullable = {field.name for field in viewset.model._meta.get_fields() if getattr(field, "null", False)}
|
|
420
417
|
uniform = len({descending for _, descending in keys}) == 1
|
|
421
418
|
if uniform and not fltr.inclusive and not any(name in nullable for name, _ in keys):
|
|
422
419
|
return _row_value_condition(keys, fltr.position, fltr.backwards)
|
|
423
420
|
return _segment_condition(
|
|
424
|
-
keys,
|
|
421
|
+
keys,
|
|
422
|
+
fltr.position,
|
|
423
|
+
fltr.backwards,
|
|
424
|
+
fltr.inclusive,
|
|
425
|
+
fltr.nulls_first,
|
|
425
426
|
)
|
|
426
427
|
|
|
427
428
|
|
|
@@ -58,9 +58,7 @@ class AsyncCollectionViewSet(
|
|
|
58
58
|
async def perform_update(self, _context: Context, pk: K, data: T, partial: bool = True) -> T:
|
|
59
59
|
return await self.container_update(pk, data, partial)
|
|
60
60
|
|
|
61
|
-
async def perform_bulk_update(
|
|
62
|
-
self, _context: Context, records: dict[K, T], partial: bool = True
|
|
63
|
-
) -> list[T]:
|
|
61
|
+
async def perform_bulk_update(self, _context: Context, records: dict[K, T], partial: bool = True) -> list[T]:
|
|
64
62
|
results = []
|
|
65
63
|
for pk, data in records.items():
|
|
66
64
|
results.append(await self.container_update(pk, data, partial))
|
|
@@ -64,7 +64,7 @@ class JWTAuthBackend(AuthBackend):
|
|
|
64
64
|
header = request.headers.get(self.header_name, "")
|
|
65
65
|
if not header.lower().startswith("bearer "):
|
|
66
66
|
return None # not a bearer token - let the next backend try it
|
|
67
|
-
return _JWTUserLazy(header[len("bearer "):].strip(), self.secret, self.algorithm)
|
|
67
|
+
return _JWTUserLazy(header[len("bearer ") :].strip(), self.secret, self.algorithm)
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
def encode_jwt(
|
|
@@ -135,9 +135,7 @@ def decode_cursor(raw: str, keys: CursorKeys, expected_query: str, annotations:
|
|
|
135
135
|
raise CursorError("cursor is not readable: no position in it")
|
|
136
136
|
|
|
137
137
|
if payload.get("q") != expected_query:
|
|
138
|
-
raise CursorError(
|
|
139
|
-
"this cursor was issued for a different ordering or filter - start from the first page"
|
|
140
|
-
)
|
|
138
|
+
raise CursorError("this cursor was issued for a different ordering or filter - start from the first page")
|
|
141
139
|
|
|
142
140
|
position = payload["p"]
|
|
143
141
|
missing = [name for name, _ in keys if name not in position]
|
|
@@ -214,7 +212,10 @@ class CursorPredicate(Filter):
|
|
|
214
212
|
def matches(self, record: Any) -> bool:
|
|
215
213
|
for name, descending in self.keys:
|
|
216
214
|
order = compare_in_order(
|
|
217
|
-
_read(record, name),
|
|
215
|
+
_read(record, name),
|
|
216
|
+
self.position.get(name),
|
|
217
|
+
descending,
|
|
218
|
+
self.nulls_first,
|
|
218
219
|
)
|
|
219
220
|
if order != 0:
|
|
220
221
|
# `order > 0` means the row sorts after the anchor; reading backwards wants before.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .celery_viewset import (
|
|
2
|
+
celery_viewset,
|
|
3
|
+
celery_viewset_client,
|
|
4
|
+
celery_viewset_server,
|
|
5
|
+
set_celery_dispatch_hook,
|
|
6
|
+
set_celery_kwargs_hook,
|
|
7
|
+
set_is_celery_worker,
|
|
8
|
+
)
|
|
9
|
+
from .route_viewset import route_viewset
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"celery_viewset",
|
|
13
|
+
"celery_viewset_client",
|
|
14
|
+
"celery_viewset_server",
|
|
15
|
+
"route_viewset",
|
|
16
|
+
"set_celery_dispatch_hook",
|
|
17
|
+
"set_celery_kwargs_hook",
|
|
18
|
+
"set_is_celery_worker",
|
|
19
|
+
]
|
|
@@ -167,16 +167,17 @@ def build_schema(cls, base_path: str = "", default_tags=None, get_wrapper=None,
|
|
|
167
167
|
# Per-viewset wording, applied here because this is where the schema-serving app is
|
|
168
168
|
# built; patching the caller's router afterwards would leave /schema showing the
|
|
169
169
|
# mixin's generic docstring. See endpoint_docs.py.
|
|
170
|
-
class_router.add_api_route(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
170
|
+
class_router.add_api_route(
|
|
171
|
+
**route_to_add_api_route_kwargs(
|
|
172
|
+
route,
|
|
173
|
+
path=full_path,
|
|
174
|
+
endpoint=endpoint_wrapper,
|
|
175
|
+
response_model=response_model,
|
|
176
|
+
tags=route.tags or default_tags,
|
|
177
|
+
openapi_extra=getattr(endpoint_wrapper, "__shape_openapi_extra__", None) or route.openapi_extra,
|
|
178
|
+
**docs_for(cls, route.endpoint.__name__),
|
|
179
|
+
)
|
|
180
|
+
)
|
|
180
181
|
|
|
181
182
|
app.include_router(class_router)
|
|
182
183
|
cls.__app = app
|
|
@@ -2,7 +2,7 @@ import logging
|
|
|
2
2
|
import sys
|
|
3
3
|
|
|
4
4
|
from ..lifecycle_runner import LifecycleType
|
|
5
|
-
from .client import celery_viewset_client, get_registered_queue_keys
|
|
5
|
+
from .client import celery_viewset_client, get_registered_queue_keys, set_celery_dispatch_hook
|
|
6
6
|
from .result_reader import (
|
|
7
7
|
get_result_queue_key,
|
|
8
8
|
get_running_queue_keys,
|
|
@@ -10,7 +10,7 @@ from .result_reader import (
|
|
|
10
10
|
start_result_reader,
|
|
11
11
|
stop_result_reader,
|
|
12
12
|
)
|
|
13
|
-
from .server import celery_viewset_server
|
|
13
|
+
from .server import celery_viewset_server, set_celery_kwargs_hook
|
|
14
14
|
|
|
15
15
|
logger = logging.getLogger(__name__)
|
|
16
16
|
|
|
@@ -68,7 +68,8 @@ def check_result_readers() -> frozenset[str]:
|
|
|
68
68
|
"No result reader is running for queue_key=%s - requests dispatched to this "
|
|
69
69
|
"celery_viewset will hang waiting for a result that never arrives. Call "
|
|
70
70
|
"start_result_reader(redis_client, %r) in your FastAPI lifespan.",
|
|
71
|
-
queue_key,
|
|
71
|
+
queue_key,
|
|
72
|
+
queue_key,
|
|
72
73
|
)
|
|
73
74
|
return frozenset(missing)
|
|
74
75
|
|
|
@@ -78,6 +79,8 @@ __all__ = [
|
|
|
78
79
|
"celery_viewset_client",
|
|
79
80
|
"celery_viewset_server",
|
|
80
81
|
"set_is_celery_worker",
|
|
82
|
+
"set_celery_kwargs_hook",
|
|
83
|
+
"set_celery_dispatch_hook",
|
|
81
84
|
"start_result_reader",
|
|
82
85
|
"stop_result_reader",
|
|
83
86
|
"check_result_readers",
|
|
@@ -2,6 +2,7 @@ import asyncio
|
|
|
2
2
|
import logging
|
|
3
3
|
import uuid
|
|
4
4
|
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
5
6
|
from functools import wraps
|
|
6
7
|
from typing import TYPE_CHECKING, TypeVar
|
|
7
8
|
|
|
@@ -20,6 +21,20 @@ if TYPE_CHECKING:
|
|
|
20
21
|
T = TypeVar("T")
|
|
21
22
|
logger = logging.getLogger(__name__)
|
|
22
23
|
|
|
24
|
+
_celery_dispatch_hook: Callable[[dict], Awaitable[dict]] | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def set_celery_dispatch_hook(hook: Callable[[dict], Awaitable[dict]] | None) -> None:
|
|
28
|
+
"""Register a callable awaited just before send_task; its return dict is merged into kwargs.
|
|
29
|
+
|
|
30
|
+
Receives the call's current kwargs (context included, if the action declares one) so a
|
|
31
|
+
registered hook can read what a context processor already put there instead of needing its own
|
|
32
|
+
request-level plumbing. Returns {} when there is nothing to add.
|
|
33
|
+
"""
|
|
34
|
+
global _celery_dispatch_hook
|
|
35
|
+
_celery_dispatch_hook = hook
|
|
36
|
+
|
|
37
|
+
|
|
23
38
|
# queue_keys registered by celery_viewset_client so far - populated at decoration (import) time,
|
|
24
39
|
# used by check_result_readers() to spot a queue_key with no running result reader.
|
|
25
40
|
_registered_queue_keys: set[str] = set()
|
|
@@ -97,10 +112,16 @@ def _patch_method(cls: type, original_endpoint, task_name: str, celery_app, queu
|
|
|
97
112
|
else:
|
|
98
113
|
serializable_kwargs[k] = await _serialize_value(v)
|
|
99
114
|
logger.info("Celery task scheduling: %s (correlation_id=%s)", task_name, correlation_id)
|
|
115
|
+
extra_kwargs = await _celery_dispatch_hook(kwargs) if _celery_dispatch_hook is not None else {}
|
|
100
116
|
celery_app.send_task(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
task_name,
|
|
118
|
+
args=serializable_args,
|
|
119
|
+
kwargs={
|
|
120
|
+
**serializable_kwargs,
|
|
121
|
+
"_correlation_id": correlation_id,
|
|
122
|
+
"_result_queue_key": queue_key,
|
|
123
|
+
**extra_kwargs,
|
|
124
|
+
},
|
|
104
125
|
)
|
|
105
126
|
return await future
|
|
106
127
|
except Exception:
|
|
@@ -2,9 +2,11 @@ import asyncio
|
|
|
2
2
|
import inspect
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
+
import types
|
|
5
6
|
|
|
7
|
+
from collections.abc import Callable
|
|
6
8
|
from functools import wraps
|
|
7
|
-
from typing import get_type_hints, TYPE_CHECKING, TypeVar
|
|
9
|
+
from typing import get_args, get_origin, get_type_hints, TYPE_CHECKING, TypeVar, Union
|
|
8
10
|
|
|
9
11
|
from fastapi import HTTPException
|
|
10
12
|
from pydantic import BaseModel
|
|
@@ -23,6 +25,30 @@ logger = logging.getLogger(__name__)
|
|
|
23
25
|
|
|
24
26
|
T = TypeVar("T")
|
|
25
27
|
|
|
28
|
+
_celery_kwargs_hook: Callable[[Callable, dict], tuple[Callable, dict]] | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def set_celery_kwargs_hook(hook: Callable[[Callable, dict], tuple[Callable, dict]] | None) -> None:
|
|
32
|
+
"""Register a callable that may wrap the runner and/or consume kwargs before reconstruction.
|
|
33
|
+
|
|
34
|
+
Receives (runner, kwargs), must return (runner, kwargs). Called with kwargs still raw - before
|
|
35
|
+
_reconstruct_kwargs, which passes through any key it has no type hint for, so a key the hook
|
|
36
|
+
does not consume would otherwise reach the action as an argument it never declared.
|
|
37
|
+
"""
|
|
38
|
+
global _celery_kwargs_hook
|
|
39
|
+
_celery_kwargs_hook = hook
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _unwrap_optional(hint):
|
|
43
|
+
"""Before Python 3.11, `get_type_hints()` implicitly wraps a None-defaulted parameter's
|
|
44
|
+
annotation in `Optional[...]`, so a hint that is actually a bare model class arrives as
|
|
45
|
+
`Union[Model, None]` and the `inspect.isclass` check below sees a `Union`, not a class."""
|
|
46
|
+
if get_origin(hint) in (Union, types.UnionType):
|
|
47
|
+
args = [arg for arg in get_args(hint) if arg is not type(None)]
|
|
48
|
+
if len(args) == 1:
|
|
49
|
+
return args[0]
|
|
50
|
+
return hint
|
|
51
|
+
|
|
26
52
|
|
|
27
53
|
def _to_jsonable(value):
|
|
28
54
|
"""Recursively convert Pydantic models and lists to JSON-serializable structures."""
|
|
@@ -59,7 +85,7 @@ def _reconstruct_kwargs(original_endpoint, kwargs: dict, cls: type = None) -> di
|
|
|
59
85
|
for key, value in remaining.items():
|
|
60
86
|
hint = hints.get(key)
|
|
61
87
|
if hint is not None:
|
|
62
|
-
hint = resolve_typevars(type_map, hint)
|
|
88
|
+
hint = _unwrap_optional(resolve_typevars(type_map, hint))
|
|
63
89
|
if hint is not None and isinstance(value, dict) and inspect.isclass(hint) and issubclass(hint, BaseModel):
|
|
64
90
|
try:
|
|
65
91
|
result[key] = hint.model_validate(value)
|
|
@@ -102,16 +128,22 @@ def celery_viewset_server(
|
|
|
102
128
|
|
|
103
129
|
logger.info("Celery task executing: %s (correlation_id=%s)", task_name, correlation_id)
|
|
104
130
|
try:
|
|
131
|
+
run = loop.run_until_complete
|
|
132
|
+
if _celery_kwargs_hook is not None:
|
|
133
|
+
run, kwargs = _celery_kwargs_hook(run, kwargs)
|
|
105
134
|
kwargs = _reconstruct_kwargs(original_endpoint, kwargs, cls)
|
|
106
|
-
result =
|
|
107
|
-
lifecycle_runner(original_endpoint, instance, cls, lifecycle, *args, **kwargs)
|
|
108
|
-
)
|
|
135
|
+
result = run(lifecycle_runner(original_endpoint, instance, cls, lifecycle, *args, **kwargs))
|
|
109
136
|
logger.info("Celery task completed: %s (correlation_id=%s)", task_name, correlation_id)
|
|
110
137
|
if correlation_id and result_queue_key and redis_client is not None:
|
|
111
|
-
redis_client.rpush(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
138
|
+
redis_client.rpush(
|
|
139
|
+
result_queue_key,
|
|
140
|
+
json.dumps(
|
|
141
|
+
{
|
|
142
|
+
"correlation_id": correlation_id,
|
|
143
|
+
"result": _to_jsonable(result),
|
|
144
|
+
}
|
|
145
|
+
),
|
|
146
|
+
)
|
|
115
147
|
return result
|
|
116
148
|
except Exception as e:
|
|
117
149
|
if isinstance(e, HTTPException):
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
def iter_routes(routes):
|
|
2
|
+
"""Flatten a FastAPI route list, recursing into an included router's own .routes.
|
|
3
|
+
|
|
4
|
+
fastapi>=0.137 wraps each include_router() call in an internal object exposing
|
|
5
|
+
.original_router instead of the route directly; older fastapi puts the real route in the
|
|
6
|
+
list itself. Duck-typed on the .original_router attribute rather than importing fastapi's
|
|
7
|
+
internal class, so this keeps working if that class is renamed or moved again.
|
|
8
|
+
"""
|
|
9
|
+
for route in routes:
|
|
10
|
+
original_router = getattr(route, "original_router", None)
|
|
11
|
+
if original_router is not None:
|
|
12
|
+
yield from iter_routes(original_router.routes)
|
|
13
|
+
else:
|
|
14
|
+
yield route
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
from typing import Any, get_origin, TypeVar
|
|
1
|
+
from typing import Annotated, Any, get_args, get_origin, TypeVar
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, create_model
|
|
4
4
|
|
|
5
5
|
T = TypeVar("T", bound=BaseModel)
|
|
6
6
|
|
|
7
|
+
|
|
7
8
|
def create_model_without_pk(model: type[T], pk_field_name: str) -> type[BaseModel]:
|
|
8
9
|
"""
|
|
9
10
|
Creates a new Pydantic model based on the given model but without the specified primary key field.
|
|
@@ -15,12 +16,16 @@ def create_model_without_pk(model: type[T], pk_field_name: str) -> type[BaseMode
|
|
|
15
16
|
}
|
|
16
17
|
return create_model(f"{model.__name__}NoPK", **fields)
|
|
17
18
|
|
|
19
|
+
|
|
18
20
|
def typecast_to_original_model(value: Any, original_annotation: type[T]) -> T:
|
|
19
21
|
"""
|
|
20
22
|
Typecasts a value (usually a Pydantic model without PK) back to the original Pydantic model.
|
|
21
23
|
If validation fails (e.g., due to missing PK that will be auto-incremented), it uses model_construct.
|
|
22
24
|
"""
|
|
23
|
-
|
|
25
|
+
annotation = original_annotation
|
|
26
|
+
if get_origin(annotation) is Annotated:
|
|
27
|
+
annotation = get_args(annotation)[0]
|
|
28
|
+
origin = get_origin(annotation) or annotation
|
|
24
29
|
if not isinstance(value, BaseModel) or isinstance(value, origin):
|
|
25
30
|
return value
|
|
26
31
|
|
|
@@ -105,6 +105,7 @@ def _make_middleware_depends(cls: type, action_name: str):
|
|
|
105
105
|
registration time; actual @action_configuration resolution stays fully dynamic; settings can
|
|
106
106
|
still change at runtime between requests.
|
|
107
107
|
"""
|
|
108
|
+
|
|
108
109
|
async def _run(request: Request) -> None:
|
|
109
110
|
action_configuration = resolve_action_configuration(cls, action_name)
|
|
110
111
|
effective_middlewares = settings.viewsets_command_middleware + extra_middlewares_for(
|
|
@@ -119,22 +120,25 @@ def _make_middleware_depends(cls: type, action_name: str):
|
|
|
119
120
|
|
|
120
121
|
scheme = settings.viewsets_security_scheme
|
|
121
122
|
if scheme is None:
|
|
123
|
+
|
|
122
124
|
async def middleware_depends(request: Request) -> None:
|
|
123
125
|
await _run(request)
|
|
126
|
+
|
|
124
127
|
return middleware_depends
|
|
125
128
|
|
|
126
129
|
async def middleware_depends(request: Request, _credential=Depends(scheme)) -> None: # noqa: B008
|
|
127
130
|
await _run(request)
|
|
131
|
+
|
|
128
132
|
return middleware_depends
|
|
129
133
|
|
|
130
134
|
|
|
131
135
|
def route_viewset(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
router: APIRouter,
|
|
137
|
+
base_path: str,
|
|
138
|
+
lifecycle: LifecycleType = "singleton",
|
|
139
|
+
pk_field_name: str = None,
|
|
140
|
+
register_muxws: bool = None,
|
|
141
|
+
register_rest: bool = None,
|
|
138
142
|
):
|
|
139
143
|
"""
|
|
140
144
|
`register_rest` and `register_muxws` decide which transports this viewset is published on.
|
|
@@ -146,6 +150,7 @@ def route_viewset(
|
|
|
146
150
|
replacement is a single mapping - `transports={"rest": False}`, absent key meaning defer -
|
|
147
151
|
rather than a third `register_*`; see GAPS.md for why a set of flags is the worse shape.
|
|
148
152
|
"""
|
|
153
|
+
|
|
149
154
|
def decorator(cls: type[T]):
|
|
150
155
|
instance = cls() if lifecycle == "singleton" else None
|
|
151
156
|
|
|
@@ -154,7 +159,7 @@ def route_viewset(
|
|
|
154
159
|
# Derive tag from class name: strip "ViewSet" suffix if present
|
|
155
160
|
cls_name = cls.__name__
|
|
156
161
|
if cls_name.endswith("ViewSet"):
|
|
157
|
-
cls_name = cls_name[
|
|
162
|
+
cls_name = cls_name[: -len("ViewSet")]
|
|
158
163
|
default_tags = [cls_name] if cls_name else None
|
|
159
164
|
# The viewset's own docstring becomes the description of the group its endpoints appear
|
|
160
165
|
# under - the section intro that was otherwise blank however well each endpoint was
|
|
@@ -164,9 +169,8 @@ def route_viewset(
|
|
|
164
169
|
|
|
165
170
|
def _is_filter_param(annotation) -> bool:
|
|
166
171
|
"""Return True when annotation is Annotated[T, FilterParam()]."""
|
|
167
|
-
return (
|
|
168
|
-
|
|
169
|
-
and any(isinstance(m, FilterParam) for m in annotation.__metadata__)
|
|
172
|
+
return hasattr(annotation, "__metadata__") and any(
|
|
173
|
+
isinstance(m, FilterParam) for m in annotation.__metadata__
|
|
170
174
|
)
|
|
171
175
|
|
|
172
176
|
def _is_model_query_param(annotation) -> bool:
|
|
@@ -208,13 +212,15 @@ def route_viewset(
|
|
|
208
212
|
if uses.get(name) is False:
|
|
209
213
|
continue
|
|
210
214
|
if name == "x_list_shape":
|
|
211
|
-
parameter = parameter.replace(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
parameter = parameter.replace(
|
|
216
|
+
annotation=Annotated[
|
|
217
|
+
Literal[(*allowed, None)],
|
|
218
|
+
Header(
|
|
219
|
+
alias=SHAPE_HEADER,
|
|
220
|
+
description=f"Response shape. Omit for this endpoint's default ({default}).",
|
|
221
|
+
),
|
|
222
|
+
]
|
|
223
|
+
)
|
|
218
224
|
params.append(parameter)
|
|
219
225
|
|
|
220
226
|
item_type = type_map.get(ItemVar, ItemVar)
|
|
@@ -267,10 +273,7 @@ def route_viewset(
|
|
|
267
273
|
continue
|
|
268
274
|
|
|
269
275
|
# If we have a PK field and this parameter is a model from which we want to exclude PK
|
|
270
|
-
if
|
|
271
|
-
inspect.isclass(annotation) and
|
|
272
|
-
issubclass(annotation, BaseModel)
|
|
273
|
-
):
|
|
276
|
+
if pk_field_name and inspect.isclass(annotation) and issubclass(annotation, BaseModel):
|
|
274
277
|
# Check if {pk} is in the path and if the method is one that normally accepts a model in the body
|
|
275
278
|
# (POST without {pk} or PUT/PATCH with {pk})
|
|
276
279
|
is_create = "POST" in route_methods and "{pk}" not in route_path
|
|
@@ -286,18 +289,14 @@ def route_viewset(
|
|
|
286
289
|
# (never forwarded to the underlying method itself, see wrapper below). Keyword-only
|
|
287
290
|
# so it can be appended regardless of what defaults precede it. Used by lifecycle_runner
|
|
288
291
|
# to run the command middleware chain and apply headers/cookies onto the real response.
|
|
289
|
-
new_params.append(
|
|
290
|
-
inspect.Parameter("response", inspect.Parameter.KEYWORD_ONLY, annotation=Response)
|
|
291
|
-
)
|
|
292
|
+
new_params.append(inspect.Parameter("response", inspect.Parameter.KEYWORD_ONLY, annotation=Response))
|
|
292
293
|
|
|
293
294
|
if needs_context:
|
|
294
295
|
# Reserved param name, only added for endpoints that declare a `context` param.
|
|
295
296
|
# FastAPI injects the REAL Request here; lifecycle_runner uses it to build the
|
|
296
297
|
# context (see fastapi_viewsets/context.py) and injects the result as `context`
|
|
297
298
|
# right before the endpoint runs - never forwarded to the endpoint itself.
|
|
298
|
-
new_params.append(
|
|
299
|
-
inspect.Parameter("request", inspect.Parameter.KEYWORD_ONLY, annotation=Request)
|
|
300
|
-
)
|
|
299
|
+
new_params.append(inspect.Parameter("request", inspect.Parameter.KEYWORD_ONLY, annotation=Request))
|
|
301
300
|
|
|
302
301
|
new_return_annotation = resolve_typevars(type_map, sig.return_annotation)
|
|
303
302
|
new_sig = sig.replace(parameters=new_params, return_annotation=new_return_annotation)
|
|
@@ -325,8 +324,15 @@ def route_viewset(
|
|
|
325
324
|
new_kwargs[param_name] = value
|
|
326
325
|
|
|
327
326
|
return await lifecycle_runner(
|
|
328
|
-
original_endpoint,
|
|
329
|
-
|
|
327
|
+
original_endpoint,
|
|
328
|
+
instance,
|
|
329
|
+
cls,
|
|
330
|
+
lifecycle,
|
|
331
|
+
*args,
|
|
332
|
+
response=response_obj,
|
|
333
|
+
needs_context=needs_context,
|
|
334
|
+
request=request_obj,
|
|
335
|
+
**new_kwargs,
|
|
330
336
|
)
|
|
331
337
|
|
|
332
338
|
wrapper.__signature__ = new_sig
|
|
@@ -339,7 +345,10 @@ def route_viewset(
|
|
|
339
345
|
# ViewSetResult - if any is configured, the endpoint's declared return type can no longer
|
|
340
346
|
# be trusted as the actual response_model, same reasoning as the old finalize_response hook.
|
|
341
347
|
build_schema(
|
|
342
|
-
cls,
|
|
348
|
+
cls,
|
|
349
|
+
base_path,
|
|
350
|
+
default_tags,
|
|
351
|
+
get_wrapper,
|
|
343
352
|
disable_response_model=bool(settings.viewsets_command_middleware),
|
|
344
353
|
)
|
|
345
354
|
|
|
@@ -348,9 +357,7 @@ def route_viewset(
|
|
|
348
357
|
for route in cls.__router.routes:
|
|
349
358
|
action_name = route.endpoint.__name__ # survives @wraps(original_endpoint) in get_wrapper
|
|
350
359
|
documented.add(action_name)
|
|
351
|
-
dependencies = list(route.dependencies or []) + [
|
|
352
|
-
Depends(_make_middleware_depends(cls, action_name))
|
|
353
|
-
]
|
|
360
|
+
dependencies = list(route.dependencies or []) + [Depends(_make_middleware_depends(cls, action_name))]
|
|
354
361
|
route_kwargs = route_to_add_api_route_kwargs(route, dependencies=dependencies)
|
|
355
362
|
# Per-viewset wording for an endpoint the mixin provided - the mixin's own docstring is
|
|
356
363
|
# the same sentence on every viewset in the application. See endpoint_docs.py.
|
|
@@ -379,11 +386,7 @@ def route_viewset(
|
|
|
379
386
|
if muxws_routes:
|
|
380
387
|
register_viewset(cls, base_path, muxws_routes, default_tags)
|
|
381
388
|
|
|
382
|
-
cls.__viewset_metadata__ = {
|
|
383
|
-
"base_path": base_path,
|
|
384
|
-
"lifecycle": lifecycle,
|
|
385
|
-
"router": router
|
|
386
|
-
}
|
|
389
|
+
cls.__viewset_metadata__ = {"base_path": base_path, "lifecycle": lifecycle, "router": router}
|
|
387
390
|
|
|
388
391
|
return cls
|
|
389
392
|
|
|
@@ -63,6 +63,7 @@ def endpoint_docs(docs: dict[str, EndpointDoc]):
|
|
|
63
63
|
the other way round it would run too late; rather than silently document nothing, that is
|
|
64
64
|
refused with a message saying which way round to put it.
|
|
65
65
|
"""
|
|
66
|
+
|
|
66
67
|
def decorator(cls):
|
|
67
68
|
if getattr(cls, "__viewset_metadata__", None) is not None:
|
|
68
69
|
raise ValueError(
|
|
@@ -91,11 +91,7 @@ def make_filter_model(model: type[BaseModel], declaration: FilterDeclaration) ->
|
|
|
91
91
|
)
|
|
92
92
|
parameter = field_name if lookup == "exact" else f"{field_name}__{lookup}"
|
|
93
93
|
annotation = _parameter_annotation(field_annotation, lookup)
|
|
94
|
-
default = (
|
|
95
|
-
Field(None, description="Comma-separated list of values")
|
|
96
|
-
if lookup in _LIST_LOOKUPS
|
|
97
|
-
else None
|
|
98
|
-
)
|
|
94
|
+
default = Field(None, description="Comma-separated list of values") if lookup in _LIST_LOOKUPS else None
|
|
99
95
|
fields[parameter] = (annotation, default)
|
|
100
96
|
spec[parameter] = (field_name, lookup, _item_annotation(field_annotation, lookup))
|
|
101
97
|
|
|
@@ -31,6 +31,7 @@ def compiles(backend: type, filter_type: type[Filter]) -> Callable[[Compiler], C
|
|
|
31
31
|
def _(viewset, fltr, queryset):
|
|
32
32
|
return queryset.filter(**{fltr.field: fltr.value})
|
|
33
33
|
"""
|
|
34
|
+
|
|
34
35
|
def decorator(compiler: Compiler) -> Compiler:
|
|
35
36
|
_compilers[(backend, filter_type)] = compiler
|
|
36
37
|
return compiler
|
|
@@ -56,8 +56,7 @@ def _placeholder(schema: dict, definitions: dict, depth: int = 0) -> Any:
|
|
|
56
56
|
kind = schema.get("type")
|
|
57
57
|
if kind == "object":
|
|
58
58
|
return {
|
|
59
|
-
name: _placeholder(field, definitions, depth + 1)
|
|
60
|
-
for name, field in schema.get("properties", {}).items()
|
|
59
|
+
name: _placeholder(field, definitions, depth + 1) for name, field in schema.get("properties", {}).items()
|
|
61
60
|
}
|
|
62
61
|
if kind == "array":
|
|
63
62
|
return [_placeholder(schema.get("items", {}), definitions, depth + 1)]
|
|
@@ -111,6 +111,7 @@ class CreateMixin(Generic[K, T], ABC):
|
|
|
111
111
|
"""
|
|
112
112
|
Create a model instance.
|
|
113
113
|
"""
|
|
114
|
+
|
|
114
115
|
__router = APIRouter()
|
|
115
116
|
|
|
116
117
|
@final
|
|
@@ -123,6 +124,7 @@ class BulkOnlyCreateMixin(Generic[K, T], ABC):
|
|
|
123
124
|
"""
|
|
124
125
|
Create model instances in bulk.
|
|
125
126
|
"""
|
|
127
|
+
|
|
126
128
|
__router = APIRouter()
|
|
127
129
|
|
|
128
130
|
@final
|
|
@@ -188,6 +190,7 @@ class SortStateColumn(BaseModel):
|
|
|
188
190
|
Mirrors the FE SortStateColumn interface: one column in the current sort order.
|
|
189
191
|
Python attribute names use snake_case; JSON serialization uses camelCase (columnName).
|
|
190
192
|
"""
|
|
193
|
+
|
|
191
194
|
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
|
|
192
195
|
|
|
193
196
|
column_name: str
|
|
@@ -239,6 +242,7 @@ class ListMixin(Generic[T, TFilter], ABC):
|
|
|
239
242
|
the response into a union of exactly those models; `route_viewset` computes both from these two
|
|
240
243
|
attributes, so nothing here is written twice.
|
|
241
244
|
"""
|
|
245
|
+
|
|
242
246
|
__router = APIRouter()
|
|
243
247
|
|
|
244
248
|
list_shape: str | None = None
|
|
@@ -333,9 +337,7 @@ class ListMixin(Generic[T, TFilter], ABC):
|
|
|
333
337
|
list_items.__list_shape_aware__ = True
|
|
334
338
|
"""Tells route_viewset to prune this signature and compute its response model per viewset."""
|
|
335
339
|
|
|
336
|
-
async def get_list(
|
|
337
|
-
self: "ImplMixin[Any, T] | ListMixin[T]", context: Context, query: ListQuery
|
|
338
|
-
) -> Any:
|
|
340
|
+
async def get_list(self: "ImplMixin[Any, T] | ListMixin[T]", context: Context, query: ListQuery) -> Any:
|
|
339
341
|
"""
|
|
340
342
|
The list pipeline, in one place and overridable as a whole.
|
|
341
343
|
|
|
@@ -405,9 +407,7 @@ class ListMixin(Generic[T, TFilter], ABC):
|
|
|
405
407
|
return records
|
|
406
408
|
return await self.sort_list(query.sort, await self.land(query, records))
|
|
407
409
|
|
|
408
|
-
async def land(
|
|
409
|
-
self: "ImplMixin[Any, T] | ListMixin[T]", query: ListQuery, records: ListRecords
|
|
410
|
-
) -> list:
|
|
410
|
+
async def land(self: "ImplMixin[Any, T] | ListMixin[T]", query: ListQuery, records: ListRecords) -> list:
|
|
411
411
|
"""
|
|
412
412
|
Ends the lazy part: materialises the source and converts it to the response model.
|
|
413
413
|
|
|
@@ -667,6 +667,7 @@ class RetrieveMixin(Generic[K, T], ABC):
|
|
|
667
667
|
"""
|
|
668
668
|
Retrieve a model instance.
|
|
669
669
|
"""
|
|
670
|
+
|
|
670
671
|
__router = APIRouter()
|
|
671
672
|
|
|
672
673
|
@final
|
|
@@ -682,6 +683,7 @@ class UpdateMixin(Generic[K, T], ABC):
|
|
|
682
683
|
"""
|
|
683
684
|
Update a model instance.
|
|
684
685
|
"""
|
|
686
|
+
|
|
685
687
|
__router = APIRouter()
|
|
686
688
|
|
|
687
689
|
@final
|
|
@@ -691,9 +693,7 @@ class UpdateMixin(Generic[K, T], ABC):
|
|
|
691
693
|
|
|
692
694
|
@final
|
|
693
695
|
@__router.patch("/{pk}", name="partial_update", responses=NOT_FOUND_RESPONSE)
|
|
694
|
-
async def partial_update(
|
|
695
|
-
self: "ImplMixin[K, T] | UpdateMixin[K, T]", context: Context, pk: K, data: T
|
|
696
|
-
) -> T:
|
|
696
|
+
async def partial_update(self: "ImplMixin[K, T] | UpdateMixin[K, T]", context: Context, pk: K, data: T) -> T:
|
|
697
697
|
return await self.perform_update(context, pk, data, partial=True)
|
|
698
698
|
|
|
699
699
|
|
|
@@ -701,6 +701,7 @@ class BulkOnlyUpdateMixin(Generic[K, T], ABC):
|
|
|
701
701
|
"""
|
|
702
702
|
Update model instances in bulk.
|
|
703
703
|
"""
|
|
704
|
+
|
|
704
705
|
__router = APIRouter()
|
|
705
706
|
|
|
706
707
|
@final
|
|
@@ -731,6 +732,7 @@ class DestroyMixin(Generic[K, T], ABC):
|
|
|
731
732
|
"""
|
|
732
733
|
Destroy a model instance. Return the destroyed key and any additional data about its destruction
|
|
733
734
|
"""
|
|
735
|
+
|
|
734
736
|
__router = APIRouter()
|
|
735
737
|
|
|
736
738
|
@final
|
|
@@ -743,6 +745,7 @@ class BulkOnlyDestroyMixin(Generic[K, T], ABC):
|
|
|
743
745
|
"""
|
|
744
746
|
Destroy model instances in bulk.
|
|
745
747
|
"""
|
|
748
|
+
|
|
746
749
|
__router = APIRouter()
|
|
747
750
|
|
|
748
751
|
@final
|
|
@@ -758,6 +761,7 @@ class BulkDestroyMixin(DestroyMixin[K, T], BulkOnlyDestroyMixin[K, T]):
|
|
|
758
761
|
Destroy model instances (single or bulk).
|
|
759
762
|
"""
|
|
760
763
|
|
|
764
|
+
|
|
761
765
|
###################################################################################################
|
|
762
766
|
# LOOKUP
|
|
763
767
|
###################################################################################################
|
|
@@ -770,6 +774,7 @@ class LookupItem(BaseModel):
|
|
|
770
774
|
|
|
771
775
|
class LookupFilter(BaseModel):
|
|
772
776
|
"""Default filter model for LookupMixin. Provides case-insensitive title search via q."""
|
|
777
|
+
|
|
773
778
|
q: str | None = None
|
|
774
779
|
|
|
775
780
|
|
|
@@ -796,6 +801,7 @@ class LookupMixin(Generic[TLookupFilter], ABC):
|
|
|
796
801
|
The default implementation filters by fltr.q (case-insensitive title match).
|
|
797
802
|
Override to customise in-memory filtering.
|
|
798
803
|
"""
|
|
804
|
+
|
|
799
805
|
__router = APIRouter()
|
|
800
806
|
|
|
801
807
|
@abstractmethod
|
|
@@ -832,9 +838,7 @@ class LookupMixin(Generic[TLookupFilter], ABC):
|
|
|
832
838
|
fltr: Annotated[TLookupFilter, Query()] = None,
|
|
833
839
|
) -> list[LookupItem]:
|
|
834
840
|
has_filter = (
|
|
835
|
-
fltr is not None
|
|
836
|
-
and hasattr(fltr, "model_dump")
|
|
837
|
-
and any(v is not None for v in fltr.model_dump().values())
|
|
841
|
+
fltr is not None and hasattr(fltr, "model_dump") and any(v is not None for v in fltr.model_dump().values())
|
|
838
842
|
)
|
|
839
843
|
if has_filter:
|
|
840
844
|
await self.setup_lookup_filter(fltr)
|
|
@@ -93,11 +93,7 @@ def parse_request(headers: dict[str, Any] | None) -> tuple[str, str, bytes, dict
|
|
|
93
93
|
else:
|
|
94
94
|
raise EnvelopeError(f"{QUERY_KEY} must be a string or a mapping, got {type(query).__name__}")
|
|
95
95
|
|
|
96
|
-
http_headers = {
|
|
97
|
-
str(key).lower(): str(value)
|
|
98
|
-
for key, value in headers.items()
|
|
99
|
-
if not str(key).startswith(":")
|
|
100
|
-
}
|
|
96
|
+
http_headers = {str(key).lower(): str(value) for key, value in headers.items() if not str(key).startswith(":")}
|
|
101
97
|
return method.upper(), path, query_string, http_headers
|
|
102
98
|
|
|
103
99
|
|
|
@@ -155,6 +155,7 @@ def _add_schema_route(app: FastAPI, registration: _Registration) -> None:
|
|
|
155
155
|
built here rather than copied from there, because the REST one closes over the REST app and
|
|
156
156
|
would report the wrong transport's endpoint set.
|
|
157
157
|
"""
|
|
158
|
+
|
|
158
159
|
def schema() -> dict[str, Any]:
|
|
159
160
|
return registration.schema()
|
|
160
161
|
|
|
@@ -7,7 +7,9 @@ from pydantic import BaseModel
|
|
|
7
7
|
class NotFoundResponse(BaseModel):
|
|
8
8
|
detail: str = "Item with pk {pk} not found"
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
NOT_FOUND_RESPONSE = {"404": {"model": NotFoundResponse, "message": NotFoundResponse().detail}}
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
class NotFoundError(HTTPException):
|
|
13
15
|
def __init__(self, pk: Any):
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.5.1"
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
from .celery_viewset import celery_viewset, celery_viewset_client, celery_viewset_server, set_is_celery_worker
|
|
2
|
-
from .route_viewset import route_viewset
|
|
3
|
-
|
|
4
|
-
__all__ = [
|
|
5
|
-
"celery_viewset",
|
|
6
|
-
"celery_viewset_client",
|
|
7
|
-
"celery_viewset_server",
|
|
8
|
-
"route_viewset",
|
|
9
|
-
"set_is_celery_worker",
|
|
10
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dynamicforms_fastapi_viewsets-0.5.1 → dynamicforms_fastapi_viewsets-0.5.4}/fastapi_viewsets/conf.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -16,8 +16,8 @@ class SerializeState(ABC):
|
|
|
16
16
|
async def deserialize_state(self, state: str):
|
|
17
17
|
pass
|
|
18
18
|
|
|
19
|
-
class SerializeStateSlots(SerializeState):
|
|
20
19
|
|
|
20
|
+
class SerializeStateSlots(SerializeState):
|
|
21
21
|
async def serialize_state(self) -> str: # noqa: B027
|
|
22
22
|
return json.dumps(
|
|
23
23
|
{slot: getattr(self, slot) for slot in self.__slots__},
|
|
File without changes
|