django-ninja-aio-crud 2.17.0__py3-none-any.whl → 2.18.1__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.18.1.dist-info/METADATA +431 -0
- {django_ninja_aio_crud-2.17.0.dist-info → django_ninja_aio_crud-2.18.1.dist-info}/RECORD +10 -10
- ninja_aio/__init__.py +1 -1
- ninja_aio/models/serializers.py +715 -99
- ninja_aio/models/utils.py +235 -35
- ninja_aio/types.py +38 -0
- ninja_aio/views/api.py +108 -9
- ninja_aio/views/mixins.py +25 -9
- django_ninja_aio_crud-2.17.0.dist-info/METADATA +0 -379
- {django_ninja_aio_crud-2.17.0.dist-info → django_ninja_aio_crud-2.18.1.dist-info}/WHEEL +0 -0
- {django_ninja_aio_crud-2.17.0.dist-info → django_ninja_aio_crud-2.18.1.dist-info}/licenses/LICENSE +0 -0
ninja_aio/models/serializers.py
CHANGED
|
@@ -11,6 +11,8 @@ from typing import (
|
|
|
11
11
|
)
|
|
12
12
|
import warnings
|
|
13
13
|
import sys
|
|
14
|
+
import threading
|
|
15
|
+
from functools import lru_cache
|
|
14
16
|
|
|
15
17
|
from django.conf import settings
|
|
16
18
|
from ninja import Schema
|
|
@@ -25,6 +27,7 @@ from django.db.models.fields.related_descriptors import (
|
|
|
25
27
|
ForwardOneToOneDescriptor,
|
|
26
28
|
)
|
|
27
29
|
from pydantic import BeforeValidator, Field
|
|
30
|
+
from pydantic._internal._decorators import PydanticDescriptorProxy
|
|
28
31
|
|
|
29
32
|
from ninja_aio.types import (
|
|
30
33
|
S_TYPES,
|
|
@@ -102,14 +105,166 @@ class BaseSerializer:
|
|
|
102
105
|
queryset_request = ModelQuerySetSchema()
|
|
103
106
|
extras: list[ModelQuerySetExtraSchema] = []
|
|
104
107
|
|
|
108
|
+
# Thread-local storage for tracking circular reference resolution
|
|
109
|
+
_resolution_context = threading.local()
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def _get_resolution_stack(cls) -> list[str]:
|
|
113
|
+
"""
|
|
114
|
+
Get the current resolution stack for detecting circular references.
|
|
115
|
+
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
list[str]
|
|
119
|
+
Stack of model names currently being resolved (thread-safe).
|
|
120
|
+
"""
|
|
121
|
+
if not hasattr(cls._resolution_context, 'stack'):
|
|
122
|
+
cls._resolution_context.stack = []
|
|
123
|
+
return cls._resolution_context.stack
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def _is_circular_reference(cls, model: models.Model) -> bool:
|
|
127
|
+
"""
|
|
128
|
+
Check if resolving this model would create a circular reference.
|
|
129
|
+
|
|
130
|
+
Security: Prevents infinite recursion and stack overflow attacks.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
model : models.Model
|
|
135
|
+
The model to check
|
|
136
|
+
|
|
137
|
+
Returns
|
|
138
|
+
-------
|
|
139
|
+
bool
|
|
140
|
+
True if the model is already being resolved (circular reference detected)
|
|
141
|
+
"""
|
|
142
|
+
model_key = f"{model._meta.app_label}.{model._meta.model_name}"
|
|
143
|
+
return model_key in cls._get_resolution_stack()
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def _push_resolution(cls, model: models.Model) -> None:
|
|
147
|
+
"""Add a model to the resolution stack."""
|
|
148
|
+
model_key = f"{model._meta.app_label}.{model._meta.model_name}"
|
|
149
|
+
cls._get_resolution_stack().append(model_key)
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def _pop_resolution(cls) -> None:
|
|
153
|
+
"""Remove the most recent model from the resolution stack."""
|
|
154
|
+
stack = cls._get_resolution_stack()
|
|
155
|
+
if stack:
|
|
156
|
+
stack.pop()
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def _collect_validators(cls, source_class) -> dict:
|
|
160
|
+
"""
|
|
161
|
+
Collect Pydantic validator descriptors from a class.
|
|
162
|
+
|
|
163
|
+
Iterates over the class attributes looking for ``PydanticDescriptorProxy``
|
|
164
|
+
instances (created by ``@field_validator`` and ``@model_validator`` decorators).
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
source_class : type | None
|
|
169
|
+
The class to scan for validators.
|
|
170
|
+
|
|
171
|
+
Returns
|
|
172
|
+
-------
|
|
173
|
+
dict
|
|
174
|
+
Mapping of attribute name to ``PydanticDescriptorProxy`` instance.
|
|
175
|
+
"""
|
|
176
|
+
validators = {}
|
|
177
|
+
if source_class is None:
|
|
178
|
+
return validators
|
|
179
|
+
for attr_name, attr_value in vars(source_class).items():
|
|
180
|
+
if isinstance(attr_value, PydanticDescriptorProxy):
|
|
181
|
+
validators[attr_name] = attr_value
|
|
182
|
+
return validators
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def _apply_validators(cls, schema, validators: dict):
|
|
186
|
+
"""
|
|
187
|
+
Create a subclass of the given schema with validators attached.
|
|
188
|
+
|
|
189
|
+
Pydantic discovers validators via ``PydanticDescriptorProxy`` instances
|
|
190
|
+
during class creation, so placing them on a subclass is sufficient.
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
schema : Schema | None
|
|
195
|
+
The base schema class generated by ``create_schema``.
|
|
196
|
+
validators : dict
|
|
197
|
+
Mapping of validator names to ``PydanticDescriptorProxy`` instances.
|
|
198
|
+
|
|
199
|
+
Returns
|
|
200
|
+
-------
|
|
201
|
+
Schema | None
|
|
202
|
+
A subclass with validators applied, or the original schema if no
|
|
203
|
+
validators are provided.
|
|
204
|
+
"""
|
|
205
|
+
if not schema or not validators:
|
|
206
|
+
return schema
|
|
207
|
+
return type(schema.__name__, (schema,), validators)
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def _get_validators(cls, schema_type: type[SCHEMA_TYPES]) -> dict:
|
|
211
|
+
"""
|
|
212
|
+
Return collected validators for the given schema type.
|
|
213
|
+
|
|
214
|
+
Subclasses must implement this to map schema types to the appropriate
|
|
215
|
+
validator source class.
|
|
216
|
+
|
|
217
|
+
Parameters
|
|
218
|
+
----------
|
|
219
|
+
schema_type : SCHEMA_TYPES
|
|
220
|
+
One of ``"In"``, ``"Patch"``, ``"Out"``, ``"Detail"``, or ``"Related"``.
|
|
221
|
+
|
|
222
|
+
Returns
|
|
223
|
+
-------
|
|
224
|
+
dict
|
|
225
|
+
Mapping of validator names to ``PydanticDescriptorProxy`` instances.
|
|
226
|
+
"""
|
|
227
|
+
return {}
|
|
228
|
+
|
|
105
229
|
@classmethod
|
|
106
230
|
def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
|
|
107
|
-
|
|
231
|
+
"""
|
|
232
|
+
Return raw configuration list for the given serializer/field category.
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
s_type : S_TYPES
|
|
237
|
+
Serializer type (``"create"`` | ``"update"`` | ``"read"`` | ``"detail"``).
|
|
238
|
+
f_type : F_TYPES
|
|
239
|
+
Field category (``"fields"`` | ``"optionals"`` | ``"customs"`` | ``"excludes"``).
|
|
240
|
+
|
|
241
|
+
Returns
|
|
242
|
+
-------
|
|
243
|
+
list
|
|
244
|
+
Raw configuration list for the requested category.
|
|
245
|
+
|
|
246
|
+
Raises
|
|
247
|
+
------
|
|
248
|
+
NotImplementedError
|
|
249
|
+
Subclasses must provide an implementation.
|
|
250
|
+
"""
|
|
108
251
|
raise NotImplementedError
|
|
109
252
|
|
|
110
253
|
@classmethod
|
|
111
254
|
def _get_model(cls) -> models.Model:
|
|
112
|
-
|
|
255
|
+
"""
|
|
256
|
+
Return the Django model class associated with this serializer.
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
models.Model
|
|
261
|
+
The Django model class.
|
|
262
|
+
|
|
263
|
+
Raises
|
|
264
|
+
------
|
|
265
|
+
NotImplementedError
|
|
266
|
+
Subclasses must provide an implementation.
|
|
267
|
+
"""
|
|
113
268
|
raise NotImplementedError
|
|
114
269
|
|
|
115
270
|
@classmethod
|
|
@@ -246,12 +401,32 @@ class BaseSerializer:
|
|
|
246
401
|
|
|
247
402
|
@classmethod
|
|
248
403
|
def _get_relations_serializers(cls) -> dict[str, "Serializer"]:
|
|
249
|
-
|
|
404
|
+
"""
|
|
405
|
+
Return mapping of relation field names to their serializer classes.
|
|
406
|
+
|
|
407
|
+
Subclasses may override to provide explicit serializer mappings for
|
|
408
|
+
relation fields used during read schema generation.
|
|
409
|
+
|
|
410
|
+
Returns
|
|
411
|
+
-------
|
|
412
|
+
dict[str, Serializer]
|
|
413
|
+
Mapping of field name to serializer class. Empty by default.
|
|
414
|
+
"""
|
|
250
415
|
return {}
|
|
251
416
|
|
|
252
417
|
@classmethod
|
|
253
418
|
def _get_relations_as_id(cls) -> list[str]:
|
|
254
|
-
|
|
419
|
+
"""
|
|
420
|
+
Return relation field names that should be serialized as IDs.
|
|
421
|
+
|
|
422
|
+
Subclasses may override to specify which relation fields should be
|
|
423
|
+
represented as primary key values instead of nested objects.
|
|
424
|
+
|
|
425
|
+
Returns
|
|
426
|
+
-------
|
|
427
|
+
list[str]
|
|
428
|
+
Field names to serialize as IDs. Empty by default.
|
|
429
|
+
"""
|
|
255
430
|
return []
|
|
256
431
|
|
|
257
432
|
@classmethod
|
|
@@ -302,45 +477,100 @@ class BaseSerializer:
|
|
|
302
477
|
-------
|
|
303
478
|
Schema | Union[Schema, ...] | None
|
|
304
479
|
Generated schema, Union of schemas, or None if cannot be resolved.
|
|
305
|
-
"""
|
|
306
|
-
# Auto-resolve ModelSerializer with readable fields
|
|
307
|
-
if isinstance(rel_model, ModelSerializerMeta):
|
|
308
|
-
has_readable_fields = rel_model.get_fields(
|
|
309
|
-
"read"
|
|
310
|
-
) or rel_model.get_custom_fields("read")
|
|
311
|
-
return rel_model.generate_related_s() if has_readable_fields else None
|
|
312
|
-
|
|
313
|
-
# Resolve from explicit serializer mapping
|
|
314
|
-
rel_serializers = cls._get_relations_serializers() or {}
|
|
315
|
-
serializer_ref = rel_serializers.get(field_name)
|
|
316
480
|
|
|
317
|
-
|
|
481
|
+
Notes
|
|
482
|
+
-----
|
|
483
|
+
Includes circular reference detection to prevent infinite recursion.
|
|
484
|
+
"""
|
|
485
|
+
# Security: Check for circular references to prevent infinite recursion
|
|
486
|
+
if cls._is_circular_reference(rel_model):
|
|
487
|
+
# Circular reference detected - return None to break the cycle
|
|
488
|
+
warnings.warn(
|
|
489
|
+
f"Circular reference detected for {rel_model._meta.label} "
|
|
490
|
+
f"in field '{field_name}' of {cls._get_model()._meta.label}. "
|
|
491
|
+
f"Skipping nested schema generation to prevent infinite recursion.",
|
|
492
|
+
UserWarning,
|
|
493
|
+
stacklevel=2
|
|
494
|
+
)
|
|
318
495
|
return None
|
|
319
496
|
|
|
320
|
-
|
|
497
|
+
# Track this model in the resolution stack
|
|
498
|
+
cls._push_resolution(rel_model)
|
|
499
|
+
try:
|
|
500
|
+
# Auto-resolve ModelSerializer with readable fields
|
|
501
|
+
if isinstance(rel_model, ModelSerializerMeta):
|
|
502
|
+
has_readable_fields = rel_model.get_fields(
|
|
503
|
+
"read"
|
|
504
|
+
) or rel_model.get_custom_fields("read")
|
|
505
|
+
return rel_model.generate_related_s() if has_readable_fields else None
|
|
506
|
+
|
|
507
|
+
# Resolve from explicit serializer mapping
|
|
508
|
+
rel_serializers = cls._get_relations_serializers() or {}
|
|
509
|
+
serializer_ref = rel_serializers.get(field_name)
|
|
510
|
+
|
|
511
|
+
if not serializer_ref:
|
|
512
|
+
return None
|
|
513
|
+
|
|
514
|
+
resolved = cls._resolve_serializer_reference(serializer_ref)
|
|
321
515
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
516
|
+
# Handle Union of serializers
|
|
517
|
+
if get_origin(resolved) is Union:
|
|
518
|
+
return cls._generate_union_schema(resolved)
|
|
325
519
|
|
|
326
|
-
|
|
327
|
-
|
|
520
|
+
# Handle single serializer
|
|
521
|
+
return resolved.generate_related_s()
|
|
522
|
+
finally:
|
|
523
|
+
# Always pop from resolution stack when done
|
|
524
|
+
cls._pop_resolution()
|
|
328
525
|
|
|
329
526
|
@classmethod
|
|
330
527
|
def _is_special_field(
|
|
331
528
|
cls, s_type: type[S_TYPES], field: str, f_type: type[F_TYPES]
|
|
332
529
|
) -> bool:
|
|
333
|
-
"""
|
|
530
|
+
"""
|
|
531
|
+
Check whether a field appears in the given category for a serializer type.
|
|
532
|
+
|
|
533
|
+
Parameters
|
|
534
|
+
----------
|
|
535
|
+
s_type : S_TYPES
|
|
536
|
+
Serializer type (``"create"`` | ``"update"`` | ``"read"`` | ``"detail"``).
|
|
537
|
+
field : str
|
|
538
|
+
The field name to look up.
|
|
539
|
+
f_type : F_TYPES
|
|
540
|
+
Field category (``"fields"`` | ``"optionals"`` | ``"customs"`` | ``"excludes"``).
|
|
541
|
+
|
|
542
|
+
Returns
|
|
543
|
+
-------
|
|
544
|
+
bool
|
|
545
|
+
``True`` if the field is found in the specified category.
|
|
546
|
+
"""
|
|
334
547
|
special_fields = cls._get_fields(s_type, f_type)
|
|
335
548
|
return any(field in special_f for special_f in special_fields)
|
|
336
549
|
|
|
337
550
|
@classmethod
|
|
338
551
|
def get_custom_fields(cls, s_type: type[S_TYPES]) -> list[tuple[str, type, Any]]:
|
|
339
552
|
"""
|
|
340
|
-
Normalize declared custom field specs into (name, py_type, default).
|
|
553
|
+
Normalize declared custom field specs into ``(name, py_type, default)`` tuples.
|
|
554
|
+
|
|
341
555
|
Accepted tuple shapes:
|
|
342
|
-
|
|
343
|
-
- (name, py_type)
|
|
556
|
+
|
|
557
|
+
- ``(name, py_type, default)`` -- field with explicit default.
|
|
558
|
+
- ``(name, py_type)`` -- required field (default set to ``Ellipsis``).
|
|
559
|
+
|
|
560
|
+
Parameters
|
|
561
|
+
----------
|
|
562
|
+
s_type : S_TYPES
|
|
563
|
+
Serializer type whose custom fields to retrieve.
|
|
564
|
+
|
|
565
|
+
Returns
|
|
566
|
+
-------
|
|
567
|
+
list[tuple[str, type, Any]]
|
|
568
|
+
Normalized list of ``(name, type, default)`` tuples.
|
|
569
|
+
|
|
570
|
+
Raises
|
|
571
|
+
------
|
|
572
|
+
ValueError
|
|
573
|
+
If a custom field spec is not a tuple or has an invalid length.
|
|
344
574
|
"""
|
|
345
575
|
raw_customs = cls._get_fields(s_type, "customs") or []
|
|
346
576
|
normalized: list[tuple[str, Any, Any]] = []
|
|
@@ -362,7 +592,19 @@ class BaseSerializer:
|
|
|
362
592
|
|
|
363
593
|
@classmethod
|
|
364
594
|
def get_optional_fields(cls, s_type: type[S_TYPES]):
|
|
365
|
-
"""
|
|
595
|
+
"""
|
|
596
|
+
Return optional field specs normalized to ``(name, type, None)`` tuples.
|
|
597
|
+
|
|
598
|
+
Parameters
|
|
599
|
+
----------
|
|
600
|
+
s_type : S_TYPES
|
|
601
|
+
Serializer type whose optional fields to retrieve.
|
|
602
|
+
|
|
603
|
+
Returns
|
|
604
|
+
-------
|
|
605
|
+
list[tuple[str, type, None]]
|
|
606
|
+
Normalized list where each entry defaults to ``None``.
|
|
607
|
+
"""
|
|
366
608
|
return [
|
|
367
609
|
(field, field_type, None)
|
|
368
610
|
for field, field_type in cls._get_fields(s_type, "optionals")
|
|
@@ -370,12 +612,39 @@ class BaseSerializer:
|
|
|
370
612
|
|
|
371
613
|
@classmethod
|
|
372
614
|
def get_excluded_fields(cls, s_type: S_TYPES):
|
|
373
|
-
"""
|
|
615
|
+
"""
|
|
616
|
+
Return excluded field names for the given serializer type.
|
|
617
|
+
|
|
618
|
+
Parameters
|
|
619
|
+
----------
|
|
620
|
+
s_type : S_TYPES
|
|
621
|
+
Serializer type whose exclusions to retrieve.
|
|
622
|
+
|
|
623
|
+
Returns
|
|
624
|
+
-------
|
|
625
|
+
list[str]
|
|
626
|
+
Field names excluded from schema generation.
|
|
627
|
+
"""
|
|
374
628
|
return cls._get_fields(s_type, "excludes")
|
|
375
629
|
|
|
376
630
|
@classmethod
|
|
377
631
|
def get_fields(cls, s_type: S_TYPES):
|
|
378
|
-
"""
|
|
632
|
+
"""
|
|
633
|
+
Return explicit declared field names for the serializer type.
|
|
634
|
+
|
|
635
|
+
Filters out inline custom field tuples from the fields list, returning
|
|
636
|
+
only string field names.
|
|
637
|
+
|
|
638
|
+
Parameters
|
|
639
|
+
----------
|
|
640
|
+
s_type : S_TYPES
|
|
641
|
+
Serializer type whose fields to retrieve.
|
|
642
|
+
|
|
643
|
+
Returns
|
|
644
|
+
-------
|
|
645
|
+
list[str]
|
|
646
|
+
Model field names (excludes inline custom tuples).
|
|
647
|
+
"""
|
|
379
648
|
fields = cls._get_fields(s_type, "fields")
|
|
380
649
|
# Filter out inline custom field tuples, return only string field names
|
|
381
650
|
return [f for f in fields if isinstance(f, str)]
|
|
@@ -411,14 +680,40 @@ class BaseSerializer:
|
|
|
411
680
|
|
|
412
681
|
@classmethod
|
|
413
682
|
def is_custom(cls, field: str) -> bool:
|
|
414
|
-
"""
|
|
683
|
+
"""
|
|
684
|
+
Check if a field is declared as a custom input in create or update.
|
|
685
|
+
|
|
686
|
+
Parameters
|
|
687
|
+
----------
|
|
688
|
+
field : str
|
|
689
|
+
The field name to check.
|
|
690
|
+
|
|
691
|
+
Returns
|
|
692
|
+
-------
|
|
693
|
+
bool
|
|
694
|
+
``True`` if the field appears in the customs category for either
|
|
695
|
+
``create`` or ``update`` serializer types.
|
|
696
|
+
"""
|
|
415
697
|
return cls._is_special_field(
|
|
416
698
|
"create", field, "customs"
|
|
417
699
|
) or cls._is_special_field("update", field, "customs")
|
|
418
700
|
|
|
419
701
|
@classmethod
|
|
420
702
|
def is_optional(cls, field: str) -> bool:
|
|
421
|
-
"""
|
|
703
|
+
"""
|
|
704
|
+
Check if a field is declared as optional in create or update.
|
|
705
|
+
|
|
706
|
+
Parameters
|
|
707
|
+
----------
|
|
708
|
+
field : str
|
|
709
|
+
The field name to check.
|
|
710
|
+
|
|
711
|
+
Returns
|
|
712
|
+
-------
|
|
713
|
+
bool
|
|
714
|
+
``True`` if the field appears in the optionals category for either
|
|
715
|
+
``create`` or ``update`` serializer types.
|
|
716
|
+
"""
|
|
422
717
|
return cls._is_special_field(
|
|
423
718
|
"create", field, "optionals"
|
|
424
719
|
) or cls._is_special_field("update", field, "optionals")
|
|
@@ -531,7 +826,20 @@ class BaseSerializer:
|
|
|
531
826
|
|
|
532
827
|
@classmethod
|
|
533
828
|
def _is_reverse_relation(cls, field_obj) -> bool:
|
|
534
|
-
"""
|
|
829
|
+
"""
|
|
830
|
+
Check if a field descriptor represents a reverse relation.
|
|
831
|
+
|
|
832
|
+
Parameters
|
|
833
|
+
----------
|
|
834
|
+
field_obj : Any
|
|
835
|
+
Django model field descriptor.
|
|
836
|
+
|
|
837
|
+
Returns
|
|
838
|
+
-------
|
|
839
|
+
bool
|
|
840
|
+
``True`` if the descriptor is a ``ManyToManyDescriptor``,
|
|
841
|
+
``ReverseManyToOneDescriptor``, or ``ReverseOneToOneDescriptor``.
|
|
842
|
+
"""
|
|
535
843
|
return isinstance(
|
|
536
844
|
field_obj,
|
|
537
845
|
(
|
|
@@ -543,14 +851,39 @@ class BaseSerializer:
|
|
|
543
851
|
|
|
544
852
|
@classmethod
|
|
545
853
|
def _is_forward_relation(cls, field_obj) -> bool:
|
|
546
|
-
"""
|
|
854
|
+
"""
|
|
855
|
+
Check if a field descriptor represents a forward relation.
|
|
856
|
+
|
|
857
|
+
Parameters
|
|
858
|
+
----------
|
|
859
|
+
field_obj : Any
|
|
860
|
+
Django model field descriptor.
|
|
861
|
+
|
|
862
|
+
Returns
|
|
863
|
+
-------
|
|
864
|
+
bool
|
|
865
|
+
``True`` if the descriptor is a ``ForwardOneToOneDescriptor``
|
|
866
|
+
or ``ForwardManyToOneDescriptor``.
|
|
867
|
+
"""
|
|
547
868
|
return isinstance(
|
|
548
869
|
field_obj, (ForwardOneToOneDescriptor, ForwardManyToOneDescriptor)
|
|
549
870
|
)
|
|
550
871
|
|
|
551
872
|
@classmethod
|
|
552
873
|
def _warn_missing_relation_serializer(cls, field_name: str, model) -> None:
|
|
553
|
-
"""
|
|
874
|
+
"""
|
|
875
|
+
Emit a warning for reverse relations without an explicit serializer mapping.
|
|
876
|
+
|
|
877
|
+
Only warns when the related model is not a ``ModelSerializer`` and the
|
|
878
|
+
``NINJA_AIO_RAISE_SERIALIZATION_WARNINGS`` setting is enabled (default).
|
|
879
|
+
|
|
880
|
+
Parameters
|
|
881
|
+
----------
|
|
882
|
+
field_name : str
|
|
883
|
+
Name of the reverse relation field.
|
|
884
|
+
model : type
|
|
885
|
+
The Django model class owning the field.
|
|
886
|
+
"""
|
|
554
887
|
if not isinstance(model, ModelSerializerMeta) and getattr(
|
|
555
888
|
settings, "NINJA_AIO_RAISE_SERIALIZATION_WARNINGS", True
|
|
556
889
|
):
|
|
@@ -669,53 +1002,50 @@ class BaseSerializer:
|
|
|
669
1002
|
)
|
|
670
1003
|
|
|
671
1004
|
@classmethod
|
|
672
|
-
def
|
|
673
|
-
cls,
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
return create_schema(
|
|
692
|
-
model=model,
|
|
693
|
-
name=f"{model._meta.model_name}{schema_name}",
|
|
694
|
-
depth=depth,
|
|
695
|
-
fields=fields,
|
|
696
|
-
custom_fields=reverse_rels + customs + optionals,
|
|
697
|
-
exclude=excludes,
|
|
698
|
-
)
|
|
1005
|
+
def _create_out_or_detail_schema(
|
|
1006
|
+
cls, schema_type: type[SCHEMA_TYPES], model, validators, depth: int = None
|
|
1007
|
+
) -> Schema | None:
|
|
1008
|
+
"""Create schema for Out or Detail types."""
|
|
1009
|
+
fields, reverse_rels, excludes, customs, optionals = (
|
|
1010
|
+
cls.get_schema_out_data(schema_type)
|
|
1011
|
+
)
|
|
1012
|
+
if not any([fields, reverse_rels, excludes, customs]):
|
|
1013
|
+
return None
|
|
1014
|
+
schema_name = "SchemaOut" if schema_type == "Out" else "DetailSchemaOut"
|
|
1015
|
+
schema = create_schema(
|
|
1016
|
+
model=model,
|
|
1017
|
+
name=f"{model._meta.model_name}{schema_name}",
|
|
1018
|
+
depth=depth,
|
|
1019
|
+
fields=fields,
|
|
1020
|
+
custom_fields=reverse_rels + customs + optionals,
|
|
1021
|
+
exclude=excludes,
|
|
1022
|
+
)
|
|
1023
|
+
return cls._apply_validators(schema, validators)
|
|
699
1024
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
1025
|
+
@classmethod
|
|
1026
|
+
def _create_related_schema(cls, model, validators) -> Schema | None:
|
|
1027
|
+
"""Create schema for Related type."""
|
|
1028
|
+
fields, customs = cls.get_related_schema_data()
|
|
1029
|
+
if not fields and not customs:
|
|
1030
|
+
return None
|
|
1031
|
+
schema = create_schema(
|
|
1032
|
+
model=model,
|
|
1033
|
+
name=f"{model._meta.model_name}SchemaRelated",
|
|
1034
|
+
fields=fields,
|
|
1035
|
+
custom_fields=customs,
|
|
1036
|
+
)
|
|
1037
|
+
return cls._apply_validators(schema, validators)
|
|
710
1038
|
|
|
711
|
-
|
|
1039
|
+
@classmethod
|
|
1040
|
+
def _create_in_or_patch_schema(
|
|
1041
|
+
cls, schema_type: type[SCHEMA_TYPES], model, validators
|
|
1042
|
+
) -> Schema | None:
|
|
1043
|
+
"""Create schema for In or Patch types."""
|
|
712
1044
|
s_type = "create" if schema_type == "In" else "update"
|
|
713
1045
|
fields = cls.get_fields(s_type)
|
|
714
1046
|
optionals = cls.get_optional_fields(s_type)
|
|
715
1047
|
customs = (
|
|
716
|
-
cls.get_custom_fields(s_type)
|
|
717
|
-
+ optionals
|
|
718
|
-
+ cls.get_inline_customs(s_type)
|
|
1048
|
+
cls.get_custom_fields(s_type) + optionals + cls.get_inline_customs(s_type)
|
|
719
1049
|
)
|
|
720
1050
|
excludes = cls.get_excluded_fields(s_type)
|
|
721
1051
|
|
|
@@ -736,13 +1066,50 @@ class BaseSerializer:
|
|
|
736
1066
|
if not any([fields, customs, excludes]):
|
|
737
1067
|
return None
|
|
738
1068
|
|
|
739
|
-
|
|
1069
|
+
schema = create_schema(
|
|
740
1070
|
model=model,
|
|
741
1071
|
name=f"{model._meta.model_name}Schema{schema_type}",
|
|
742
1072
|
fields=fields,
|
|
743
1073
|
custom_fields=customs,
|
|
744
1074
|
exclude=excludes,
|
|
745
1075
|
)
|
|
1076
|
+
return cls._apply_validators(schema, validators)
|
|
1077
|
+
|
|
1078
|
+
@classmethod
|
|
1079
|
+
def _generate_model_schema(
|
|
1080
|
+
cls,
|
|
1081
|
+
schema_type: type[SCHEMA_TYPES],
|
|
1082
|
+
depth: int = None,
|
|
1083
|
+
) -> Schema:
|
|
1084
|
+
"""
|
|
1085
|
+
Core schema factory bridging serializer configuration to ``ninja.orm.create_schema``.
|
|
1086
|
+
|
|
1087
|
+
Dispatches to the appropriate field/custom/exclude gathering logic based
|
|
1088
|
+
on the requested schema type and delegates to Django Ninja's
|
|
1089
|
+
``create_schema`` for the actual Pydantic model construction.
|
|
1090
|
+
|
|
1091
|
+
Parameters
|
|
1092
|
+
----------
|
|
1093
|
+
schema_type : SCHEMA_TYPES
|
|
1094
|
+
One of ``"In"``, ``"Patch"``, ``"Out"``, ``"Detail"``, or ``"Related"``.
|
|
1095
|
+
depth : int, optional
|
|
1096
|
+
Nesting depth for related model schemas (used by ``Out`` and ``Detail``).
|
|
1097
|
+
|
|
1098
|
+
Returns
|
|
1099
|
+
-------
|
|
1100
|
+
Schema | None
|
|
1101
|
+
Generated Pydantic schema, or ``None`` if no fields are configured.
|
|
1102
|
+
"""
|
|
1103
|
+
model = cls._get_model()
|
|
1104
|
+
validators = cls._get_validators(schema_type)
|
|
1105
|
+
|
|
1106
|
+
if schema_type in ("Out", "Detail"):
|
|
1107
|
+
return cls._create_out_or_detail_schema(schema_type, model, validators, depth)
|
|
1108
|
+
|
|
1109
|
+
if schema_type == "Related":
|
|
1110
|
+
return cls._create_related_schema(model, validators)
|
|
1111
|
+
|
|
1112
|
+
return cls._create_in_or_patch_schema(schema_type, model, validators)
|
|
746
1113
|
|
|
747
1114
|
@classmethod
|
|
748
1115
|
def get_related_schema_data(cls):
|
|
@@ -779,28 +1146,94 @@ class BaseSerializer:
|
|
|
779
1146
|
return non_relation_fields, customs
|
|
780
1147
|
|
|
781
1148
|
@classmethod
|
|
1149
|
+
@lru_cache(maxsize=128)
|
|
782
1150
|
def generate_read_s(cls, depth: int = 1) -> Schema:
|
|
783
|
-
"""
|
|
1151
|
+
"""
|
|
1152
|
+
Generate the read (Out) schema for list responses.
|
|
1153
|
+
|
|
1154
|
+
Performance: Results are cached per (class, depth) combination.
|
|
1155
|
+
|
|
1156
|
+
Parameters
|
|
1157
|
+
----------
|
|
1158
|
+
depth : int, optional
|
|
1159
|
+
Nesting depth for related models. Defaults to ``1``.
|
|
1160
|
+
|
|
1161
|
+
Returns
|
|
1162
|
+
-------
|
|
1163
|
+
Schema | None
|
|
1164
|
+
Generated Pydantic schema, or ``None`` if no read fields are configured.
|
|
1165
|
+
"""
|
|
784
1166
|
return cls._generate_model_schema("Out", depth)
|
|
785
1167
|
|
|
786
1168
|
@classmethod
|
|
1169
|
+
@lru_cache(maxsize=128)
|
|
787
1170
|
def generate_detail_s(cls, depth: int = 1) -> Schema:
|
|
788
|
-
"""
|
|
1171
|
+
"""
|
|
1172
|
+
Generate the detail (single-object) read schema.
|
|
1173
|
+
|
|
1174
|
+
Falls back to the standard read schema if no detail-specific
|
|
1175
|
+
configuration is defined.
|
|
1176
|
+
|
|
1177
|
+
Performance: Results are cached per (class, depth) combination.
|
|
1178
|
+
|
|
1179
|
+
Parameters
|
|
1180
|
+
----------
|
|
1181
|
+
depth : int, optional
|
|
1182
|
+
Nesting depth for related models. Defaults to ``1``.
|
|
1183
|
+
|
|
1184
|
+
Returns
|
|
1185
|
+
-------
|
|
1186
|
+
Schema
|
|
1187
|
+
Generated Pydantic schema (never ``None``; falls back to read schema).
|
|
1188
|
+
"""
|
|
789
1189
|
return cls._generate_model_schema("Detail", depth) or cls.generate_read_s(depth)
|
|
790
1190
|
|
|
791
1191
|
@classmethod
|
|
1192
|
+
@lru_cache(maxsize=128)
|
|
792
1193
|
def generate_create_s(cls) -> Schema:
|
|
793
|
-
"""
|
|
1194
|
+
"""
|
|
1195
|
+
Generate the create (In) schema for input validation.
|
|
1196
|
+
|
|
1197
|
+
Performance: Results are cached per class.
|
|
1198
|
+
|
|
1199
|
+
Returns
|
|
1200
|
+
-------
|
|
1201
|
+
Schema | None
|
|
1202
|
+
Generated Pydantic schema, or ``None`` if no create fields are configured.
|
|
1203
|
+
"""
|
|
794
1204
|
return cls._generate_model_schema("In")
|
|
795
1205
|
|
|
796
1206
|
@classmethod
|
|
1207
|
+
@lru_cache(maxsize=128)
|
|
797
1208
|
def generate_update_s(cls) -> Schema:
|
|
798
|
-
"""
|
|
1209
|
+
"""
|
|
1210
|
+
Generate the update (Patch) schema for partial updates.
|
|
1211
|
+
|
|
1212
|
+
Performance: Results are cached per class.
|
|
1213
|
+
|
|
1214
|
+
Returns
|
|
1215
|
+
-------
|
|
1216
|
+
Schema | None
|
|
1217
|
+
Generated Pydantic schema, or ``None`` if no update fields are configured.
|
|
1218
|
+
"""
|
|
799
1219
|
return cls._generate_model_schema("Patch")
|
|
800
1220
|
|
|
801
1221
|
@classmethod
|
|
1222
|
+
@lru_cache(maxsize=128)
|
|
802
1223
|
def generate_related_s(cls) -> Schema:
|
|
803
|
-
"""
|
|
1224
|
+
"""
|
|
1225
|
+
Generate the related (nested) schema for embedding in parent schemas.
|
|
1226
|
+
|
|
1227
|
+
Includes only non-relational model fields and custom fields, preventing
|
|
1228
|
+
infinite nesting of related objects.
|
|
1229
|
+
|
|
1230
|
+
Performance: Results are cached per class.
|
|
1231
|
+
|
|
1232
|
+
Returns
|
|
1233
|
+
-------
|
|
1234
|
+
Schema | None
|
|
1235
|
+
Generated Pydantic schema, or ``None`` if no fields are configured.
|
|
1236
|
+
"""
|
|
804
1237
|
return cls._generate_model_schema("Related")
|
|
805
1238
|
|
|
806
1239
|
@classmethod
|
|
@@ -861,8 +1294,8 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
|
|
|
861
1294
|
Disallowed model fields on create (e.g., id, timestamps).
|
|
862
1295
|
"""
|
|
863
1296
|
|
|
864
|
-
fields: list[str | tuple[str, Any, Any]] = []
|
|
865
|
-
customs: list[tuple[str, Any, Any]] = []
|
|
1297
|
+
fields: list[str | tuple[str, Any, Any] | tuple[str, Any]] = []
|
|
1298
|
+
customs: list[tuple[str, Any, Any] | tuple[str, Any]] = []
|
|
866
1299
|
optionals: list[tuple[str, Any]] = []
|
|
867
1300
|
excludes: list[str] = []
|
|
868
1301
|
|
|
@@ -883,8 +1316,8 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
|
|
|
883
1316
|
Relation fields to serialize as IDs instead of nested objects.
|
|
884
1317
|
"""
|
|
885
1318
|
|
|
886
|
-
fields: list[str | tuple[str, Any, Any]] = []
|
|
887
|
-
customs: list[tuple[str, Any, Any]] = []
|
|
1319
|
+
fields: list[str | tuple[str, Any, Any] | tuple[str, Any]] = []
|
|
1320
|
+
customs: list[tuple[str, Any, Any] | tuple[str, Any]] = []
|
|
888
1321
|
optionals: list[tuple[str, Any]] = []
|
|
889
1322
|
excludes: list[str] = []
|
|
890
1323
|
relations_as_id: list[str] = []
|
|
@@ -904,8 +1337,8 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
|
|
|
904
1337
|
Optional output fields.
|
|
905
1338
|
"""
|
|
906
1339
|
|
|
907
|
-
fields: list[str | tuple[str, Any, Any]] = []
|
|
908
|
-
customs: list[tuple[str, Any, Any]] = []
|
|
1340
|
+
fields: list[str | tuple[str, Any, Any] | tuple[str, Any]] = []
|
|
1341
|
+
customs: list[tuple[str, Any, Any] | tuple[str, Any]] = []
|
|
909
1342
|
optionals: list[tuple[str, Any]] = []
|
|
910
1343
|
excludes: list[str] = []
|
|
911
1344
|
|
|
@@ -937,9 +1370,47 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
|
|
|
937
1370
|
"detail": "DetailSerializer",
|
|
938
1371
|
}
|
|
939
1372
|
|
|
1373
|
+
# Schema type to serializer type mapping for validator resolution
|
|
1374
|
+
_SCHEMA_TO_S_TYPE = {
|
|
1375
|
+
"In": "create",
|
|
1376
|
+
"Patch": "update",
|
|
1377
|
+
"Out": "read",
|
|
1378
|
+
"Detail": "detail",
|
|
1379
|
+
"Related": "read",
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
@classmethod
|
|
1383
|
+
def _get_validators(cls, schema_type: type[SCHEMA_TYPES]) -> dict:
|
|
1384
|
+
"""
|
|
1385
|
+
Collect validators from the inner serializer class for the given schema type.
|
|
1386
|
+
|
|
1387
|
+
Parameters
|
|
1388
|
+
----------
|
|
1389
|
+
schema_type : SCHEMA_TYPES
|
|
1390
|
+
One of ``"In"``, ``"Patch"``, ``"Out"``, ``"Detail"``, or ``"Related"``.
|
|
1391
|
+
|
|
1392
|
+
Returns
|
|
1393
|
+
-------
|
|
1394
|
+
dict
|
|
1395
|
+
Mapping of validator names to ``PydanticDescriptorProxy`` instances.
|
|
1396
|
+
"""
|
|
1397
|
+
s_type = cls._SCHEMA_TO_S_TYPE.get(schema_type)
|
|
1398
|
+
config_name = cls._SERIALIZER_CONFIG_MAP.get(s_type)
|
|
1399
|
+
config_class = getattr(cls, config_name, None) if config_name else None
|
|
1400
|
+
return cls._collect_validators(config_class)
|
|
1401
|
+
|
|
940
1402
|
@classmethod
|
|
941
1403
|
def _get_relations_as_id(cls) -> list[str]:
|
|
942
|
-
"""
|
|
1404
|
+
"""
|
|
1405
|
+
Return relation fields to serialize as primary key values.
|
|
1406
|
+
|
|
1407
|
+
Reads the ``relations_as_id`` attribute from ``ReadSerializer``.
|
|
1408
|
+
|
|
1409
|
+
Returns
|
|
1410
|
+
-------
|
|
1411
|
+
list[str]
|
|
1412
|
+
Field names whose related objects should be serialized as IDs.
|
|
1413
|
+
"""
|
|
943
1414
|
return getattr(cls.ReadSerializer, "relations_as_id", [])
|
|
944
1415
|
|
|
945
1416
|
@classmethod
|
|
@@ -970,17 +1441,30 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
|
|
|
970
1441
|
|
|
971
1442
|
@classmethod
|
|
972
1443
|
def _get_model(cls) -> "ModelSerializer":
|
|
973
|
-
"""
|
|
1444
|
+
"""
|
|
1445
|
+
Return the model class itself.
|
|
1446
|
+
|
|
1447
|
+
Since ``ModelSerializer`` is mixed directly into the model, the model
|
|
1448
|
+
class is the serializer class.
|
|
1449
|
+
|
|
1450
|
+
Returns
|
|
1451
|
+
-------
|
|
1452
|
+
ModelSerializer
|
|
1453
|
+
The model/serializer class.
|
|
1454
|
+
"""
|
|
974
1455
|
return cls
|
|
975
1456
|
|
|
976
1457
|
@classmethod
|
|
977
1458
|
def verbose_name_path_resolver(cls) -> str:
|
|
978
1459
|
"""
|
|
979
|
-
Slugify plural verbose name for URL path segment.
|
|
1460
|
+
Slugify the plural verbose name for use as a URL path segment.
|
|
1461
|
+
|
|
1462
|
+
Replaces spaces with hyphens in the model's ``verbose_name_plural``.
|
|
980
1463
|
|
|
981
1464
|
Returns
|
|
982
1465
|
-------
|
|
983
1466
|
str
|
|
1467
|
+
Hyphen-separated URL-safe path segment.
|
|
984
1468
|
"""
|
|
985
1469
|
return "-".join(cls._meta.verbose_name_plural.split(" "))
|
|
986
1470
|
|
|
@@ -1103,14 +1587,16 @@ class SchemaModelConfig(Schema):
|
|
|
1103
1587
|
Optional model fields. Type can be any valid type annotation including Union.
|
|
1104
1588
|
exclude : Optional[List[str]]
|
|
1105
1589
|
Model fields to exclude.
|
|
1106
|
-
customs : Optional[List[tuple[str, Any, Any]]]
|
|
1590
|
+
customs : Optional[List[tuple[str, Any, Any] | tuple[str, Any]]]
|
|
1107
1591
|
Custom / synthetic fields. Type can be any valid type annotation including Union.
|
|
1592
|
+
- 2-tuple: (name, type) - required field
|
|
1593
|
+
- 3-tuple: (name, type, default) - optional field with default
|
|
1108
1594
|
"""
|
|
1109
1595
|
|
|
1110
1596
|
fields: Optional[List[str | tuple[str, Any, Any] | tuple[str, Any]]] = None
|
|
1111
1597
|
optionals: Optional[List[tuple[str, Any]]] = None
|
|
1112
1598
|
exclude: Optional[List[str]] = None
|
|
1113
|
-
customs: Optional[List[tuple[str, Any, Any]]] = None
|
|
1599
|
+
customs: Optional[List[tuple[str, Any, Any] | tuple[str, Any]]] = None
|
|
1114
1600
|
|
|
1115
1601
|
|
|
1116
1602
|
class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
@@ -1131,6 +1617,15 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
|
1131
1617
|
"detail": "detail",
|
|
1132
1618
|
}
|
|
1133
1619
|
|
|
1620
|
+
# Schema type to validators inner class mapping
|
|
1621
|
+
_VALIDATORS_CLASS_MAP = {
|
|
1622
|
+
"In": "CreateValidators",
|
|
1623
|
+
"Patch": "UpdateValidators",
|
|
1624
|
+
"Out": "ReadValidators",
|
|
1625
|
+
"Detail": "DetailValidators",
|
|
1626
|
+
"Related": "ReadValidators",
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1134
1629
|
def __init_subclass__(cls, **kwargs):
|
|
1135
1630
|
super().__init_subclass__(**kwargs)
|
|
1136
1631
|
from ninja_aio.models.utils import ModelUtil
|
|
@@ -1150,26 +1645,118 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
|
1150
1645
|
relations_serializers: dict[str, "Serializer"] = {}
|
|
1151
1646
|
relations_as_id: list[str] = []
|
|
1152
1647
|
|
|
1648
|
+
def _parse_payload(self, payload: dict[str, Any] | Schema) -> dict[str, Any]:
|
|
1649
|
+
"""
|
|
1650
|
+
Parse and return the input payload.
|
|
1651
|
+
|
|
1652
|
+
Can be overridden to implement custom parsing logic.
|
|
1653
|
+
|
|
1654
|
+
Parameters
|
|
1655
|
+
----------
|
|
1656
|
+
payload : dict | Schema
|
|
1657
|
+
Input data.
|
|
1658
|
+
|
|
1659
|
+
Returns
|
|
1660
|
+
-------
|
|
1661
|
+
dict
|
|
1662
|
+
Parsed payload.
|
|
1663
|
+
"""
|
|
1664
|
+
return payload.model_dump() if isinstance(payload, Schema) else payload
|
|
1665
|
+
|
|
1666
|
+
@classmethod
|
|
1667
|
+
def _get_validators(cls, schema_type: type[SCHEMA_TYPES]) -> dict:
|
|
1668
|
+
"""
|
|
1669
|
+
Collect validators from the inner validators class for the given schema type.
|
|
1670
|
+
|
|
1671
|
+
Looks for inner classes named ``CreateValidators``, ``ReadValidators``,
|
|
1672
|
+
``UpdateValidators``, or ``DetailValidators`` on the serializer.
|
|
1673
|
+
|
|
1674
|
+
Parameters
|
|
1675
|
+
----------
|
|
1676
|
+
schema_type : SCHEMA_TYPES
|
|
1677
|
+
One of ``"In"``, ``"Patch"``, ``"Out"``, ``"Detail"``, or ``"Related"``.
|
|
1678
|
+
|
|
1679
|
+
Returns
|
|
1680
|
+
-------
|
|
1681
|
+
dict
|
|
1682
|
+
Mapping of validator names to ``PydanticDescriptorProxy`` instances.
|
|
1683
|
+
"""
|
|
1684
|
+
class_name = cls._VALIDATORS_CLASS_MAP.get(schema_type)
|
|
1685
|
+
validators_class = getattr(cls, class_name, None) if class_name else None
|
|
1686
|
+
return cls._collect_validators(validators_class)
|
|
1687
|
+
|
|
1153
1688
|
@classmethod
|
|
1154
1689
|
def _get_relations_as_id(cls) -> list[str]:
|
|
1690
|
+
"""
|
|
1691
|
+
Return relation fields to serialize as primary key values.
|
|
1692
|
+
|
|
1693
|
+
Reads the ``relations_as_id`` attribute from ``Meta``.
|
|
1694
|
+
|
|
1695
|
+
Returns
|
|
1696
|
+
-------
|
|
1697
|
+
list[str]
|
|
1698
|
+
Field names whose related objects should be serialized as IDs.
|
|
1699
|
+
"""
|
|
1155
1700
|
relations_as_id = cls._get_meta_data("relations_as_id")
|
|
1156
1701
|
return relations_as_id or []
|
|
1157
1702
|
|
|
1158
1703
|
@classmethod
|
|
1159
1704
|
def _get_meta_data(cls, attr_name: str) -> Any:
|
|
1705
|
+
"""
|
|
1706
|
+
Retrieve an attribute from the nested ``Meta`` class.
|
|
1707
|
+
|
|
1708
|
+
Parameters
|
|
1709
|
+
----------
|
|
1710
|
+
attr_name : str
|
|
1711
|
+
Name of the ``Meta`` attribute to look up.
|
|
1712
|
+
|
|
1713
|
+
Returns
|
|
1714
|
+
-------
|
|
1715
|
+
Any
|
|
1716
|
+
The attribute value, or ``None`` if not defined.
|
|
1717
|
+
"""
|
|
1160
1718
|
return getattr(cls.Meta, attr_name, None)
|
|
1161
1719
|
|
|
1162
1720
|
@classmethod
|
|
1163
1721
|
def _get_model(cls) -> models.Model:
|
|
1722
|
+
"""
|
|
1723
|
+
Return the Django model class from ``Meta.model``.
|
|
1724
|
+
|
|
1725
|
+
Returns
|
|
1726
|
+
-------
|
|
1727
|
+
models.Model
|
|
1728
|
+
The validated Django model class.
|
|
1729
|
+
"""
|
|
1164
1730
|
return cls._validate_model()
|
|
1165
1731
|
|
|
1166
1732
|
@classmethod
|
|
1167
1733
|
def _get_relations_serializers(cls) -> dict[str, "Serializer"]:
|
|
1734
|
+
"""
|
|
1735
|
+
Return the explicit relation-to-serializer mapping from ``Meta``.
|
|
1736
|
+
|
|
1737
|
+
Returns
|
|
1738
|
+
-------
|
|
1739
|
+
dict[str, Serializer]
|
|
1740
|
+
Mapping of relation field names to serializer classes.
|
|
1741
|
+
"""
|
|
1168
1742
|
relations_serializers = cls._get_meta_data("relations_serializers")
|
|
1169
1743
|
return relations_serializers or {}
|
|
1170
1744
|
|
|
1171
1745
|
@classmethod
|
|
1172
1746
|
def _get_schema_meta(cls, schema_type: str) -> SchemaModelConfig | None:
|
|
1747
|
+
"""
|
|
1748
|
+
Retrieve the ``SchemaModelConfig`` for the given schema type.
|
|
1749
|
+
|
|
1750
|
+
Parameters
|
|
1751
|
+
----------
|
|
1752
|
+
schema_type : str
|
|
1753
|
+
One of ``"in"``, ``"out"``, ``"update"``, or ``"detail"``.
|
|
1754
|
+
|
|
1755
|
+
Returns
|
|
1756
|
+
-------
|
|
1757
|
+
SchemaModelConfig | None
|
|
1758
|
+
The configuration object, or ``None`` if not defined.
|
|
1759
|
+
"""
|
|
1173
1760
|
match schema_type:
|
|
1174
1761
|
case "in":
|
|
1175
1762
|
return cls._get_meta_data("schema_in")
|
|
@@ -1184,6 +1771,19 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
|
1184
1771
|
|
|
1185
1772
|
@classmethod
|
|
1186
1773
|
def _validate_model(cls):
|
|
1774
|
+
"""
|
|
1775
|
+
Validate and return the model defined in ``Meta.model``.
|
|
1776
|
+
|
|
1777
|
+
Returns
|
|
1778
|
+
-------
|
|
1779
|
+
models.Model
|
|
1780
|
+
The validated Django model class.
|
|
1781
|
+
|
|
1782
|
+
Raises
|
|
1783
|
+
------
|
|
1784
|
+
ValueError
|
|
1785
|
+
If ``Meta.model`` is not defined or is not a Django model subclass.
|
|
1786
|
+
"""
|
|
1187
1787
|
model = cls._get_meta_data("model")
|
|
1188
1788
|
if not model:
|
|
1189
1789
|
raise ValueError("Meta.model must be defined for Serializer.")
|
|
@@ -1193,7 +1793,23 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
|
1193
1793
|
|
|
1194
1794
|
@classmethod
|
|
1195
1795
|
def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
|
|
1196
|
-
"""
|
|
1796
|
+
"""
|
|
1797
|
+
Return raw configuration list from the Meta schema for the given categories.
|
|
1798
|
+
|
|
1799
|
+
Falls back to the ``out`` schema when ``detail`` is requested but not defined.
|
|
1800
|
+
|
|
1801
|
+
Parameters
|
|
1802
|
+
----------
|
|
1803
|
+
s_type : S_TYPES
|
|
1804
|
+
Serializer type (``"create"`` | ``"update"`` | ``"read"`` | ``"detail"``).
|
|
1805
|
+
f_type : F_TYPES
|
|
1806
|
+
Field category (``"fields"`` | ``"optionals"`` | ``"customs"`` | ``"excludes"``).
|
|
1807
|
+
|
|
1808
|
+
Returns
|
|
1809
|
+
-------
|
|
1810
|
+
list
|
|
1811
|
+
Raw configuration list, or empty list if not configured.
|
|
1812
|
+
"""
|
|
1197
1813
|
schema_key = cls._SCHEMA_META_MAP.get(s_type)
|
|
1198
1814
|
if not schema_key:
|
|
1199
1815
|
return []
|
|
@@ -1248,13 +1864,13 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
|
1248
1864
|
self.after_save(instance)
|
|
1249
1865
|
return instance
|
|
1250
1866
|
|
|
1251
|
-
async def create(self, payload: dict[str, Any]) -> models.Model:
|
|
1867
|
+
async def create(self, payload: dict[str, Any] | Schema) -> models.Model:
|
|
1252
1868
|
"""
|
|
1253
1869
|
Create a new model instance from the provided payload.
|
|
1254
1870
|
|
|
1255
1871
|
Parameters
|
|
1256
1872
|
----------
|
|
1257
|
-
payload : dict
|
|
1873
|
+
payload : dict | Schema
|
|
1258
1874
|
Input data.
|
|
1259
1875
|
|
|
1260
1876
|
Returns
|
|
@@ -1262,11 +1878,11 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
|
1262
1878
|
models.Model
|
|
1263
1879
|
Created model instance.
|
|
1264
1880
|
"""
|
|
1265
|
-
instance: models.Model = self.model(**payload)
|
|
1881
|
+
instance: models.Model = self.model(**self._parse_payload(payload))
|
|
1266
1882
|
return await self.save(instance)
|
|
1267
1883
|
|
|
1268
1884
|
async def update(
|
|
1269
|
-
self, instance: models.Model, payload: dict[str, Any]
|
|
1885
|
+
self, instance: models.Model, payload: dict[str, Any] | Schema
|
|
1270
1886
|
) -> models.Model:
|
|
1271
1887
|
"""
|
|
1272
1888
|
Update an existing model instance with the provided payload.
|
|
@@ -1275,7 +1891,7 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
|
1275
1891
|
----------
|
|
1276
1892
|
instance : models.Model
|
|
1277
1893
|
The model instance to update.
|
|
1278
|
-
payload : dict
|
|
1894
|
+
payload : dict | Schema
|
|
1279
1895
|
Input data.
|
|
1280
1896
|
|
|
1281
1897
|
Returns
|
|
@@ -1283,7 +1899,7 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
|
1283
1899
|
models.Model
|
|
1284
1900
|
Updated model instance.
|
|
1285
1901
|
"""
|
|
1286
|
-
for attr, value in payload.items():
|
|
1902
|
+
for attr, value in self._parse_payload(payload).items():
|
|
1287
1903
|
setattr(instance, attr, value)
|
|
1288
1904
|
return await self.save(instance)
|
|
1289
1905
|
|