django-ninja-aio-crud 2.13.0__py3-none-any.whl → 2.15.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.

@@ -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.15.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=1ZReAODrup4BI5sHOAuanMqhDqfl36W8gVvhBrf5IZ0,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=tm2ACQO-n877NTYN0YR-6qVZrZQpWgPDh0EeOZo_TWY,42824
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.15.0.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
28
+ django_ninja_aio_crud-2.15.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
29
+ django_ninja_aio_crud-2.15.0.dist-info/METADATA,sha256=m89g4FLCQgQvwCkSoJLxzsUwCuf4ESwyqKJLX31qKAo,9964
30
+ django_ninja_aio_crud-2.15.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.15.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,32 @@ 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
+ class PkFromModel:
50
+ """Subscriptable type for extracting PK from model instances during serialization.
51
+
52
+ Usage:
53
+ PkFromModel[int] -> for integer PKs
54
+ PkFromModel[str] -> for string PKs
55
+ PkFromModel[UUID] -> for UUID PKs
56
+ PkFromModel -> defaults to int (backwards compatible)
57
+ """
58
+
59
+ _default = Annotated[int, BeforeValidator(_extract_pk)]
60
+
61
+ def __class_getitem__(cls, pk_type: type) -> type:
62
+ return Annotated[pk_type, BeforeValidator(_extract_pk)]
63
+
64
+ def __new__(cls):
65
+ return cls._default
66
+
67
+
31
68
  class BaseSerializer:
32
69
  """
33
70
  BaseSerializer
@@ -212,6 +249,11 @@ class BaseSerializer:
212
249
  # Optional in subclasses. Default to no explicit relation serializers.
213
250
  return {}
214
251
 
252
+ @classmethod
253
+ def _get_relations_as_id(cls) -> list[str]:
254
+ # Optional in subclasses. Default to no relations as ID.
255
+ return []
256
+
215
257
  @classmethod
216
258
  def _generate_union_schema(cls, resolved_union: Any) -> Any:
217
259
  """
@@ -350,10 +392,25 @@ class BaseSerializer:
350
392
  ) or cls._is_special_field("update", field, "optionals")
351
393
 
352
394
  @classmethod
353
- def _build_schema_reverse_rel(cls, field_name: str, descriptor: Any):
395
+ def _build_schema_reverse_rel(
396
+ cls, field_name: str, descriptor: Any, relations_as_id: list[str]
397
+ ):
354
398
  """
355
399
  Build a reverse relation schema component for 'Out' schema generation.
356
- Returns a custom field tuple or None to skip.
400
+
401
+ Parameters
402
+ ----------
403
+ field_name : str
404
+ Name of the relation field.
405
+ descriptor : Any
406
+ Django field descriptor (ManyToManyDescriptor, ReverseManyToOneDescriptor, etc.).
407
+ relations_as_id : list[str]
408
+ Pre-fetched list of fields to serialize as IDs.
409
+
410
+ Returns
411
+ -------
412
+ tuple | None
413
+ Custom field tuple for schema generation, or None to skip.
357
414
  """
358
415
  # Resolve related model and cardinality
359
416
  if isinstance(descriptor, ManyToManyDescriptor):
@@ -371,6 +428,17 @@ class BaseSerializer:
371
428
  rel_model = descriptor.related.related_model
372
429
  many = False
373
430
 
431
+ # Handle relations_as_id for reverse relations
432
+ if field_name in relations_as_id:
433
+ from ninja_aio.models.utils import ModelUtil
434
+ pk_field_type = ModelUtil(rel_model).pk_field_type
435
+ if many:
436
+ # For many relations, use PkFromModel to extract PKs from model instances
437
+ return (field_name, list[PkFromModel[pk_field_type]], Field(default_factory=list))
438
+ else:
439
+ # For single reverse relations (ReverseOneToOne), extract pk
440
+ return (field_name, PkFromModel[pk_field_type] | None, None)
441
+
374
442
  schema = cls._resolve_relation_schema(field_name, rel_model)
375
443
  if not schema:
376
444
  return None
@@ -379,14 +447,36 @@ class BaseSerializer:
379
447
  return (field_name, rel_schema_type | None, None)
380
448
 
381
449
  @classmethod
382
- def _build_schema_forward_rel(cls, field_name: str, descriptor: Any):
450
+ def _build_schema_forward_rel(
451
+ cls, field_name: str, descriptor: Any, relations_as_id: list[str]
452
+ ):
383
453
  """
384
454
  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.
455
+
456
+ Parameters
457
+ ----------
458
+ field_name : str
459
+ Name of the relation field.
460
+ descriptor : Any
461
+ Django field descriptor (ForwardOneToOneDescriptor, ForwardManyToOneDescriptor).
462
+ relations_as_id : list[str]
463
+ Pre-fetched list of fields to serialize as IDs.
464
+
465
+ Returns
466
+ -------
467
+ True | tuple | None
468
+ True to treat as plain field, a custom field tuple to include relation schema,
469
+ or None to skip entirely.
387
470
  """
388
471
  rel_model = descriptor.field.related_model
389
472
 
473
+ # Handle relations_as_id: serialize as the raw FK ID
474
+ if field_name in relations_as_id:
475
+ from ninja_aio.models.utils import ModelUtil
476
+ pk_field_type = ModelUtil(rel_model).pk_field_type
477
+ # Use PkFromModel to extract pk from the related instance during serialization
478
+ return (field_name, PkFromModel[pk_field_type] | None, None)
479
+
390
480
  # Special case: ModelSerializer with no readable fields should be skipped entirely
