django-ninja-aio-crud 2.7.0__py3-none-any.whl → 2.9.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.
Potentially problematic release.
This version of django-ninja-aio-crud might be problematic. Click here for more details.
- {django_ninja_aio_crud-2.7.0.dist-info → django_ninja_aio_crud-2.9.0.dist-info}/METADATA +1 -1
- {django_ninja_aio_crud-2.7.0.dist-info → django_ninja_aio_crud-2.9.0.dist-info}/RECORD +12 -12
- ninja_aio/__init__.py +1 -1
- ninja_aio/helpers/api.py +1 -1
- ninja_aio/helpers/query.py +3 -0
- ninja_aio/models/serializers.py +153 -70
- ninja_aio/models/utils.py +115 -64
- ninja_aio/schemas/helpers.py +34 -6
- ninja_aio/types.py +11 -6
- ninja_aio/views/api.py +26 -9
- {django_ninja_aio_crud-2.7.0.dist-info → django_ninja_aio_crud-2.9.0.dist-info}/WHEEL +0 -0
- {django_ninja_aio_crud-2.7.0.dist-info → django_ninja_aio_crud-2.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
ninja_aio/__init__.py,sha256=
|
|
1
|
+
ninja_aio/__init__.py,sha256=YE75I1ewNyWylWEf1tZjFFmQlFLdsdYyYiQgxSr1Jdk,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=
|
|
15
|
-
ninja_aio/helpers/query.py,sha256=
|
|
14
|
+
ninja_aio/helpers/api.py,sha256=2beyexep-ehgaA_1bV5Yuh3zRDVcRCMkrW94nmfDWEA,20819
|
|
15
|
+
ninja_aio/helpers/query.py,sha256=NMGkS_v-ZVYKNtf1XohEUzfwca52Eq5FTcQ5lehHjus,4682
|
|
16
16
|
ninja_aio/models/__init__.py,sha256=L3UQnQAlKoI3F7jinadL-Nn55hkPvnSRPYW0JtnbWFo,114
|
|
17
|
-
ninja_aio/models/serializers.py,sha256=
|
|
18
|
-
ninja_aio/models/utils.py,sha256=
|
|
17
|
+
ninja_aio/models/serializers.py,sha256=I7pUz_vl0FElaVsrGaogT_Lj9T4uaNG7UDMGf5VMwW4,38468
|
|
18
|
+
ninja_aio/models/utils.py,sha256=N7waJGOCsoFvbbO1xXy94acKeG85WRK3hjZJz7IukhU,32885
|
|
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=Vti5BfHWpxaJXj_ixZBJb34VRwhHODrlVjRlIuHh_ug,8428
|
|
23
23
|
ninja_aio/views/__init__.py,sha256=DEzjWA6y3WF0V10nNF8eEurLNEodgxKzyFd09AqVp3s,148
|
|
24
|
-
ninja_aio/views/api.py,sha256=
|
|
24
|
+
ninja_aio/views/api.py,sha256=CW74JB1tsdWfv3Y3x4K9o8yXsrMxCKWZ4QalhqJGan8,22072
|
|
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.9.0.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
|
|
27
|
+
django_ninja_aio_crud-2.9.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
28
|
+
django_ninja_aio_crud-2.9.0.dist-info/METADATA,sha256=7hbmX5K1ixoBn0RnFlkpnp3runSgZ1pi70v606cUX3Q,9963
|
|
29
|
+
django_ninja_aio_crud-2.9.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/helpers/query.py
CHANGED
|
@@ -75,6 +75,9 @@ class QueryUtil:
|
|
|
75
75
|
self.queryset_request_config: ModelQuerySetSchema = self._configs.get(
|
|
76
76
|
self.SCOPES.QUERYSET_REQUEST, ModelQuerySetSchema()
|
|
77
77
|
)
|
|
78
|
+
self.detail_config: ModelQuerySetSchema = self._configs.get(
|
|
79
|
+
self.SCOPES.DETAIL, ModelQuerySetSchema()
|
|
80
|
+
)
|
|
78
81
|
|
|
79
82
|
def _get_config(self, conf_name: str) -> ModelQuerySetSchema:
|
|
80
83
|
"""Helper method to retrieve configuration attributes."""
|
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,
|
|
@@ -55,6 +61,7 @@ class BaseSerializer:
|
|
|
55
61
|
"""
|
|
56
62
|
|
|
57
63
|
read = ModelQuerySetSchema()
|
|
64
|
+
detail = ModelQuerySetSchema()
|
|
58
65
|
queryset_request = ModelQuerySetSchema()
|
|
59
66
|
extras: list[ModelQuerySetExtraSchema] = []
|
|
60
67
|
|
|
@@ -98,6 +105,7 @@ class BaseSerializer:
|
|
|
98
105
|
module = sys.modules.get(module_path)
|
|
99
106
|
if module is None:
|
|
100
107
|
import importlib
|
|
108
|
+
|
|
101
109
|
module = importlib.import_module(module_path)
|
|
102
110
|
|
|
103
111
|
# Get the serializer class from the module
|
|
@@ -136,7 +144,9 @@ class BaseSerializer:
|
|
|
136
144
|
return serializer_class
|
|
137
145
|
|
|
138
146
|
@classmethod
|
|
139
|
-
def _resolve_serializer_reference(
|
|
147
|
+
def _resolve_serializer_reference(
|
|
148
|
+
cls, serializer_ref: str | type | Any
|
|
149
|
+
) -> type | Any:
|
|
140
150
|
"""
|
|
141
151
|
Resolve a serializer reference that may be a string, a class, or a Union of serializers.
|
|
142
152
|
|
|
@@ -217,11 +227,11 @@ class BaseSerializer:
|
|
|
217
227
|
Schema | Union[Schema, ...] | None
|
|
218
228
|
Union of generated schemas or None if all schemas are None.
|
|
219
229
|
"""
|
|
220
|
-
# Generate schemas for each serializer in the Union
|
|
230
|
+
# Generate schemas for each serializer in the Union (single call per serializer)
|
|
221
231
|
schemas = tuple(
|
|
222
|
-
|
|
232
|
+
schema
|
|
223
233
|
for serializer_type in get_args(resolved_union)
|
|
224
|
-
if serializer_type.generate_related_s() is not None
|
|
234
|
+
if (schema := serializer_type.generate_related_s()) is not None
|
|
225
235
|
)
|
|
226
236
|
|
|
227
237
|
if not schemas:
|
|
@@ -316,14 +326,18 @@ class BaseSerializer:
|
|
|
316
326
|
]
|
|
317
327
|
|
|
318
328
|
@classmethod
|
|
319
|
-
def get_excluded_fields(cls, s_type:
|
|
329
|
+
def get_excluded_fields(cls, s_type: S_TYPES):
|
|
320
330
|
"""Return excluded field names for the serializer type."""
|
|
321
331
|
return cls._get_fields(s_type, "excludes")
|
|
322
332
|
|
|
323
333
|
@classmethod
|
|
324
|
-
def get_fields(cls, s_type:
|
|
334
|
+
def get_fields(cls, s_type: S_TYPES):
|
|
325
335
|
"""Return explicit declared fields for the serializer type."""
|
|
326
|
-
|
|
336
|
+
fields = cls._get_fields(s_type, "fields")
|
|
337
|
+
# Detail schema falls back to read fields if none declared
|
|
338
|
+
if not fields and s_type == "detail":
|
|
339
|
+
return cls._get_fields("read", "fields")
|
|
340
|
+
return fields
|
|
327
341
|
|
|
328
342
|
@classmethod
|
|
329
343
|
def is_custom(cls, field: str) -> bool:
|
|
@@ -393,72 +407,105 @@ class BaseSerializer:
|
|
|
393
407
|
return (field_name, schema | None, None)
|
|
394
408
|
|
|
395
409
|
@classmethod
|
|
396
|
-
def
|
|
410
|
+
def _is_reverse_relation(cls, field_obj) -> bool:
|
|
411
|
+
"""Check if field is a reverse relation (M2M, reverse FK, reverse O2O)."""
|
|
412
|
+
return isinstance(
|
|
413
|
+
field_obj,
|
|
414
|
+
(
|
|
415
|
+
ManyToManyDescriptor,
|
|
416
|
+
ReverseManyToOneDescriptor,
|
|
417
|
+
ReverseOneToOneDescriptor,
|
|
418
|
+
),
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
@classmethod
|
|
422
|
+
def _is_forward_relation(cls, field_obj) -> bool:
|
|
423
|
+
"""Check if field is a forward relation (FK, O2O)."""
|
|
424
|
+
return isinstance(
|
|
425
|
+
field_obj, (ForwardOneToOneDescriptor, ForwardManyToOneDescriptor)
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
@classmethod
|
|
429
|
+
def _warn_missing_relation_serializer(cls, field_name: str, model) -> None:
|
|
430
|
+
"""Emit warning for reverse relations without explicit serializer mapping."""
|
|
431
|
+
if not isinstance(model, ModelSerializerMeta) and not getattr(
|
|
432
|
+
settings, "NINJA_AIO_TESTING", False
|
|
433
|
+
):
|
|
434
|
+
warnings.warn(
|
|
435
|
+
f"{cls.__name__}: reverse relation '{field_name}' is listed in read fields "
|
|
436
|
+
"but has no entry in relations_serializers; it will be auto-resolved only "
|
|
437
|
+
"for ModelSerializer relations, otherwise skipped.",
|
|
438
|
+
UserWarning,
|
|
439
|
+
stacklevel=3,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
@classmethod
|
|
443
|
+
def _process_field(
|
|
444
|
+
cls,
|
|
445
|
+
field_name: str,
|
|
446
|
+
model,
|
|
447
|
+
relations_serializers: dict,
|
|
448
|
+
) -> tuple[str | None, tuple | None, tuple | None]:
|
|
397
449
|
"""
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
450
|
+
Process a single field and determine its classification.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
(plain_field, reverse_rel, forward_rel) - only one will be non-None
|
|
401
454
|
"""
|
|
402
|
-
|
|
403
|
-
reverse_rels: list[tuple] = []
|
|
404
|
-
rels: list[tuple] = []
|
|
405
|
-
relations_serializers = cls._get_relations_serializers() or {}
|
|
406
|
-
model = cls._get_model()
|
|
455
|
+
field_obj = getattr(model, field_name)
|
|
407
456
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
457
|
+
if cls._is_reverse_relation(field_obj):
|
|
458
|
+
if field_name not in relations_serializers:
|
|
459
|
+
cls._warn_missing_relation_serializer(field_name, model)
|
|
460
|
+
rel_tuple = cls._build_schema_reverse_rel(field_name, field_obj)
|
|
461
|
+
return (None, rel_tuple, None)
|
|
462
|
+
|
|
463
|
+
if cls._is_forward_relation(field_obj):
|
|
464
|
+
rel_tuple = cls._build_schema_forward_rel(field_name, field_obj)
|
|
465
|
+
if rel_tuple is True:
|
|
466
|
+
return (field_name, None, None)
|
|
467
|
+
return (None, None, rel_tuple)
|
|
468
|
+
|
|
469
|
+
return (field_name, None, None)
|
|
470
|
+
|
|
471
|
+
@classmethod
|
|
472
|
+
def get_schema_out_data(cls, schema_type: Literal["Out", "Detail"] = "Out"):
|
|
473
|
+
"""
|
|
474
|
+
Collect components for output schema generation (Out or Detail).
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
tuple: (fields, reverse_rels, excludes, customs_with_forward_rels, optionals)
|
|
478
|
+
"""
|
|
479
|
+
if schema_type not in ("Out", "Detail"):
|
|
480
|
+
raise ValueError(
|
|
481
|
+
"get_schema_out_data only supports 'Out' or 'Detail' types"
|
|
420
482
|
)
|
|
421
483
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
)
|
|
484
|
+
fields_type = "read" if schema_type == "Out" else "detail"
|
|
485
|
+
model = cls._get_model()
|
|
486
|
+
relations_serializers = cls._get_relations_serializers() or {}
|
|
487
|
+
|
|
488
|
+
fields: list[str] = []
|
|
489
|
+
reverse_rels: list[tuple] = []
|
|
490
|
+
forward_rels: list[tuple] = []
|
|
435
491
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
if
|
|
445
|
-
|
|
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)
|
|
492
|
+
for field_name in cls.get_fields(fields_type):
|
|
493
|
+
plain, reverse, forward = cls._process_field(
|
|
494
|
+
field_name, model, relations_serializers
|
|
495
|
+
)
|
|
496
|
+
if plain:
|
|
497
|
+
fields.append(plain)
|
|
498
|
+
if reverse:
|
|
499
|
+
reverse_rels.append(reverse)
|
|
500
|
+
if forward:
|
|
501
|
+
forward_rels.append(forward)
|
|
455
502
|
|
|
456
503
|
return (
|
|
457
504
|
fields,
|
|
458
505
|
reverse_rels,
|
|
459
|
-
cls.get_excluded_fields(
|
|
460
|
-
cls.get_custom_fields(
|
|
461
|
-
cls.get_optional_fields(
|
|
506
|
+
cls.get_excluded_fields(fields_type),
|
|
507
|
+
cls.get_custom_fields(fields_type) + forward_rels,
|
|
508
|
+
cls.get_optional_fields(fields_type),
|
|
462
509
|
)
|
|
463
510
|
|
|
464
511
|
@classmethod
|
|
@@ -474,15 +521,16 @@ class BaseSerializer:
|
|
|
474
521
|
model = cls._get_model()
|
|
475
522
|
|
|
476
523
|
# Handle special schema types with custom logic
|
|
477
|
-
if schema_type == "Out":
|
|
524
|
+
if schema_type == "Out" or schema_type == "Detail":
|
|
478
525
|
fields, reverse_rels, excludes, customs, optionals = (
|
|
479
|
-
cls.get_schema_out_data()
|
|
526
|
+
cls.get_schema_out_data(schema_type)
|
|
480
527
|
)
|
|
481
528
|
if not any([fields, reverse_rels, excludes, customs]):
|
|
482
529
|
return None
|
|
530
|
+
schema_name = "SchemaOut" if schema_type == "Out" else "DetailSchemaOut"
|
|
483
531
|
return create_schema(
|
|
484
532
|
model=model,
|
|
485
|
-
name=f"{model._meta.model_name}
|
|
533
|
+
name=f"{model._meta.model_name}{schema_name}",
|
|
486
534
|
depth=depth,
|
|
487
535
|
fields=fields,
|
|
488
536
|
custom_fields=reverse_rels + customs + optionals,
|
|
@@ -561,6 +609,11 @@ class BaseSerializer:
|
|
|
561
609
|
"""Generate read (Out) schema."""
|
|
562
610
|
return cls._generate_model_schema("Out", depth)
|
|
563
611
|
|
|
612
|
+
@classmethod
|
|
613
|
+
def generate_detail_s(cls, depth: int = 1) -> Schema:
|
|
614
|
+
"""Generate detail (single object Out) schema."""
|
|
615
|
+
return cls._generate_model_schema("Detail", depth) or cls.generate_read_s(depth)
|
|
616
|
+
|
|
564
617
|
@classmethod
|
|
565
618
|
def generate_create_s(cls) -> Schema:
|
|
566
619
|
"""Generate create (In) schema."""
|
|
@@ -659,6 +712,26 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
|
|
|
659
712
|
optionals: list[tuple[str, type]] = []
|
|
660
713
|
excludes: list[str] = []
|
|
661
714
|
|
|
715
|
+
class DetailSerializer:
|
|
716
|
+
"""Configuration describing detail (single object) read schema.
|
|
717
|
+
|
|
718
|
+
Attributes
|
|
719
|
+
----------
|
|
720
|
+
fields : list[str]
|
|
721
|
+
Explicit model fields to include.
|
|
722
|
+
excludes : list[str]
|
|
723
|
+
Fields to force exclude (safety).
|
|
724
|
+
customs : list[tuple[str, type, Any]]
|
|
725
|
+
Computed / synthetic output attributes.
|
|
726
|
+
optionals : list[tuple[str, type]]
|
|
727
|
+
Optional output fields.
|
|
728
|
+
"""
|
|
729
|
+
|
|
730
|
+
fields: list[str] = []
|
|
731
|
+
customs: list[tuple[str, type, Any]] = []
|
|
732
|
+
optionals: list[tuple[str, type]] = []
|
|
733
|
+
excludes: list[str] = []
|
|
734
|
+
|
|
662
735
|
class UpdateSerializer:
|
|
663
736
|
"""Configuration describing update (PATCH/PUT) schema.
|
|
664
737
|
|
|
@@ -684,6 +757,7 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
|
|
|
684
757
|
"create": "CreateSerializer",
|
|
685
758
|
"update": "UpdateSerializer",
|
|
686
759
|
"read": "ReadSerializer",
|
|
760
|
+
"detail": "DetailSerializer",
|
|
687
761
|
}
|
|
688
762
|
|
|
689
763
|
@classmethod
|
|
@@ -852,7 +926,7 @@ class SchemaModelConfig(Schema):
|
|
|
852
926
|
customs: Optional[List[tuple[str, type, Any]]] = None
|
|
853
927
|
|
|
854
928
|
|
|
855
|
-
class Serializer(BaseSerializer):
|
|
929
|
+
class Serializer(BaseSerializer, metaclass=SerializerMeta):
|
|
856
930
|
"""
|
|
857
931
|
Serializer
|
|
858
932
|
----------
|
|
@@ -867,6 +941,7 @@ class Serializer(BaseSerializer):
|
|
|
867
941
|
"create": "in",
|
|
868
942
|
"update": "update",
|
|
869
943
|
"read": "out",
|
|
944
|
+
"detail": "detail",
|
|
870
945
|
}
|
|
871
946
|
|
|
872
947
|
def __init_subclass__(cls, **kwargs):
|
|
@@ -884,6 +959,7 @@ class Serializer(BaseSerializer):
|
|
|
884
959
|
schema_in: Optional[SchemaModelConfig] = None
|
|
885
960
|
schema_out: Optional[SchemaModelConfig] = None
|
|
886
961
|
schema_update: Optional[SchemaModelConfig] = None
|
|
962
|
+
schema_detail: Optional[SchemaModelConfig] = None
|
|
887
963
|
relations_serializers: dict[str, "Serializer"] = {}
|
|
888
964
|
|
|
889
965
|
@classmethod
|
|
@@ -908,6 +984,8 @@ class Serializer(BaseSerializer):
|
|
|
908
984
|
return cls._get_meta_data("schema_out")
|
|
909
985
|
case "update":
|
|
910
986
|
return cls._get_meta_data("schema_update")
|
|
987
|
+
case "detail":
|
|
988
|
+
return cls._get_meta_data("schema_detail")
|
|
911
989
|
case _:
|
|
912
990
|
return None
|
|
913
991
|
|
|
@@ -1027,7 +1105,12 @@ class Serializer(BaseSerializer):
|
|
|
1027
1105
|
dict
|
|
1028
1106
|
Serialized data.
|
|
1029
1107
|
"""
|
|
1030
|
-
|
|
1108
|
+
schema = (
|
|
1109
|
+
self.generate_read_s()
|
|
1110
|
+
if self.generate_detail_s() is None
|
|
1111
|
+
else self.generate_detail_s()
|
|
1112
|
+
)
|
|
1113
|
+
return await self.util.read_s(schema=schema, instance=instance)
|
|
1031
1114
|
|
|
1032
1115
|
async def models_dump(
|
|
1033
1116
|
self, instances: models.QuerySet[models.Model]
|
ninja_aio/models/utils.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import base64
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any, Literal
|
|
4
4
|
|
|
5
5
|
from ninja import Schema
|
|
6
6
|
from ninja.orm import fields
|
|
@@ -167,9 +167,19 @@ class ModelUtil:
|
|
|
167
167
|
list[str]
|
|
168
168
|
Explicit read fields if ModelSerializerMeta, otherwise all model fields.
|
|
169
169
|
"""
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
return self._get_serializable_field_names("read")
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def serializable_detail_fields(self):
|
|
174
|
+
"""
|
|
175
|
+
List of fields considered serializable for detail operations.
|
|
176
|
+
|
|
177
|
+
Returns
|
|
178
|
+
-------
|
|
179
|
+
list[str]
|
|
180
|
+
Explicit detail fields if ModelSerializerMeta, otherwise all model fields.
|
|
181
|
+
"""
|
|
182
|
+
return self._get_serializable_field_names("detail")
|
|
173
183
|
|
|
174
184
|
@property
|
|
175
185
|
def model_fields(self):
|
|
@@ -235,12 +245,29 @@ class ModelUtil:
|
|
|
235
245
|
"""
|
|
236
246
|
return self.model_verbose_name_plural.replace(" ", "")
|
|
237
247
|
|
|
248
|
+
def _get_serializable_field_names(
|
|
249
|
+
self, fields_type: Literal["read", "detail"]
|
|
250
|
+
) -> list[str]:
|
|
251
|
+
"""
|
|
252
|
+
Get serializable field names for the model.
|
|
253
|
+
|
|
254
|
+
Returns
|
|
255
|
+
-------
|
|
256
|
+
list[str]
|
|
257
|
+
List of serializable field names.
|
|
258
|
+
"""
|
|
259
|
+
if isinstance(self.model, ModelSerializerMeta):
|
|
260
|
+
return self.model.get_fields(fields_type)
|
|
261
|
+
if self.with_serializer:
|
|
262
|
+
return self.serializer_class.get_fields(fields_type)
|
|
263
|
+
return self.model_fields
|
|
264
|
+
|
|
238
265
|
async def _get_base_queryset(
|
|
239
266
|
self,
|
|
240
267
|
request: HttpRequest,
|
|
241
268
|
query_data: QuerySchema,
|
|
242
269
|
with_qs_request: bool,
|
|
243
|
-
|
|
270
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
244
271
|
) -> models.QuerySet[type["ModelSerializer"] | models.Model]:
|
|
245
272
|
"""
|
|
246
273
|
Build base queryset with optimizations and filters.
|
|
@@ -253,8 +280,9 @@ class ModelUtil:
|
|
|
253
280
|
Query configuration with filters and optimizations.
|
|
254
281
|
with_qs_request : bool
|
|
255
282
|
Whether to apply queryset_request hook.
|
|
256
|
-
|
|
257
|
-
|
|
283
|
+
is_for : Literal["read", "detail"] | None
|
|
284
|
+
Purpose of the query, determines which serializable fields to use.
|
|
285
|
+
If None, only query_data optimizations are applied.
|
|
258
286
|
|
|
259
287
|
Returns
|
|
260
288
|
-------
|
|
@@ -269,7 +297,7 @@ class ModelUtil:
|
|
|
269
297
|
)
|
|
270
298
|
|
|
271
299
|
# Apply query optimizations
|
|
272
|
-
obj_qs = self._apply_query_optimizations(obj_qs, query_data,
|
|
300
|
+
obj_qs = self._apply_query_optimizations(obj_qs, query_data, is_for)
|
|
273
301
|
|
|
274
302
|
# Apply queryset_request hook if available
|
|
275
303
|
if isinstance(self.model, ModelSerializerMeta) and with_qs_request:
|
|
@@ -286,7 +314,7 @@ class ModelUtil:
|
|
|
286
314
|
request: HttpRequest,
|
|
287
315
|
query_data: ObjectsQuerySchema = None,
|
|
288
316
|
with_qs_request=True,
|
|
289
|
-
|
|
317
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
290
318
|
) -> models.QuerySet[type["ModelSerializer"] | models.Model]:
|
|
291
319
|
"""
|
|
292
320
|
Retrieve a queryset with optimized database queries.
|
|
@@ -305,9 +333,9 @@ class ModelUtil:
|
|
|
305
333
|
with_qs_request : bool, optional
|
|
306
334
|
Whether to apply the model's queryset_request hook if available.
|
|
307
335
|
Defaults to True.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
336
|
+
is_for : Literal["read", "detail"] | None, optional
|
|
337
|
+
Purpose of the query, determines which serializable fields to use.
|
|
338
|
+
If None, only query_data optimizations are applied.
|
|
311
339
|
|
|
312
340
|
Returns
|
|
313
341
|
-------
|
|
@@ -323,7 +351,7 @@ class ModelUtil:
|
|
|
323
351
|
query_data = ObjectsQuerySchema()
|
|
324
352
|
|
|
325
353
|
return await self._get_base_queryset(
|
|
326
|
-
request, query_data, with_qs_request,
|
|
354
|
+
request, query_data, with_qs_request, is_for
|
|
327
355
|
)
|
|
328
356
|
|
|
329
357
|
async def get_object(
|
|
@@ -332,7 +360,7 @@ class ModelUtil:
|
|
|
332
360
|
pk: int | str = None,
|
|
333
361
|
query_data: ObjectQuerySchema = None,
|
|
334
362
|
with_qs_request=True,
|
|
335
|
-
|
|
363
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
336
364
|
) -> type["ModelSerializer"] | models.Model:
|
|
337
365
|
"""
|
|
338
366
|
Retrieve a single object with optimized database queries.
|
|
@@ -353,9 +381,9 @@ class ModelUtil:
|
|
|
353
381
|
with_qs_request : bool, optional
|
|
354
382
|
Whether to apply the model's queryset_request hook if available.
|
|
355
383
|
Defaults to True.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
384
|
+
is_for : Literal["read", "detail"] | None, optional
|
|
385
|
+
Purpose of the query, determines which serializable fields to use.
|
|
386
|
+
If None, only query_data optimizations are applied.
|
|
359
387
|
|
|
360
388
|
Returns
|
|
361
389
|
-------
|
|
@@ -385,7 +413,7 @@ class ModelUtil:
|
|
|
385
413
|
# Build lookup query and get optimized queryset
|
|
386
414
|
get_q = self._build_lookup_query(pk, query_data.getters)
|
|
387
415
|
obj_qs = await self._get_base_queryset(
|
|
388
|
-
request, query_data, with_qs_request,
|
|
416
|
+
request, query_data, with_qs_request, is_for
|
|
389
417
|
)
|
|
390
418
|
|
|
391
419
|
# Perform lookup
|
|
@@ -421,7 +449,7 @@ class ModelUtil:
|
|
|
421
449
|
self,
|
|
422
450
|
queryset: models.QuerySet,
|
|
423
451
|
query_data: QuerySchema,
|
|
424
|
-
|
|
452
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
425
453
|
) -> models.QuerySet:
|
|
426
454
|
"""
|
|
427
455
|
Apply select_related and prefetch_related optimizations to queryset.
|
|
@@ -432,8 +460,9 @@ class ModelUtil:
|
|
|
432
460
|
Base queryset to optimize.
|
|
433
461
|
query_data : ModelQuerySchema
|
|
434
462
|
Query configuration with select_related/prefetch_related lists.
|
|
435
|
-
|
|
436
|
-
|
|
463
|
+
is_for : Literal["read", "detail"] | None
|
|
464
|
+
Purpose of the query, determines which serializable fields to use.
|
|
465
|
+
If None, only query_data optimizations are applied.
|
|
437
466
|
|
|
438
467
|
Returns
|
|
439
468
|
-------
|
|
@@ -441,13 +470,13 @@ class ModelUtil:
|
|
|
441
470
|
Optimized queryset.
|
|
442
471
|
"""
|
|
443
472
|
select_related = (
|
|
444
|
-
query_data.select_related + self.get_select_relateds()
|
|
445
|
-
if
|
|
473
|
+
query_data.select_related + self.get_select_relateds(is_for)
|
|
474
|
+
if is_for
|
|
446
475
|
else query_data.select_related
|
|
447
476
|
)
|
|
448
477
|
prefetch_related = (
|
|
449
|
-
query_data.prefetch_related + self.get_reverse_relations()
|
|
450
|
-
if
|
|
478
|
+
query_data.prefetch_related + self.get_reverse_relations(is_for)
|
|
479
|
+
if is_for
|
|
451
480
|
else query_data.prefetch_related
|
|
452
481
|
)
|
|
453
482
|
|
|
@@ -458,36 +487,52 @@ class ModelUtil:
|
|
|
458
487
|
|
|
459
488
|
return queryset
|
|
460
489
|
|
|
461
|
-
def _get_read_optimizations(
|
|
490
|
+
def _get_read_optimizations(
|
|
491
|
+
self, is_for: Literal["read", "detail"] = "read"
|
|
492
|
+
) -> ModelQuerySetSchema:
|
|
462
493
|
"""
|
|
463
494
|
Retrieve read optimizations from model or serializer class.
|
|
464
495
|
|
|
496
|
+
When is_for="detail" and no detail config exists, falls back to read config.
|
|
497
|
+
|
|
465
498
|
Returns
|
|
466
499
|
-------
|
|
467
500
|
ModelQuerySetSchema
|
|
468
501
|
Read optimization configuration.
|
|
469
502
|
"""
|
|
470
503
|
if isinstance(self.model, ModelSerializerMeta):
|
|
471
|
-
|
|
504
|
+
result = getattr(self.model.QuerySet, is_for, None)
|
|
505
|
+
if result is None and is_for == "detail":
|
|
506
|
+
result = getattr(self.model.QuerySet, "read", None)
|
|
507
|
+
return result or ModelQuerySetSchema()
|
|
472
508
|
if self.with_serializer:
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
509
|
+
result = getattr(self.serializer_class.QuerySet, is_for, None)
|
|
510
|
+
if result is None and is_for == "detail":
|
|
511
|
+
result = getattr(self.serializer_class.QuerySet, "read", None)
|
|
512
|
+
return result or ModelQuerySetSchema()
|
|
476
513
|
return ModelQuerySetSchema()
|
|
477
514
|
|
|
478
|
-
def get_reverse_relations(
|
|
515
|
+
def get_reverse_relations(
|
|
516
|
+
self, is_for: Literal["read", "detail"] = "read"
|
|
517
|
+
) -> list[str]:
|
|
479
518
|
"""
|
|
480
519
|
Discover reverse relation names for safe prefetching.
|
|
481
520
|
|
|
521
|
+
Parameters
|
|
522
|
+
----------
|
|
523
|
+
is_for : Literal["read", "detail"]
|
|
524
|
+
Purpose of the query, determines which serializable fields to use.
|
|
525
|
+
|
|
482
526
|
Returns
|
|
483
527
|
-------
|
|
484
528
|
list[str]
|
|
485
529
|
Relation attribute names.
|
|
486
530
|
"""
|
|
487
|
-
reverse_rels = self._get_read_optimizations().prefetch_related.copy()
|
|
531
|
+
reverse_rels = self._get_read_optimizations(is_for).prefetch_related.copy()
|
|
488
532
|
if reverse_rels:
|
|
489
533
|
return reverse_rels
|
|
490
|
-
|
|
534
|
+
serializable_fields = self._get_serializable_field_names(is_for)
|
|
535
|
+
for f in serializable_fields:
|
|
491
536
|
field_obj = getattr(self.model, f)
|
|
492
537
|
if isinstance(field_obj, ManyToManyDescriptor):
|
|
493
538
|
reverse_rels.append(f)
|
|
@@ -499,19 +544,27 @@ class ModelUtil:
|
|
|
499
544
|
reverse_rels.append(field_obj.related.name)
|
|
500
545
|
return reverse_rels
|
|
501
546
|
|
|
502
|
-
def get_select_relateds(
|
|
547
|
+
def get_select_relateds(
|
|
548
|
+
self, is_for: Literal["read", "detail"] = "read"
|
|
549
|
+
) -> list[str]:
|
|
503
550
|
"""
|
|
504
551
|
Discover forward relation names for safe select_related.
|
|
505
552
|
|
|
553
|
+
Parameters
|
|
554
|
+
----------
|
|
555
|
+
is_for : Literal["read", "detail"]
|
|
556
|
+
Purpose of the query, determines which serializable fields to use.
|
|
557
|
+
|
|
506
558
|
Returns
|
|
507
559
|
-------
|
|
508
560
|
list[str]
|
|
509
561
|
Relation attribute names.
|
|
510
562
|
"""
|
|
511
|
-
select_rels = self._get_read_optimizations().select_related.copy()
|
|
563
|
+
select_rels = self._get_read_optimizations(is_for).select_related.copy()
|
|
512
564
|
if select_rels:
|
|
513
565
|
return select_rels
|
|
514
|
-
|
|
566
|
+
serializable_fields = self._get_serializable_field_names(is_for)
|
|
567
|
+
for f in serializable_fields:
|
|
515
568
|
field_obj = getattr(self.model, f)
|
|
516
569
|
if isinstance(field_obj, ForwardManyToOneDescriptor):
|
|
517
570
|
select_rels.append(f)
|
|
@@ -577,17 +630,15 @@ class ModelUtil:
|
|
|
577
630
|
request: HttpRequest,
|
|
578
631
|
query_data: QuerySchema,
|
|
579
632
|
schema: Schema,
|
|
580
|
-
|
|
633
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
581
634
|
):
|
|
582
635
|
"""Handle different query modes (filters vs getters)."""
|
|
583
636
|
if hasattr(query_data, "filters") and query_data.filters:
|
|
584
|
-
return await self._serialize_queryset(
|
|
585
|
-
request, query_data, schema, is_for_read
|
|
586
|
-
)
|
|
637
|
+
return await self._serialize_queryset(request, query_data, schema, is_for)
|
|
587
638
|
|
|
588
639
|
if hasattr(query_data, "getters") and query_data.getters:
|
|
589
640
|
return await self._serialize_single_object(
|
|
590
|
-
request, query_data, schema,
|
|
641
|
+
request, query_data, schema, is_for
|
|
591
642
|
)
|
|
592
643
|
|
|
593
644
|
raise SerializeError(
|
|
@@ -599,12 +650,10 @@ class ModelUtil:
|
|
|
599
650
|
request: HttpRequest,
|
|
600
651
|
query_data: QuerySchema,
|
|
601
652
|
schema: Schema,
|
|
602
|
-
|
|
653
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
603
654
|
):
|
|
604
655
|
"""Serialize a queryset of objects."""
|
|
605
|
-
objs = await self.get_objects(
|
|
606
|
-
request, query_data=query_data, is_for_read=is_for_read
|
|
607
|
-
)
|
|
656
|
+
objs = await self.get_objects(request, query_data=query_data, is_for=is_for)
|
|
608
657
|
return [await self._bump_object_from_schema(obj, schema) async for obj in objs]
|
|
609
658
|
|
|
610
659
|
async def _serialize_single_object(
|
|
@@ -612,12 +661,10 @@ class ModelUtil:
|
|
|
612
661
|
request: HttpRequest,
|
|
613
662
|
query_data: QuerySchema,
|
|
614
663
|
obj_schema: Schema,
|
|
615
|
-
|
|
664
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
616
665
|
):
|
|
617
666
|
"""Serialize a single object."""
|
|
618
|
-
obj = await self.get_object(
|
|
619
|
-
request, query_data=query_data, is_for_read=is_for_read
|
|
620
|
-
)
|
|
667
|
+
obj = await self.get_object(request, query_data=query_data, is_for=is_for)
|
|
621
668
|
return await self._bump_object_from_schema(obj, obj_schema)
|
|
622
669
|
|
|
623
670
|
async def parse_input_data(self, request: HttpRequest, data: Schema):
|
|
@@ -649,7 +696,9 @@ class ModelUtil:
|
|
|
649
696
|
"""
|
|
650
697
|
payload = data.model_dump(mode="json")
|
|
651
698
|
|
|
652
|
-
is_serializer =
|
|
699
|
+
is_serializer = (
|
|
700
|
+
isinstance(self.model, ModelSerializerMeta) or self.with_serializer
|
|
701
|
+
)
|
|
653
702
|
serializer = self.serializer if self.with_serializer else self.model
|
|
654
703
|
|
|
655
704
|
# Collect custom and optional fields (only if ModelSerializerMeta)
|
|
@@ -732,7 +781,7 @@ class ModelUtil:
|
|
|
732
781
|
| type["ModelSerializer"]
|
|
733
782
|
| models.Model = None,
|
|
734
783
|
query_data: QuerySchema = None,
|
|
735
|
-
|
|
784
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
736
785
|
):
|
|
737
786
|
"""
|
|
738
787
|
Internal serialization method handling both single instances and querysets.
|
|
@@ -747,8 +796,8 @@ class ModelUtil:
|
|
|
747
796
|
Instance(s) to serialize. If None, fetches based on query_data.
|
|
748
797
|
query_data : QuerySchema, optional
|
|
749
798
|
Query parameters for fetching objects when instance is None.
|
|
750
|
-
|
|
751
|
-
|
|
799
|
+
is_for : Literal["read", "detail"] | None, optional
|
|
800
|
+
Purpose of the query, determines which serializable fields to use.
|
|
752
801
|
|
|
753
802
|
Returns
|
|
754
803
|
-------
|
|
@@ -772,7 +821,7 @@ class ModelUtil:
|
|
|
772
821
|
return await self._bump_object_from_schema(instance, schema)
|
|
773
822
|
|
|
774
823
|
self._validate_read_params(request, query_data)
|
|
775
|
-
return await self._handle_query_mode(request, query_data, schema,
|
|
824
|
+
return await self._handle_query_mode(request, query_data, schema, is_for)
|
|
776
825
|
|
|
777
826
|
async def read_s(
|
|
778
827
|
self,
|
|
@@ -780,7 +829,7 @@ class ModelUtil:
|
|
|
780
829
|
request: HttpRequest = None,
|
|
781
830
|
instance: type["ModelSerializer"] = None,
|
|
782
831
|
query_data: ObjectQuerySchema = None,
|
|
783
|
-
|
|
832
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
784
833
|
) -> dict:
|
|
785
834
|
"""
|
|
786
835
|
Serialize a single model instance or fetch and serialize using query parameters.
|
|
@@ -799,8 +848,9 @@ class ModelUtil:
|
|
|
799
848
|
query_data : ObjectQuerySchema, optional
|
|
800
849
|
Query parameters with getters for single object lookup.
|
|
801
850
|
Required when instance is None.
|
|
802
|
-
|
|
803
|
-
|
|
851
|
+
is_for : Literal["read", "detail"] | None, optional
|
|
852
|
+
Purpose of the query, determines which serializable fields to use.
|
|
853
|
+
Defaults to None.
|
|
804
854
|
|
|
805
855
|
Returns
|
|
806
856
|
-------
|
|
@@ -820,14 +870,14 @@ class ModelUtil:
|
|
|
820
870
|
-----
|
|
821
871
|
- Uses Pydantic's from_orm() with mode="json" for serialization
|
|
822
872
|
- When instance is provided, request and query_data are ignored
|
|
823
|
-
- Query optimizations applied when
|
|
873
|
+
- Query optimizations applied when is_for is specified
|
|
824
874
|
"""
|
|
825
875
|
return await self._read_s(
|
|
826
876
|
schema,
|
|
827
877
|
request,
|
|
828
878
|
instance,
|
|
829
879
|
query_data,
|
|
830
|
-
|
|
880
|
+
is_for,
|
|
831
881
|
)
|
|
832
882
|
|
|
833
883
|
async def list_read_s(
|
|
@@ -836,7 +886,7 @@ class ModelUtil:
|
|
|
836
886
|
request: HttpRequest = None,
|
|
837
887
|
instances: models.QuerySet[type["ModelSerializer"] | models.Model] = None,
|
|
838
888
|
query_data: ObjectsQuerySchema = None,
|
|
839
|
-
|
|
889
|
+
is_for: Literal["read", "detail"] | None = None,
|
|
840
890
|
) -> list[dict]:
|
|
841
891
|
"""
|
|
842
892
|
Serialize multiple model instances or fetch and serialize using query parameters.
|
|
@@ -855,8 +905,9 @@ class ModelUtil:
|
|
|
855
905
|
query_data : ObjectsQuerySchema, optional
|
|
856
906
|
Query parameters with filters for multiple object lookup.
|
|
857
907
|
Required when instances is None.
|
|
858
|
-
|
|
859
|
-
|
|
908
|
+
is_for : Literal["read", "detail"] | None, optional
|
|
909
|
+
Purpose of the query, determines which serializable fields to use.
|
|
910
|
+
Defaults to None.
|
|
860
911
|
|
|
861
912
|
Returns
|
|
862
913
|
-------
|
|
@@ -874,7 +925,7 @@ class ModelUtil:
|
|
|
874
925
|
-----
|
|
875
926
|
- Uses Pydantic's from_orm() with mode="json" for serialization
|
|
876
927
|
- When instances is provided, request and query_data are ignored
|
|
877
|
-
- Query optimizations applied when
|
|
928
|
+
- Query optimizations applied when is_for is specified
|
|
878
929
|
- Processes queryset asynchronously for efficiency
|
|
879
930
|
"""
|
|
880
931
|
return await self._read_s(
|
|
@@ -882,7 +933,7 @@ class ModelUtil:
|
|
|
882
933
|
request,
|
|
883
934
|
instances,
|
|
884
935
|
query_data,
|
|
885
|
-
|
|
936
|
+
is_for,
|
|
886
937
|
)
|
|
887
938
|
|
|
888
939
|
async def update_s(
|
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
|
|
|
@@ -138,9 +162,12 @@ class QueryUtilBaseScopesSchema(BaseModel):
|
|
|
138
162
|
Schema defining base scopes for query utilities.
|
|
139
163
|
Attributes:
|
|
140
164
|
READ (str): Scope for read operations.
|
|
165
|
+
DETAIL (str): Scope for detail operations.
|
|
141
166
|
QUERYSET_REQUEST (str): Scope for queryset request operations.
|
|
142
167
|
"""
|
|
168
|
+
|
|
143
169
|
READ: str = "read"
|
|
170
|
+
DETAIL: str = "detail"
|
|
144
171
|
QUERYSET_REQUEST: str = "queryset_request"
|
|
145
172
|
|
|
146
173
|
|
|
@@ -163,6 +190,7 @@ class DecoratorsSchema(Schema):
|
|
|
163
190
|
Consider initializing these in __init__ or using default_factory (if using pydantic/dataclasses)
|
|
164
191
|
to avoid unintended side effects.
|
|
165
192
|
"""
|
|
193
|
+
|
|
166
194
|
list: Optional[List] = []
|
|
167
195
|
retrieve: Optional[List] = []
|
|
168
196
|
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
|
|
@@ -448,35 +454,46 @@ class APIViewSet(API):
|
|
|
448
454
|
qs = await self.model_util.get_objects(
|
|
449
455
|
request,
|
|
450
456
|
query_data=self._get_query_data(),
|
|
451
|
-
|
|
457
|
+
is_for="read",
|
|
452
458
|
)
|
|
453
459
|
if filters is not None:
|
|
454
460
|
qs = await self.query_params_handler(qs, filters.model_dump())
|
|
455
|
-
return await self.model_util.list_read_s(
|
|
461
|
+
return await self.model_util.list_read_s(
|
|
462
|
+
self.schema_out, request, qs, is_for="read"
|
|
463
|
+
)
|
|
456
464
|
|
|
457
465
|
return list
|
|
458
466
|
|
|
467
|
+
def _get_retrieve_schema(self) -> Schema:
|
|
468
|
+
"""
|
|
469
|
+
Return the schema to use for retrieve endpoint.
|
|
470
|
+
Uses schema_detail if available, otherwise falls back to schema_out.
|
|
471
|
+
"""
|
|
472
|
+
return self.schema_detail or self.schema_out
|
|
473
|
+
|
|
459
474
|
def retrieve_view(self):
|
|
460
475
|
"""
|
|
461
476
|
Register retrieve endpoint.
|
|
462
477
|
"""
|
|
478
|
+
retrieve_schema = self._get_retrieve_schema()
|
|
463
479
|
|
|
464
480
|
@self.router.get(
|
|
465
481
|
self.get_path_retrieve,
|
|
466
482
|
auth=self.get_view_auth(),
|
|
467
483
|
summary=f"Retrieve {self.model_verbose_name}",
|
|
468
484
|
description=self.retrieve_docs,
|
|
469
|
-
response={200:
|
|
485
|
+
response={200: retrieve_schema, self.error_codes: GenericMessageSchema},
|
|
470
486
|
)
|
|
471
487
|
@decorate_view(unique_view(self), *self.extra_decorators.retrieve)
|
|
472
488
|
async def retrieve(request: HttpRequest, pk: Path[self.path_schema]): # type: ignore
|
|
473
489
|
query_data = self._get_query_data()
|
|
474
490
|
return await self.model_util.read_s(
|
|
475
|
-
|
|
491
|
+
retrieve_schema,
|
|
476
492
|
request,
|
|
477
493
|
query_data=QuerySchema(
|
|
478
494
|
getters={"pk": self._get_pk(pk)}, **query_data.model_dump()
|
|
479
495
|
),
|
|
496
|
+
is_for="detail" if self.schema_detail else "read",
|
|
480
497
|
)
|
|
481
498
|
|
|
482
499
|
return retrieve
|
|
File without changes
|
{django_ninja_aio_crud-2.7.0.dist-info → django_ninja_aio_crud-2.9.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|