django-ninja-aio-crud 2.13.0__py3-none-any.whl → 2.14.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-ninja-aio-crud
3
- Version: 2.13.0
3
+ Version: 2.14.0
4
4
  Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
6
  Requires-Python: >=3.10, <3.15
@@ -1,4 +1,4 @@
1
- ninja_aio/__init__.py,sha256=927xxMRmWc-R42YP-42eXiVvPNa8N9YLu1m4VPIaPvY,120
1
+ ninja_aio/__init__.py,sha256=cdoKMOyGNe8UDjxTJQBqIMhJ_-OLa8J5FBMGB-6C-6E,120
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
@@ -14,7 +14,7 @@ ninja_aio/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
14
14
  ninja_aio/helpers/api.py,sha256=O2nGvP3VSsG-AReQRn90yDH8vS3kMKh125j-ikwgGoQ,20987
15
15
  ninja_aio/helpers/query.py,sha256=Lqv4nrWYr543tC5K-SEcBottLID8cb83aDc26i2Wxj4,5053
16
16
  ninja_aio/models/__init__.py,sha256=L3UQnQAlKoI3F7jinadL-Nn55hkPvnSRPYW0JtnbWFo,114
17
- ninja_aio/models/serializers.py,sha256=VtyWiSGA9vOmxC2S-2lG6mvF33T61sUkNaDmGkAIiOM,38535
17
+ ninja_aio/models/serializers.py,sha256=Bv5Guqc_OuOGPos6A8HD0EdfGJZI_NQ9mmHhKb8jyZI,42114
18
18
  ninja_aio/models/utils.py,sha256=lAXtc3YY7_n4f0jIacX4DSXhUOzMy7y5MsBnInNxtfk,32874
19
19
  ninja_aio/schemas/__init__.py,sha256=dHILiYBKMb51lDcyQdiXRw_0nzqM7Lu81UX2hv7kEfo,837
20
20
  ninja_aio/schemas/api.py,sha256=dGUpJXR1iAf93QNR4kYj1uqIkTjiMfXultCotY6GtaQ,361
@@ -24,7 +24,7 @@ ninja_aio/schemas/helpers.py,sha256=h4zQRf21NVLMQbIVH-psAE4FICUBc857EqngblEy7og,
24
24
  ninja_aio/views/__init__.py,sha256=DEzjWA6y3WF0V10nNF8eEurLNEodgxKzyFd09AqVp3s,148
25
25
  ninja_aio/views/api.py,sha256=AAqkj0xT8J3PmJvsbluZ33cfrmrXJHiV9ARe2BqnfQ8,22492
26
26
  ninja_aio/views/mixins.py,sha256=Zl6J8gbVagwT85bzDuKyJTk3iFxxFgX0YgYkjiUxZGg,17040
27
- django_ninja_aio_crud-2.13.0.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
28
- django_ninja_aio_crud-2.13.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
29
- django_ninja_aio_crud-2.13.0.dist-info/METADATA,sha256=dj3-NSAn8DeyBSViefyGt8rLp_f5bfSaZmkbVaQ2gbA,9964
30
- django_ninja_aio_crud-2.13.0.dist-info/RECORD,,
27
+ django_ninja_aio_crud-2.14.0.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
28
+ django_ninja_aio_crud-2.14.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
29
+ django_ninja_aio_crud-2.14.0.dist-info/METADATA,sha256=M2D8ZIAziKDs1Xo2AibR1QzcTXXe2Tlxeh3h7CYi6bo,9964
30
+ django_ninja_aio_crud-2.14.0.dist-info/RECORD,,
ninja_aio/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "2.13.0"
3
+ __version__ = "2.14.0"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
@@ -1,4 +1,14 @@
1
- from typing import Any, List, Literal, Optional, Union, get_args, get_origin, ForwardRef
1
+ from typing import (
2
+ Annotated,
3
+ Any,
4
+ List,
5
+ Literal,
6
+ Optional,
7
+ Union,
8
+ get_args,
9
+ get_origin,
10
+ ForwardRef,
11
+ )
2
12
  import warnings
3
13
  import sys
4
14
 
@@ -14,6 +24,7 @@ from django.db.models.fields.related_descriptors import (
14
24
  ForwardManyToOneDescriptor,
15
25
  ForwardOneToOneDescriptor,
16
26
  )
27
+ from pydantic import BeforeValidator, Field
17
28
 
