dynamicforms-fastapi-viewsets 0.14.1__tar.gz → 0.15.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.
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/PKG-INFO +17 -33
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/__init__.py +3 -1
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/backends/django_orm.py +2 -5
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/collection_viewset.py +1 -8
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/changes.py +10 -3
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/mixins.py +42 -12
- dynamicforms_fastapi_viewsets-0.15.0/pyproject.toml +169 -0
- dynamicforms_fastapi_viewsets-0.14.1/pyproject.toml → dynamicforms_fastapi_viewsets-0.15.0/pyproject.toml.orig +14 -22
- dynamicforms_fastapi_viewsets-0.14.1/.gitignore +0 -21
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/LICENSE +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/README.md +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/action_configuration.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/backends/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/conf.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/context/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/context/auth/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/context/auth/django.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/context/auth/jwt.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/context/auth/static.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/cursor.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/build_schema.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/celery_viewset/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/celery_viewset/client.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/celery_viewset/payload.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/celery_viewset/result_reader.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/celery_viewset/server.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/celery_viewset/transport.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/conftest.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/lifecycle_runner.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/primary_key_model_helper.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/query_model.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/route_viewset.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/sub_viewsets.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/endpoint_docs.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/error_namespace.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/error_responses.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/authorizers.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/conftest.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/delivery.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/event.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/lifecycle.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/redis.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/scopes.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/sources.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/store.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/topics.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/viewset.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/exceptions.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/filters/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/filters/base.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/filters/declaration.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/filters/operators.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/filters/registry.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/list_query.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/list_shapes.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/middleware/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/middleware/auth/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/middleware/auth/authorization.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/middleware/cookies.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/middleware/idempotency.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/middleware/rate_limiter.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/mux_ws/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/mux_ws/asgi.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/mux_ws/protocol.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/mux_ws/registry.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/mux_ws/server.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/mux_ws/transports.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/py.typed +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/response_classes.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/routing.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/save_state/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/save_state/save_state.py +0 -0
- {dynamicforms_fastapi_viewsets-0.14.1 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/save_state/serialize_state.py +0 -0
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: dynamicforms-fastapi-viewsets
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.0
|
|
4
4
|
Summary: Django REST Framework-style viewsets for FastAPI, with optional Celery-backed async execution and a Vue/TypeScript client counterpart.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Author-email: Jure Erznožnik <jure.erznoznik@gmail.com>
|
|
10
|
-
License: MIT
|
|
5
|
+
Keywords: fastapi,viewsets,rest,dynamicforms,celery,django-rest-framework
|
|
6
|
+
Author: Jure Erznožnik
|
|
7
|
+
Author-email: Jure Erznožnik <jure.erznoznik@velis.si>
|
|
8
|
+
License-Expression: MIT
|
|
11
9
|
License-File: LICENSE
|
|
12
|
-
Keywords: celery,django-rest-framework,dynamicforms,fastapi,rest,viewsets
|
|
13
10
|
Classifier: Development Status :: 3 - Alpha
|
|
14
11
|
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
16
12
|
Classifier: Operating System :: OS Independent
|
|
17
13
|
Classifier: Programming Language :: Python :: 3
|
|
18
14
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -21,36 +17,24 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
21
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
18
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
19
|
Classifier: Typing :: Typed
|
|
24
|
-
Requires-Python: >=3.10
|
|
25
20
|
Requires-Dist: fastapi>=0.100
|
|
26
21
|
Requires-Dist: pydantic>=2.11
|
|
27
22
|
Requires-Dist: typing-extensions>=4.10
|
|
23
|
+
Requires-Dist: celery>=5.3 ; extra == 'celery'
|
|
24
|
+
Requires-Dist: redis>=5.0 ; extra == 'celery'
|
|
25
|
+
Requires-Dist: django>=4.2 ; extra == 'django'
|
|
26
|
+
Requires-Dist: asgiref>=3.6 ; extra == 'django'
|
|
27
|
+
Requires-Dist: pyjwt>=2.8 ; extra == 'jwt'
|
|
28
|
+
Requires-Dist: muxws[starlette]>=0.4.2 ; extra == 'muxws'
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Project-URL: Homepage, https://github.com/dynamicforms/fastapi-viewsets
|
|
31
|
+
Project-URL: Repository, https://github.com/dynamicforms/fastapi-viewsets
|
|
32
|
+
Project-URL: Issues, https://github.com/dynamicforms/fastapi-viewsets/issues
|
|
33
|
+
Project-URL: Documentation, https://docs.velis.si/dynamicforms/fastapi-viewsets/
|
|
28
34
|
Provides-Extra: celery
|
|
29
|
-
Requires-Dist: celery>=5.3; extra == 'celery'
|
|
30
|
-
Requires-Dist: redis>=5.0; extra == 'celery'
|
|
31
|
-
Provides-Extra: dev
|
|
32
|
-
Requires-Dist: asgiref; extra == 'dev'
|
|
33
|
-
Requires-Dist: celery; extra == 'dev'
|
|
34
|
-
Requires-Dist: coverage; extra == 'dev'
|
|
35
|
-
Requires-Dist: django; extra == 'dev'
|
|
36
|
-
Requires-Dist: gevent; extra == 'dev'
|
|
37
|
-
Requires-Dist: httpx2; extra == 'dev'
|
|
38
|
-
Requires-Dist: muxws[starlette,websockets]>=0.4.2; extra == 'dev'
|
|
39
|
-
Requires-Dist: pyjwt; extra == 'dev'
|
|
40
|
-
Requires-Dist: pytest; extra == 'dev'
|
|
41
|
-
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
42
|
-
Requires-Dist: pytest-cov; extra == 'dev'
|
|
43
|
-
Requires-Dist: python-multipart; extra == 'dev'
|
|
44
|
-
Requires-Dist: redis; extra == 'dev'
|
|
45
|
-
Requires-Dist: ruff; extra == 'dev'
|
|
46
|
-
Requires-Dist: uvicorn; extra == 'dev'
|
|
47
35
|
Provides-Extra: django
|
|
48
|
-
Requires-Dist: asgiref>=3.6; extra == 'django'
|
|
49
|
-
Requires-Dist: django>=4.2; extra == 'django'
|
|
50
36
|
Provides-Extra: jwt
|
|
51
|
-
Requires-Dist: pyjwt>=2.8; extra == 'jwt'
|
|
52
37
|
Provides-Extra: muxws
|
|
53
|
-
Requires-Dist: muxws[starlette]>=0.4.2; extra == 'muxws'
|
|
54
38
|
Description-Content-Type: text/markdown
|
|
55
39
|
|
|
56
40
|
# DynamicForms FastAPI Viewsets
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
from importlib.metadata import version
|
|
2
2
|
|
|
3
3
|
from .error_namespace import ERROR_NAMESPACE, ERROR_NAMESPACE_PYDANTIC
|
|
4
4
|
|
|
5
|
+
__version__ = version("dynamicforms-fastapi-viewsets")
|
|
6
|
+
|
|
5
7
|
__all__ = ["ERROR_NAMESPACE", "ERROR_NAMESPACE_PYDANTIC", "__version__"]
|
|
@@ -286,13 +286,10 @@ class DjangoORMViewSet(ImplMixin[K, T], Generic[K, T]):
|
|
|
286
286
|
instance.save()
|
|
287
287
|
return [self.to_record(rows[pk]) for pk in records]
|
|
288
288
|
|
|
289
|
-
async def perform_destroy(self, context: Context, pk: K) -> dict:
|
|
289
|
+
async def perform_destroy(self, context: Context, pk: K) -> dict[K, None]:
|
|
290
290
|
instance = await self._get_or_404(context, pk)
|
|
291
291
|
await instance.adelete()
|
|
292
|
-
return {
|
|
293
|
-
|
|
294
|
-
async def perform_bulk_destroy(self, context: Context, pk: list[K]) -> list[dict]:
|
|
295
|
-
return [await self.perform_destroy(context, one) for one in pk]
|
|
292
|
+
return {pk: None}
|
|
296
293
|
|
|
297
294
|
# -----------------------------------------------------------------------
|
|
298
295
|
|
|
@@ -66,17 +66,10 @@ class AsyncCollectionViewSet(
|
|
|
66
66
|
results.append(await self.container_update(pk, data, partial))
|
|
67
67
|
return results
|
|
68
68
|
|
|
69
|
-
async def perform_destroy(self, _context: Context, pk: K) -> dict[K,
|
|
69
|
+
async def perform_destroy(self, _context: Context, pk: K) -> dict[K, None]:
|
|
70
70
|
await self.container_delete(pk)
|
|
71
71
|
return {pk: None}
|
|
72
72
|
|
|
73
|
-
async def perform_bulk_destroy(self, _context: Context, pk: list[K]) -> list[dict[K, Any]]:
|
|
74
|
-
results = []
|
|
75
|
-
for p in pk:
|
|
76
|
-
await self.container_delete(p)
|
|
77
|
-
results.append({p: None})
|
|
78
|
-
return results
|
|
79
|
-
|
|
80
73
|
|
|
81
74
|
class CollectionViewSet(AsyncCollectionViewSet[K, T]):
|
|
82
75
|
def __init__(self, container: ReadableCollection[T] | MutableCollection[K, T], pk_field: str = "id"):
|
|
@@ -241,12 +241,19 @@ class ChangeEvents(Middleware):
|
|
|
241
241
|
return name
|
|
242
242
|
|
|
243
243
|
def changes_for(self, execution: ActionExecution, op: str, result: Any) -> list[Change]:
|
|
244
|
-
"""
|
|
244
|
+
"""
|
|
245
|
+
For `deleted`, one change per pk of the `pk` argument; of a bulk, only the pks the result maps to None
|
|
246
|
+
(destroyed). Otherwise one per returned record. A `ViewSetResult` contributes its `body`.
|
|
247
|
+
"""
|
|
245
248
|
bulk = execution.action in _BULK_ACTIONS
|
|
249
|
+
body = result.body if isinstance(result, ViewSetResult) else result
|
|
246
250
|
if op == "deleted":
|
|
247
251
|
pk = _argument(execution, "pk")
|
|
248
|
-
|
|
249
|
-
|
|
252
|
+
if not bulk:
|
|
253
|
+
return [Change(pk=pk)]
|
|
254
|
+
if isinstance(body, Mapping):
|
|
255
|
+
return [Change(pk=one) for one in dict.fromkeys(pk) if one in body and body[one] is None]
|
|
256
|
+
return [Change(pk=one) for one in pk]
|
|
250
257
|
field = self.pk_field_for(execution)
|
|
251
258
|
return [Change(pk=_read_pk(execution, record, field)) for record in (body if bulk else [body])]
|
|
252
259
|
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
|
|
1
3
|
from abc import ABC, abstractmethod
|
|
2
4
|
from enum import Enum
|
|
3
5
|
from typing import Annotated, Any, final, Generic, get_args, get_origin, Union
|
|
4
6
|
|
|
5
7
|
from fastapi import APIRouter, Header, Query
|
|
8
|
+
from fastapi.exceptions import RequestValidationError
|
|
6
9
|
from pydantic import BaseModel, ConfigDict, create_model
|
|
7
10
|
from pydantic.alias_generators import to_camel
|
|
11
|
+
from starlette.exceptions import HTTPException
|
|
8
12
|
from typing_extensions import TypeVar
|
|
9
13
|
|
|
10
14
|
from fastapi_viewsets.conf import settings
|
|
@@ -21,6 +25,7 @@ from fastapi_viewsets.cursor import (
|
|
|
21
25
|
make_predicate,
|
|
22
26
|
position_of,
|
|
23
27
|
)
|
|
28
|
+
from fastapi_viewsets.error_responses import ApiErrorBody, error_body
|
|
24
29
|
from fastapi_viewsets.exceptions import CursorRequestError, UnsupportedListShapeError
|
|
25
30
|
from fastapi_viewsets.filters import filters_from, FilterSet
|
|
26
31
|
from fastapi_viewsets.list_query import (
|
|
@@ -39,6 +44,15 @@ K = TypeVar("K")
|
|
|
39
44
|
TFilter = TypeVar("TFilter", default=None)
|
|
40
45
|
|
|
41
46
|
|
|
47
|
+
@functools.cache
|
|
48
|
+
def _item_type(cls: type) -> Any:
|
|
49
|
+
"""The viewset's list item type `T`; `Any` when unbound. A page of it encodes as the declared result."""
|
|
50
|
+
from fastapi_viewsets.decorators.route_viewset import build_type_map
|
|
51
|
+
|
|
52
|
+
item = build_type_map(cls).get(T, Any)
|
|
53
|
+
return Any if isinstance(item, TypeVar) else item
|
|
54
|
+
|
|
55
|
+
|
|
42
56
|
class ImplMixin(Generic[K, T], ABC):
|
|
43
57
|
@abstractmethod
|
|
44
58
|
async def perform_create(self, context: Context, data: T) -> T:
|
|
@@ -84,18 +98,29 @@ class ImplMixin(Generic[K, T], ABC):
|
|
|
84
98
|
raise NotImplementedError("Method 'perform_bulk_update' must be implemented.")
|
|
85
99
|
|
|
86
100
|
@abstractmethod
|
|
87
|
-
async def perform_destroy(self, context: Context, pk: K) -> dict[K,
|
|
101
|
+
async def perform_destroy(self, context: Context, pk: K) -> dict[K, None]:
|
|
88
102
|
"""
|
|
89
|
-
|
|
103
|
+
Destroys record `pk` and returns `{pk: None}`; raises `NotFoundError` for a missing record.
|
|
90
104
|
"""
|
|
91
105
|
raise NotImplementedError("Method 'perform_destroy' must be implemented.")
|
|
92
106
|
|
|
93
|
-
|
|
94
|
-
async def perform_bulk_destroy(self, context: Context, pk: list[K]) -> list[dict[K, Any]]:
|
|
107
|
+
async def perform_bulk_destroy(self, context: Context, pk: list[K]) -> dict[K, ApiErrorBody | None]:
|
|
95
108
|
"""
|
|
96
|
-
|
|
109
|
+
Destroys each key with `perform_destroy`, independently of the others: None for a destroyed key, the
|
|
110
|
+
`ApiErrorBody` of an `HTTPException` or `RequestValidationError` its destruction raised. Any other
|
|
111
|
+
exception propagates. A repeated key is destroyed once.
|
|
97
112
|
"""
|
|
98
|
-
|
|
113
|
+
results: dict[K, ApiErrorBody | None] = {}
|
|
114
|
+
for one in pk:
|
|
115
|
+
if one in results:
|
|
116
|
+
continue
|
|
117
|
+
try:
|
|
118
|
+
await self.perform_destroy(context, one)
|
|
119
|
+
except (HTTPException, RequestValidationError) as error:
|
|
120
|
+
results[one] = ApiErrorBody.model_validate(error_body(error))
|
|
121
|
+
else:
|
|
122
|
+
results[one] = None
|
|
123
|
+
return results
|
|
99
124
|
|
|
100
125
|
|
|
101
126
|
###################################################################################################
|
|
@@ -456,7 +481,7 @@ class ListMixin(Generic[T, TFilter], ABC):
|
|
|
456
481
|
has_more_forward = has_more if not backwards else bool(query.cursor)
|
|
457
482
|
has_previous = bool(query.cursor) if not backwards else has_more
|
|
458
483
|
|
|
459
|
-
return CursorPage[
|
|
484
|
+
return CursorPage[_item_type(type(self))](
|
|
460
485
|
results=results,
|
|
461
486
|
limit=query.limit,
|
|
462
487
|
has_more=has_more_forward,
|
|
@@ -487,7 +512,7 @@ class ListMixin(Generic[T, TFilter], ABC):
|
|
|
487
512
|
page, has_more = await self.take_page(query, records)
|
|
488
513
|
# Already converted if an in-memory stage had to land the source earlier; see land().
|
|
489
514
|
results = page if not query.needs("conversion") else self._to_records(page)
|
|
490
|
-
return PaginatedList[
|
|
515
|
+
return PaginatedList[_item_type(type(self))](
|
|
491
516
|
results=results,
|
|
492
517
|
offset=query.offset,
|
|
493
518
|
limit=query.limit,
|
|
@@ -540,7 +565,6 @@ class ListMixin(Generic[T, TFilter], ABC):
|
|
|
540
565
|
In-memory sort hook. The default is a stable multi-key sort using the cursor's comparison
|
|
541
566
|
(`compare_in_order`), so NULL placement matches the cursor's.
|
|
542
567
|
"""
|
|
543
|
-
import functools
|
|
544
568
|
|
|
545
569
|
def compare(a: T, b: T) -> int:
|
|
546
570
|
for col in sort:
|
|
@@ -639,14 +663,15 @@ class BulkUpdateMixin(UpdateMixin[K, T], BulkOnlyUpdateMixin[K, T]):
|
|
|
639
663
|
###################################################################################################
|
|
640
664
|
class DestroyMixin(Generic[K, T], ABC):
|
|
641
665
|
"""
|
|
642
|
-
Destroy a model instance.
|
|
666
|
+
Destroy a model instance.
|
|
643
667
|
"""
|
|
644
668
|
|
|
645
669
|
__router = APIRouter()
|
|
646
670
|
|
|
647
671
|
@final
|
|
648
672
|
@__router.delete("/{pk}", responses=NOT_FOUND_RESPONSE)
|
|
649
|
-
async def destroy(self: "ImplMixin[K, T] | DestroyMixin[K, T]", context: Context, pk: K) -> dict[K,
|
|
673
|
+
async def destroy(self: "ImplMixin[K, T] | DestroyMixin[K, T]", context: Context, pk: K) -> dict[K, None]:
|
|
674
|
+
"""Destroy one record. Answers `{pk: null}`; 404 `fastapi-viewsets.not_found` for a missing record."""
|
|
650
675
|
return await self.perform_destroy(context, pk)
|
|
651
676
|
|
|
652
677
|
|
|
@@ -661,7 +686,12 @@ class BulkOnlyDestroyMixin(Generic[K, T], ABC):
|
|
|
661
686
|
@__router.delete("bulk")
|
|
662
687
|
async def bulk_destroy(
|
|
663
688
|
self: "ImplMixin[K, T] | BulkOnlyDestroyMixin[K, T]", context: Context, pk: list[K]
|
|
664
|
-
) ->
|
|
689
|
+
) -> dict[K, ApiErrorBody | None]:
|
|
690
|
+
"""
|
|
691
|
+
Destroy the records of the keys in the body, each independently of the others. Answers 200 with an
|
|
692
|
+
object keyed by pk: `null` for a destroyed record, the `ApiErrorBody` of the failure otherwise
|
|
693
|
+
(`fastapi-viewsets.not_found` for a missing record). A failed key does not stop the others.
|
|
694
|
+
"""
|
|
665
695
|
return await self.perform_bulk_destroy(context, pk)
|
|
666
696
|
|
|
667
697
|
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.13.6,<0.14"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dynamicforms-fastapi-viewsets"
|
|
7
|
+
version = "0.15.0"
|
|
8
|
+
description = "Django REST Framework-style viewsets for FastAPI, with optional Celery-backed async execution and a Vue/TypeScript client counterpart."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
keywords = [
|
|
14
|
+
"fastapi",
|
|
15
|
+
"viewsets",
|
|
16
|
+
"rest",
|
|
17
|
+
"dynamicforms",
|
|
18
|
+
"celery",
|
|
19
|
+
"django-rest-framework",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 3 - Alpha",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.10",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
31
|
+
"Typing :: Typed",
|
|
32
|
+
]
|
|
33
|
+
dependencies = [
|
|
34
|
+
"fastapi>=0.100",
|
|
35
|
+
"pydantic>=2.11",
|
|
36
|
+
"typing_extensions>=4.10",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[[project.authors]]
|
|
40
|
+
name = "Jure Erznožnik"
|
|
41
|
+
email = "jure.erznoznik@velis.si"
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
celery = [
|
|
45
|
+
"celery>=5.3",
|
|
46
|
+
"redis>=5.0",
|
|
47
|
+
]
|
|
48
|
+
muxws = ["muxws[starlette]>=0.4.2"]
|
|
49
|
+
django = [
|
|
50
|
+
"django>=4.2",
|
|
51
|
+
"asgiref>=3.6",
|
|
52
|
+
]
|
|
53
|
+
jwt = ["PyJWT>=2.8"]
|
|
54
|
+
|
|
55
|
+
[project.urls]
|
|
56
|
+
Homepage = "https://github.com/dynamicforms/fastapi-viewsets"
|
|
57
|
+
Repository = "https://github.com/dynamicforms/fastapi-viewsets"
|
|
58
|
+
Issues = "https://github.com/dynamicforms/fastapi-viewsets/issues"
|
|
59
|
+
Documentation = "https://docs.velis.si/dynamicforms/fastapi-viewsets/"
|
|
60
|
+
|
|
61
|
+
[dependency-groups]
|
|
62
|
+
dev = [
|
|
63
|
+
"coverage",
|
|
64
|
+
"pytest",
|
|
65
|
+
"pytest-asyncio",
|
|
66
|
+
"pytest-cov",
|
|
67
|
+
"httpx2",
|
|
68
|
+
"python-multipart",
|
|
69
|
+
"redis",
|
|
70
|
+
"celery",
|
|
71
|
+
"gevent",
|
|
72
|
+
"django",
|
|
73
|
+
"asgiref",
|
|
74
|
+
"PyJWT",
|
|
75
|
+
"muxws[starlette,websockets]>=0.4.2",
|
|
76
|
+
"ruff",
|
|
77
|
+
"uvicorn",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
[tool.uv.build-backend]
|
|
81
|
+
module-name = "fastapi_viewsets"
|
|
82
|
+
module-root = ""
|
|
83
|
+
source-include = ["LICENSE"]
|
|
84
|
+
source-exclude = ["**/*_test.py"]
|
|
85
|
+
|
|
86
|
+
[tool.publish]
|
|
87
|
+
tests = "pytest"
|
|
88
|
+
|
|
89
|
+
[tool.ruff]
|
|
90
|
+
exclude = [
|
|
91
|
+
"dockertest",
|
|
92
|
+
"tests",
|
|
93
|
+
"docs",
|
|
94
|
+
"examples",
|
|
95
|
+
"scripts",
|
|
96
|
+
"setup.py",
|
|
97
|
+
"setup.cfg",
|
|
98
|
+
"README.md",
|
|
99
|
+
"LICENSE",
|
|
100
|
+
"MANIFEST.in",
|
|
101
|
+
"pyproject.toml",
|
|
102
|
+
"Pipfile",
|
|
103
|
+
"Pipfile.lock",
|
|
104
|
+
"ruff.egg-info",
|
|
105
|
+
"dist",
|
|
106
|
+
"build",
|
|
107
|
+
".gitignore",
|
|
108
|
+
".git",
|
|
109
|
+
".idea",
|
|
110
|
+
".vscode",
|
|
111
|
+
".pytest_cache",
|
|
112
|
+
".coverage",
|
|
113
|
+
".ruff",
|
|
114
|
+
".venv",
|
|
115
|
+
"*/migrations/*",
|
|
116
|
+
]
|
|
117
|
+
line-length = 120
|
|
118
|
+
|
|
119
|
+
[tool.ruff.lint]
|
|
120
|
+
select = [
|
|
121
|
+
"E",
|
|
122
|
+
"W",
|
|
123
|
+
"F",
|
|
124
|
+
"I",
|
|
125
|
+
"Q",
|
|
126
|
+
"B",
|
|
127
|
+
"UP",
|
|
128
|
+
"N",
|
|
129
|
+
"PT",
|
|
130
|
+
"S",
|
|
131
|
+
"C4",
|
|
132
|
+
"ARG",
|
|
133
|
+
]
|
|
134
|
+
ignore = [
|
|
135
|
+
"E731",
|
|
136
|
+
"E722",
|
|
137
|
+
"C408",
|
|
138
|
+
]
|
|
139
|
+
fixable = ["ALL"]
|
|
140
|
+
unfixable = []
|
|
141
|
+
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|
142
|
+
|
|
143
|
+
[tool.ruff.lint.per-file-ignores]
|
|
144
|
+
"*_test.py" = ["S101"]
|
|
145
|
+
|
|
146
|
+
[tool.ruff.lint.isort]
|
|
147
|
+
case-sensitive = false
|
|
148
|
+
lines-between-types = 1
|
|
149
|
+
combine-as-imports = true
|
|
150
|
+
order-by-type = false
|
|
151
|
+
|
|
152
|
+
[tool.ruff.format]
|
|
153
|
+
quote-style = "double"
|
|
154
|
+
indent-style = "space"
|
|
155
|
+
skip-magic-trailing-comma = false
|
|
156
|
+
line-ending = "auto"
|
|
157
|
+
|
|
158
|
+
[tool.pytest.ini_options]
|
|
159
|
+
markers = ["celery_worker: runs a real Celery worker against the Redis named by FASTAPI_VIEWSETS_TEST_REDIS_URL"]
|
|
160
|
+
filterwarnings = ["ignore:.*endpoints are unreachable:UserWarning"]
|
|
161
|
+
|
|
162
|
+
[tool.coverage.run]
|
|
163
|
+
omit = [
|
|
164
|
+
"demo/*",
|
|
165
|
+
"demo/**/*",
|
|
166
|
+
"demo.py",
|
|
167
|
+
"test_*.py",
|
|
168
|
+
"*_test.py",
|
|
169
|
+
]
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["
|
|
3
|
-
build-backend = "
|
|
2
|
+
requires = ["uv_build>=0.13.6,<0.14"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dynamicforms-fastapi-viewsets"
|
|
7
|
-
|
|
7
|
+
version = "0.15.0"
|
|
8
8
|
description = "Django REST Framework-style viewsets for FastAPI, with optional Celery-backed async execution and a Vue/TypeScript client counterpart."
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
license =
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
11
12
|
authors = [
|
|
12
|
-
{ name = "Jure Erznožnik", email = "jure.erznoznik@
|
|
13
|
+
{ name = "Jure Erznožnik", email = "jure.erznoznik@velis.si" },
|
|
13
14
|
]
|
|
14
15
|
requires-python = ">=3.10"
|
|
15
16
|
keywords = ["fastapi", "viewsets", "rest", "dynamicforms", "celery", "django-rest-framework"]
|
|
16
17
|
classifiers = [
|
|
17
18
|
"Development Status :: 3 - Alpha",
|
|
18
19
|
"Intended Audience :: Developers",
|
|
19
|
-
"License :: OSI Approved :: MIT License",
|
|
20
20
|
"Operating System :: OS Independent",
|
|
21
21
|
"Programming Language :: Python :: 3",
|
|
22
22
|
"Programming Language :: Python :: 3.10",
|
|
@@ -37,6 +37,8 @@ celery = ["celery>=5.3", "redis>=5.0"]
|
|
|
37
37
|
muxws = ["muxws[starlette]>=0.4.2"]
|
|
38
38
|
django = ["django>=4.2", "asgiref>=3.6"]
|
|
39
39
|
jwt = ["PyJWT>=2.8"]
|
|
40
|
+
|
|
41
|
+
[dependency-groups]
|
|
40
42
|
dev = [
|
|
41
43
|
"coverage",
|
|
42
44
|
"pytest",
|
|
@@ -61,21 +63,11 @@ Repository = "https://github.com/dynamicforms/fastapi-viewsets"
|
|
|
61
63
|
Issues = "https://github.com/dynamicforms/fastapi-viewsets/issues"
|
|
62
64
|
Documentation = "https://docs.velis.si/dynamicforms/fastapi-viewsets/"
|
|
63
65
|
|
|
64
|
-
[tool.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
[
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
[tool.hatch.build.targets.sdist]
|
|
71
|
-
include = [
|
|
72
|
-
"/fastapi_viewsets",
|
|
73
|
-
"/README.md",
|
|
74
|
-
"/LICENSE",
|
|
75
|
-
]
|
|
76
|
-
exclude = [
|
|
77
|
-
"**/*_test.py",
|
|
78
|
-
]
|
|
66
|
+
[tool.uv.build-backend]
|
|
67
|
+
module-name = "fastapi_viewsets"
|
|
68
|
+
module-root = ""
|
|
69
|
+
source-include = ["LICENSE"]
|
|
70
|
+
source-exclude = ["**/*_test.py"]
|
|
79
71
|
|
|
80
72
|
[tool.publish]
|
|
81
73
|
tests = "pytest"
|
|
@@ -84,7 +76,7 @@ tests = "pytest"
|
|
|
84
76
|
exclude = ['dockertest',
|
|
85
77
|
'tests', 'docs', 'examples', 'scripts', 'setup.py', 'setup.cfg', 'README.md', 'LICENSE', 'MANIFEST.in',
|
|
86
78
|
'pyproject.toml', 'Pipfile', 'Pipfile.lock', 'ruff.egg-info', 'dist', 'build', '.gitignore', '.git', '.idea',
|
|
87
|
-
'.vscode', '.pytest_cache', '.coverage', '.ruff', '*/migrations/*']
|
|
79
|
+
'.vscode', '.pytest_cache', '.coverage', '.ruff', '.venv', '*/migrations/*']
|
|
88
80
|
line-length = 120
|
|
89
81
|
|
|
90
82
|
[tool.ruff.lint]
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
.idea
|
|
2
|
-
node_modules
|
|
3
|
-
coverage
|
|
4
|
-
dist
|
|
5
|
-
/demo/test-results
|
|
6
|
-
/demo/backend/demo.sqlite3
|
|
7
|
-
|
|
8
|
-
/celerybeat-schedule.db
|
|
9
|
-
/demo/backend/demo.sqlite3
|
|
10
|
-
|
|
11
|
-
/docs/.vitepress/cache
|
|
12
|
-
|
|
13
|
-
__pycache__/
|
|
14
|
-
*.py[cod]
|
|
15
|
-
*.egg-info/
|
|
16
|
-
.eggs/
|
|
17
|
-
build/
|
|
18
|
-
.pytest_cache/
|
|
19
|
-
.ruff_cache/
|
|
20
|
-
.coverage
|
|
21
|
-
coverage.json
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|