django-ninja-aio-crud 2.0.0rc5__tar.gz → 2.0.0rc7__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 (95) hide show
  1. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/.github/workflows/docs.yml +2 -2
  2. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/PKG-INFO +1 -1
  3. django_ninja_aio_crud-2.0.0rc7/docs/api/renderers/orjson_renderer.md +20 -0
  4. django_ninja_aio_crud-2.0.0rc7/docs/api/views/decorators.md +65 -0
  5. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/ninja_aio/__init__.py +1 -1
  6. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/ninja_aio/decorators.py +73 -0
  7. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/ninja_aio/renders.py +8 -2
  8. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/ninja_aio/schemas/helpers.py +10 -2
  9. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/ninja_aio/views.py +12 -8
  10. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/.github/dependabot.yml +0 -0
  11. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/.github/workflows/coverage.yml +0 -0
  12. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/.github/workflows/publish.yml +0 -0
  13. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/.gitignore +0 -0
  14. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/.pre-commit-config.yaml +0 -0
  15. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/LICENSE +0 -0
  16. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/README.md +0 -0
  17. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/CNAME +0 -0
  18. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/api/authentication.md +0 -0
  19. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/api/models/model_serializer.md +0 -0
  20. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/api/models/model_util.md +0 -0
  21. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/api/pagination.md +0 -0
  22. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/api/views/api_view.md +0 -0
  23. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/api/views/api_view_set.md +0 -0
  24. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/contributing.md +0 -0
  25. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/extra.css +0 -0
  26. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
  27. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
  28. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
  29. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
  30. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
  31. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
  32. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/getting_started/installation.md +0 -0
  33. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/getting_started/quick_start.md +0 -0
  34. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/images/bar-swagger.png +0 -0
  35. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/images/favicon.ico +0 -0
  36. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/images/foo-swagger.png +0 -0
  37. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/images/logo.png +0 -0
  38. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
  39. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/index.md +0 -0
  40. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/release_notes.md +0 -0
  41. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/requirements.txt +0 -0
  42. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/tutorial/authentication.md +0 -0
  43. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/tutorial/crud.md +0 -0
  44. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/tutorial/filtering.md +0 -0
  45. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/docs/tutorial/model.md +0 -0
  46. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/examples/ex_1/models.py +0 -0
  47. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/examples/ex_1/urls.py +0 -0
  48. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/examples/ex_1/views.py +0 -0
  49. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/examples/ex_2/auth.py +0 -0
  50. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/examples/ex_2/models.py +0 -0
  51. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/examples/ex_2/urls.py +0 -0
  52. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/examples/ex_2/views.py +0 -0
  53. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/main.py +0 -0
  54. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/mkdocs.yml +0 -0
  55. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/ninja_aio/api.py +0 -0
  56. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/ninja_aio/auth.py +0 -0
  57. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/ninja_aio/exceptions.py +0 -0
  58. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/ninja_aio/helpers/__init__.py +0 -0
  59. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/ninja_aio/helpers/api.py +0 -0
  60. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/ninja_aio/helpers/query.py +0 -0
  61. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/ninja_aio/models.py +0 -0
  62. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/ninja_aio/parsers.py +0 -0
  63. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/ninja_aio/schemas/__init__.py +0 -0
  64. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/ninja_aio/schemas/api.py +0 -0
  65. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/ninja_aio/schemas/generics.py +0 -0
  66. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/ninja_aio/types.py +0 -0
  67. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/pyproject.toml +0 -0
  68. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/requirements.dev.txt +0 -0
  69. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/run-local-coverage.sh +0 -0
  70. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/__init__.py +0 -0
  71. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/core/__init__.py +0 -0
  72. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/core/test_decorators.py +0 -0
  73. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/core/test_exceptions_api.py +0 -0
  74. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/core/test_renderer_parser.py +0 -0
  75. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/generics/__init__.py +0 -0
  76. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/generics/literals.py +0 -0
  77. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/generics/models.py +0 -0
  78. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/generics/request.py +0 -0
  79. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/generics/views.py +0 -0
  80. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/helpers/__init__.py +0 -0
  81. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/helpers/test_many_to_many_api.py +0 -0
  82. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/models/__init__.py +0 -0
  83. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/models/test_model_util.py +0 -0
  84. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/models/test_models_extra.py +0 -0
  85. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/test_app/__init__.py +0 -0
  86. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/test_app/models.py +0 -0
  87. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/test_app/schema.py +0 -0
  88. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/test_app/views.py +0 -0
  89. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/test_decorators.py +0 -0
  90. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/test_exceptions.py +0 -0
  91. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/test_query_util.py +0 -0
  92. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/test_settings.py +0 -0
  93. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/views/__init__.py +0 -0
  94. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/views/test_views.py +0 -0
  95. {django_ninja_aio_crud-2.0.0rc5 → django_ninja_aio_crud-2.0.0rc7}/tests/views/test_viewset.py +0 -0