18
29
  from ninja_aio.types import (
19
30
  S_TYPES,
@@ -28,6 +39,17 @@ from ninja_aio.schemas.helpers import (
28
39
  )
29
40
 
30
41
 
42
+ def _extract_pk(v: Any) -> Any:
43
+ """Extract primary key from a model instance or return value as-is."""
44
+ if hasattr(v, "pk"):
45
+ return v.pk
46
+ return v
47
+
48
+
49
+ # Annotated type for extracting PK from model instances during serialization
50
+ PkFromModel = Annotated[int, BeforeValidator(_extract_pk)]
51
+
52
+
31
53
  class BaseSerializer:
32
54
  """
33
55
  BaseSerializer
@@ -212,6 +234,11 @@ class BaseSerializer:
212
234
  # Optional in subclasses. Default to no explicit relation serializers.
213
235
  return {}
214
236
 
237
+ @classmethod
238
+ def _get_relations_as_id(cls) -> list[str]:
239
+ # Optional in subclasses. Default to no relations as ID.
240
+ return []
241
+
215
242
  @classmethod
216
243
  def _generate_union_schema(cls, resolved_union: Any) -> Any:
217
244
  """
@@ -350,10 +377,25 @@ class BaseSerializer:
350
377
  ) or cls._is_special_field("update", field, "optionals")
351
378
 
352
379
  @classmethod
353
- def _build_schema_reverse_rel(cls, field_name: str, descriptor: Any):
380
+ def _build_schema_reverse_rel(
381
+ cls, field_name: str, descriptor: Any, relations_as_id: list[str]
382
+ ):
354
383
  """
355
384
  Build a reverse relation schema component for 'Out' schema generation.
356
- Returns a custom field tuple or None to skip.
385
+
386
+ Parameters
387
+ ----------
388
+ field_name : str
389
+ Name of the relation field.
390
+ descriptor : Any
391
+ Django field descriptor (ManyToManyDescriptor, ReverseManyToOneDescriptor, etc.).
392
+ relations_as_id : list[str]
393
+ Pre-fetched list of fields to serialize as IDs.
394
+
395
+ Returns
396
+ -------
397
+ tuple | None
398
+ Custom field tuple for schema generation, or None to skip.
357
399
  """
358
400
  # Resolve related model and cardinality
359
401
  if isinstance(descriptor, ManyToManyDescriptor):
@@ -371,6 +413,15 @@ class BaseSerializer:
371
413
  rel_model = descriptor.related.related_model
372
414
  many = False
373
415
 
416
+ # Handle relations_as_id for reverse relations
417
+ if field_name in relations_as_id:
418
+ if many:
419
+ # For many relations, use PkFromModel to extract PKs from model instances
420
+ return (field_name, list[PkFromModel], Field(default_factory=list))
421
+ else:
422
+ # For single reverse relations (ReverseOneToOne), extract pk
423
+ return (field_name, PkFromModel | None, None)
424
+
374
425
  schema = cls._resolve_relation_schema(field_name, rel_model)
375
426
  if not schema:
376
427
  return None
@@ -379,14 +430,34 @@ class BaseSerializer:
379
430
  return (field_name, rel_schema_type | None, None)
380
431
 
381
432
  @classmethod
382
- def _build_schema_forward_rel(cls, field_name: str, descriptor: Any):
433
+ def _build_schema_forward_rel(
434
+ cls, field_name: str, descriptor: Any, relations_as_id: list[str]
435
+ ):
383
436
  """
384
437
  Build a forward relation schema component for 'Out' schema generation.
385
- Returns True to treat as plain field, a custom field tuple to include relation schema,
386
- or None to skip entirely.
438
+
439
+ Parameters
440
+ ----------
441
+ field_name : str
442
+ Name of the relation field.
443
+ descriptor : Any
444
+ Django field descriptor (ForwardOneToOneDescriptor, ForwardManyToOneDescriptor).
445
+ relations_as_id : list[str]
446
+ Pre-fetched list of fields to serialize as IDs.
447
+
448
+ Returns
449
+ -------
450
+ True | tuple | None
451
+ True to treat as plain field, a custom field tuple to include relation schema,
452
+ or None to skip entirely.
387
453
  """
388
454
  rel_model = descriptor.field.related_model
389
455
 
456
+ # Handle relations_as_id: serialize as the raw FK ID
457
+ if field_name in relations_as_id:
458
+ # Use PkFromModel to extract pk from the related instance during serialization
459
+ return (field_name, PkFromModel | None, None)
460
+
390
461
  # Special case: ModelSerializer with no readable fields should be skipped entirely
391
462
  if isinstance(rel_model, ModelSerializerMeta):
392
463
  if not (
@@ -441,23 +512,44 @@ class BaseSerializer:
441
512
  field_name: str,
442
513
  model,
443
514
  relations_serializers: dict,
515
+ relations_as_id: list[str],
444
516
  ) -> tuple[str | None, tuple | None, tuple | None]:
