django-ninja-aio-crud 2.5.0__py3-none-any.whl → 2.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

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.5.0
3
+ Version: 2.6.1
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=LBFfoQaFXisUD0DcYa1xZV-zyiAJqx_3QAY-hyxB6OA,119
1
+ ninja_aio/__init__.py,sha256=ww2hnE9C7lFXzVyNnpZ97wv8erYcXdxhjzu8PRiM8bc,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
@@ -14,16 +14,16 @@ ninja_aio/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
14
14
  ninja_aio/helpers/api.py,sha256=YMzuZ4-ZpUrJBQIabE26gb_GYwsH2rVosWRE95YfdPQ,20775
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=-6NsyTJBdUQl3fkAusC7Kx_2keMRV7CNI9yAlH_fQns,29340
17
+ ninja_aio/models/serializers.py,sha256=zV3gRI4isnCuLCAQbPhQlut3nKT0XuQOLy2ABiaJ6Y4,31372
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
22
  ninja_aio/schemas/helpers.py,sha256=W6IeHi5Tmbjh3FXwDYqjqlLBTVj5uTYq3_JVkNUWayo,7355
23
23
  ninja_aio/views/__init__.py,sha256=DEzjWA6y3WF0V10nNF8eEurLNEodgxKzyFd09AqVp3s,148
24
- ninja_aio/views/api.py,sha256=xdzxM1XOPTZmGrRxlIz1QH4UKgxRfg9ls-geatTJrXI,21179
24
+ ninja_aio/views/api.py,sha256=GRtjCvAv7jAp3TxpOirsbMVKpBd8hymSMILdE-JLxvI,21327
25
25
  ninja_aio/views/mixins.py,sha256=Jh6BG8Cs823nurVlODlzCquTxKrLH7Pmo5udPqUGZek,11378
26
- django_ninja_aio_crud-2.5.0.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
27
- django_ninja_aio_crud-2.5.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
28
- django_ninja_aio_crud-2.5.0.dist-info/METADATA,sha256=Gyk2TCPm3oQaT9YsZrXC7btzZzmDmntKyk_wz0M6Hfg,9963
29
- django_ninja_aio_crud-2.5.0.dist-info/RECORD,,
26
+ django_ninja_aio_crud-2.6.1.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
27
+ django_ninja_aio_crud-2.6.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
28
+ django_ninja_aio_crud-2.6.1.dist-info/METADATA,sha256=aBT8gpjs9TVS_jcs_c4m--5K8YFLjpAUpNPa0ib2T-0,9963
29
+ django_ninja_aio_crud-2.6.1.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.5.0"
3
+ __version__ = "2.6.1"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
@@ -1,5 +1,6 @@
1
1
  from typing import Any, List, Optional
2
2
  import warnings
3
+ import sys
3
4
 
4
5
  from django.conf import settings
5
6
  from ninja import Schema
@@ -67,11 +68,92 @@ class BaseSerializer:
67
68
  # Subclasses provide implementation.
68
69
  raise NotImplementedError
69
70
 
71
+ @classmethod
72
+ def _resolve_serializer_reference(cls, serializer_ref: str | type) -> type:
73
+ """
74
+ Resolve a serializer reference that may be a string or a class.
75
+
76
+ This method performs lazy resolution, meaning it will attempt to resolve
77
+ string references only when called, allowing for forward references and
78
+ circular dependencies between serializers in the same module.
79
+
80
+ Parameters
81
+ ----------
82
+ serializer_ref : str | type
83
+ Either a string reference to a serializer class name in the same module,
84
+ or an actual serializer class.
85
+
86
+ Returns
87
+ -------
88
+ type
89
+ The resolved serializer class.
90
+
91
+ Raises
92
+ ------
93
+ ValueError
94
+ If the string reference cannot be resolved.
95
+ """
96
+ # If it's already a class, return it directly
97
+ if not isinstance(serializer_ref, str):
98
+ return serializer_ref
99
+
100
+ # Get the module where the current serializer class is defined
101
+ module = sys.modules.get(cls.__module__)
102
+
103
+ if module is None:
104
+ raise ValueError(
105
+ f"Cannot resolve serializer reference '{serializer_ref}': "
106
+ f"module '{cls.__module__}' not found in sys.modules."
107
+ )
108
+
109
+ # Try to get the serializer class from the module
110
+ serializer_class = getattr(module, serializer_ref, None)
111
+
112
+ if serializer_class is None:
113
+ raise ValueError(
114
+ f"Cannot resolve serializer reference '{serializer_ref}' in module '{cls.__module__}'. "
115
+ f"Make sure the serializer class '{serializer_ref}' is defined in the same module as {cls.__name__}."
116
+ )
117
+
118
+ return serializer_class
119
+
70
120
  @classmethod
71
121
  def _get_relations_serializers(cls) -> dict[str, "Serializer"]:
72
122
  # Optional in subclasses. Default to no explicit relation serializers.
73
123
  return {}
74
124
 
125
+ @classmethod
126
+ def _resolve_relation_schema(cls, field_name: str, rel_model: models.Model):
127
+ """
128
+ Resolve and generate schema for a related model.
129
+
130
+ Parameters
131
+ ----------
132
+ field_name : str
133
+ Name of the relation field.
134
+ rel_model : models.Model
135
+ The related model class.
136
+
137
+ Returns
138
+ -------
139
+ Schema | None
140
+ Generated schema or None if cannot be resolved.
141
+ """
142
+ # Check if related model is a ModelSerializer with readable fields
143
+ if isinstance(rel_model, ModelSerializerMeta):
144
+ if rel_model.get_fields("read") or rel_model.get_custom_fields("read"):
145
+ return rel_model.generate_related_s()
146
+ return None
147
+
148
+ # Fall back to explicit serializer mapping
149
+ rel_serializers = cls._get_relations_serializers() or {}
150
+ serializer_ref = rel_serializers.get(field_name)
151
+ if serializer_ref:
152
+ serializer = cls._resolve_serializer_reference(serializer_ref)
153
+ return serializer.generate_related_s()
154
+
155
+ return None
156
+
75
157
  @classmethod
76
158
  def _is_special_field(
77
159
  cls, s_type: type[S_TYPES], field: str, f_type: type[F_TYPES]
@@ -160,18 +242,7 @@ class BaseSerializer:
160
242
  rel_model = descriptor.related.related_model
161
243
  many = False
162
244
 
163
- schema = None
164
- if isinstance(rel_model, ModelSerializerMeta):
165
- # Auto-include if related model exposes readable data
166
- if rel_model.get_fields("read") or rel_model.get_custom_fields("read"):
167
- schema = rel_model.generate_related_s()
168
- else:
169
- # Use explicit serializer when provided by subclasses
170
- rel_serializers = cls._get_relations_serializers() or {}
171
- serializer = rel_serializers.get(field_name)
172
- if serializer:
173
- schema = serializer.generate_related_s()
174
-
245
+ schema = cls._resolve_relation_schema(field_name, rel_model)
175
246
  if not schema:
176
247
  return None
177
248
 
@@ -187,21 +258,14 @@ class BaseSerializer:
187
258
  """
188
259
  rel_model = descriptor.field.related_model
189
260
 
190
- schema = None
261
+ # Special case: ModelSerializer with no readable fields should be skipped entirely
191
262
  if isinstance(rel_model, ModelSerializerMeta):
192
- # Prefer auto-inclusion when the related model is a ModelSerializer
193
- if rel_model.get_fields("read") or rel_model.get_custom_fields("read"):
194
- schema = rel_model.generate_related_s()
195
- else:
196
- # Explicit ModelSerializer with no readable fields -> skip entirely
263
+ if not (
264
+ rel_model.get_fields("read") or rel_model.get_custom_fields("read")
265
+ ):
197
266
  return None
198
- else:
199
- # Fall back to an explicitly provided serializer mapping
200
- rel_serializers = cls._get_relations_serializers() or {}
201
- serializer = rel_serializers.get(field_name)
202
- if serializer:
203
- schema = serializer.generate_related_s()
204
267
 
268
+ schema = cls._resolve_relation_schema(field_name, rel_model)
205
269
  if not schema:
206
270
  # Could not build a schema: treat as a plain field (serialize as-is)
207
271
  return True
@@ -289,52 +353,55 @@ class BaseSerializer:
289
353
  Handles In/Patch/Out/Related.
290
354
  """
291
355
  model = cls._get_model()
292
- match schema_type:
293
- case "In":
294
- s_type = "create"
295
- case "Patch":
296
- s_type = "update"
297
- case "Out":
298
- fields, reverse_rels, excludes, customs, optionals = (
299
- cls.get_schema_out_data()
300
- )
301
- if not fields and not reverse_rels and not excludes and not customs:
302
- return None
303
- return create_schema(
304
- model=model,
305
- name=f"{model._meta.model_name}SchemaOut",
306
- depth=depth,
307
- fields=fields,
308
- custom_fields=reverse_rels + customs + optionals,
309
- exclude=excludes,
310
- )
311
- case "Related":
312
- # Related schema includes only non-relational declared fields + customs
313
- fields, customs = cls.get_related_schema_data()
314
- if not fields and not customs:
315
- return None
316
- return create_schema(
317
- model=model,
318
- name=f"{model._meta.model_name}SchemaRelated",
319
- fields=fields,
320
- custom_fields=customs,
321
- )
356
+
357
+ # Handle special schema types with custom logic
358
+ if schema_type == "Out":
359
+ fields, reverse_rels, excludes, customs, optionals = (
360
+ cls.get_schema_out_data()
361
+ )
362
+ if not any([fields, reverse_rels, excludes, customs]):
363
+ return None
364
+ return create_schema(
365
+ model=model,
366
+ name=f"{model._meta.model_name}SchemaOut",
367
+ depth=depth,
368
+ fields=fields,
369
+ custom_fields=reverse_rels + customs + optionals,
370
+ exclude=excludes,
371
+ )
372
+
373
+ if schema_type == "Related":
374
+ fields, customs = cls.get_related_schema_data()
375
+ if not fields and not customs:
376
+ return None
377
+ return create_schema(
378
+ model=model,
379
+ name=f"{model._meta.model_name}SchemaRelated",
380
+ fields=fields,
381
+ custom_fields=customs,
382
+ )
383
+
384
+ # Handle standard In/Patch schema types
385
+ s_type = "create" if schema_type == "In" else "update"
322
386
  fields = cls.get_fields(s_type)
323
387
  optionals = cls.get_optional_fields(s_type)
324
388
  customs = cls.get_custom_fields(s_type) + optionals
325
389
  excludes = cls.get_excluded_fields(s_type)
390
+
391
+ # If no explicit fields but have optionals, use optional field names as fields
326
392
  if not fields and not excludes:
327
393
  fields = [f[0] for f in optionals]
328
- return (
329
- create_schema(
330
- model=model,
331
- name=f"{model._meta.model_name}Schema{schema_type}",
332
- fields=fields,
333
- custom_fields=customs,
334
- exclude=excludes,
335
- )
336
- if fields or customs or excludes
337
- else None
394
+
395
+ # Only create schema if we have something to include
396
+ if not any([fields, customs, excludes]):
397
+ return None
398
+
399
+ return create_schema(
400
+ model=model,
401
+ name=f"{model._meta.model_name}Schema{schema_type}",
402
+ fields=fields,
403
+ custom_fields=customs,
404
+ exclude=excludes,
338
405
  )
339
406
 
340
407
  @classmethod
@@ -493,6 +560,13 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
493
560
  optionals: list[tuple[str, type]] = []
494
561
  excludes: list[str] = []
495
562
 
563
+ # Serializer type to configuration class mapping
564
+ _SERIALIZER_CONFIG_MAP = {
565
+ "create": "CreateSerializer",
566
+ "update": "UpdateSerializer",
567
+ "read": "ReadSerializer",
568
+ }
569
+
496
570
  @classmethod
497
571
  def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
498
572
  """
@@ -510,14 +584,11 @@ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMet
510
584
  list
511
585
  Raw configuration list or empty list.
512
586
  """
513
- match s_type:
514
- case "create":
515
- fields = getattr(cls.CreateSerializer, f_type, [])
516
- case "update":
517
- fields = getattr(cls.UpdateSerializer, f_type, [])
518
- case "read":
519
- fields = getattr(cls.ReadSerializer, f_type, [])
520
- return fields
587
+ config_class_name = cls._SERIALIZER_CONFIG_MAP.get(s_type)
588
+ if not config_class_name:
589
+ return []
590
+ config_class = getattr(cls, config_class_name)
591
+ return getattr(config_class, f_type, [])
521
592
 
522
593
  @classmethod
523
594
  def _get_model(cls) -> "ModelSerializer":
@@ -672,18 +743,22 @@ class Serializer(BaseSerializer):
672
743
  schema components during read schema generation.
673
744
  """
674
745
 
746
+ # Serializer type to Meta schema attribute mapping
747
+ _SCHEMA_META_MAP = {
748
+ "create": "in",
749
+ "update": "update",
750
+ "read": "out",
751
+ }
752
+
675
753
  def __init_subclass__(cls, **kwargs):
676
754
  super().__init_subclass__(**kwargs)
677
755
  from ninja_aio.models.utils import ModelUtil
678
756
  from ninja_aio.helpers.query import QueryUtil
679
757
 
680
758
  cls.model = cls._get_model()
681
- cls.schema_in = cls.generate_create_s()
682
- cls.schema_out = cls.generate_read_s()
683
- cls.schema_update = cls.generate_update_s()
684
- cls.schema_related = cls.generate_related_s()
685
- cls.util = ModelUtil(cls._get_model(), serializer_class=cls)
759
+ cls.util = ModelUtil(cls.model, serializer_class=cls)
686
760
  cls.query_util = QueryUtil(cls)
761
+ cls._meta = cls.Meta
687
762
 
688
763
  class Meta:
689
764
  model: models.Model = None
@@ -729,16 +804,14 @@ class Serializer(BaseSerializer):
729
804
  @classmethod
730
805
  def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
731
806
  """Internal accessor for raw configuration lists from Meta schemas."""
732
- schema = {
733
- "create": cls._get_schema_meta("in"),
734
- "update": cls._get_schema_meta("update"),
735
- "read": cls._get_schema_meta("out"),
736
- }.get(s_type)
807
+ schema_key = cls._SCHEMA_META_MAP.get(s_type)
808
+ if not schema_key:
809
+ return []
810
+ schema = cls._get_schema_meta(schema_key)
737
811
  if not schema:
738
812
  return []
739
813
  return getattr(schema, f_type, []) or []
740
814
 
741
-
742
815
  @classmethod
743
816
  async def queryset_request(cls, request: HttpRequest):
744
817
  return cls.query_util.apply_queryset_optimizations(
@@ -835,7 +908,7 @@ class Serializer(BaseSerializer):
835
908
  dict
836
909
  Serialized data.
837
910
  """
838
- return await self.model_util.read_s(schema=self.schema_out, instance=instance)
911
+ return await self.util.read_s(schema=self.generate_read_s(), instance=instance)
839
912
 
840
913
  async def models_dump(
841
914
  self, instances: models.QuerySet[models.Model]
@@ -853,8 +926,8 @@ class Serializer(BaseSerializer):
853
926
  list[dict]
854
927
  List of serialized data.
855
928
  """
856
- return await self.model_util.list_read_s(
857
- schema=self.schema_out, instances=instances
929
+ return await self.util.list_read_s(
930
+ schema=self.generate_read_s(), instances=instances
858
931
  )
859
932
 
860
933
  def after_save(self, instance: models.Model):
ninja_aio/views/api.py CHANGED
@@ -254,6 +254,9 @@ class APIViewSet(API):
254
254
  self.api = api or self.api
255
255
  self.error_codes = ERROR_CODES
256
256
  self.model = model or self.model
257
+ self.serializer: serializers.Serializer | None = (
258
+ None if self.serializer_class is None else self.serializer_class()
259
+ )
257
260
  self.model_util = (
258
261
  ModelUtil(self.model, serializer_class=self.serializer_class)
259
262
  if not isinstance(self.model, ModelSerializerMeta)