@@ -34,10 +34,10 @@ jobs:
34
34
  if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
35
35
 
36
36
  steps:
37
- - uses: actions/checkout@v4
37
+ - uses: actions/checkout@v6
38
38
 
39
39
  - name: Set up Python
40
- uses: actions/setup-python@v5
40
+ uses: actions/setup-python@v6
41
41
  with:
42
42
  python-version: "3.13"
43
43
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-ninja-aio-crud
3
- Version: 2.0.0rc5
3
+ Version: 2.0.0rc7
4
4
  Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
6
  Requires-Python: >=3.10
@@ -0,0 +1,20 @@
1
+ # ORJSON renderer option
2
+
3
+ This package uses an internal ORJSON-based renderer. Configure serialization options via Django settings:
4
+
5
+ ```python
6
+ # settings.py
7
+ import orjson
8
+
9
+ # Single option
10
+ NINJA_AIO_ORJSON_RENDERER_OPTION = orjson.OPT_INDENT_2
11
+
12
+ # Multiple options (bitwise OR)
13
+ NINJA_AIO_ORJSON_RENDERER_OPTION = (
14
+ orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS
15
+ )
16
+ ```
17
+
18
+ Notes:
19
+ - The value is an orjson option bitmask (e.g., `orjson.OPT_INDENT_2`, `orjson.OPT_NON_STR_KEYS`), and you can combine multiple options using `|`.
20
+ - If not set, the default `orjson.dumps` options are used.
@@ -0,0 +1,65 @@
1
+ # View decorators
2
+
3
+ This package provides:
4
+
5
+ - decorate_view: compose multiple decorators (sync/async views), preserving Python stacking order, and skipping None values.
6
+ - APIViewSet.extra_decorators: declarative per-operation decorators.
7
+
8
+ ## decorate_view
9
+
10
+ Behavior:
11
+
12
+ - Order matches normal stacking: `@d1` over `@d2` ≡ `d1(d2(view))`.
13
+ - Works with sync/async views.
14
+ - Ignores None values, useful for conditional decoration.
15
+
16
+ Example:
17
+
18
+ ```python
19
+ from ninja_aio.decorators import decorate_view
20
+ from ninja_aio.views import APIViewSet
21
+
22
+ class MyViewSet(APIViewSet):
23
+ api = api
24
+ model = MyModel
25
+
26
+ def views(self):
27
+ @self.router.get("health/")
28
+ @decorate_view(authenticate, log_request)
29
+ async def health(request):
30
+ return {"ok": True}
31
+ ```
32
+
33
+ Conditional decoration:
34
+
35
+ ```python
36
+ cache_dec = cache_page(60) if settings.ENABLE_CACHE else None
37
+
38
+ @self.router.get("data/")
39
+ @decorate_view(cache_dec, authenticate)
40
+ async def data(request):
41
+ ...
42
+ ```
43
+
44
+ Note: decorate_view does not add an extra wrapper; each decorator should preserve metadata itself (e.g., functools.wraps).
45
+
46
+ ## APIViewSet.extra_decorators
47
+
48
+ Attach decorators to CRUD operations without redefining views:
49
+
50
+ ```python
51
+ from ninja_aio.schemas.helpers import DecoratorsSchema
52
+
53
+ class MyViewSet(APIViewSet):
54
+ api = api
55
+ model = MyModel
56
+ extra_decorators = DecoratorsSchema(
57
+ list=[require_auth, cache_page(30)],
58
+ retrieve=[require_auth],
59
+ create=[require_auth],
60
+ update=[require_auth],
61
+ delete=[require_auth],
62
+ )
63
+ ```
64
+
65
+ These are applied in combination with built-ins (e.g., unique_view, paginate) using decorate_view in the implementation.
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "2.0.0-rc5"
3
+ __version__ = "2.0.0-rc7"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
@@ -143,3 +143,76 @@ def unique_view(self: object | str, plural: bool = False):
143
143
  return func # Return original function (no wrapper)
144
144
 
145
145
  return decorator
146
+
147
+
148
+ def decorate_view(*decorators):
149
+ """
150
+ Compose and apply multiple decorators to a view (sync or async) without adding an extra wrapper.
151
+
152
+ This utility was introduced to support class-based patterns where Django Ninja’s
153
+ built-in `decorate_view` does not fit well. For APIs implemented with vanilla
154
+ Django Ninja (function-based style), you should continue using Django Ninja’s
155
+ native `decorate_view`.
156
+
157
+ Behavior:
158
+ - Applies decorators in the same order as Python’s stacking syntax:
159
+ @d1
160
+ @d2
161
+ is equivalent to: view = d1(d2(view))
162
+ - Supports both synchronous and asynchronous views.
163
+ - Ignores None values, enabling conditional decoration.
164
+ - Does not introduce an additional wrapper; composition depends on each
165
+ decorator for signature/metadata preservation (e.g., using functools.wraps).
166
+
167
+ *decorators: Decorator callables to apply to the target view. Any None values
168
+ are skipped.
169
+
170
+ Callable: A decorator that applies the provided decorators in Python stacking order.
171
+
172
+ Method usage in class-based patterns:
173
+
174
+ Args:
175
+ *decorators: Decorator callables to apply to the target view. Any None
176
+ values are skipped.
177
+
178
+ Returns:
179
+ A decorator that applies the provided decorators in Python stacking order.
180
+
181
+ Examples:
182
+ Basic usage:
183
+ class MyAPIViewSet(APIViewSet):
184
+ api = api
185
+ model = MyModel
186
+
187
+ def views(self):
188
+ @self.router.get('some-endpoint/')
189
+ @decorate_view(authenticate, log_request)
190
+ async def some_view(request):
191
+ ...
192
+
193
+ Conditional decoration (skips None):
194
+ class MyAPIViewSet(APIViewSet):
195
+ api = api
196
+ model = MyModel
197
+ cache_dec = cache_page(60) if settings.ENABLE_CACHE else None
198
+ def views(self):
199
+ @self.router.get('data/')
200
+ @decorate_view(self.cache_dec, authenticate)
201
+ async def data_view(request):
202
+ ...
203
+
204
+ Notes:
205
+ - Each decorator is applied in the order provided, with the first decorator
206
+ wrapping the result of the second, and so on.
207
+ - Ensure that each decorator is compatible with the view’s sync/async nature.
208
+ """
209
+
210
+ def _decorator(view):
211
+ wrapped = view
212
+ for dec in reversed(decorators):
213
+ if dec is None:
214
+ continue
215
+ wrapped = dec(wrapped)
216
+ return wrapped
217
+
218
+ return _decorator
@@ -4,11 +4,13 @@ from typing import Any
4
4
 
5
5
  import orjson
6
6
  from django.http import HttpRequest
7
+ from django.conf import settings
7
8
  from ninja.renderers import BaseRenderer
8
9
 
9
10
 
10
11
  class ORJSONRenderer(BaseRenderer):
11
12
  media_type = "application/json"
13
+ option = getattr(settings, "NINJA_AIO_ORJSON_RENDERER_OPTION", None)
12
14
 
13
15
  def render(self, request: HttpRequest, data: dict, *, response_status):
14
16
  try:
@@ -16,9 +18,13 @@ class ORJSONRenderer(BaseRenderer):
16
18
  for k, v in old_d.items():
17
19
  if isinstance(v, list):
18
20
  data |= {k: self.render_list(v)}
19
- return orjson.dumps(self.render_dict(data))
21
+ return self.dumps(self.render_dict(data))
20
22
  except AttributeError:
21
- return orjson.dumps(data)
23
+ return self.dumps(data)
24
+
25
+ @classmethod
26
+ def dumps(cls, data: dict) -> bytes:
27
+ return orjson.dumps(data, option=cls.option)
22
28
 
23
29
  @classmethod
24
30
  def render_list(cls, data: list[dict]) -> list[dict]:
@@ -1,4 +1,4 @@
1
- from typing import Optional, Type
1
+ from typing import List, Optional, Type
2
2
 
3
3
  from ninja import Schema
4
4
  from ninja_aio.types import ModelSerializerMeta
@@ -79,4 +79,12 @@ class QuerySchema(ModelQuerySetSchema):
79
79
 
80
80
  class QueryUtilBaseScopesSchema(BaseModel):
81
81
  READ: str = "read"
82
- QUERYSET_REQUEST: str = "queryset_request"
82
+ QUERYSET_REQUEST: str = "queryset_request"
83
+
84
+
85
+ class DecoratorsSchema(Schema):
86
+ list: Optional[List] = []
87
+ retrieve: Optional[List] = []
88
+ create: Optional[List] = []
89
+ update: Optional[List] = []
90
+ delete: Optional[List] = []
@@ -7,7 +7,7 @@ from django.http import HttpRequest
7
7
  from django.db.models import Model, QuerySet
8
8
  from pydantic import create_model
9
9
 
10
- from ninja_aio.schemas.helpers import ModelQuerySetSchema, QuerySchema
10
+ from ninja_aio.schemas.helpers import ModelQuerySetSchema, QuerySchema, DecoratorsSchema
11
11
 
12
12
  from .models import ModelSerializer, ModelUtil
13
13
  from .schemas import (
@@ -16,7 +16,7 @@ from .schemas import (
16
16
  )
17
17
  from .helpers.api import ManyToManyAPI
18
18
  from .types import ModelSerializerMeta, VIEW_TYPES
19
- from .decorators import unique_view
19
+ from .decorators import unique_view, decorate_view
20
20
 
21
21
  ERROR_CODES = frozenset({400, 401, 404, 428})
22
22
 
@@ -178,6 +178,7 @@ class APIViewSet:
178
178
  delete_docs = "Delete an object by its primary key."
179
179
  m2m_relations: list[M2MRelationSchema] = []
180
180
  m2m_auth: list | None = NOT_SET
181
+ extra_decorators: DecoratorsSchema = DecoratorsSchema()
181
182
 
182
183
  def __init__(self) -> None:
183
184
  self.error_codes = ERROR_CODES
@@ -309,7 +310,7 @@ class APIViewSet:
309
310
  description=self.create_docs,
310
311
  response={201: self.schema_out, self.error_codes: GenericMessageSchema},
311
312
  )
312
- @unique_view(self)
313
+ @decorate_view(unique_view(self), *self.extra_decorators.create)
313
314
  async def create(request: HttpRequest, data: self.schema_in): # type: ignore
314
315
  return 201, await self.model_util.create_s(request, data, self.schema_out)
315
316
 
@@ -330,8 +331,11 @@ class APIViewSet:
330
331
  self.error_codes: GenericMessageSchema,
331
332
  },
332
333
  )
333
- @unique_view(self, plural=True)
334
- @paginate(self.pagination_class)
334
+ @decorate_view(
335
+ paginate(self.pagination_class),
336
+ unique_view(self, plural=True),
337
+ *self.extra_decorators.list,
338
+ )
335
339
  async def list(
336
340
  request: HttpRequest,
337
341
  filters: Query[self.filters_schema] = None, # type: ignore
@@ -359,7 +363,7 @@ class APIViewSet:
359
363
  description=self.retrieve_docs,
360
364
  response={200: self.schema_out, self.error_codes: GenericMessageSchema},
361
365
  )
362
- @unique_view(self)
366
+ @decorate_view(unique_view(self), *self.extra_decorators.retrieve)
363
367
  async def retrieve(request: HttpRequest, pk: Path[self.path_schema]): # type: ignore
364
368
  query_data = self._get_query_data()
365
369
  return await self.model_util.read_s(
@@ -384,7 +388,7 @@ class APIViewSet:
384
388
  description=self.update_docs,
385
389
  response={200: self.schema_out, self.error_codes: GenericMessageSchema},
386
390
  )
387
- @unique_view(self)
391
+ @decorate_view(unique_view(self), *self.extra_decorators.update)
388
392
  async def update(
389
393
  request: HttpRequest,
390
394
  data: self.schema_update, # type: ignore
@@ -408,7 +412,7 @@ class APIViewSet:
408
412
  description=self.delete_docs,
409
413
  response={204: None, self.error_codes: GenericMessageSchema},
410
414
  )
411
- @unique_view(self)
415
+ @decorate_view(unique_view(self), *self.extra_decorators.delete)
412
416
  async def delete(request: HttpRequest, pk: Path[self.path_schema]): # type: ignore
413
417
  return 204, await self.model_util.delete_s(request, self._get_pk(pk))
414
418