445
517
  """
446
518
  Process a single field and determine its classification.
447
519
 
448
- Returns:
520
+ Parameters
521
+ ----------
522
+ field_name : str
523
+ Name of the field to process.
524
+ model : Model
525
+ Django model class.
526
+ relations_serializers : dict
527
+ Mapping of relation field names to serializer classes.
528
+ relations_as_id : list[str]
529
+ Pre-fetched list of fields to serialize as IDs.
530
+
531
+ Returns
532
+ -------
533
+ tuple
449
534
  (plain_field, reverse_rel, forward_rel) - only one will be non-None
450
535
  """
451
536
  field_obj = getattr(model, field_name)
452
537
 
453
538
  if cls._is_reverse_relation(field_obj):
454
- if field_name not in relations_serializers:
539
+ if (
540
+ field_name not in relations_serializers
541
+ and field_name not in relations_as_id
542
+ ):
455
543
  cls._warn_missing_relation_serializer(field_name, model)
456
- rel_tuple = cls._build_schema_reverse_rel(field_name, field_obj)
544
+ rel_tuple = cls._build_schema_reverse_rel(
545
+ field_name, field_obj, relations_as_id
546
+ )
457
547
  return (None, rel_tuple, None)
458
548
 
459
549
  if cls._is_forward_relation(field_obj):
460
- rel_tuple = cls._build_schema_forward_rel(field_name, field_obj)
550
+ rel_tuple = cls._build_schema_forward_rel(
551
+ field_name, field_obj, relations_as_id
552
+ )
461
553
  if rel_tuple is True:
462
554
  return (field_name, None, None)
463
555
  return (None, None, rel_tuple)
@@ -469,8 +561,15 @@ class BaseSerializer:
469
561
  """
470
562
  Collect components for output schema generation (Out or Detail).
471
563
 
472
- Returns:
473
- tuple: (fields, reverse_rels, excludes, customs_with_forward_rels, optionals)
564
+ Parameters
565
+ ----------
566
+ schema_type : Literal["Out", "Detail"]
567
+ Type of schema to generate.
568
+
569
+ Returns
570
+ -------
571
+ tuple
572
+ (fields, reverse_rels, excludes, customs_with_forward_rels, optionals)
474
573
  """
475
574
  if schema_type not in ("Out", "Detail"):
476
575
  raise ValueError(
@@ -480,6 +579,8 @@ class BaseSerializer:
480
579
  fields_type = "read" if schema_type == "Out" else "detail"
481
580
  model = cls._get_model()
482
581
  relations_serializers = cls._get_relations_serializers() or {}
582
+ # Fetch once to avoid repeated method calls during field processing
583
+ relations_as_id = cls._get_relations_as_id()
483
584
 
484
585
  fields: list[str] = []
485
586
  reverse_rels: list[tuple] = []
@@ -487,7 +588,7 @@ class BaseSerializer:
487
588
 
488
589
  for field_name in cls.get_fields(fields_type):
489
590
  plain, reverse, forward = cls._process_field(
490
- field_name, model, relations_serializers
591
+ field_name, model, relations_serializers, relations_as_id
491
592
  )
492
593
  if plain:
493
594
  fields.append(plain)
@@ -701,12 +802,15 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
701
802
  Computed / synthetic output attributes.
702
803
  optionals : list[tuple[str, type]]
703
804
  Optional output fields.
805
+ relations_as_id : list[str]
806
+ Relation fields to serialize as IDs instead of nested objects.
704
807
  """
705
808
 
706
809
  fields: list[str] = []
707
810
  customs: list[tuple[str, type, Any]] = []
708
811
  optionals: list[tuple[str, type]] = []
709
812
  excludes: list[str] = []
813
+ relations_as_id: list[str] = []
710
814
 
711
815
  class DetailSerializer:
712
816
  """Configuration describing detail (single object) read schema.
@@ -756,6 +860,11 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
756
860
  "detail": "DetailSerializer",
757
861
  }
758
862
 
863
+ @classmethod
864
+ def _get_relations_as_id(cls) -> list[str]:
865
+ """Return relation fields to serialize as IDs instead of nested objects."""
866
+ return getattr(cls.ReadSerializer, "relations_as_id", [])
867
+
759
868
  @classmethod
760
869
  def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
761
870
  """
@@ -960,6 +1069,12 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
960
1069
  schema_update: Optional[SchemaModelConfig] = None
961
1070
  schema_detail: Optional[SchemaModelConfig] = None
962
1071
  relations_serializers: dict[str, "Serializer"] = {}
1072
+ relations_as_id: list[str] = []
1073
+
1074
+ @classmethod
1075
+ def _get_relations_as_id(cls) -> list[str]:
1076
+ relations_as_id = cls._get_meta_data("relations_as_id")
1077
+ return relations_as_id or []
963
1078
 
964
1079
  @classmethod
965
1080
  def _get_meta_data(cls, attr_name: str) -> Any: