django-ninja-aio-crud 2.0.0rc5__py3-none-any.whl → 2.0.0rc7__py3-none-any.whl

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.
@@ -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
@@ -1,21 +1,21 @@
1
- ninja_aio/__init__.py,sha256=hgSPEKupWCv5rcygFq1KE5oNzJIhRpaH3Mc6Qodk8IU,123
1
+ ninja_aio/__init__.py,sha256=b0K4gr4xA4QngOZ7PrtTuDcxgyk3yFTNwNxsCObFqug,123
2
2
  ninja_aio/api.py,sha256=SS1TYUiFkdYjfJLVy6GI90GOzvIHzPEeL-UcqWFRHkM,1684
3
3
  ninja_aio/auth.py,sha256=8jaEp7oEJvUUB9EuyE2fOYk-khyAaekT3i80E7AbgOA,5101
4
- ninja_aio/decorators.py,sha256=gswkwl1zWSpW8VxGCe8MlgXcHMg6Y7V1f2ertey9Tjo,5522
4
+ ninja_aio/decorators.py,sha256=BHoFIiqdIVMFqSxGh-F6WeZFo1xZK4ieDw3dzKfxZIM,8147
5
5
  ninja_aio/exceptions.py,sha256=1-iRbrloIyi0CR6Tcrn5YR4_LloA7PPohKIBaxXJ0-8,2596
6
6
  ninja_aio/models.py,sha256=aJlo5a64O4o-fB8QESLMUJpoA5kcjRJxPBiAIMxg46k,47652
7
7
  ninja_aio/parsers.py,sha256=e_4lGCPV7zs-HTqtdJTc8yQD2KPAn9njbL8nF_Mmgkc,153
8
- ninja_aio/renders.py,sha256=5TdSQI8e4x3Gb2tAw1AaxrbU-asVjf2chWMr8x2Tt80,1485
8
+ ninja_aio/renders.py,sha256=VtmSliRJyZ6gjyoib8AXMVUBYF1jPNsiceCHujI_mAs,1699
9
9
  ninja_aio/types.py,sha256=TJSGlA7bt4g9fvPhJ7gzH5tKbLagPmZUzfgttEOp4xs,468
10
- ninja_aio/views.py,sha256=h_embl52o5WOVbhbB05vkNKEGOm-s4PvdAjy81-6ISw,16424
10
+ ninja_aio/views.py,sha256=8vMFw-8au9O0Hpf-79jvB47PwokCzm7P8UY0DDWpN5A,16786
11
11
  ninja_aio/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  ninja_aio/helpers/api.py,sha256=kgHxYPfbV6kQbp8Z4qMwQtwFiFIcRAenxI9MDowGtis,20181
13
13
  ninja_aio/helpers/query.py,sha256=tE8RjXvSig-WB_0LRQ0LqoE4G_HMHsu0Na5QzTNIm6U,4262
14
14
  ninja_aio/schemas/__init__.py,sha256=iLBwHg0pmL9k_UkIui5Q8QIl_gO4fgxSv2JHxDzqnSI,549
15
15
  ninja_aio/schemas/api.py,sha256=-VwXhBRhmMsZLIAmWJ-P7tB5klxXS75eukjabeKKYsc,360
16
16
  ninja_aio/schemas/generics.py,sha256=frjJsKJMAdM_NdNKv-9ddZNGxYy5PNzjIRGtuycgr-w,112
17
- ninja_aio/schemas/helpers.py,sha256=al1G5-CdB9CF3eIPdZcUSE7vKRJiTU5g1ltoPhI2Onw,2622
18
- django_ninja_aio_crud-2.0.0rc5.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
19
- django_ninja_aio_crud-2.0.0rc5.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
20
- django_ninja_aio_crud-2.0.0rc5.dist-info/METADATA,sha256=odaxrUtTHc6kMBn_037arcbP4iz1GBMchNfUgmaJtts,8675
21
- django_ninja_aio_crud-2.0.0rc5.dist-info/RECORD,,
17
+ ninja_aio/schemas/helpers.py,sha256=rmE0D15lJg95Unv8PU44Hbf0VDTcErMCZZFG3D_znTo,2823
18
+ django_ninja_aio_crud-2.0.0rc7.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
19
+ django_ninja_aio_crud-2.0.0rc7.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
20
+ django_ninja_aio_crud-2.0.0rc7.dist-info/METADATA,sha256=bxFgTP1QMN1oSLPw4fhV3Nu9-U90pNN92FoiVqJq6hY,8675
21
+ django_ninja_aio_crud-2.0.0rc7.dist-info/RECORD,,
ninja_aio/__init__.py CHANGED
@@ -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
 
ninja_aio/decorators.py CHANGED
@@ -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
ninja_aio/renders.py CHANGED
@@ -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] = []
ninja_aio/views.py CHANGED
@@ -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