django-ninja-aio-crud 2.3.2__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.3.2.dist-info → django_ninja_aio_crud-2.5.0.dist-info}/METADATA +31 -6
- {django_ninja_aio_crud-2.3.2.dist-info → django_ninja_aio_crud-2.5.0.dist-info}/RECORD +10 -8
- ninja_aio/__init__.py +1 -1
- ninja_aio/helpers/query.py +2 -2
- ninja_aio/models/__init__.py +4 -0
- ninja_aio/models/serializers.py +888 -0
- ninja_aio/{models.py → models/utils.py} +75 -670
- ninja_aio/views/api.py +44 -19
- {django_ninja_aio_crud-2.3.2.dist-info → django_ninja_aio_crud-2.5.0.dist-info}/WHEEL +0 -0
- {django_ninja_aio_crud-2.3.2.dist-info → django_ninja_aio_crud-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import base64
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any
|
|
4
4
|
|
|
5
5
|
from ninja import Schema
|
|
6
|
-
from ninja.orm import
|
|
6
|
+
from ninja.orm import fields
|
|
7
7
|
from ninja.errors import ConfigError
|
|
8
8
|
|
|
9
9
|
from django.db import models
|
|
@@ -19,15 +19,14 @@ from django.db.models.fields.related_descriptors import (
|
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
from ninja_aio.exceptions import SerializeError, NotFoundError
|
|
22
|
-
from ninja_aio.
|
|
22
|
+
from ninja_aio.models.serializers import ModelSerializer
|
|
23
|
+
from ninja_aio.types import ModelSerializerMeta
|
|
23
24
|
from ninja_aio.schemas.helpers import (
|
|
24
25
|
ModelQuerySetSchema,
|
|
25
|
-
ModelQuerySetExtraSchema,
|
|
26
26
|
QuerySchema,
|
|
27
27
|
ObjectQuerySchema,
|
|
28
28
|
ObjectsQuerySchema,
|
|
29
29
|
)
|
|
30
|
-
from ninja_aio.helpers.query import QueryUtil
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
async def agetattr(obj, name: str, default=None):
|
|
@@ -95,7 +94,9 @@ class ModelUtil:
|
|
|
95
94
|
- Stateless wrapper; safe per-request instantiation.
|
|
96
95
|
"""
|
|
97
96
|
|
|
98
|
-
def __init__(
|
|
97
|
+
def __init__(
|
|
98
|
+
self, model: type["ModelSerializer"] | models.Model, serializer_class=None
|
|
99
|
+
):
|
|
99
100
|
"""
|
|
100
101
|
Initialize with a Django model or ModelSerializer subclass.
|
|
101
102
|
|
|
@@ -104,7 +105,26 @@ class ModelUtil:
|
|
|
104
105
|
model : Model | ModelSerializerMeta
|
|
105
106
|
Target model class.
|
|
106
107
|
"""
|
|
108
|
+
from ninja_aio.models.serializers import Serializer
|
|
109
|
+
|
|
107
110
|
self.model = model
|
|
111
|
+
self.serializer_class: Serializer = serializer_class
|
|
112
|
+
if serializer_class is not None and isinstance(model, ModelSerializerMeta):
|
|
113
|
+
raise ConfigError(
|
|
114
|
+
"ModelUtil cannot accept both model and serializer_class if the model is a ModelSerializer."
|
|
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
|
|
108
128
|
|
|
109
129
|
@property
|
|
110
130
|
def pk_field_type(self):
|
|
@@ -242,7 +262,11 @@ class ModelUtil:
|
|
|
242
262
|
Optimized and filtered queryset.
|
|
243
263
|
"""
|
|
244
264
|
# Start with base queryset
|
|
245
|
-
obj_qs =
|
|
265
|
+
obj_qs = (
|
|
266
|
+
self.model.objects.all()
|
|
267
|
+
if self.serializer_class is None
|
|
268
|
+
else await self.serializer_class.queryset_request(request)
|
|
269
|
+
)
|
|
246
270
|
|
|
247
271
|
# Apply query optimizations
|
|
248
272
|
obj_qs = self._apply_query_optimizations(obj_qs, query_data, is_for_read)
|
|
@@ -434,6 +458,23 @@ class ModelUtil:
|
|
|
434
458
|
|
|
435
459
|
return queryset
|
|
436
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
|
+
|
|
437
478
|
def get_reverse_relations(self) -> list[str]:
|
|
438
479
|
"""
|
|
439
480
|
Discover reverse relation names for safe prefetching.
|
|
@@ -443,7 +484,9 @@ class ModelUtil:
|
|
|
443
484
|
list[str]
|
|
444
485
|
Relation attribute names.
|
|
445
486
|
"""
|
|
446
|
-
reverse_rels =
|
|
487
|
+
reverse_rels = self._get_read_optimizations().prefetch_related.copy()
|
|
488
|
+
if reverse_rels:
|
|
489
|
+
return reverse_rels
|
|
447
490
|
for f in self.serializable_fields:
|
|
448
491
|
field_obj = getattr(self.model, f)
|
|
449
492
|
if isinstance(field_obj, ManyToManyDescriptor):
|
|
@@ -465,7 +508,9 @@ class ModelUtil:
|
|
|
465
508
|
list[str]
|
|
466
509
|
Relation attribute names.
|
|
467
510
|
"""
|
|
468
|
-
select_rels =
|
|
511
|
+
select_rels = self._get_read_optimizations().select_related.copy()
|
|
512
|
+
if select_rels:
|
|
513
|
+
return select_rels
|
|
469
514
|
for f in self.serializable_fields:
|
|
470
515
|
field_obj = getattr(self.model, f)
|
|
471
516
|
if isinstance(field_obj, ForwardManyToOneDescriptor):
|
|
@@ -604,7 +649,8 @@ class ModelUtil:
|
|
|
604
649
|
"""
|
|
605
650
|
payload = data.model_dump(mode="json")
|
|
606
651
|
|
|
607
|
-
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
|
|
608
654
|
|
|
609
655
|
# Collect custom and optional fields (only if ModelSerializerMeta)
|
|
610
656
|
customs: dict[str, Any] = {}
|
|
@@ -613,10 +659,10 @@ class ModelUtil:
|
|
|
613
659
|
customs = {
|
|
614
660
|
k: v
|
|
615
661
|
for k, v in payload.items()
|
|
616
|
-
if
|
|
662
|
+
if serializer.is_custom(k) and k not in self.model_fields
|
|
617
663
|
}
|
|
618
664
|
optionals = [
|
|
619
|
-
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
|
|
620
666
|
]
|
|
621
667
|
|
|
622
668
|
skip_keys = set()
|
|
@@ -625,8 +671,8 @@ class ModelUtil:
|
|
|
625
671
|
skip_keys = {
|
|
626
672
|
k
|
|
627
673
|
for k, v in payload.items()
|
|
628
|
-
if (
|
|
629
|
-
or (
|
|
674
|
+
if (serializer.is_custom(k) and k not in self.model_fields)
|
|
675
|
+
or (serializer.is_optional(k) and v is None)
|
|
630
676
|
}
|
|
631
677
|
|
|
632
678
|
# Process payload fields
|
|
@@ -663,10 +709,19 @@ class ModelUtil:
|
|
|
663
709
|
Serialized created object.
|
|
664
710
|
"""
|
|
665
711
|
payload, customs = await self.parse_input_data(request, data)
|
|
666
|
-
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
|
+
)
|
|
667
717
|
obj = await self.get_object(request, pk)
|
|
668
718
|
if isinstance(self.model, ModelSerializerMeta):
|
|
669
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
|
+
)
|
|
670
725
|
return await self.read_s(obj_schema, request, obj)
|
|
671
726
|
|
|
672
727
|
async def _read_s(
|
|
@@ -860,7 +915,11 @@ class ModelUtil:
|
|
|
860
915
|
setattr(obj, k, v)
|
|
861
916
|
if isinstance(self.model, ModelSerializerMeta):
|
|
862
917
|
await obj.custom_actions(customs)
|
|
863
|
-
|
|
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()
|
|
864
923
|
updated_object = await self.get_object(request, pk)
|
|
865
924
|
return await self.read_s(obj_schema, request, updated_object)
|
|
866
925
|
|
|
@@ -881,657 +940,3 @@ class ModelUtil:
|
|
|
881
940
|
obj = await self.get_object(request, pk)
|
|
882
941
|
await obj.adelete()
|
|
883
942
|
return None
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
|
|
887
|
-
"""
|
|
888
|
-
ModelSerializer
|
|
889
|
-
=================
|
|
890
|
-
Abstract mixin for Django models centralizing (on the model class itself) the
|
|
891
|
-
declarative configuration required to auto-generate create / update / read /
|
|
892
|
-
related schemas.
|
|
893
|
-
|
|
894
|
-
Goals
|
|
895
|
-
-----
|
|
896
|
-
- Remove duplication between Model and separate serializer classes.
|
|
897
|
-
- Provide clear extension points (sync + async hooks, custom synthetic fields).
|
|
898
|
-
|
|
899
|
-
See inline docstrings for per-method behavior.
|
|
900
|
-
"""
|
|
901
|
-
|
|
902
|
-
util: ClassVar[ModelUtil]
|
|
903
|
-
query_util: ClassVar[QueryUtil]
|
|
904
|
-
|
|
905
|
-
class Meta:
|
|
906
|
-
abstract = True
|
|
907
|
-
|
|
908
|
-
def __init_subclass__(cls, **kwargs):
|
|
909
|
-
super().__init_subclass__(**kwargs)
|
|
910
|
-
# Bind a ModelUtil instance to the subclass for convenient access
|
|
911
|
-
cls.util = ModelUtil(cls)
|
|
912
|
-
cls.query_util = QueryUtil(cls)
|
|
913
|
-
|
|
914
|
-
class QuerySet:
|
|
915
|
-
"""
|
|
916
|
-
Configuration container describing how to build query schemas for a model.
|
|
917
|
-
Purpose
|
|
918
|
-
-------
|
|
919
|
-
Describes which fields and extras are available when querying for model
|
|
920
|
-
instances. A factory/metaclass can read this configuration to generate
|
|
921
|
-
Pydantic / Ninja query schemas.
|
|
922
|
-
Attributes
|
|
923
|
-
----------
|
|
924
|
-
read : ModelQuerySetSchema
|
|
925
|
-
Schema configuration for read operations.
|
|
926
|
-
queryset_request : ModelQuerySetSchema
|
|
927
|
-
Schema configuration for queryset_request hook.
|
|
928
|
-
extras : list[ModelQuerySetExtraSchema]
|
|
929
|
-
Additional computed / synthetic query parameters.
|
|
930
|
-
"""
|
|
931
|
-
read = ModelQuerySetSchema()
|
|
932
|
-
queryset_request = ModelQuerySetSchema()
|
|
933
|
-
extras: list[ModelQuerySetExtraSchema] = []
|
|
934
|
-
|
|
935
|
-
class CreateSerializer:
|
|
936
|
-
"""Configuration container describing how to build a create (input) schema for a model.
|
|
937
|
-
|
|
938
|
-
Purpose
|
|
939
|
-
-------
|
|
940
|
-
Describes which fields are accepted (and in what form) when creating a new
|
|
941
|
-
instance. A factory/metaclass can read this configuration to generate a
|
|
942
|
-
Pydantic / Ninja input schema.
|
|
943
|
-
|
|
944
|
-
Attributes
|
|
945
|
-
----------
|
|
946
|
-
fields : list[str]
|
|
947
|
-
REQUIRED model fields.
|
|
948
|
-
optionals : list[tuple[str, type]]
|
|
949
|
-
Optional model fields (nullable / patch-like).
|
|
950
|
-
customs : list[tuple[str, type, Any]]
|
|
951
|
-
Synthetic input fields (non-model).
|
|
952
|
-
excludes : list[str]
|
|
953
|
-
Disallowed model fields on create (e.g., id, timestamps).
|
|
954
|
-
"""
|
|
955
|
-
|
|
956
|
-
fields: list[str] = []
|
|
957
|
-
customs: list[tuple[str, type, Any]] = []
|
|
958
|
-
optionals: list[tuple[str, type]] = []
|
|
959
|
-
excludes: list[str] = []
|
|
960
|
-
|
|
961
|
-
class ReadSerializer:
|
|
962
|
-
"""Configuration describing how to build a read (output) schema.
|
|
963
|
-
|
|
964
|
-
Attributes
|
|
965
|
-
----------
|
|
966
|
-
fields : list[str]
|
|
967
|
-
Explicit model fields to include.
|
|
968
|
-
excludes : list[str]
|
|
969
|
-
Fields to force exclude (safety).
|
|
970
|
-
customs : list[tuple[str, type, Any]]
|
|
971
|
-
Computed / synthetic output attributes.
|
|
972
|
-
"""
|
|
973
|
-
|
|
974
|
-
fields: list[str] = []
|
|
975
|
-
excludes: list[str] = []
|
|
976
|
-
customs: list[tuple[str, type, Any]] = []
|
|
977
|
-
|
|
978
|
-
class UpdateSerializer:
|
|
979
|
-
"""Configuration describing update (PATCH/PUT) schema.
|
|
980
|
-
|
|
981
|
-
Attributes
|
|
982
|
-
----------
|
|
983
|
-
fields : list[str]
|
|
984
|
-
Required update fields (rare).
|
|
985
|
-
optionals : list[tuple[str, type]]
|
|
986
|
-
Editable optional fields.
|
|
987
|
-
customs : list[tuple[str, type, Any]]
|
|
988
|
-
Synthetic operational inputs.
|
|
989
|
-
excludes : list[str]
|
|
990
|
-
Immutable / blocked fields.
|
|
991
|
-
"""
|
|
992
|
-
|
|
993
|
-
fields: list[str] = []
|
|
994
|
-
customs: list[tuple[str, type, Any]] = []
|
|
995
|
-
optionals: list[tuple[str, type]] = []
|
|
996
|
-
excludes: list[str] = []
|
|
997
|
-
|
|
998
|
-
@classmethod
|
|
999
|
-
def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
|
|
1000
|
-
"""
|
|
1001
|
-
Internal accessor for raw configuration lists.
|
|
1002
|
-
|
|
1003
|
-
Parameters
|
|
1004
|
-
----------
|
|
1005
|
-
s_type : str
|
|
1006
|
-
Serializer type ("create" | "update" | "read").
|
|
1007
|
-
f_type : str
|
|
1008
|
-
Field category ("fields" | "optionals" | "customs" | "excludes").
|
|
1009
|
-
|
|
1010
|
-
Returns
|
|
1011
|
-
-------
|
|
1012
|
-
list
|
|
1013
|
-
Raw configuration list or empty list.
|
|
1014
|
-
"""
|
|
1015
|
-
match s_type:
|
|
1016
|
-
case "create":
|
|
1017
|
-
fields = getattr(cls.CreateSerializer, f_type, [])
|
|
1018
|
-
case "update":
|
|
1019
|
-
fields = getattr(cls.UpdateSerializer, f_type, [])
|
|
1020
|
-
case "read":
|
|
1021
|
-
fields = getattr(cls.ReadSerializer, f_type, [])
|
|
1022
|
-
return fields
|
|
1023
|
-
|
|
1024
|
-
@classmethod
|
|
1025
|
-
def _is_special_field(
|
|
1026
|
-
cls, s_type: type[S_TYPES], field: str, f_type: type[F_TYPES]
|
|
1027
|
-
):
|
|
1028
|
-
"""
|
|
1029
|
-
Determine if a field is declared in a given category for a serializer type.
|
|
1030
|
-
|
|
1031
|
-
Parameters
|
|
1032
|
-
----------
|
|
1033
|
-
s_type : str
|
|
1034
|
-
field : str
|
|
1035
|
-
f_type : str
|
|
1036
|
-
|
|
1037
|
-
Returns
|
|
1038
|
-
-------
|
|
1039
|
-
bool
|
|
1040
|
-
"""
|
|
1041
|
-
special_fields = cls._get_fields(s_type, f_type)
|
|
1042
|
-
return any(field in special_f for special_f in special_fields)
|
|
1043
|
-
|
|
1044
|
-
@classmethod
|
|
1045
|
-
def _generate_model_schema(
|
|
1046
|
-
cls,
|
|
1047
|
-
schema_type: type[SCHEMA_TYPES],
|
|
1048
|
-
depth: int = None,
|
|
1049
|
-
) -> Schema:
|
|
1050
|
-
"""
|
|
1051
|
-
Core schema factory bridging configuration and ninja.orm.create_schema.
|
|
1052
|
-
|
|
1053
|
-
Parameters
|
|
1054
|
-
----------
|
|
1055
|
-
schema_type : str
|
|
1056
|
-
"In" | "Patch" | "Out" | "Related".
|
|
1057
|
-
depth : int, optional
|
|
1058
|
-
Relation depth for read schema.
|
|
1059
|
-
|
|
1060
|
-
Returns
|
|
1061
|
-
-------
|
|
1062
|
-
Schema | None
|
|
1063
|
-
Generated schema class or None if no fields.
|
|
1064
|
-
"""
|
|
1065
|
-
match schema_type:
|
|
1066
|
-
case "In":
|
|
1067
|
-
s_type = "create"
|
|
1068
|
-
case "Patch":
|
|
1069
|
-
s_type = "update"
|
|
1070
|
-
case "Out":
|
|
1071
|
-
fields, reverse_rels, excludes, customs = cls.get_schema_out_data()
|
|
1072
|
-
if not fields and not reverse_rels and not excludes and not customs:
|
|
1073
|
-
return None
|
|
1074
|
-
return create_schema(
|
|
1075
|
-
model=cls,
|
|
1076
|
-
name=f"{cls._meta.model_name}SchemaOut",
|
|
1077
|
-
depth=depth,
|
|
1078
|
-
fields=fields,
|
|
1079
|
-
custom_fields=reverse_rels + customs,
|
|
1080
|
-
exclude=excludes,
|
|
1081
|
-
)
|
|
1082
|
-
case "Related":
|
|
1083
|
-
fields, customs = cls.get_related_schema_data()
|
|
1084
|
-
if not fields and not customs:
|
|
1085
|
-
return None
|
|
1086
|
-
return create_schema(
|
|
1087
|
-
model=cls,
|
|
1088
|
-
name=f"{cls._meta.model_name}SchemaRelated",
|
|
1089
|
-
fields=fields,
|
|
1090
|
-
custom_fields=customs,
|
|
1091
|
-
)
|
|
1092
|
-
|
|
1093
|
-
fields = cls.get_fields(s_type)
|
|
1094
|
-
optionals = cls.get_optional_fields(s_type)
|
|
1095
|
-
customs = cls.get_custom_fields(s_type) + optionals
|
|
1096
|
-
excludes = cls.get_excluded_fields(s_type)
|
|
1097
|
-
if not fields and not excludes:
|
|
1098
|
-
fields = [f[0] for f in optionals]
|
|
1099
|
-
return (
|
|
1100
|
-
create_schema(
|
|
1101
|
-
model=cls,
|
|
1102
|
-
name=f"{cls._meta.model_name}Schema{schema_type}",
|
|
1103
|
-
fields=fields,
|
|
1104
|
-
custom_fields=customs,
|
|
1105
|
-
exclude=excludes,
|
|
1106
|
-
)
|
|
1107
|
-
if fields or customs or excludes
|
|
1108
|
-
else None
|
|
1109
|
-
)
|
|
1110
|
-
|
|
1111
|
-
@classmethod
|
|
1112
|
-
def verbose_name_path_resolver(cls) -> str:
|
|
1113
|
-
"""
|
|
1114
|
-
Slugify plural verbose name for URL path segment.
|
|
1115
|
-
|
|
1116
|
-
Returns
|
|
1117
|
-
-------
|
|
1118
|
-
str
|
|
1119
|
-
"""
|
|
1120
|
-
return "-".join(cls._meta.verbose_name_plural.split(" "))
|
|
1121
|
-
|
|
1122
|
-
def has_changed(self, field: str) -> bool:
|
|
1123
|
-
"""
|
|
1124
|
-
Check if a model field has changed compared to the persisted value.
|
|
1125
|
-
|
|
1126
|
-
Parameters
|
|
1127
|
-
----------
|
|
1128
|
-
field : str
|
|
1129
|
-
Field name.
|
|
1130
|
-
|
|
1131
|
-
Returns
|
|
1132
|
-
-------
|
|
1133
|
-
bool
|
|
1134
|
-
True if in-memory value differs from DB value.
|
|
1135
|
-
"""
|
|
1136
|
-
if not self.pk:
|
|
1137
|
-
return False
|
|
1138
|
-
old_value = (
|
|
1139
|
-
self.__class__._default_manager.filter(pk=self.pk)
|
|
1140
|
-
.values(field)
|
|
1141
|
-
.get()[field]
|
|
1142
|
-
)
|
|
1143
|
-
return getattr(self, field) != old_value
|
|
1144
|
-
|
|
1145
|
-
@classmethod
|
|
1146
|
-
async def queryset_request(cls, request: HttpRequest):
|
|
1147
|
-
"""
|
|
1148
|
-
Override to return a request-scoped filtered queryset.
|
|
1149
|
-
|
|
1150
|
-
Parameters
|
|
1151
|
-
----------
|
|
1152
|
-
request : HttpRequest
|
|
1153
|
-
|
|
1154
|
-
Returns
|
|
1155
|
-
-------
|
|
1156
|
-
QuerySet
|
|
1157
|
-
"""
|
|
1158
|
-
return cls.query_util.apply_queryset_optimizations(
|
|
1159
|
-
queryset=cls.objects.all(),
|
|
1160
|
-
scope=cls.query_util.SCOPES.QUERYSET_REQUEST,
|
|
1161
|
-
)
|
|
1162
|
-
|
|
1163
|
-
async def post_create(self) -> None:
|
|
1164
|
-
"""
|
|
1165
|
-
Async hook executed after first persistence (create path).
|
|
1166
|
-
"""
|
|
1167
|
-
pass
|
|
1168
|
-
|
|
1169
|
-
async def custom_actions(self, payload: dict[str, Any]):
|
|
1170
|
-
"""
|
|
1171
|
-
Async hook for reacting to provided custom (synthetic) fields.
|
|
1172
|
-
|
|
1173
|
-
Parameters
|
|
1174
|
-
----------
|
|
1175
|
-
payload : dict
|
|
1176
|
-
Custom field name/value pairs.
|
|
1177
|
-
"""
|
|
1178
|
-
pass
|
|
1179
|
-
|
|
1180
|
-
@classmethod
|
|
1181
|
-
def get_related_schema_data(cls):
|
|
1182
|
-
"""
|
|
1183
|
-
Build field/custom lists for 'Related' schema (flattening non-relational fields).
|
|
1184
|
-
|
|
1185
|
-
Returns
|
|
1186
|
-
-------
|
|
1187
|
-
tuple[list[str] | None, list[tuple] | None]
|
|
1188
|
-
(related_fields, custom_related_fields) or (None, None)
|
|
1189
|
-
"""
|
|
1190
|
-
fields = cls.get_fields("read")
|
|
1191
|
-
custom_f = {
|
|
1192
|
-
name: (value, default)
|
|
1193
|
-
for name, value, default in cls.get_custom_fields("read")
|
|
1194
|
-
}
|
|
1195
|
-
_related_fields = []
|
|
1196
|
-
for f in fields + list(custom_f.keys()):
|
|
1197
|
-
field_obj = getattr(cls, f)
|
|
1198
|
-
if not isinstance(
|
|
1199
|
-
field_obj,
|
|
1200
|
-
(
|
|
1201
|
-
ManyToManyDescriptor,
|
|
1202
|
-
ReverseManyToOneDescriptor,
|
|
1203
|
-
ReverseOneToOneDescriptor,
|
|
1204
|
-
ForwardManyToOneDescriptor,
|
|
1205
|
-
ForwardOneToOneDescriptor,
|
|
1206
|
-
),
|
|
1207
|
-
):
|
|
1208
|
-
_related_fields.append(f)
|
|
1209
|
-
|
|
1210
|
-
if not _related_fields:
|
|
1211
|
-
return None, None
|
|
1212
|
-
|
|
1213
|
-
custom_related_fields = [
|
|
1214
|
-
(f, *custom_f[f]) for f in _related_fields if f in custom_f
|
|
1215
|
-
]
|
|
1216
|
-
related_fields = [f for f in _related_fields if f not in custom_f]
|
|
1217
|
-
return related_fields, custom_related_fields
|
|
1218
|
-
|
|
1219
|
-
@classmethod
|
|
1220
|
-
def _build_schema_reverse_rel(cls, field_name: str, descriptor: Any):
|
|
1221
|
-
"""
|
|
1222
|
-
Build a reverse relation schema component for 'Out' schema generation.
|
|
1223
|
-
"""
|
|
1224
|
-
if isinstance(descriptor, ManyToManyDescriptor):
|
|
1225
|
-
rel_model: ModelSerializer = descriptor.field.related_model
|
|
1226
|
-
if descriptor.reverse: # reverse side of M2M
|
|
1227
|
-
rel_model = descriptor.field.model
|
|
1228
|
-
rel_type = "many"
|
|
1229
|
-
elif isinstance(descriptor, ReverseManyToOneDescriptor):
|
|
1230
|
-
rel_model = descriptor.field.model
|
|
1231
|
-
rel_type = "many"
|
|
1232
|
-
else: # ReverseOneToOneDescriptor
|
|
1233
|
-
rel_model = descriptor.related.related_model
|
|
1234
|
-
rel_type = "one"
|
|
1235
|
-
|
|
1236
|
-
if not isinstance(rel_model, ModelSerializerMeta):
|
|
1237
|
-
return None
|
|
1238
|
-
if not rel_model.get_fields("read") and not rel_model.get_custom_fields("read"):
|
|
1239
|
-
return None
|
|
1240
|
-
|
|
1241
|
-
rel_schema = (
|
|
1242
|
-
rel_model.generate_related_s()
|
|
1243
|
-
if rel_type == "one"
|
|
1244
|
-
else list[rel_model.generate_related_s()]
|
|
1245
|
-
)
|
|
1246
|
-
return (field_name, rel_schema | None, None)
|
|
1247
|
-
|
|
1248
|
-
@classmethod
|
|
1249
|
-
def _build_schema_forward_rel(cls, field_name: str, descriptor: Any):
|
|
1250
|
-
"""
|
|
1251
|
-
Build a forward relation schema component for 'Out' schema generation.
|
|
1252
|
-
"""
|
|
1253
|
-
rel_model = descriptor.field.related_model
|
|
1254
|
-
if not isinstance(rel_model, ModelSerializerMeta):
|
|
1255
|
-
return True # Signal: treat as plain field
|
|
1256
|
-
if not rel_model.get_fields("read") and not rel_model.get_custom_fields("read"):
|
|
1257
|
-
return None # Skip entirely
|
|
1258
|
-
rel_schema = rel_model.generate_related_s()
|
|
1259
|
-
return (field_name, rel_schema | None, None)
|
|
1260
|
-
|
|
1261
|
-
@classmethod
|
|
1262
|
-
def get_schema_out_data(cls):
|
|
1263
|
-
"""
|
|
1264
|
-
Collect components for 'Out' read schema generation.
|
|
1265
|
-
|
|
1266
|
-
Returns
|
|
1267
|
-
-------
|
|
1268
|
-
tuple
|
|
1269
|
-
(fields, reverse_rel_descriptors, excludes, custom_fields_with_forward_relations)
|
|
1270
|
-
"""
|
|
1271
|
-
|
|
1272
|
-
fields: list[str] = []
|
|
1273
|
-
reverse_rels: list[tuple] = []
|
|
1274
|
-
rels: list[tuple] = []
|
|
1275
|
-
|
|
1276
|
-
for f in cls.get_fields("read"):
|
|
1277
|
-
field_obj = getattr(cls, f)
|
|
1278
|
-
|
|
1279
|
-
# Reverse relations
|
|
1280
|
-
if isinstance(
|
|
1281
|
-
field_obj,
|
|
1282
|
-
(
|
|
1283
|
-
ManyToManyDescriptor,
|
|
1284
|
-
ReverseManyToOneDescriptor,
|
|
1285
|
-
ReverseOneToOneDescriptor,
|
|
1286
|
-
),
|
|
1287
|
-
):
|
|
1288
|
-
rel_tuple = cls._build_schema_reverse_rel(f, field_obj)
|
|
1289
|
-
if rel_tuple:
|
|
1290
|
-
reverse_rels.append(rel_tuple)
|
|
1291
|
-
continue
|
|
1292
|
-
|
|
1293
|
-
# Forward relations
|
|
1294
|
-
if isinstance(
|
|
1295
|
-
field_obj, (ForwardOneToOneDescriptor, ForwardManyToOneDescriptor)
|
|
1296
|
-
):
|
|
1297
|
-
rel_tuple = cls._build_schema_forward_rel(f, field_obj)
|
|
1298
|
-
if rel_tuple is True:
|
|
1299
|
-
fields.append(f)
|
|
1300
|
-
elif rel_tuple:
|
|
1301
|
-
rels.append(rel_tuple)
|
|
1302
|
-
# If rel_tuple is None -> skip
|
|
1303
|
-
continue
|
|
1304
|
-
|
|
1305
|
-
# Plain field
|
|
1306
|
-
fields.append(f)
|
|
1307
|
-
|
|
1308
|
-
return (
|
|
1309
|
-
fields,
|
|
1310
|
-
reverse_rels,
|
|
1311
|
-
cls.get_excluded_fields("read"),
|
|
1312
|
-
cls.get_custom_fields("read") + rels,
|
|
1313
|
-
)
|
|
1314
|
-
|
|
1315
|
-
@classmethod
|
|
1316
|
-
def is_custom(cls, field: str):
|
|
1317
|
-
"""
|
|
1318
|
-
Check if a field is declared as a custom input (create or update).
|
|
1319
|
-
|
|
1320
|
-
Parameters
|
|
1321
|
-
----------
|
|
1322
|
-
field : str
|
|
1323
|
-
|
|
1324
|
-
Returns
|
|
1325
|
-
-------
|
|
1326
|
-
bool
|
|
1327
|
-
"""
|
|
1328
|
-
return cls._is_special_field(
|
|
1329
|
-
"create", field, "customs"
|
|
1330
|
-
) or cls._is_special_field("update", field, "customs")
|
|
1331
|
-
|
|
1332
|
-
@classmethod
|
|
1333
|
-
def is_optional(cls, field: str):
|
|
1334
|
-
"""
|
|
1335
|
-
Check if a field is declared as optional (create or update).
|
|
1336
|
-
|
|
1337
|
-
Parameters
|
|
1338
|
-
----------
|
|
1339
|
-
field : str
|
|
1340
|
-
|
|
1341
|
-
Returns
|
|
1342
|
-
-------
|
|
1343
|
-
bool
|
|
1344
|
-
"""
|
|
1345
|
-
return cls._is_special_field(
|
|
1346
|
-
"create", field, "optionals"
|
|
1347
|
-
) or cls._is_special_field("update", field, "optionals")
|
|
1348
|
-
|
|
1349
|
-
@classmethod
|
|
1350
|
-
def get_custom_fields(cls, s_type: type[S_TYPES]) -> list[tuple[str, type, Any]]:
|
|
1351
|
-
"""
|
|
1352
|
-
Normalize declared custom field specs into (name, py_type, default) triples.
|
|
1353
|
-
|
|
1354
|
-
Accepted tuple shapes:
|
|
1355
|
-
(name, py_type, default) -> keeps provided default (callable or literal)
|
|
1356
|
-
(name, py_type) -> marks as required (default = Ellipsis)
|
|
1357
|
-
Any other arity raises ValueError.
|
|
1358
|
-
|
|
1359
|
-
Parameters
|
|
1360
|
-
----------
|
|
1361
|
-
s_type : str
|
|
1362
|
-
"create" | "update" | "read"
|
|
1363
|
-
|
|
1364
|
-
Returns
|
|
1365
|
-
-------
|
|
1366
|
-
list[tuple[str, type, Any]]
|
|
1367
|
-
"""
|
|
1368
|
-
raw_customs = cls._get_fields(s_type, "customs") or []
|
|
1369
|
-
normalized: list[tuple[str, type, Any]] = []
|
|
1370
|
-
for spec in raw_customs:
|
|
1371
|
-
if not isinstance(spec, tuple):
|
|
1372
|
-
raise ValueError(f"Custom field spec must be a tuple, got {type(spec)}")
|
|
1373
|
-
match len(spec):
|
|
1374
|
-
case 3:
|
|
1375
|
-
name, py_type, default = spec
|
|
1376
|
-
case 2:
|
|
1377
|
-
name, py_type = spec
|
|
1378
|
-
default = ...
|
|
1379
|
-
case _:
|
|
1380
|
-
raise ValueError(
|
|
1381
|
-
f"Custom field tuple must have length 2 or 3 (name, type[, default]); got {len(spec)}"
|
|
1382
|
-
)
|
|
1383
|
-
normalized.append((name, py_type, default))
|
|
1384
|
-
return normalized
|
|
1385
|
-
|
|
1386
|
-
@classmethod
|
|
1387
|
-
def get_optional_fields(cls, s_type: type[S_TYPES]):
|
|
1388
|
-
"""
|
|
1389
|
-
Return optional field specifications normalized to (name, type, None).
|
|
1390
|
-
|
|
1391
|
-
Parameters
|
|
1392
|
-
----------
|
|
1393
|
-
s_type : str
|
|
1394
|
-
|
|
1395
|
-
Returns
|
|
1396
|
-
-------
|
|
1397
|
-
list[tuple[str, type, None]]
|
|
1398
|
-
"""
|
|
1399
|
-
return [
|
|
1400
|
-
(field, field_type, None)
|
|
1401
|
-
for field, field_type in cls._get_fields(s_type, "optionals")
|
|
1402
|
-
]
|
|
1403
|
-
|
|
1404
|
-
@classmethod
|
|
1405
|
-
def get_excluded_fields(cls, s_type: type[S_TYPES]):
|
|
1406
|
-
"""
|
|
1407
|
-
Return excluded field names for a serializer type.
|
|
1408
|
-
|
|
1409
|
-
Parameters
|
|
1410
|
-
----------
|
|
1411
|
-
s_type : str
|
|
1412
|
-
|
|
1413
|
-
Returns
|
|
1414
|
-
-------
|
|
1415
|
-
list[str]
|
|
1416
|
-
"""
|
|
1417
|
-
return cls._get_fields(s_type, "excludes")
|
|
1418
|
-
|
|
1419
|
-
@classmethod
|
|
1420
|
-
def get_fields(cls, s_type: type[S_TYPES]):
|
|
1421
|
-
"""
|
|
1422
|
-
Return explicit declared fields for a serializer type.
|
|
1423
|
-
|
|
1424
|
-
Parameters
|
|
1425
|
-
----------
|
|
1426
|
-
s_type : str
|
|
1427
|
-
|
|
1428
|
-
Returns
|
|
1429
|
-
-------
|
|
1430
|
-
list[str]
|
|
1431
|
-
"""
|
|
1432
|
-
return cls._get_fields(s_type, "fields")
|
|
1433
|
-
|
|
1434
|
-
@classmethod
|
|
1435
|
-
def generate_read_s(cls, depth: int = 1) -> Schema:
|
|
1436
|
-
"""
|
|
1437
|
-
Generate read (Out) schema.
|
|
1438
|
-
|
|
1439
|
-
Parameters
|
|
1440
|
-
----------
|
|
1441
|
-
depth : int
|
|
1442
|
-
Relation depth.
|
|
1443
|
-
|
|
1444
|
-
Returns
|
|
1445
|
-
-------
|
|
1446
|
-
Schema | None
|
|
1447
|
-
"""
|
|
1448
|
-
return cls._generate_model_schema("Out", depth)
|
|
1449
|
-
|
|
1450
|
-
@classmethod
|
|
1451
|
-
def generate_create_s(cls) -> Schema:
|
|
1452
|
-
"""
|
|
1453
|
-
Generate create (In) schema.
|
|
1454
|
-
|
|
1455
|
-
Returns
|
|
1456
|
-
-------
|
|
1457
|
-
Schema | None
|
|
1458
|
-
"""
|
|
1459
|
-
return cls._generate_model_schema("In")
|
|
1460
|
-
|
|
1461
|
-
@classmethod
|
|
1462
|
-
def generate_update_s(cls) -> Schema:
|
|
1463
|
-
"""
|
|
1464
|
-
Generate update (Patch) schema.
|
|
1465
|
-
|
|
1466
|
-
Returns
|
|
1467
|
-
-------
|
|
1468
|
-
Schema | None
|
|
1469
|
-
"""
|
|
1470
|
-
return cls._generate_model_schema("Patch")
|
|
1471
|
-
|
|
1472
|
-
@classmethod
|
|
1473
|
-
def generate_related_s(cls) -> Schema:
|
|
1474
|
-
"""
|
|
1475
|
-
Generate related (nested) schema.
|
|
1476
|
-
|
|
1477
|
-
Returns
|
|
1478
|
-
-------
|
|
1479
|
-
Schema | None
|
|
1480
|
-
"""
|
|
1481
|
-
return cls._generate_model_schema("Related")
|
|
1482
|
-
|
|
1483
|
-
def after_save(self):
|
|
1484
|
-
"""
|
|
1485
|
-
Sync hook executed after any save (create or update).
|
|
1486
|
-
"""
|
|
1487
|
-
pass
|
|
1488
|
-
|
|
1489
|
-
def before_save(self):
|
|
1490
|
-
"""
|
|
1491
|
-
Sync hook executed before any save (create or update).
|
|
1492
|
-
"""
|
|
1493
|
-
pass
|
|
1494
|
-
|
|
1495
|
-
def on_create_after_save(self):
|
|
1496
|
-
"""
|
|
1497
|
-
Sync hook executed only after initial creation save.
|
|
1498
|
-
"""
|
|
1499
|
-
pass
|
|
1500
|
-
|
|
1501
|
-
def on_create_before_save(self):
|
|
1502
|
-
"""
|
|
1503
|
-
Sync hook executed only before initial creation save.
|
|
1504
|
-
"""
|
|
1505
|
-
pass
|
|
1506
|
-
|
|
1507
|
-
def on_delete(self):
|
|
1508
|
-
"""
|
|
1509
|
-
Sync hook executed after delete.
|
|
1510
|
-
"""
|
|
1511
|
-
pass
|
|
1512
|
-
|
|
1513
|
-
def save(self, *args, **kwargs):
|
|
1514
|
-
"""
|
|
1515
|
-
Override save lifecycle to inject create/update hooks.
|
|
1516
|
-
"""
|
|
1517
|
-
state_adding = self._state.adding
|
|
1518
|
-
if state_adding:
|
|
1519
|
-
self.on_create_before_save()
|
|
1520
|
-
self.before_save()
|
|
1521
|
-
super().save(*args, **kwargs)
|
|
1522
|
-
if state_adding:
|
|
1523
|
-
self.on_create_after_save()
|
|
1524
|
-
self.after_save()
|
|
1525
|
-
|
|
1526
|
-
def delete(self, *args, **kwargs):
|
|
1527
|
-
"""
|
|
1528
|
-
Override delete to inject on_delete hook.
|
|
1529
|
-
|
|
1530
|
-
Returns
|
|
1531
|
-
-------
|
|
1532
|
-
tuple(int, dict)
|
|
1533
|
-
Django delete return signature.
|
|
1534
|
-
"""
|
|
1535
|
-
res = super().delete(*args, **kwargs)
|
|
1536
|
-
self.on_delete()
|
|
1537
|
-
return res
|