django-ninja-aio-crud 2.4.0__py3-none-any.whl → 2.5.0__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.
- {django_ninja_aio_crud-2.4.0.dist-info → django_ninja_aio_crud-2.5.0.dist-info}/METADATA +26 -29
- {django_ninja_aio_crud-2.4.0.dist-info → django_ninja_aio_crud-2.5.0.dist-info}/RECORD +9 -9
- ninja_aio/__init__.py +1 -1
- ninja_aio/helpers/query.py +2 -2
- ninja_aio/models/serializers.py +194 -44
- ninja_aio/models/utils.py +57 -9
- ninja_aio/views/api.py +18 -10
- {django_ninja_aio_crud-2.4.0.dist-info → django_ninja_aio_crud-2.5.0.dist-info}/WHEEL +0 -0
- {django_ninja_aio_crud-2.4.0.dist-info → django_ninja_aio_crud-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-ninja-aio-crud
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
4
4
|
Summary: Django Ninja AIO CRUD - Rest Framework
|
|
5
5
|
Author: Giuseppe Casillo
|
|
6
6
|
Requires-Python: >=3.10, <3.15
|
|
@@ -48,6 +48,7 @@ Provides-Extra: test
|
|
|
48
48
|
|
|
49
49
|
## ✨ Features
|
|
50
50
|
|
|
51
|
+
- Serializer (Meta-driven) first-class: dynamic schemas for existing Django models without inheriting ModelSerializer
|
|
51
52
|
- Async CRUD ViewSets (create, list, retrieve, update, delete)
|
|
52
53
|
- Automatic Pydantic schemas from `ModelSerializer` (read/create/update)
|
|
53
54
|
- Dynamic query params (runtime schema via `pydantic.create_model`)
|
|
@@ -61,17 +62,35 @@ Provides-Extra: test
|
|
|
61
62
|
|
|
62
63
|
---
|
|
63
64
|
|
|
64
|
-
##
|
|
65
|
+
## 🚀 Quick Start (Serializer)
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
If you already have Django models, start with the Meta-driven Serializer for instant CRUD without changing model base classes.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from ninja_aio.models import serializers
|
|
71
|
+
from ninja_aio.views import APIViewSet
|
|
72
|
+
from ninja_aio import NinjaAIO
|
|
73
|
+
from . import models
|
|
74
|
+
|
|
75
|
+
class BookSerializer(serializers.Serializer):
|
|
76
|
+
class Meta:
|
|
77
|
+
model = models.Book
|
|
78
|
+
schema_in = serializers.SchemaModelConfig(fields=["title", "published"])
|
|
79
|
+
schema_out = serializers.SchemaModelConfig(fields=["id", "title", "published"])
|
|
80
|
+
schema_update = serializers.SchemaModelConfig(optionals=[("title", str), ("published", bool)])
|
|
81
|
+
|
|
82
|
+
api = NinjaAIO()
|
|
83
|
+
|
|
84
|
+
@api.viewset(models.Book)
|
|
85
|
+
class BookViewSet(APIViewSet):
|
|
86
|
+
serializer_class = BookSerializer
|
|
68
87
|
```
|
|
69
88
|
|
|
70
|
-
|
|
89
|
+
Visit `/docs` → CRUD endpoints ready.
|
|
71
90
|
|
|
72
91
|
---
|
|
73
92
|
|
|
74
|
-
## 🚀 Quick Start
|
|
93
|
+
## 🚀 Quick Start (ModelSerializer)
|
|
75
94
|
|
|
76
95
|
models.py
|
|
77
96
|
|
|
@@ -264,29 +283,7 @@ class BookViewSet(APIViewSet):
|
|
|
264
283
|
|
|
265
284
|
## Meta-driven Serializer (for vanilla Django models)
|
|
266
285
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
Example:
|
|
270
|
-
|
|
271
|
-
```python
|
|
272
|
-
from ninja_aio.models import serializers
|
|
273
|
-
from . import models
|
|
274
|
-
|
|
275
|
-
class BookSerializer(serializers.Serializer):
|
|
276
|
-
class Meta:
|
|
277
|
-
model = models.Book
|
|
278
|
-
schema_in = serializers.SchemaModelConfig(fields=["title", "published"])
|
|
279
|
-
schema_out = serializers.SchemaModelConfig(fields=["id", "title", "published"])
|
|
280
|
-
schema_update = serializers.SchemaModelConfig(optionals=[("title", str), ("published", bool)])
|
|
281
|
-
|
|
282
|
-
@api.viewset(models.Book)
|
|
283
|
-
class BookViewSet(APIViewSet):
|
|
284
|
-
serializer_class = BookSerializer
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
- Works without modifying existing models
|
|
288
|
-
- Supports nested relations via relations_serializers
|
|
289
|
-
- APIViewSet will auto-generate missing schemas from the serializer
|
|
286
|
+
Moved above as the primary quick start.
|
|
290
287
|
|
|
291
288
|
---
|
|
292
289
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
ninja_aio/__init__.py,sha256=
|
|
1
|
+
ninja_aio/__init__.py,sha256=LBFfoQaFXisUD0DcYa1xZV-zyiAJqx_3QAY-hyxB6OA,119
|
|
2
2
|
ninja_aio/api.py,sha256=tuC7vdvn7s1GkCnSFy9Kn1zv0glZfYptRQVvo8ZRtGQ,2429
|
|
3
3
|
ninja_aio/auth.py,sha256=4sWdFPjKiQgUL1d_CSGDblVjnY5ptP6LQha6XXdluJA,9157
|
|
4
4
|
ninja_aio/exceptions.py,sha256=_3xFqfFCOfrrMhSA0xbMqgXy8R0UQjhXaExrFvaDAjY,3891
|
|
@@ -12,18 +12,18 @@ ninja_aio/factory/__init__.py,sha256=IdH2z1ZZpv_IqonaDfVo7IsMzkgop6lHqz42RphUYBU
|
|
|
12
12
|
ninja_aio/factory/operations.py,sha256=OgWGqq4WJ4arSQrH9FGAby9kx-HTdS7MOITxHdYMk18,12051
|
|
13
13
|
ninja_aio/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
ninja_aio/helpers/api.py,sha256=YMzuZ4-ZpUrJBQIabE26gb_GYwsH2rVosWRE95YfdPQ,20775
|
|
15
|
-
ninja_aio/helpers/query.py,sha256=
|
|
15
|
+
ninja_aio/helpers/query.py,sha256=lzaH-htswoJVRT-W736HGMkpMba1VmN98TBLv5cZx9Q,4549
|
|
16
16
|
ninja_aio/models/__init__.py,sha256=L3UQnQAlKoI3F7jinadL-Nn55hkPvnSRPYW0JtnbWFo,114
|
|
17
|
-
ninja_aio/models/serializers.py,sha256
|
|
18
|
-
ninja_aio/models/utils.py,sha256=
|
|
17
|
+
ninja_aio/models/serializers.py,sha256=-6NsyTJBdUQl3fkAusC7Kx_2keMRV7CNI9yAlH_fQns,29340
|
|
18
|
+
ninja_aio/models/utils.py,sha256=P-YfbVyzUfxm_s1BrgSd6Zs0HIGdZ79PU1qM0Ud9-Xs,30492
|
|
19
19
|
ninja_aio/schemas/__init__.py,sha256=iLBwHg0pmL9k_UkIui5Q8QIl_gO4fgxSv2JHxDzqnSI,549
|
|
20
20
|
ninja_aio/schemas/api.py,sha256=-VwXhBRhmMsZLIAmWJ-P7tB5klxXS75eukjabeKKYsc,360
|
|
21
21
|
ninja_aio/schemas/generics.py,sha256=frjJsKJMAdM_NdNKv-9ddZNGxYy5PNzjIRGtuycgr-w,112
|
|
22
22
|
ninja_aio/schemas/helpers.py,sha256=W6IeHi5Tmbjh3FXwDYqjqlLBTVj5uTYq3_JVkNUWayo,7355
|
|
23
23
|
ninja_aio/views/__init__.py,sha256=DEzjWA6y3WF0V10nNF8eEurLNEodgxKzyFd09AqVp3s,148
|
|
24
|
-
ninja_aio/views/api.py,sha256=
|
|
24
|
+
ninja_aio/views/api.py,sha256=xdzxM1XOPTZmGrRxlIz1QH4UKgxRfg9ls-geatTJrXI,21179
|
|
25
25
|
ninja_aio/views/mixins.py,sha256=Jh6BG8Cs823nurVlODlzCquTxKrLH7Pmo5udPqUGZek,11378
|
|
26
|
-
django_ninja_aio_crud-2.
|
|
27
|
-
django_ninja_aio_crud-2.
|
|
28
|
-
django_ninja_aio_crud-2.
|
|
29
|
-
django_ninja_aio_crud-2.
|
|
26
|
+
django_ninja_aio_crud-2.5.0.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
|
|
27
|
+
django_ninja_aio_crud-2.5.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
28
|
+
django_ninja_aio_crud-2.5.0.dist-info/METADATA,sha256=Gyk2TCPm3oQaT9YsZrXC7btzZzmDmntKyk_wz0M6Hfg,9963
|
|
29
|
+
django_ninja_aio_crud-2.5.0.dist-info/RECORD,,
|
ninja_aio/__init__.py
CHANGED
ninja_aio/helpers/query.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from ninja_aio.
|
|
1
|
+
from ninja_aio.models.serializers import Serializer, ModelSerializer
|
|
2
2
|
from ninja_aio.schemas.helpers import (
|
|
3
3
|
ModelQuerySetSchema,
|
|
4
4
|
QueryUtilBaseScopesSchema,
|
|
@@ -52,7 +52,7 @@ class QueryUtil:
|
|
|
52
52
|
|
|
53
53
|
SCOPES: QueryUtilBaseScopesSchema
|
|
54
54
|
|
|
55
|
-
def __init__(self, model:
|
|
55
|
+
def __init__(self, model: ModelSerializer | Serializer):
|
|
56
56
|
"""Initialize QueryUtil, resolving base and extra scope configurations for a model."""
|
|
57
57
|
self.model = model
|
|
58
58
|
self._configuration = getattr(self.model, "QuerySet", None)
|
ninja_aio/models/serializers.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any,
|
|
1
|
+
from typing import Any, List, Optional
|
|
2
2
|
import warnings
|
|
3
3
|
|
|
4
4
|
from django.conf import settings
|
|
@@ -19,7 +19,6 @@ from ninja_aio.schemas.helpers import (
|
|
|
19
19
|
ModelQuerySetSchema,
|
|
20
20
|
ModelQuerySetExtraSchema,
|
|
21
21
|
)
|
|
22
|
-
from ninja_aio.helpers.query import QueryUtil
|
|
23
22
|
|
|
24
23
|
|
|
25
24
|
class BaseSerializer:
|
|
@@ -36,6 +35,28 @@ class BaseSerializer:
|
|
|
36
35
|
- _get_relations_serializers(): optional mapping of relation field -> serializer (may be empty)
|
|
37
36
|
"""
|
|
38
37
|
|
|
38
|
+
class QuerySet:
|
|
39
|
+
"""
|
|
40
|
+
Configuration container describing how to build query schemas for a model.
|
|
41
|
+
Purpose
|
|
42
|
+
-------
|
|
43
|
+
Describes which fields and extras are available when querying for model
|
|
44
|
+
instances. A factory/metaclass can read this configuration to generate
|
|
45
|
+
Pydantic / Ninja query schemas.
|
|
46
|
+
Attributes
|
|
47
|
+
----------
|
|
48
|
+
read : ModelQuerySetSchema
|
|
49
|
+
Schema configuration for read operations.
|
|
50
|
+
queryset_request : ModelQuerySetSchema
|
|
51
|
+
Schema configuration for queryset_request hook.
|
|
52
|
+
extras : list[ModelQuerySetExtraSchema]
|
|
53
|
+
Additional computed / synthetic query parameters.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
read = ModelQuerySetSchema()
|
|
57
|
+
queryset_request = ModelQuerySetSchema()
|
|
58
|
+
extras: list[ModelQuerySetExtraSchema] = []
|
|
59
|
+
|
|
39
60
|
@classmethod
|
|
40
61
|
def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
|
|
41
62
|
# Subclasses provide implementation.
|
|
@@ -199,9 +220,10 @@ class BaseSerializer:
|
|
|
199
220
|
reverse_rels: list[tuple] = []
|
|
200
221
|
rels: list[tuple] = []
|
|
201
222
|
relations_serializers = cls._get_relations_serializers() or {}
|
|
223
|
+
model = cls._get_model()
|
|
202
224
|
|
|
203
225
|
for f in cls.get_fields("read"):
|
|
204
|
-
field_obj = getattr(
|
|
226
|
+
field_obj = getattr(model, f)
|
|
205
227
|
is_reverse = isinstance(
|
|
206
228
|
field_obj,
|
|
207
229
|
(
|
|
@@ -217,7 +239,7 @@ class BaseSerializer:
|
|
|
217
239
|
# If explicit relation serializers are declared, require mapping presence.
|
|
218
240
|
if (
|
|
219
241
|
is_reverse
|
|
220
|
-
and not isinstance(
|
|
242
|
+
and not isinstance(model, ModelSerializerMeta)
|
|
221
243
|
and f not in relations_serializers
|
|
222
244
|
and not getattr(settings, "NINJA_AIO_TESTING", False)
|
|
223
245
|
):
|
|
@@ -393,41 +415,17 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
|
|
|
393
415
|
lifecycle hooks and query utilities.
|
|
394
416
|
"""
|
|
395
417
|
|
|
396
|
-
util: ClassVar
|
|
397
|
-
query_util: ClassVar[QueryUtil]
|
|
398
|
-
|
|
399
|
-
class Meta:
|
|
400
|
-
abstract = True
|
|
401
|
-
|
|
402
418
|
def __init_subclass__(cls, **kwargs):
|
|
403
419
|
super().__init_subclass__(**kwargs)
|
|
404
420
|
from ninja_aio.models.utils import ModelUtil
|
|
421
|
+
from ninja_aio.helpers.query import QueryUtil
|
|
405
422
|
|
|
406
423
|
# Bind a ModelUtil instance to the subclass for convenient access
|
|
407
424
|
cls.util = ModelUtil(cls)
|
|
408
425
|
cls.query_util = QueryUtil(cls)
|
|
409
426
|
|
|
410
|
-
class
|
|
411
|
-
|
|
412
|
-
Configuration container describing how to build query schemas for a model.
|
|
413
|
-
Purpose
|
|
414
|
-
-------
|
|
415
|
-
Describes which fields and extras are available when querying for model
|
|
416
|
-
instances. A factory/metaclass can read this configuration to generate
|
|
417
|
-
Pydantic / Ninja query schemas.
|
|
418
|
-
Attributes
|
|
419
|
-
----------
|
|
420
|
-
read : ModelQuerySetSchema
|
|
421
|
-
Schema configuration for read operations.
|
|
422
|
-
queryset_request : ModelQuerySetSchema
|
|
423
|
-
Schema configuration for queryset_request hook.
|
|
424
|
-
extras : list[ModelQuerySetExtraSchema]
|
|
425
|
-
Additional computed / synthetic query parameters.
|
|
426
|
-
"""
|
|
427
|
-
|
|
428
|
-
read = ModelQuerySetSchema()
|
|
429
|
-
queryset_request = ModelQuerySetSchema()
|
|
430
|
-
extras: list[ModelQuerySetExtraSchema] = []
|
|
427
|
+
class Meta:
|
|
428
|
+
abstract = True
|
|
431
429
|
|
|
432
430
|
class CreateSerializer:
|
|
433
431
|
"""Configuration container describing how to build a create (input) schema for a model.
|
|
@@ -674,20 +672,26 @@ class Serializer(BaseSerializer):
|
|
|
674
672
|
schema components during read schema generation.
|
|
675
673
|
"""
|
|
676
674
|
|
|
675
|
+
def __init_subclass__(cls, **kwargs):
|
|
676
|
+
super().__init_subclass__(**kwargs)
|
|
677
|
+
from ninja_aio.models.utils import ModelUtil
|
|
678
|
+
from ninja_aio.helpers.query import QueryUtil
|
|
679
|
+
|
|
680
|
+
cls.model = cls._get_model()
|
|
681
|
+
cls.schema_in = cls.generate_create_s()
|
|
682
|
+
cls.schema_out = cls.generate_read_s()
|
|
683
|
+
cls.schema_update = cls.generate_update_s()
|
|
684
|
+
cls.schema_related = cls.generate_related_s()
|
|
685
|
+
cls.util = ModelUtil(cls._get_model(), serializer_class=cls)
|
|
686
|
+
cls.query_util = QueryUtil(cls)
|
|
687
|
+
|
|
677
688
|
class Meta:
|
|
678
689
|
model: models.Model = None
|
|
679
|
-
schema_in: SchemaModelConfig = None
|
|
680
|
-
schema_out: SchemaModelConfig = None
|
|
681
|
-
schema_update: SchemaModelConfig = None
|
|
690
|
+
schema_in: Optional[SchemaModelConfig] = None
|
|
691
|
+
schema_out: Optional[SchemaModelConfig] = None
|
|
692
|
+
schema_update: Optional[SchemaModelConfig] = None
|
|
682
693
|
relations_serializers: dict[str, "Serializer"] = {}
|
|
683
694
|
|
|
684
|
-
def __init__(self):
|
|
685
|
-
self.model = self._validate_model()
|
|
686
|
-
self.schema_in = self.generate_create_s()
|
|
687
|
-
self.schema_out = self.generate_read_s()
|
|
688
|
-
self.schema_update = self.generate_update_s()
|
|
689
|
-
self.schema_related = self.generate_related_s()
|
|
690
|
-
|
|
691
695
|
@classmethod
|
|
692
696
|
def _get_meta_data(cls, attr_name: str) -> Any:
|
|
693
697
|
return getattr(cls.Meta, attr_name, None)
|
|
@@ -702,7 +706,7 @@ class Serializer(BaseSerializer):
|
|
|
702
706
|
return relations_serializers or {}
|
|
703
707
|
|
|
704
708
|
@classmethod
|
|
705
|
-
def _get_schema_meta(cls, schema_type: str) -> SchemaModelConfig:
|
|
709
|
+
def _get_schema_meta(cls, schema_type: str) -> SchemaModelConfig | None:
|
|
706
710
|
match schema_type:
|
|
707
711
|
case "in":
|
|
708
712
|
return cls._get_meta_data("schema_in")
|
|
@@ -710,7 +714,8 @@ class Serializer(BaseSerializer):
|
|
|
710
714
|
return cls._get_meta_data("schema_out")
|
|
711
715
|
case "update":
|
|
712
716
|
return cls._get_meta_data("schema_update")
|
|
713
|
-
|
|
717
|
+
case _:
|
|
718
|
+
return None
|
|
714
719
|
|
|
715
720
|
@classmethod
|
|
716
721
|
def _validate_model(cls):
|
|
@@ -733,6 +738,151 @@ class Serializer(BaseSerializer):
|
|
|
733
738
|
return []
|
|
734
739
|
return getattr(schema, f_type, []) or []
|
|
735
740
|
|
|
741
|
+
|
|
736
742
|
@classmethod
|
|
737
743
|
async def queryset_request(cls, request: HttpRequest):
|
|
738
|
-
return cls.
|
|
744
|
+
return cls.query_util.apply_queryset_optimizations(
|
|
745
|
+
queryset=cls.model._default_manager.all(),
|
|
746
|
+
scope=cls.query_util.SCOPES.QUERYSET_REQUEST,
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
async def post_create(self, instance: models.Model) -> None:
|
|
750
|
+
"""
|
|
751
|
+
Async hook executed after first persistence (create path).
|
|
752
|
+
"""
|
|
753
|
+
pass
|
|
754
|
+
|
|
755
|
+
async def custom_actions(self, payload: dict[str, Any], instance: models.Model):
|
|
756
|
+
"""
|
|
757
|
+
Async hook for reacting to provided custom (synthetic) fields.
|
|
758
|
+
|
|
759
|
+
Parameters
|
|
760
|
+
----------
|
|
761
|
+
payload : dict
|
|
762
|
+
Custom field name/value pairs.
|
|
763
|
+
"""
|
|
764
|
+
pass
|
|
765
|
+
|
|
766
|
+
async def save(self, instance: models.Model) -> models.Model:
|
|
767
|
+
"""
|
|
768
|
+
Async helper to save a model instance with lifecycle hooks.
|
|
769
|
+
|
|
770
|
+
Parameters
|
|
771
|
+
----------
|
|
772
|
+
instance : models.Model
|
|
773
|
+
The model instance to save.
|
|
774
|
+
"""
|
|
775
|
+
creation = instance._state.adding
|
|
776
|
+
if creation:
|
|
777
|
+
self.on_create_before_save(instance)
|
|
778
|
+
self.before_save(instance)
|
|
779
|
+
await instance.asave()
|
|
780
|
+
if creation:
|
|
781
|
+
self.on_create_after_save(instance)
|
|
782
|
+
self.after_save(instance)
|
|
783
|
+
return instance
|
|
784
|
+
|
|
785
|
+
async def create(self, payload: dict[str, Any]) -> models.Model:
|
|
786
|
+
"""
|
|
787
|
+
Create a new model instance from the provided payload.
|
|
788
|
+
|
|
789
|
+
Parameters
|
|
790
|
+
----------
|
|
791
|
+
payload : dict
|
|
792
|
+
Input data.
|
|
793
|
+
|
|
794
|
+
Returns
|
|
795
|
+
-------
|
|
796
|
+
models.Model
|
|
797
|
+
Created model instance.
|
|
798
|
+
"""
|
|
799
|
+
instance: models.Model = self.model(**payload)
|
|
800
|
+
return await self.save(instance)
|
|
801
|
+
|
|
802
|
+
async def update(
|
|
803
|
+
self, instance: models.Model, payload: dict[str, Any]
|
|
804
|
+
) -> models.Model:
|
|
805
|
+
"""
|
|
806
|
+
Update an existing model instance with the provided payload.
|
|
807
|
+
|
|
808
|
+
Parameters
|
|
809
|
+
----------
|
|
810
|
+
instance : models.Model
|
|
811
|
+
The model instance to update.
|
|
812
|
+
payload : dict
|
|
813
|
+
Input data.
|
|
814
|
+
|
|
815
|
+
Returns
|
|
816
|
+
-------
|
|
817
|
+
models.Model
|
|
818
|
+
Updated model instance.
|
|
819
|
+
"""
|
|
820
|
+
for attr, value in payload.items():
|
|
821
|
+
setattr(instance, attr, value)
|
|
822
|
+
return await self.save(instance)
|
|
823
|
+
|
|
824
|
+
async def model_dump(self, instance: models.Model) -> dict[str, Any]:
|
|
825
|
+
"""
|
|
826
|
+
Serialize a model instance to a dictionary using the Out schema.
|
|
827
|
+
|
|
828
|
+
Parameters
|
|
829
|
+
----------
|
|
830
|
+
instance : models.Model
|
|
831
|
+
The model instance to serialize.
|
|
832
|
+
|
|
833
|
+
Returns
|
|
834
|
+
-------
|
|
835
|
+
dict
|
|
836
|
+
Serialized data.
|
|
837
|
+
"""
|
|
838
|
+
return await self.model_util.read_s(schema=self.schema_out, instance=instance)
|
|
839
|
+
|
|
840
|
+
async def models_dump(
|
|
841
|
+
self, instances: models.QuerySet[models.Model]
|
|
842
|
+
) -> list[dict[str, Any]]:
|
|
843
|
+
"""
|
|
844
|
+
Serialize a list of model instances to a list of dictionaries using the Out schema.
|
|
845
|
+
|
|
846
|
+
Parameters
|
|
847
|
+
----------
|
|
848
|
+
instances : list[models.Model]
|
|
849
|
+
The list of model instances to serialize.
|
|
850
|
+
|
|
851
|
+
Returns
|
|
852
|
+
-------
|
|
853
|
+
list[dict]
|
|
854
|
+
List of serialized data.
|
|
855
|
+
"""
|
|
856
|
+
return await self.model_util.list_read_s(
|
|
857
|
+
schema=self.schema_out, instances=instances
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
def after_save(self, instance: models.Model):
|
|
861
|
+
"""
|
|
862
|
+
Sync hook executed after any save (create or update).
|
|
863
|
+
"""
|
|
864
|
+
pass
|
|
865
|
+
|
|
866
|
+
def before_save(self, instance: models.Model):
|
|
867
|
+
"""
|
|
868
|
+
Sync hook executed before any save (create or update).
|
|
869
|
+
"""
|
|
870
|
+
pass
|
|
871
|
+
|
|
872
|
+
def on_create_after_save(self, instance: models.Model):
|
|
873
|
+
"""
|
|
874
|
+
Sync hook executed only after initial creation save.
|
|
875
|
+
"""
|
|
876
|
+
pass
|
|
877
|
+
|
|
878
|
+
def on_create_before_save(self, instance: models.Model):
|
|
879
|
+
"""
|
|
880
|
+
Sync hook executed only before initial creation save.
|
|
881
|
+
"""
|
|
882
|
+
pass
|
|
883
|
+
|
|
884
|
+
def on_delete(self, instance: models.Model):
|
|
885
|
+
"""
|
|
886
|
+
Sync hook executed after delete.
|
|
887
|
+
"""
|
|
888
|
+
pass
|
ninja_aio/models/utils.py
CHANGED
|
@@ -22,6 +22,7 @@ from ninja_aio.exceptions import SerializeError, NotFoundError
|
|
|
22
22
|
from ninja_aio.models.serializers import ModelSerializer
|
|
23
23
|
from ninja_aio.types import ModelSerializerMeta
|
|
24
24
|
from ninja_aio.schemas.helpers import (
|
|
25
|
+
ModelQuerySetSchema,
|
|
25
26
|
QuerySchema,
|
|
26
27
|
ObjectQuerySchema,
|
|
27
28
|
ObjectsQuerySchema,
|
|
@@ -112,6 +113,18 @@ class ModelUtil:
|
|
|
112
113
|
raise ConfigError(
|
|
113
114
|
"ModelUtil cannot accept both model and serializer_class if the model is a ModelSerializer."
|
|
114
115
|
)
|
|
116
|
+
self.serializer: Serializer = serializer_class() if serializer_class else None
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def with_serializer(self) -> bool:
|
|
120
|
+
"""
|
|
121
|
+
Indicates if a serializer_class is associated.
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
bool
|
|
126
|
+
"""
|
|
127
|
+
return self.serializer_class is not None
|
|
115
128
|
|
|
116
129
|
@property
|
|
117
130
|
def pk_field_type(self):
|
|
@@ -445,6 +458,23 @@ class ModelUtil:
|
|
|
445
458
|
|
|
446
459
|
return queryset
|
|
447
460
|
|
|
461
|
+
def _get_read_optimizations(self) -> ModelQuerySetSchema:
|
|
462
|
+
"""
|
|
463
|
+
Retrieve read optimizations from model or serializer class.
|
|
464
|
+
|
|
465
|
+
Returns
|
|
466
|
+
-------
|
|
467
|
+
ModelQuerySetSchema
|
|
468
|
+
Read optimization configuration.
|
|
469
|
+
"""
|
|
470
|
+
if isinstance(self.model, ModelSerializerMeta):
|
|
471
|
+
return getattr(self.model.QuerySet, "read", ModelQuerySetSchema())
|
|
472
|
+
if self.with_serializer:
|
|
473
|
+
return getattr(
|
|
474
|
+
self.serializer_class.QuerySet, "read", ModelQuerySetSchema()
|
|
475
|
+
)
|
|
476
|
+
return ModelQuerySetSchema()
|
|
477
|
+
|
|
448
478
|
def get_reverse_relations(self) -> list[str]:
|
|
449
479
|
"""
|
|
450
480
|
Discover reverse relation names for safe prefetching.
|
|
@@ -454,7 +484,9 @@ class ModelUtil:
|
|
|
454
484
|
list[str]
|
|
455
485
|
Relation attribute names.
|
|
456
486
|
"""
|
|
457
|
-
reverse_rels =
|
|
487
|
+
reverse_rels = self._get_read_optimizations().prefetch_related.copy()
|
|
488
|
+
if reverse_rels:
|
|
489
|
+
return reverse_rels
|
|
458
490
|
for f in self.serializable_fields:
|
|
459
491
|
field_obj = getattr(self.model, f)
|
|
460
492
|
if isinstance(field_obj, ManyToManyDescriptor):
|
|
@@ -476,7 +508,9 @@ class ModelUtil:
|
|
|
476
508
|
list[str]
|
|
477
509
|
Relation attribute names.
|
|
478
510
|
"""
|
|
479
|
-
select_rels =
|
|
511
|
+
select_rels = self._get_read_optimizations().select_related.copy()
|
|
512
|
+
if select_rels:
|
|
513
|
+
return select_rels
|
|
480
514
|
for f in self.serializable_fields:
|
|
481
515
|
field_obj = getattr(self.model, f)
|
|
482
516
|
if isinstance(field_obj, ForwardManyToOneDescriptor):
|
|
@@ -615,7 +649,8 @@ class ModelUtil:
|
|
|
615
649
|
"""
|
|
616
650
|
payload = data.model_dump(mode="json")
|
|
617
651
|
|
|
618
|
-
is_serializer = isinstance(self.model, ModelSerializerMeta)
|
|
652
|
+
is_serializer = isinstance(self.model, ModelSerializerMeta) or self.with_serializer
|
|
653
|
+
serializer = self.serializer if self.with_serializer else self.model
|
|
619
654
|
|
|
620
655
|
# Collect custom and optional fields (only if ModelSerializerMeta)
|
|
621
656
|
customs: dict[str, Any] = {}
|
|
@@ -624,10 +659,10 @@ class ModelUtil:
|
|
|
624
659
|
customs = {
|
|
625
660
|
k: v
|
|
626
661
|
for k, v in payload.items()
|
|
627
|
-
if
|
|
662
|
+
if serializer.is_custom(k) and k not in self.model_fields
|
|
628
663
|
}
|
|
629
664
|
optionals = [
|
|
630
|
-
k for k, v in payload.items() if
|
|
665
|
+
k for k, v in payload.items() if serializer.is_optional(k) and v is None
|
|
631
666
|
]
|
|
632
667
|
|
|
633
668
|
skip_keys = set()
|
|
@@ -636,8 +671,8 @@ class ModelUtil:
|
|
|
636
671
|
skip_keys = {
|
|
637
672
|
k
|
|
638
673
|
for k, v in payload.items()
|
|
639
|
-
if (
|
|
640
|
-
or (
|
|
674
|
+
if (serializer.is_custom(k) and k not in self.model_fields)
|
|
675
|
+
or (serializer.is_optional(k) and v is None)
|
|
641
676
|
}
|
|
642
677
|
|
|
643
678
|
# Process payload fields
|
|
@@ -674,10 +709,19 @@ class ModelUtil:
|
|
|
674
709
|
Serialized created object.
|
|
675
710
|
"""
|
|
676
711
|
payload, customs = await self.parse_input_data(request, data)
|
|
677
|
-
pk = (
|
|
712
|
+
pk = (
|
|
713
|
+
(await self.model.objects.acreate(**payload)).pk
|
|
714
|
+
if not self.with_serializer
|
|
715
|
+
else (await self.serializer.create(payload)).pk
|
|
716
|
+
)
|
|
678
717
|
obj = await self.get_object(request, pk)
|
|
679
718
|
if isinstance(self.model, ModelSerializerMeta):
|
|
680
719
|
await asyncio.gather(obj.custom_actions(customs), obj.post_create())
|
|
720
|
+
if self.with_serializer:
|
|
721
|
+
await asyncio.gather(
|
|
722
|
+
self.serializer.custom_actions(customs, obj),
|
|
723
|
+
self.serializer.post_create(obj),
|
|
724
|
+
)
|
|
681
725
|
return await self.read_s(obj_schema, request, obj)
|
|
682
726
|
|
|
683
727
|
async def _read_s(
|
|
@@ -871,7 +915,11 @@ class ModelUtil:
|
|
|
871
915
|
setattr(obj, k, v)
|
|
872
916
|
if isinstance(self.model, ModelSerializerMeta):
|
|
873
917
|
await obj.custom_actions(customs)
|
|
874
|
-
|
|
918
|
+
if self.with_serializer:
|
|
919
|
+
await self.serializer.custom_actions(customs, obj)
|
|
920
|
+
await self.serializer.save(obj)
|
|
921
|
+
else:
|
|
922
|
+
await obj.asave()
|
|
875
923
|
updated_object = await self.get_object(request, pk)
|
|
876
924
|
return await self.read_s(obj_schema, request, updated_object)
|
|
877
925
|
|
ninja_aio/views/api.py
CHANGED
|
@@ -17,7 +17,7 @@ from ninja_aio.schemas import (
|
|
|
17
17
|
)
|
|
18
18
|
from ninja_aio.helpers.api import ManyToManyAPI
|
|
19
19
|
from ninja_aio.types import ModelSerializerMeta, VIEW_TYPES
|
|
20
|
-
from ninja_aio.decorators import unique_view, decorate_view
|
|
20
|
+
from ninja_aio.decorators import unique_view, decorate_view, aatomic
|
|
21
21
|
from ninja_aio.models import serializers
|
|
22
22
|
|
|
23
23
|
ERROR_CODES = frozenset({400, 401, 404})
|
|
@@ -241,6 +241,8 @@ class APIViewSet(API):
|
|
|
241
241
|
m2m_relations: list[M2MRelationSchema] = []
|
|
242
242
|
m2m_auth: list | None = NOT_SET
|
|
243
243
|
extra_decorators: DecoratorsSchema = DecoratorsSchema()
|
|
244
|
+
model_verbose_name: str = ""
|
|
245
|
+
model_verbose_name_plural: str = ""
|
|
244
246
|
|
|
245
247
|
def __init__(
|
|
246
248
|
self,
|
|
@@ -260,7 +262,13 @@ class APIViewSet(API):
|
|
|
260
262
|
self.schema_out, self.schema_in, self.schema_update = self.get_schemas()
|
|
261
263
|
self.path_schema = self._generate_path_schema()
|
|
262
264
|
self.filters_schema = self._generate_filters_schema()
|
|
263
|
-
self.model_verbose_name =
|
|
265
|
+
self.model_verbose_name = (
|
|
266
|
+
self.model_verbose_name or self.model._meta.verbose_name.capitalize()
|
|
267
|
+
)
|
|
268
|
+
self.model_verbose_name_plural = (
|
|
269
|
+
self.model_verbose_name_plural
|
|
270
|
+
or self.model._meta.verbose_name_plural.capitalize()
|
|
271
|
+
)
|
|
264
272
|
self.router_tag = self.router_tag or self.model_verbose_name
|
|
265
273
|
self.router_tags = self.router_tags or tags or [self.router_tag]
|
|
266
274
|
self.router = Router(tags=self.router_tags)
|
|
@@ -400,11 +408,11 @@ class APIViewSet(API):
|
|
|
400
408
|
@self.router.post(
|
|
401
409
|
self.path,
|
|
402
410
|
auth=self.post_view_auth(),
|
|
403
|
-
summary=f"Create {self.
|
|
411
|
+
summary=f"Create {self.model_verbose_name}",
|
|
404
412
|
description=self.create_docs,
|
|
405
413
|
response={201: self.schema_out, self.error_codes: GenericMessageSchema},
|
|
406
414
|
)
|
|
407
|
-
@decorate_view(unique_view(self), *self.extra_decorators.create)
|
|
415
|
+
@decorate_view(aatomic, unique_view(self), *self.extra_decorators.create)
|
|
408
416
|
async def create(request: HttpRequest, data: self.schema_in): # type: ignore
|
|
409
417
|
return 201, await self.model_util.create_s(request, data, self.schema_out)
|
|
410
418
|
|
|
@@ -418,7 +426,7 @@ class APIViewSet(API):
|
|
|
418
426
|
@self.router.get(
|
|
419
427
|
self.get_path,
|
|
420
428
|
auth=self.get_view_auth(),
|
|
421
|
-
summary=f"List {self.
|
|
429
|
+
summary=f"List {self.model_verbose_name_plural}",
|
|
422
430
|
description=self.list_docs,
|
|
423
431
|
response={
|
|
424
432
|
200: List[self.schema_out],
|
|
@@ -453,7 +461,7 @@ class APIViewSet(API):
|
|
|
453
461
|
@self.router.get(
|
|
454
462
|
self.get_path_retrieve,
|
|
455
463
|
auth=self.get_view_auth(),
|
|
456
|
-
summary=f"Retrieve {self.
|
|
464
|
+
summary=f"Retrieve {self.model_verbose_name}",
|
|
457
465
|
description=self.retrieve_docs,
|
|
458
466
|
response={200: self.schema_out, self.error_codes: GenericMessageSchema},
|
|
459
467
|
)
|
|
@@ -478,11 +486,11 @@ class APIViewSet(API):
|
|
|
478
486
|
@self.router.patch(
|
|
479
487
|
self.path_retrieve,
|
|
480
488
|
auth=self.patch_view_auth(),
|
|
481
|
-
summary=f"Update {self.
|
|
489
|
+
summary=f"Update {self.model_verbose_name}",
|
|
482
490
|
description=self.update_docs,
|
|
483
491
|
response={200: self.schema_out, self.error_codes: GenericMessageSchema},
|
|
484
492
|
)
|
|
485
|
-
@decorate_view(unique_view(self), *self.extra_decorators.update)
|
|
493
|
+
@decorate_view(aatomic, unique_view(self), *self.extra_decorators.update)
|
|
486
494
|
async def update(
|
|
487
495
|
request: HttpRequest,
|
|
488
496
|
data: self.schema_update, # type: ignore
|
|
@@ -502,11 +510,11 @@ class APIViewSet(API):
|
|
|
502
510
|
@self.router.delete(
|
|
503
511
|
self.path_retrieve,
|
|
504
512
|
auth=self.delete_view_auth(),
|
|
505
|
-
summary=f"Delete {self.
|
|
513
|
+
summary=f"Delete {self.model_verbose_name}",
|
|
506
514
|
description=self.delete_docs,
|
|
507
515
|
response={204: None, self.error_codes: GenericMessageSchema},
|
|
508
516
|
)
|
|
509
|
-
@decorate_view(unique_view(self), *self.extra_decorators.delete)
|
|
517
|
+
@decorate_view(aatomic, unique_view(self), *self.extra_decorators.delete)
|
|
510
518
|
async def delete(request: HttpRequest, pk: Path[self.path_schema]): # type: ignore
|
|
511
519
|
return 204, await self.model_util.delete_s(request, self._get_pk(pk))
|
|
512
520
|
|
|
File without changes
|
{django_ninja_aio_crud-2.4.0.dist-info → django_ninja_aio_crud-2.5.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|