django-ninja-aio-crud 2.7.0__py3-none-any.whl → 2.8.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.7.0.dist-info → django_ninja_aio_crud-2.8.0.dist-info}/METADATA +1 -1
- {django_ninja_aio_crud-2.7.0.dist-info → django_ninja_aio_crud-2.8.0.dist-info}/RECORD +10 -10
- ninja_aio/__init__.py +1 -1
- ninja_aio/helpers/api.py +1 -1
- ninja_aio/models/serializers.py +140 -67
- ninja_aio/schemas/helpers.py +32 -6
- ninja_aio/types.py +11 -6
- ninja_aio/views/api.py +21 -7
- {django_ninja_aio_crud-2.7.0.dist-info → django_ninja_aio_crud-2.8.0.dist-info}/WHEEL +0 -0
- {django_ninja_aio_crud-2.7.0.dist-info → django_ninja_aio_crud-2.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
ninja_aio/__init__.py,sha256=
|
|
1
|
+
ninja_aio/__init__.py,sha256=s2uQ_vbFbWOcD6laB1hG40Mv67xvXbSehMTUbkWCcrA,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
|
|
5
5
|
ninja_aio/parsers.py,sha256=e_4lGCPV7zs-HTqtdJTc8yQD2KPAn9njbL8nF_Mmgkc,153
|
|
6
6
|
ninja_aio/renders.py,sha256=89g46NWUT8nmDG-rG0nxUYbAQWhuXcYKrPh7e1r_Fc4,1735
|
|
7
|
-
ninja_aio/types.py,sha256=
|
|
7
|
+
ninja_aio/types.py,sha256=E3yfXbNKkvLVcr8bvkHTSyIiCRZ4zumzJaXj1aiBi5U,735
|
|
8
8
|
ninja_aio/decorators/__init__.py,sha256=cDDHD_9EI4CP7c5eL1m2mGNl9bR24i8FAkQsT3_RNGM,371
|
|
9
9
|
ninja_aio/decorators/operations.py,sha256=L9yt2ku5oo4CpOLixCADmkcFjLGsWAn-cg-sDcjFhMA,343
|
|
10
10
|
ninja_aio/decorators/views.py,sha256=0RVU4XaM1HvTQ-BOt_NwUtbhwfHau06lh-O8El1LqQk,8139
|
|
11
11
|
ninja_aio/factory/__init__.py,sha256=IdH2z1ZZpv_IqonaDfVo7IsMzkgop6lHqz42RphUYBU,72
|
|
12
12
|
ninja_aio/factory/operations.py,sha256=OgWGqq4WJ4arSQrH9FGAby9kx-HTdS7MOITxHdYMk18,12051
|
|
13
13
|
ninja_aio/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
ninja_aio/helpers/api.py,sha256=
|
|
14
|
+
ninja_aio/helpers/api.py,sha256=2beyexep-ehgaA_1bV5Yuh3zRDVcRCMkrW94nmfDWEA,20819
|
|
15
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=
|
|
17
|
+
ninja_aio/models/serializers.py,sha256=uviKndPf9HRySup-0t_nULJs-vDLCeGFLtkkQGZvi2E,38142
|
|
18
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
|
-
ninja_aio/schemas/helpers.py,sha256=
|
|
22
|
+
ninja_aio/schemas/helpers.py,sha256=KkbDgT7DwvdeBHZ__wurQQ9A1AIy-toCIL9dCzkTFhM,8350
|
|
23
23
|
ninja_aio/views/__init__.py,sha256=DEzjWA6y3WF0V10nNF8eEurLNEodgxKzyFd09AqVp3s,148
|
|
24
|
-
ninja_aio/views/api.py,sha256=
|
|
24
|
+
ninja_aio/views/api.py,sha256=l7z-Cg_BUPRi3TAGpO2EppVA2tUAWUftQAn_qOn6XlM,21963
|
|
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.8.0.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
|
|
27
|
+
django_ninja_aio_crud-2.8.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
28
|
+
django_ninja_aio_crud-2.8.0.dist-info/METADATA,sha256=k3VWPUTlkXEe0tZRt7cWWtdA_5i_31gjkUM3t402nBI,9963
|
|
29
|
+
django_ninja_aio_crud-2.8.0.dist-info/RECORD,,
|
ninja_aio/__init__.py
CHANGED
ninja_aio/helpers/api.py
CHANGED
|
@@ -472,7 +472,7 @@ class ManyToManyAPI:
|
|
|
472
472
|
model = relation.model
|
|
473
473
|
related_name = relation.related_name
|
|
474
474
|
m2m_auth = relation.auth or self.default_auth
|
|
475
|
-
rel_util = ModelUtil(model)
|
|
475
|
+
rel_util = ModelUtil(model, serializer_class=relation.serializer_class)
|
|
476
476
|
rel_path = relation.path or rel_util.verbose_name_path_resolver()
|
|
477
477
|
related_schema = relation.related_schema
|
|
478
478
|
m2m_add, m2m_remove, m2m_get = relation.add, relation.remove, relation.get
|
ninja_aio/models/serializers.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any, List, Optional, Union, get_args, get_origin, ForwardRef
|
|
1
|
+
from typing import Any, List, Literal, Optional, Union, get_args, get_origin, ForwardRef
|
|
2
2
|
import warnings
|
|
3
3
|
import sys
|
|
4
4
|
|
|
@@ -15,7 +15,13 @@ from django.db.models.fields.related_descriptors import (
|
|
|
15
15
|
ForwardOneToOneDescriptor,
|
|
16
16
|
)
|
|
17
17
|
|
|
18
|
-
from ninja_aio.types import
|
|
18
|
+
from ninja_aio.types import (
|
|
19
|
+
S_TYPES,
|
|
20
|
+
F_TYPES,
|
|
21
|
+
SCHEMA_TYPES,
|
|
22
|
+
ModelSerializerMeta,
|
|
23
|
+
SerializerMeta,
|
|
24
|
+
)
|
|
19
25
|
from ninja_aio.schemas.helpers import (
|
|
20
26
|
ModelQuerySetSchema,
|
|
21
27
|
ModelQuerySetExtraSchema,
|
|
@@ -98,6 +104,7 @@ class BaseSerializer:
|
|
|
98
104
|
module = sys.modules.get(module_path)
|
|
99
105
|
if module is None:
|
|
100
106
|
import importlib
|
|
107
|
+
|
|
101
108
|
module = importlib.import_module(module_path)
|
|
102
109
|
|
|
103
110
|
# Get the serializer class from the module
|
|
@@ -136,7 +143,9 @@ class BaseSerializer:
|
|
|
136
143
|
return serializer_class
|
|
137
144
|
|
|
138
145
|
@classmethod
|
|
139
|
-
def _resolve_serializer_reference(
|
|
146
|
+
def _resolve_serializer_reference(
|
|
147
|
+
cls, serializer_ref: str | type | Any
|
|
148
|
+
) -> type | Any:
|
|
140
149
|
"""
|
|
141
150
|
Resolve a serializer reference that may be a string, a class, or a Union of serializers.
|
|
142
151
|
|
|
@@ -217,11 +226,11 @@ class BaseSerializer:
|
|
|
217
226
|
Schema | Union[Schema, ...] | None
|
|
218
227
|
Union of generated schemas or None if all schemas are None.
|
|
219
228
|
"""
|
|
220
|
-
# Generate schemas for each serializer in the Union
|
|
229
|
+
# Generate schemas for each serializer in the Union (single call per serializer)
|
|
221
230
|
schemas = tuple(
|
|
222
|
-
|
|
231
|
+
schema
|
|
223
232
|
for serializer_type in get_args(resolved_union)
|
|
224
|
-
if serializer_type.generate_related_s() is not None
|
|
233
|
+
if (schema := serializer_type.generate_related_s()) is not None
|
|
225
234
|
)
|
|
226
235
|
|
|
227
236
|
if not schemas:
|
|
@@ -393,72 +402,100 @@ class BaseSerializer:
|
|
|
393
402
|
return (field_name, schema | None, None)
|
|
394
403
|
|
|
395
404
|
@classmethod
|
|
396
|
-
def
|
|
405
|
+
def _is_reverse_relation(cls, field_obj) -> bool:
|
|
406
|
+
"""Check if field is a reverse relation (M2M, reverse FK, reverse O2O)."""
|
|
407
|
+
return isinstance(
|
|
408
|
+
field_obj,
|
|
409
|
+
(ManyToManyDescriptor, ReverseManyToOneDescriptor, ReverseOneToOneDescriptor),
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
@classmethod
|
|
413
|
+
def _is_forward_relation(cls, field_obj) -> bool:
|
|
414
|
+
"""Check if field is a forward relation (FK, O2O)."""
|
|
415
|
+
return isinstance(
|
|
416
|
+
field_obj, (ForwardOneToOneDescriptor, ForwardManyToOneDescriptor)
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
@classmethod
|
|
420
|
+
def _warn_missing_relation_serializer(cls, field_name: str, model) -> None:
|
|
421
|
+
"""Emit warning for reverse relations without explicit serializer mapping."""
|
|
422
|
+
if (
|
|
423
|
+
not isinstance(model, ModelSerializerMeta)
|
|
424
|
+
and not getattr(settings, "NINJA_AIO_TESTING", False)
|
|
425
|
+
):
|
|
426
|
+
warnings.warn(
|
|
427
|
+
f"{cls.__name__}: reverse relation '{field_name}' is listed in read fields "
|
|
428
|
+
"but has no entry in relations_serializers; it will be auto-resolved only "
|
|
429
|
+
"for ModelSerializer relations, otherwise skipped.",
|
|
430
|
+
UserWarning,
|
|
431
|
+
stacklevel=3,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
@classmethod
|
|
435
|
+
def _process_field(
|
|
436
|
+
cls,
|
|
437
|
+
field_name: str,
|
|
438
|
+
model,
|
|
439
|
+
relations_serializers: dict,
|
|
440
|
+
) -> tuple[str | None, tuple | None, tuple | None]:
|
|
441
|
+
"""
|
|
442
|
+
Process a single field and determine its classification.
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
(plain_field, reverse_rel, forward_rel) - only one will be non-None
|
|
446
|
+
"""
|
|
447
|
+
field_obj = getattr(model, field_name)
|
|
448
|
+
|
|
449
|
+
if cls._is_reverse_relation(field_obj):
|
|
450
|
+
if field_name not in relations_serializers:
|
|
451
|
+
cls._warn_missing_relation_serializer(field_name, model)
|
|
452
|
+
rel_tuple = cls._build_schema_reverse_rel(field_name, field_obj)
|
|
453
|
+
return (None, rel_tuple, None)
|
|
454
|
+
|
|
455
|
+
if cls._is_forward_relation(field_obj):
|
|
456
|
+
rel_tuple = cls._build_schema_forward_rel(field_name, field_obj)
|
|
457
|
+
if rel_tuple is True:
|
|
458
|
+
return (field_name, None, None)
|
|
459
|
+
return (None, None, rel_tuple)
|
|
460
|
+
|
|
461
|
+
return (field_name, None, None)
|
|
462
|
+
|
|
463
|
+
@classmethod
|
|
464
|
+
def get_schema_out_data(cls, schema_type: Literal["Out", "Detail"] = "Out"):
|
|
397
465
|
"""
|
|
398
|
-
Collect components for
|
|
399
|
-
|
|
400
|
-
|
|
466
|
+
Collect components for output schema generation (Out or Detail).
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
tuple: (fields, reverse_rels, excludes, customs_with_forward_rels, optionals)
|
|
401
470
|
"""
|
|
471
|
+
if schema_type not in ("Out", "Detail"):
|
|
472
|
+
raise ValueError("get_schema_out_data only supports 'Out' or 'Detail' types")
|
|
473
|
+
|
|
474
|
+
fields_type = "read" if schema_type == "Out" else "detail"
|
|
475
|
+
model = cls._get_model()
|
|
476
|
+
relations_serializers = cls._get_relations_serializers() or {}
|
|
477
|
+
|
|
402
478
|
fields: list[str] = []
|
|
403
479
|
reverse_rels: list[tuple] = []
|
|
404
|
-
|
|
405
|
-
relations_serializers = cls._get_relations_serializers() or {}
|
|
406
|
-
model = cls._get_model()
|
|
480
|
+
forward_rels: list[tuple] = []
|
|
407
481
|
|
|
408
|
-
for
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
field_obj,
|
|
412
|
-
(
|
|
413
|
-
ManyToManyDescriptor,
|
|
414
|
-
ReverseManyToOneDescriptor,
|
|
415
|
-
ReverseOneToOneDescriptor,
|
|
416
|
-
),
|
|
482
|
+
for field_name in cls.get_fields(fields_type):
|
|
483
|
+
plain, reverse, forward = cls._process_field(
|
|
484
|
+
field_name, model, relations_serializers
|
|
417
485
|
)
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
is_reverse
|
|
425
|
-
and not isinstance(model, ModelSerializerMeta)
|
|
426
|
-
and f not in relations_serializers
|
|
427
|
-
and not getattr(settings, "NINJA_AIO_TESTING", False)
|
|
428
|
-
):
|
|
429
|
-
warnings.warn(
|
|
430
|
-
f"{cls.__name__}: reverse relation '{f}' is listed in read fields but has no entry in relations_serializers; "
|
|
431
|
-
"it will be auto-resolved only for ModelSerializer relations, otherwise skipped.",
|
|
432
|
-
UserWarning,
|
|
433
|
-
stacklevel=2,
|
|
434
|
-
)
|
|
435
|
-
|
|
436
|
-
# Reverse relations
|
|
437
|
-
if is_reverse:
|
|
438
|
-
rel_tuple = cls._build_schema_reverse_rel(f, field_obj)
|
|
439
|
-
if rel_tuple:
|
|
440
|
-
reverse_rels.append(rel_tuple)
|
|
441
|
-
continue
|
|
442
|
-
|
|
443
|
-
# Forward relations
|
|
444
|
-
if is_forward:
|
|
445
|
-
rel_tuple = cls._build_schema_forward_rel(f, field_obj)
|
|
446
|
-
if rel_tuple is True:
|
|
447
|
-
fields.append(f)
|
|
448
|
-
elif rel_tuple:
|
|
449
|
-
rels.append(rel_tuple)
|
|
450
|
-
# None -> skip entirely
|
|
451
|
-
continue
|
|
452
|
-
|
|
453
|
-
# Plain field
|
|
454
|
-
fields.append(f)
|
|
486
|
+
if plain:
|
|
487
|
+
fields.append(plain)
|
|
488
|
+
if reverse:
|
|
489
|
+
reverse_rels.append(reverse)
|
|
490
|
+
if forward:
|
|
491
|
+
forward_rels.append(forward)
|
|
455
492
|
|
|
456
493
|
return (
|
|
457
494
|
fields,
|
|
458
495
|
reverse_rels,
|
|
459
|
-
cls.get_excluded_fields(
|
|
460
|
-
cls.get_custom_fields(
|
|
461
|
-
cls.get_optional_fields(
|
|
496
|
+
cls.get_excluded_fields(fields_type),
|
|
497
|
+
cls.get_custom_fields(fields_type) + forward_rels,
|
|
498
|
+
cls.get_optional_fields(fields_type),
|
|
462
499
|
)
|
|
463
500
|
|
|
464
501
|
@classmethod
|
|
@@ -474,15 +511,16 @@ class BaseSerializer:
|
|
|
474
511
|
model = cls._get_model()
|
|
475
512
|
|
|
476
513
|
# Handle special schema types with custom logic
|
|
477
|
-
if schema_type == "Out":
|
|
514
|
+
if schema_type == "Out" or schema_type == "Detail":
|
|
478
515
|
fields, reverse_rels, excludes, customs, optionals = (
|
|
479
|
-
cls.get_schema_out_data()
|
|
516
|
+
cls.get_schema_out_data(schema_type)
|
|
480
517
|
)
|
|
481
518
|
if not any([fields, reverse_rels, excludes, customs]):
|
|
482
519
|
return None
|
|
520
|
+
schema_name = "SchemaOut" if schema_type == "Out" else "DetailSchemaOut"
|
|
483
521
|
return create_schema(
|
|
484
522
|
model=model,
|
|
485
|
-
name=f"{model._meta.model_name}
|
|
523
|
+
name=f"{model._meta.model_name}{schema_name}",
|
|
486
524
|
depth=depth,
|
|
487
525
|
fields=fields,
|
|
488
526
|
custom_fields=reverse_rels + customs + optionals,
|
|
@@ -561,6 +599,11 @@ class BaseSerializer:
|
|
|
561
599
|
"""Generate read (Out) schema."""
|
|
562
600
|
return cls._generate_model_schema("Out", depth)
|
|
563
601
|
|
|
602
|
+
@classmethod
|
|
603
|
+
def generate_detail_s(cls, depth: int = 1) -> Schema:
|
|
604
|
+
"""Generate detail (single object Out) schema."""
|
|
605
|
+
return cls._generate_model_schema("Detail", depth)
|
|
606
|
+
|
|
564
607
|
@classmethod
|
|
565
608
|
def generate_create_s(cls) -> Schema:
|
|
566
609
|
"""Generate create (In) schema."""
|
|
@@ -659,6 +702,26 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
|
|
|
659
702
|
optionals: list[tuple[str, type]] = []
|
|
660
703
|
excludes: list[str] = []
|
|
661
704
|
|
|
705
|
+
class DetailSerializer:
|
|
706
|
+
"""Configuration describing detail (single object) read schema.
|
|
707
|
+
|
|
708
|
+
Attributes
|
|
709
|
+
----------
|
|
710
|
+
fields : list[str]
|
|
711
|
+
Explicit model fields to include.
|
|
712
|
+
excludes : list[str]
|
|
713
|
+
Fields to force exclude (safety).
|
|
714
|
+
customs : list[tuple[str, type, Any]]
|
|
715
|
+
Computed / synthetic output attributes.
|
|
716
|
+
optionals : list[tuple[str, type]]
|
|
717
|
+
Optional output fields.
|
|
718
|
+
"""
|
|
719
|
+
|
|
720
|
+
fields: list[str] = []
|
|
721
|
+
customs: list[tuple[str, type, Any]] = []
|
|
722
|
+
optionals: list[tuple[str, type]] = []
|
|
723
|
+
excludes: list[str] = []
|
|
724
|
+
|
|
662
725
|
class UpdateSerializer:
|
|
663
726
|
"""Configuration describing update (PATCH/PUT) schema.
|
|
664
727
|
|
|
@@ -684,6 +747,7 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
|
|
|
684
747
|
"create": "CreateSerializer",
|
|
685
748
|
"update": "UpdateSerializer",
|
|
686
749
|
"read": "ReadSerializer",
|
|
750
|
+
"detail": "DetailSerializer",
|
|
687
751
|
}
|
|
688
752
|
|
|
689
753
|
@classmethod
|
|
@@ -852,7 +916,7 @@ class SchemaModelConfig(Schema):
|
|
|
852
916
|
customs: Optional[List[tuple[str, type, Any]]] = None
|
|
853
917
|
|
|
854
918
|
|
|
855
|
-
class Serializer(BaseSerializer):
|
|
919
|
+
class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
856
920
|
"""
|
|
857
921
|
Serializer
|
|
858
922
|
----------
|
|
@@ -867,6 +931,7 @@ class Serializer(BaseSerializer):
|
|
|
867
931
|
"create": "in",
|
|
868
932
|
"update": "update",
|
|
869
933
|
"read": "out",
|
|
934
|
+
"detail": "detail",
|
|
870
935
|
}
|
|
871
936
|
|
|
872
937
|
def __init_subclass__(cls, **kwargs):
|
|
@@ -884,6 +949,7 @@ class Serializer(BaseSerializer):
|
|
|
884
949
|
schema_in: Optional[SchemaModelConfig] = None
|
|
885
950
|
schema_out: Optional[SchemaModelConfig] = None
|
|
886
951
|
schema_update: Optional[SchemaModelConfig] = None
|
|
952
|
+
schema_detail: Optional[SchemaModelConfig] = None
|
|
887
953
|
relations_serializers: dict[str, "Serializer"] = {}
|
|
888
954
|
|
|
889
955
|
@classmethod
|
|
@@ -908,6 +974,8 @@ class Serializer(BaseSerializer):
|
|
|
908
974
|
return cls._get_meta_data("schema_out")
|
|
909
975
|
case "update":
|
|
910
976
|
return cls._get_meta_data("schema_update")
|
|
977
|
+
case "detail":
|
|
978
|
+
return cls._get_meta_data("schema_detail")
|
|
911
979
|
case _:
|
|
912
980
|
return None
|
|
913
981
|
|
|
@@ -1027,7 +1095,12 @@ class Serializer(BaseSerializer):
|
|
|
1027
1095
|
dict
|
|
1028
1096
|
Serialized data.
|
|
1029
1097
|
"""
|
|
1030
|
-
|
|
1098
|
+
schema = (
|
|
1099
|
+
self.generate_read_s()
|
|
1100
|
+
if self.generate_detail_s() is None
|
|
1101
|
+
else self.generate_detail_s()
|
|
1102
|
+
)
|
|
1103
|
+
return await self.util.read_s(schema=schema, instance=instance)
|
|
1031
1104
|
|
|
1032
1105
|
async def models_dump(
|
|
1033
1106
|
self, instances: models.QuerySet[models.Model]
|
ninja_aio/schemas/helpers.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from typing import List, Optional, Type
|
|
2
2
|
|
|
3
3
|
from ninja import Schema
|
|
4
|
-
from ninja_aio.types import ModelSerializerMeta
|
|
4
|
+
from ninja_aio.types import ModelSerializerMeta, SerializerMeta
|
|
5
5
|
from django.db.models import Model
|
|
6
6
|
from pydantic import BaseModel, ConfigDict, model_validator
|
|
7
7
|
|
|
@@ -38,6 +38,10 @@ class M2MRelationSchema(BaseModel):
|
|
|
38
38
|
Optional explicit schema to represent related objects in responses.
|
|
39
39
|
If `model` is a ModelSerializerMeta, this is auto-derived via `model.generate_related_s()`.
|
|
40
40
|
If `model` is a plain Django model, this must be provided.
|
|
41
|
+
If `model` is a plain DJango model and this is not provided but serializer_class is provided this last one would be used to generate it.
|
|
42
|
+
serializer_class (Serializer | None):
|
|
43
|
+
Optional serializer class associated with the related model. If provided alongside a plain Django model,
|
|
44
|
+
it can be used to auto-generate the `related_schema`.
|
|
41
45
|
append_slash (bool):
|
|
42
46
|
Whether to append a trailing slash to the generated GET endpoint path. Defaults to False for backward compatibility.
|
|
43
47
|
|
|
@@ -63,6 +67,7 @@ class M2MRelationSchema(BaseModel):
|
|
|
63
67
|
auth: Optional[list] = None
|
|
64
68
|
filters: Optional[dict[str, tuple]] = None
|
|
65
69
|
related_schema: Optional[Type[Schema]] = None
|
|
70
|
+
serializer_class: Optional[SerializerMeta] = None
|
|
66
71
|
append_slash: bool = False
|
|
67
72
|
|
|
68
73
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
@@ -70,15 +75,30 @@ class M2MRelationSchema(BaseModel):
|
|
|
70
75
|
@model_validator(mode="before")
|
|
71
76
|
@classmethod
|
|
72
77
|
def validate_related_schema(cls, data):
|
|
73
|
-
related_schema
|
|
74
|
-
if related_schema is not None:
|
|
78
|
+
# Early return if related_schema is already provided
|
|
79
|
+
if data.get("related_schema") is not None:
|
|
75
80
|
return data
|
|
81
|
+
|
|
76
82
|
model = data.get("model")
|
|
77
|
-
|
|
83
|
+
serializer_class = data.get("serializer_class")
|
|
84
|
+
is_model_serializer = isinstance(model, ModelSerializerMeta)
|
|
85
|
+
|
|
86
|
+
# Validate incompatible parameters
|
|
87
|
+
if is_model_serializer and serializer_class:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
"Cannot provide serializer_class when model is a ModelSerializerMeta"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Generate related_schema based on available information
|
|
93
|
+
if is_model_serializer:
|
|
94
|
+
data["related_schema"] = model.generate_related_s()
|
|
95
|
+
elif serializer_class:
|
|
96
|
+
data["related_schema"] = serializer_class.generate_related_s()
|
|
97
|
+
else:
|
|
78
98
|
raise ValueError(
|
|
79
|
-
"related_schema must be provided if model is not a ModelSerializer"
|
|
99
|
+
"related_schema must be provided if model is not a ModelSerializer"
|
|
80
100
|
)
|
|
81
|
-
|
|
101
|
+
|
|
82
102
|
return data
|
|
83
103
|
|
|
84
104
|
|
|
@@ -95,6 +115,7 @@ class ModelQuerySetExtraSchema(ModelQuerySetSchema):
|
|
|
95
115
|
select_related (Optional[list[str]]): List of related fields for select_related optimization.
|
|
96
116
|
prefetch_related (Optional[list[str]]): List of related fields for prefetch_related optimization
|
|
97
117
|
"""
|
|
118
|
+
|
|
98
119
|
scope: str
|
|
99
120
|
|
|
100
121
|
|
|
@@ -106,6 +127,7 @@ class ObjectQuerySchema(ModelQuerySetSchema):
|
|
|
106
127
|
select_related (Optional[list[str]]): List of related fields for select_related optimization.
|
|
107
128
|
prefetch_related (Optional[list[str]]): List of related fields for prefetch_related optimization
|
|
108
129
|
"""
|
|
130
|
+
|
|
109
131
|
getters: Optional[dict] = {}
|
|
110
132
|
|
|
111
133
|
|
|
@@ -117,6 +139,7 @@ class ObjectsQuerySchema(ModelQuerySetSchema):
|
|
|
117
139
|
select_related (Optional[list[str]]): List of related fields for select_related optimization.
|
|
118
140
|
prefetch_related (Optional[list[str]]): List of related fields for prefetch_related optimization
|
|
119
141
|
"""
|
|
142
|
+
|
|
120
143
|
filters: Optional[dict] = {}
|
|
121
144
|
|
|
122
145
|
|
|
@@ -129,6 +152,7 @@ class QuerySchema(ModelQuerySetSchema):
|
|
|
129
152
|
select_related (Optional[list[str]]): List of related fields for select_related optimization.
|
|
130
153
|
prefetch_related (Optional[list[str]]): List of related fields for prefetch_related optimization
|
|
131
154
|
"""
|
|
155
|
+
|
|
132
156
|
filters: Optional[dict] = {}
|
|
133
157
|
getters: Optional[dict] = {}
|
|
134
158
|
|
|
@@ -140,6 +164,7 @@ class QueryUtilBaseScopesSchema(BaseModel):
|
|
|
140
164
|
READ (str): Scope for read operations.
|
|
141
165
|
QUERYSET_REQUEST (str): Scope for queryset request operations.
|
|
142
166
|
"""
|
|
167
|
+
|
|
143
168
|
READ: str = "read"
|
|
144
169
|
QUERYSET_REQUEST: str = "queryset_request"
|
|
145
170
|
|
|
@@ -163,6 +188,7 @@ class DecoratorsSchema(Schema):
|
|
|
163
188
|
Consider initializing these in __init__ or using default_factory (if using pydantic/dataclasses)
|
|
164
189
|
to avoid unintended side effects.
|
|
165
190
|
"""
|
|
191
|
+
|
|
166
192
|
list: Optional[List] = []
|
|
167
193
|
retrieve: Optional[List] = []
|
|
168
194
|
create: Optional[List] = []
|
ninja_aio/types.py
CHANGED
|
@@ -4,16 +4,21 @@ from joserfc import jwk
|
|
|
4
4
|
from django.db.models import Model
|
|
5
5
|
from typing import TypeAlias
|
|
6
6
|
|
|
7
|
-
S_TYPES = Literal["read", "create", "update"]
|
|
7
|
+
S_TYPES = Literal["read", "detail", "create", "update"]
|
|
8
8
|
F_TYPES = Literal["fields", "customs", "optionals", "excludes"]
|
|
9
|
-
SCHEMA_TYPES = Literal["In", "Out", "Patch", "Related"]
|
|
9
|
+
SCHEMA_TYPES = Literal["In", "Out", "Detail", "Patch", "Related"]
|
|
10
10
|
VIEW_TYPES = Literal["list", "retrieve", "create", "update", "delete", "all"]
|
|
11
11
|
JwtKeys: TypeAlias = jwk.RSAKey | jwk.ECKey | jwk.OctKey
|
|
12
12
|
|
|
13
|
-
class ModelSerializerType(type):
|
|
14
|
-
def __repr__(self):
|
|
15
|
-
return self.__name__
|
|
16
13
|
|
|
14
|
+
class SerializerMeta(type):
|
|
15
|
+
"""Metaclass for serializers - extend with custom behavior as needed."""
|
|
16
|
+
|
|
17
|
+
def __repr__(cls):
|
|
18
|
+
return cls.__name__
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ModelSerializerMeta(SerializerMeta, type(Model)):
|
|
22
|
+
"""Metaclass combining SerializerMeta with Django's ModelBase."""
|
|
17
23
|
|
|
18
|
-
class ModelSerializerMeta(ModelSerializerType, type(Model)):
|
|
19
24
|
pass
|
ninja_aio/views/api.py
CHANGED
|
@@ -225,6 +225,7 @@ class APIViewSet(API):
|
|
|
225
225
|
serializer_class: serializers.Serializer | None = None
|
|
226
226
|
schema_in: Schema | None = None
|
|
227
227
|
schema_out: Schema | None = None
|
|
228
|
+
schema_detail: Schema | None = None
|
|
228
229
|
schema_update: Schema | None = None
|
|
229
230
|
get_auth: list | None = NOT_SET
|
|
230
231
|
post_auth: list | None = NOT_SET
|
|
@@ -262,7 +263,9 @@ class APIViewSet(API):
|
|
|
262
263
|
if not isinstance(self.model, ModelSerializerMeta)
|
|
263
264
|
else self.model.util
|
|
264
265
|
)
|
|
265
|
-
self.schema_out, self.schema_in, self.schema_update =
|
|
266
|
+
self.schema_out, self.schema_detail, self.schema_in, self.schema_update = (
|
|
267
|
+
self.get_schemas()
|
|
268
|
+
)
|
|
266
269
|
self.path_schema = self._generate_path_schema()
|
|
267
270
|
self.filters_schema = self._generate_filters_schema()
|
|
268
271
|
self.model_verbose_name = (
|
|
@@ -365,22 +368,24 @@ class APIViewSet(API):
|
|
|
365
368
|
|
|
366
369
|
def get_schemas(self):
|
|
367
370
|
"""
|
|
368
|
-
Compute and return (schema_out, schema_in, schema_update).
|
|
371
|
+
Compute and return (schema_out, schema_detail, schema_in, schema_update).
|
|
369
372
|
|
|
370
|
-
- If model is a ModelSerializer (ModelSerializerMeta), auto-generate read/create/update schemas.
|
|
373
|
+
- If model is a ModelSerializer (ModelSerializerMeta), auto-generate read/detail/create/update schemas.
|
|
371
374
|
- Otherwise, use existing schemas or generate from serializer_class if provided.
|
|
372
375
|
"""
|
|
373
376
|
# ModelSerializer case: prefer explicitly set schemas, otherwise generate from the model
|
|
374
377
|
if isinstance(self.model, ModelSerializerMeta):
|
|
375
378
|
return (
|
|
376
379
|
self.schema_out or self.model.generate_read_s(),
|
|
380
|
+
self.schema_detail or self.model.generate_detail_s(),
|
|
377
381
|
self.schema_in or self.model.generate_create_s(),
|
|
378
382
|
self.schema_update or self.model.generate_update_s(),
|
|
379
383
|
)
|
|
380
384
|
|
|
381
385
|
# Non-ModelSerializer: start from provided schemas
|
|
382
|
-
schema_out, schema_in, schema_update = (
|
|
386
|
+
schema_out, schema_detail, schema_in, schema_update = (
|
|
383
387
|
self.schema_out,
|
|
388
|
+
self.schema_detail,
|
|
384
389
|
self.schema_in,
|
|
385
390
|
self.schema_update,
|
|
386
391
|
)
|
|
@@ -389,9 +394,10 @@ class APIViewSet(API):
|
|
|
389
394
|
if self.serializer_class:
|
|
390
395
|
schema_in = schema_in or self.serializer_class.generate_create_s()
|
|
391
396
|
schema_out = schema_out or self.serializer_class.generate_read_s()
|
|
397
|
+
schema_detail = schema_detail or self.serializer_class.generate_detail_s()
|
|
392
398
|
schema_update = schema_update or self.serializer_class.generate_update_s()
|
|
393
399
|
|
|
394
|
-
return (schema_out, schema_in, schema_update)
|
|
400
|
+
return (schema_out, schema_detail, schema_in, schema_update)
|
|
395
401
|
|
|
396
402
|
async def query_params_handler(
|
|
397
403
|
self, queryset: QuerySet[ModelSerializer], filters: dict
|
|
@@ -456,23 +462,31 @@ class APIViewSet(API):
|
|
|
456
462
|
|
|
457
463
|
return list
|
|
458
464
|
|
|
465
|
+
def _get_retrieve_schema(self) -> Schema:
|
|
466
|
+
"""
|
|
467
|
+
Return the schema to use for retrieve endpoint.
|
|
468
|
+
Uses schema_detail if available, otherwise falls back to schema_out.
|
|
469
|
+
"""
|
|
470
|
+
return self.schema_detail or self.schema_out
|
|
471
|
+
|
|
459
472
|
def retrieve_view(self):
|
|
460
473
|
"""
|
|
461
474
|
Register retrieve endpoint.
|
|
462
475
|
"""
|
|
476
|
+
retrieve_schema = self._get_retrieve_schema()
|
|
463
477
|
|
|
464
478
|
@self.router.get(
|
|
465
479
|
self.get_path_retrieve,
|
|
466
480
|
auth=self.get_view_auth(),
|
|
467
481
|
summary=f"Retrieve {self.model_verbose_name}",
|
|
468
482
|
description=self.retrieve_docs,
|
|
469
|
-
response={200:
|
|
483
|
+
response={200: retrieve_schema, self.error_codes: GenericMessageSchema},
|
|
470
484
|
)
|
|
471
485
|
@decorate_view(unique_view(self), *self.extra_decorators.retrieve)
|
|
472
486
|
async def retrieve(request: HttpRequest, pk: Path[self.path_schema]): # type: ignore
|
|
473
487
|
query_data = self._get_query_data()
|
|
474
488
|
return await self.model_util.read_s(
|
|
475
|
-
|
|
489
|
+
retrieve_schema,
|
|
476
490
|
request,
|
|
477
491
|
query_data=QuerySchema(
|
|
478
492
|
getters={"pk": self._get_pk(pk)}, **query_data.model_dump()
|
|
File without changes
|
{django_ninja_aio_crud-2.7.0.dist-info → django_ninja_aio_crud-2.8.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|