dynamicforms-fastapi-viewsets 0.14.2__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.
Files changed (74) hide show
  1. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/PKG-INFO +1 -1
  2. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/backends/django_orm.py +2 -5
  3. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/collection_viewset.py +1 -8
  4. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/changes.py +10 -3
  5. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/mixins.py +29 -9
  6. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/pyproject.toml +1 -1
  7. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/pyproject.toml.orig +1 -1
  8. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/LICENSE +0 -0
  9. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/README.md +0 -0
  10. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/__init__.py +0 -0
  11. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/action_configuration.py +0 -0
  12. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/backends/__init__.py +0 -0
  13. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/conf.py +0 -0
  14. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/context/__init__.py +0 -0
  15. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/context/auth/__init__.py +0 -0
  16. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/context/auth/django.py +0 -0
  17. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/context/auth/jwt.py +0 -0
  18. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/context/auth/static.py +0 -0
  19. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/cursor.py +0 -0
  20. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/__init__.py +0 -0
  21. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/build_schema.py +0 -0
  22. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/celery_viewset/__init__.py +0 -0
  23. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/celery_viewset/client.py +0 -0
  24. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/celery_viewset/payload.py +0 -0
  25. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/celery_viewset/result_reader.py +0 -0
  26. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/celery_viewset/server.py +0 -0
  27. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/celery_viewset/transport.py +0 -0
  28. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/conftest.py +0 -0
  29. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/lifecycle_runner.py +0 -0
  30. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/primary_key_model_helper.py +0 -0
  31. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/query_model.py +0 -0
  32. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/route_viewset.py +0 -0
  33. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/decorators/sub_viewsets.py +0 -0
  34. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/endpoint_docs.py +0 -0
  35. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/error_namespace.py +0 -0
  36. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/error_responses.py +0 -0
  37. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/__init__.py +0 -0
  38. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/authorizers.py +0 -0
  39. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/conftest.py +0 -0
  40. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/delivery.py +0 -0
  41. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/event.py +0 -0
  42. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/lifecycle.py +0 -0
  43. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/redis.py +0 -0
  44. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/scopes.py +0 -0
  45. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/sources.py +0 -0
  46. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/store.py +0 -0
  47. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/topics.py +0 -0
  48. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/events/viewset.py +0 -0
  49. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/exceptions.py +0 -0
  50. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/filters/__init__.py +0 -0
  51. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/filters/base.py +0 -0
  52. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/filters/declaration.py +0 -0
  53. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/filters/operators.py +0 -0
  54. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/filters/registry.py +0 -0
  55. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/list_query.py +0 -0
  56. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/list_shapes.py +0 -0
  57. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/middleware/__init__.py +0 -0
  58. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/middleware/auth/__init__.py +0 -0
  59. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/middleware/auth/authorization.py +0 -0
  60. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/middleware/cookies.py +0 -0
  61. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/middleware/idempotency.py +0 -0
  62. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/middleware/rate_limiter.py +0 -0
  63. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/mux_ws/__init__.py +0 -0
  64. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/mux_ws/asgi.py +0 -0
  65. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/mux_ws/protocol.py +0 -0
  66. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/mux_ws/registry.py +0 -0
  67. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/mux_ws/server.py +0 -0
  68. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/mux_ws/transports.py +0 -0
  69. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/py.typed +0 -0
  70. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/response_classes.py +0 -0
  71. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/routing.py +0 -0
  72. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/save_state/__init__.py +0 -0
  73. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/save_state/save_state.py +0 -0
  74. {dynamicforms_fastapi_viewsets-0.14.2 → dynamicforms_fastapi_viewsets-0.15.0}/fastapi_viewsets/save_state/serialize_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dynamicforms-fastapi-viewsets
3
- Version: 0.14.2
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
5
  Keywords: fastapi,viewsets,rest,dynamicforms,celery,django-rest-framework
6
6
  Author: Jure Erznožnik
@@ -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 {"id": pk}
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, Any]:
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
- """One change per pk of the `pk` argument for `deleted`, else per returned record (`ViewSetResult.body`)."""
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
- return [Change(pk=one) for one in (pk if bulk else [pk])]
249
- body = result.body if isinstance(result, ViewSetResult) else result
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
 
@@ -5,8 +5,10 @@ from enum import Enum
5
5
  from typing import Annotated, Any, final, Generic, get_args, get_origin, Union
6
6
 
7
7
  from fastapi import APIRouter, Header, Query
8
+ from fastapi.exceptions import RequestValidationError
8
9
  from pydantic import BaseModel, ConfigDict, create_model
9
10
  from pydantic.alias_generators import to_camel
11
+ from starlette.exceptions import HTTPException
10
12
  from typing_extensions import TypeVar
11
13
 
12
14
  from fastapi_viewsets.conf import settings
@@ -23,6 +25,7 @@ from fastapi_viewsets.cursor import (
23
25
  make_predicate,
24
26
  position_of,
25
27
  )
28
+ from fastapi_viewsets.error_responses import ApiErrorBody, error_body
26
29
  from fastapi_viewsets.exceptions import CursorRequestError, UnsupportedListShapeError
27
30
  from fastapi_viewsets.filters import filters_from, FilterSet
28
31
  from fastapi_viewsets.list_query import (
@@ -95,18 +98,29 @@ class ImplMixin(Generic[K, T], ABC):
95
98
  raise NotImplementedError("Method 'perform_bulk_update' must be implemented.")
96
99
 
97
100
  @abstractmethod
98
- async def perform_destroy(self, context: Context, pk: K) -> dict[K, Any]:
101
+ async def perform_destroy(self, context: Context, pk: K) -> dict[K, None]:
99
102
  """
100
- Subclasses should implement this method to perform the actual destruction.
103
+ Destroys record `pk` and returns `{pk: None}`; raises `NotFoundError` for a missing record.
101
104
  """
102
105
  raise NotImplementedError("Method 'perform_destroy' must be implemented.")
103
106
 
104
- @abstractmethod
105
- 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]:
106
108
  """
107
- Subclasses should implement this method to perform the actual bulk destruction.
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.
108
112
  """
109
- raise NotImplementedError("Method 'perform_bulk_destroy' must be implemented.")
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
110
124
 
111
125
 
112
126
  ###################################################################################################
@@ -649,14 +663,15 @@ class BulkUpdateMixin(UpdateMixin[K, T], BulkOnlyUpdateMixin[K, T]):
649
663
  ###################################################################################################
650
664
  class DestroyMixin(Generic[K, T], ABC):
651
665
  """
652
- Destroy a model instance. Return the destroyed key and any additional data about its destruction
666
+ Destroy a model instance.
653
667
  """
654
668
 
655
669
  __router = APIRouter()
656
670
 
657
671
  @final
658
672
  @__router.delete("/{pk}", responses=NOT_FOUND_RESPONSE)
659
- async def destroy(self: "ImplMixin[K, T] | DestroyMixin[K, T]", context: Context, pk: K) -> dict[K, Any]:
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."""
660
675
  return await self.perform_destroy(context, pk)
661
676
 
662
677
 
@@ -671,7 +686,12 @@ class BulkOnlyDestroyMixin(Generic[K, T], ABC):
671
686
  @__router.delete("bulk")
672
687
  async def bulk_destroy(
673
688
  self: "ImplMixin[K, T] | BulkOnlyDestroyMixin[K, T]", context: Context, pk: list[K]
674
- ) -> list[dict[K, Any]]:
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
+ """
675
695
  return await self.perform_bulk_destroy(context, pk)
676
696
 
677
697
 
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "dynamicforms-fastapi-viewsets"
7
- version = "0.14.2"
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
10
  license = "MIT"
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "dynamicforms-fastapi-viewsets"
7
- version = "0.14.2"
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
10
  license = "MIT"