391
481
  if isinstance(rel_model, ModelSerializerMeta):
392
482
  if not (
@@ -441,23 +531,44 @@ class BaseSerializer:
441
531
  field_name: str,
442
532
  model,
443
533
  relations_serializers: dict,
534
+ relations_as_id: list[str],
444
535
  ) -> tuple[str | None, tuple | None, tuple | None]:
445
536
  """
446
537
  Process a single field and determine its classification.
447
538
 
448
- Returns:
539
+ Parameters
540
+ ----------
541
+ field_name : str
542
+ Name of the field to process.
543
+ model : Model
544
+ Django model class.
545
+ relations_serializers : dict
546
+ Mapping of relation field names to serializer classes.
547
+ relations_as_id : list[str]
548
+ Pre-fetched list of fields to serialize as IDs.
549
+
550
+ Returns
551
+ -------
552
+ tuple
449
553
  (plain_field, reverse_rel, forward_rel) - only one will be non-None
450
554
  """
451
555
  field_obj = getattr(model, field_name)
452
556
 
453
557
  if cls._is_reverse_relation(field_obj):
454
- if field_name not in relations_serializers:
558
+ if (
559
+ field_name not in relations_serializers
560
+ and field_name not in relations_as_id
561
+ ):
455
562
  cls._warn_missing_relation_serializer(field_name, model)
456
- rel_tuple = cls._build_schema_reverse_rel(field_name, field_obj)
563
+ rel_tuple = cls._build_schema_reverse_rel(
564
+ field_name, field_obj, relations_as_id
565
+ )
457
566
  return (None, rel_tuple, None)
458
567
 
459
568
  if cls._is_forward_relation(field_obj):
460
- rel_tuple = cls._build_schema_forward_rel(field_name, field_obj)
569
+ rel_tuple = cls._build_schema_forward_rel(
570
+ field_name, field_obj, relations_as_id
571
+ )
461
572
  if rel_tuple is True:
462
573
  return (field_name, None, None)
463
574
  return (None, None, rel_tuple)
@@ -469,8 +580,15 @@ class BaseSerializer:
469
580
  """
470
581
  Collect components for output schema generation (Out or Detail).
471
582
 
472
- Returns:
473
- tuple: (fields, reverse_rels, excludes, customs_with_forward_rels, optionals)
583
+ Parameters
584
+ ----------
585
+ schema_type : Literal["Out", "Detail"]
586
+ Type of schema to generate.
587
+
588
+ Returns
589
+ -------
590
+ tuple
591
+ (fields, reverse_rels, excludes, customs_with_forward_rels, optionals)
474
592
  """
475
593
  if schema_type not in ("Out", "Detail"):
476
594
  raise ValueError(
@@ -480,6 +598,8 @@ class BaseSerializer:
480
598
  fields_type = "read" if schema_type == "Out" else "detail"
481
599
  model = cls._get_model()
482
600
  relations_serializers = cls._get_relations_serializers() or {}
601
+ # Fetch once to avoid repeated method calls during field processing
602
+ relations_as_id = cls._get_relations_as_id()
483
603
 
484
604
  fields: list[str] = []
485
605
  reverse_rels: list[tuple] = []
@@ -487,7 +607,7 @@ class BaseSerializer:
487
607
 
488
608
  for field_name in cls.get_fields(fields_type):
489
609
  plain, reverse, forward = cls._process_field(
490
- field_name, model, relations_serializers
610
+ field_name, model, relations_serializers, relations_as_id
491
611
  )
492
612
  if plain:
493
613
  fields.append(plain)
@@ -701,12 +821,15 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
701
821
  Computed / synthetic output attributes.
702
822
  optionals : list[tuple[str, type]]
703
823
  Optional output fields.
824
+ relations_as_id : list[str]
825
+ Relation fields to serialize as IDs instead of nested objects.
704
826
  """
705
827
 
706
828
  fields: list[str] = []
707
829
  customs: list[tuple[str, type, Any]] = []
708
830
  optionals: list[tuple[str, type]] = []
709
831
  excludes: list[str] = []
832
+ relations_as_id: list[str] = []
710
833
 
711
834
  class DetailSerializer:
712
835
  """Configuration describing detail (single object) read schema.
@@ -756,6 +879,11 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
756
879
  "detail": "DetailSerializer",
757
880
  }
758
881
 
882
+ @classmethod
883
+ def _get_relations_as_id(cls) -> list[str]:
884
+ """Return relation fields to serialize as IDs instead of nested objects."""
885
+ return getattr(cls.ReadSerializer, "relations_as_id", [])
886
+
759
887
  @classmethod
760
888
  def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
761
889
  """
@@ -960,6 +1088,12 @@ class Serializer(BaseSerializer, metaclass=SerializerMeta):
960
1088
  schema_update: Optional[SchemaModelConfig] = None
961
1089
  schema_detail: Optional[SchemaModelConfig] = None
962
1090
  relations_serializers: dict[str, "Serializer"] = {}
1091
+ relations_as_id: list[str] = []
1092
+
1093
+ @classmethod
1094
+ def _get_relations_as_id(cls) -> list[str]:
1095
+ relations_as_id = cls._get_meta_data("relations_as_id")
1096
+ return relations_as_id or []
963
1097
 
964
1098
  @classmethod
965
1099
  def _get_meta_data(cls, attr_name: str) -> Any: