django-ninja-aio-crud 1.0.4__py3-none-any.whl → 2.0.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.
ninja_aio/models.py CHANGED
@@ -1,9 +1,10 @@
1
1
  import asyncio
2
2
  import base64
3
- from typing import Any
3
+ from typing import Any, ClassVar
4
4
 
5
5
  from ninja import Schema
6
- from ninja.orm import create_schema
6
+ from ninja.orm import create_schema, fields
7
+ from ninja.errors import ConfigError
7
8
 
8
9
  from django.db import models
9
10
  from django.http import HttpRequest
@@ -17,8 +18,16 @@ from django.db.models.fields.related_descriptors import (
17
18
  ForwardOneToOneDescriptor,
18
19
  )
19
20
 
20
- from .exceptions import SerializeError, NotFoundError
21
- from .types import S_TYPES, F_TYPES, SCHEMA_TYPES, ModelSerializerMeta
21
+ from ninja_aio.exceptions import SerializeError, NotFoundError
22
+ from ninja_aio.types import S_TYPES, F_TYPES, SCHEMA_TYPES, ModelSerializerMeta
23
+ from ninja_aio.schemas.helpers import (
24
+ ModelQuerySetSchema,
25
+ ModelQuerySetExtraSchema,
26
+ QuerySchema,
27
+ ObjectQuerySchema,
28
+ ObjectsQuerySchema,
29
+ )
30
+ from ninja_aio.helpers.query import QueryUtil
22
31
 
23
32
 
24
33
  async def agetattr(obj, name: str, default=None):
@@ -97,6 +106,37 @@ class ModelUtil:
97
106
  """
98
107
  self.model = model
99
108
 
109
+ @property
110
+ def pk_field_type(self):
111
+ """
112
+ Python type corresponding to the model's primary key field.
113
+
114
+ Resolution
115
+ ----------
116
+ Uses the Django field's internal type and ninja.orm.fields.TYPES mapping.
117
+ If the internal type is unknown, instructs how to register a custom mapping.
118
+
119
+ Returns
120
+ -------
121
+ type
122
+ Native Python type for the PK suitable for schema generation.
123
+
124
+ Raises
125
+ ------
126
+ ConfigError
127
+ If the internal type is not registered in ninja.orm.fields.TYPES.
128
+ """
129
+ try:
130
+ internal_type = self.model._meta.pk.get_internal_type()
131
+ return fields.TYPES[internal_type]
132
+ except KeyError as e:
133
+ msg = [
134
+ f"Do not know how to convert django field '{internal_type}'.",
135
+ "Try: from ninja.orm import register_field",
136
+ "register_field('{internal_type}', <your-python-type>)",
137
+ ]
138
+ raise ConfigError("\n".join(msg)) from e
139
+
100
140
  @property
101
141
  def serializable_fields(self):
102
142
  """
@@ -175,61 +215,156 @@ class ModelUtil:
175
215
  """
176
216
  return self.model_verbose_name_plural.replace(" ", "")
177
217
 
218
+ async def _get_base_queryset(
219
+ self,
220
+ request: HttpRequest,
221
+ query_data: QuerySchema,
222
+ with_qs_request: bool,
223
+ is_for_read: bool,
224
+ ) -> models.QuerySet[type["ModelSerializer"] | models.Model]:
225
+ """
226
+ Build base queryset with optimizations and filters.
227
+
228
+ Parameters
229
+ ----------
230
+ request : HttpRequest
231
+ The HTTP request object.
232
+ query_data : QuerySchema
233
+ Query configuration with filters and optimizations.
234
+ with_qs_request : bool
235
+ Whether to apply queryset_request hook.
236
+ is_for_read : bool
237
+ Whether this is a read operation.
238
+
239
+ Returns
240
+ -------
241
+ models.QuerySet
242
+ Optimized and filtered queryset.
243
+ """
244
+ # Start with base queryset
245
+ obj_qs = self.model.objects.all()
246
+
247
+ # Apply query optimizations
248
+ obj_qs = self._apply_query_optimizations(obj_qs, query_data, is_for_read)
249
+
250
+ # Apply queryset_request hook if available
251
+ if isinstance(self.model, ModelSerializerMeta) and with_qs_request:
252
+ obj_qs = await self.model.queryset_request(request)
253
+
254
+ # Apply filters if present
255
+ if hasattr(query_data, "filters") and query_data.filters:
256
+ obj_qs = obj_qs.filter(**query_data.filters)
257
+
258
+ return obj_qs
259
+
260
+ async def get_objects(
261
+ self,
262
+ request: HttpRequest,
263
+ query_data: ObjectsQuerySchema = None,
264
+ with_qs_request=True,
265
+ is_for_read: bool = False,
266
+ ) -> models.QuerySet[type["ModelSerializer"] | models.Model]:
267
+ """
268
+ Retrieve a queryset with optimized database queries.
269
+
270
+ This method fetches a queryset applying query optimizations including
271
+ select_related and prefetch_related based on the model's relationships
272
+ and the query parameters.
273
+
274
+ Parameters
275
+ ----------
276
+ request : HttpRequest
277
+ The HTTP request object, used for queryset_request hooks.
278
+ query_data : ObjectsQuerySchema, optional
279
+ Schema containing filters and query optimization parameters.
280
+ Defaults to an empty ObjectsQuerySchema instance.
281
+ with_qs_request : bool, optional
282
+ Whether to apply the model's queryset_request hook if available.
283
+ Defaults to True.
284
+ is_for_read : bool, optional
285
+ Flag indicating if the query is for read operations, which may affect
286
+ query optimization strategies. Defaults to False.
287
+
288
+ Returns
289
+ -------
290
+ models.QuerySet[type["ModelSerializer"] | models.Model]
291
+ A QuerySet of model instances.
292
+
293
+ Notes
294
+ -----
295
+ - Query optimizations are automatically applied based on discovered relationships
296
+ - The queryset_request hook is called if the model implements ModelSerializerMeta
297
+ """
298
+ if query_data is None:
299
+ query_data = ObjectsQuerySchema()
300
+
301
+ return await self._get_base_queryset(
302
+ request, query_data, with_qs_request, is_for_read
303
+ )
304
+
178
305
  async def get_object(
179
306
  self,
180
307
  request: HttpRequest,
181
308
  pk: int | str = None,
182
- filters: dict = None,
183
- getters: dict = None,
309
+ query_data: ObjectQuerySchema = None,
184
310
  with_qs_request=True,
185
- ) -> (
186
- type["ModelSerializer"]
187
- | models.Model
188
- | models.QuerySet[type["ModelSerializer"] | models.Model]
189
- ):
311
+ is_for_read: bool = False,
312
+ ) -> type["ModelSerializer"] | models.Model:
190
313
  """
191
- Retrieve a single instance (by pk/getters) or a queryset if no lookup criteria.
314
+ Retrieve a single object with optimized database queries.
192
315
 
193
- Applies queryset_request (if ModelSerializerMeta), select_related, and
194
- prefetch_related on discovered reverse relations.
316
+ This method handles single-object retrieval with automatic query optimizations
317
+ including select_related and prefetch_related based on the model's relationships
318
+ and the query parameters.
195
319
 
196
320
  Parameters
197
321
  ----------
198
322
  request : HttpRequest
323
+ The HTTP request object, used for queryset_request hooks.
199
324
  pk : int | str, optional
200
- Primary key lookup.
201
- filters : dict, optional
202
- Additional filter kwargs.
203
- getters : dict, optional
204
- Field lookups combined with pk lookup.
205
- with_qs_request : bool
206
- Whether to apply model-level queryset_request hook.
325
+ Primary key value for single object lookup. Defaults to None.
326
+ query_data : ObjectQuerySchema, optional
327
+ Schema containing getters and query optimization parameters.
328
+ Defaults to an empty ObjectQuerySchema instance.
329
+ with_qs_request : bool, optional
330
+ Whether to apply the model's queryset_request hook if available.
331
+ Defaults to True.
332
+ is_for_read : bool, optional
333
+ Flag indicating if the query is for read operations, which may affect
334
+ query optimization strategies. Defaults to False.
207
335
 
208
336
  Returns
209
337
  -------
210
- Model | QuerySet
211
- Instance if lookup provided; otherwise queryset.
338
+ type["ModelSerializer"] | models.Model
339
+ A single model instance.
212
340
 
213
341
  Raises
214
342
  ------
343
+ ValueError
344
+ If neither pk nor getters are provided.
215
345
  NotFoundError
216
- If instance not found by lookup criteria.
217
- """
218
- get_q = {self.model_pk_name: pk} if pk is not None else {}
219
- if getters:
220
- get_q |= getters
346
+ If no matching object exists in the database.
221
347
 
222
- obj_qs = self.model.objects.select_related()
223
- if isinstance(self.model, ModelSerializerMeta) and with_qs_request:
224
- obj_qs = await self.model.queryset_request(request)
348
+ Notes
349
+ -----
350
+ - Query optimizations are automatically applied based on discovered relationships
351
+ - The queryset_request hook is called if the model implements ModelSerializerMeta
352
+ """
353
+ if query_data is None:
354
+ query_data = ObjectQuerySchema()
225
355
 
226
- obj_qs = obj_qs.prefetch_related(*self.get_reverse_relations())
227
- if filters:
228
- obj_qs = obj_qs.filter(**filters)
356
+ if not query_data.getters and pk is None:
357
+ raise ValueError(
358
+ "Either pk or getters must be provided for single object retrieval."
359
+ )
229
360
 
230
- if not get_q:
231
- return obj_qs
361
+ # Build lookup query and get optimized queryset
362
+ get_q = self._build_lookup_query(pk, query_data.getters)
363
+ obj_qs = await self._get_base_queryset(
364
+ request, query_data, with_qs_request, is_for_read
365
+ )
232
366
 
367
+ # Perform lookup
233
368
  try:
234
369
  obj = await obj_qs.aget(**get_q)
235
370
  except ObjectDoesNotExist:
@@ -237,6 +372,68 @@ class ModelUtil:
237
372
 
238
373
  return obj
239
374
 
375
+ def _build_lookup_query(self, pk: int | str = None, getters: dict = None) -> dict:
376
+ """
377
+ Build lookup query dict from pk and additional getters.
378
+
379
+ Parameters
380
+ ----------
381
+ pk : int | str, optional
382
+ Primary key value.
383
+ getters : dict, optional
384
+ Additional field lookups.
385
+
386
+ Returns
387
+ -------
388
+ dict
389
+ Combined lookup criteria.
390
+ """
391
+ get_q = {self.model_pk_name: pk} if pk is not None else {}
392
+ if getters:
393
+ get_q |= getters
394
+ return get_q
395
+
396
+ def _apply_query_optimizations(
397
+ self,
398
+ queryset: models.QuerySet,
399
+ query_data: QuerySchema,
400
+ is_for_read: bool,
401
+ ) -> models.QuerySet:
402
+ """
403
+ Apply select_related and prefetch_related optimizations to queryset.
404
+
405
+ Parameters
406
+ ----------
407
+ queryset : QuerySet
408
+ Base queryset to optimize.
409
+ query_data : ModelQuerySchema
410
+ Query configuration with select_related/prefetch_related lists.
411
+ is_for_read : bool
412
+ Whether to include model-level relation discovery.
413
+
414
+ Returns
415
+ -------
416
+ QuerySet
417
+ Optimized queryset.
418
+ """
419
+ select_related = (
420
+ query_data.select_related + self.get_select_relateds()
421
+ if is_for_read
422
+ else query_data.select_related
423
+ )
424
+ prefetch_related = (
425
+ query_data.prefetch_related + self.get_reverse_relations()
426
+ if is_for_read
427
+ else query_data.prefetch_related
428
+ )
429
+
430
+ if select_related:
431
+ queryset = queryset.select_related(*select_related)
432
+ if prefetch_related:
433
+ queryset = queryset.prefetch_related(*prefetch_related)
434
+
435
+ return queryset
436
+
240
437
  def get_reverse_relations(self) -> list[str]:
241
438
  """
242
439
  Discover reverse relation names for safe prefetching.
@@ -259,6 +456,25 @@ class ModelUtil:
259
456
  reverse_rels.append(field_obj.related.name)
260
457
  return reverse_rels
261
458
 
459
+ def get_select_relateds(self) -> list[str]:
460
+ """
461
+ Discover forward relation names for safe select_related.
462
+
463
+ Returns
464
+ -------
465
+ list[str]
466
+ Relation attribute names.
467
+ """
468
+ select_rels = []
469
+ for f in self.serializable_fields:
470
+ field_obj = getattr(self.model, f)
471
+ if isinstance(field_obj, ForwardManyToOneDescriptor):
472
+ select_rels.append(f)
473
+ continue
474
+ if isinstance(field_obj, ForwardOneToOneDescriptor):
475
+ select_rels.append(f)
476
+ return select_rels
477
+
262
478
  async def _get_field(self, k: str):
263
479
  return (await agetattr(self.model, k)).field
264
480
 
@@ -284,49 +500,80 @@ class ModelUtil:
284
500
  rel = await rel_util.get_object(request, v, with_qs_request=False)
285
501
  payload[k] = rel
286
502
 
287
- async def _extract_field_obj(self, field_name: str):
288
- """
289
- Return the underlying Django Field (if any) for a given attribute name.
290
- """
291
- descriptor = await agetattr(self.model, field_name, None)
292
- if descriptor is None:
293
- return None
294
- return await agetattr(descriptor, "field", None) or await agetattr(
295
- descriptor, "related", None
296
- )
503
+ async def _bump_object_from_schema(
504
+ self, obj: type["ModelSerializer"] | models.Model, schema: Schema
505
+ ):
506
+ return (await sync_to_async(schema.from_orm)(obj)).model_dump(mode="json")
297
507
 
298
- def _should_process_nested(self, value: Any, field_obj: Any) -> bool:
299
- """
300
- Determine if a payload entry represents a nested FK / O2O relation dict.
301
- """
302
- if not isinstance(value, dict):
303
- return False
304
- return isinstance(field_obj, (models.ForeignKey, models.OneToOneField))
508
+ def _validate_read_params(self, request: HttpRequest, query_data: QuerySchema):
509
+ """Validate required parameters for read operations."""
510
+ if request is None:
511
+ raise SerializeError(
512
+ {"request": "must be provided when object is not given"}, 400
513
+ )
514
+
515
+ if query_data is None:
516
+ raise SerializeError(
517
+ {"query_data": "must be provided when object is not given"}, 400
518
+ )
305
519
 
306
- async def _fetch_related_instance(
307
- self, request, field_obj: models.Field, nested_dict: dict
520
+ if (
521
+ hasattr(query_data, "filters")
522
+ and hasattr(query_data, "getters")
523
+ and query_data.filters
524
+ and query_data.getters
525
+ ):
526
+ raise SerializeError(
527
+ {"query_data": "cannot contain both filters and getters"}, 400
528
+ )
529
+
530
+ async def _handle_query_mode(
531
+ self,
532
+ request: HttpRequest,
533
+ query_data: QuerySchema,
534
+ schema: Schema,
535
+ is_for_read: bool,
308
536
  ):
309
- """
310
- Resolve the related instance from its primary key inside the nested dict.
311
- """
312
- rel_util = ModelUtil(field_obj.related_model)
313
- rel_pk = nested_dict.get(rel_util.model_pk_name)
314
- return await rel_util.get_object(request, rel_pk)
537
+ """Handle different query modes (filters vs getters)."""
538
+ if hasattr(query_data, "filters") and query_data.filters:
539
+ return await self._serialize_queryset(
540
+ request, query_data, schema, is_for_read
541
+ )
315
542
 
316
- async def _rewrite_nested_foreign_keys(self, rel_obj, nested_dict: dict):
317
- """
318
- Rewrite foreign key keys inside a nested dict from <key> to <key>_id.
319
- """
320
- keys_to_rewrite: list[str] = []
321
- new_nested = nested_dict
322
- for rel_k in nested_dict.keys():
323
- attr = await agetattr(rel_obj, rel_k)
324
- if isinstance(attr, models.ForeignKey):
325
- keys_to_rewrite.append(rel_k)
326
- for old_k in keys_to_rewrite:
327
- new_nested[f"{old_k}_id"] = new_nested.pop(old_k)
328
- return new_nested
543
+ if hasattr(query_data, "getters") and query_data.getters:
544
+ return await self._serialize_single_object(
545
+ request, query_data, schema, is_for_read
546
+ )
329
547
 
548
+ raise SerializeError(
549
+ {"query_data": "must contain either filters or getters"}, 400
550
+ )
551
+
552
+ async def _serialize_queryset(
553
+ self,
554
+ request: HttpRequest,
555
+ query_data: QuerySchema,
556
+ schema: Schema,
557
+ is_for_read: bool,
558
+ ):
559
+ """Serialize a queryset of objects."""
560
+ objs = await self.get_objects(
561
+ request, query_data=query_data, is_for_read=is_for_read
562
+ )
563
+ return [await self._bump_object_from_schema(obj, schema) async for obj in objs]
564
+
565
+ async def _serialize_single_object(
566
+ self,
567
+ request: HttpRequest,
568
+ query_data: QuerySchema,
569
+ obj_schema: Schema,
570
+ is_for_read: bool,
571
+ ):
572
+ """Serialize a single object."""
573
+ obj = await self.get_object(
574
+ request, query_data=query_data, is_for_read=is_for_read
575
+ )
576
+ return await self._bump_object_from_schema(obj, obj_schema)
330
577
 
331
578
  async def parse_input_data(self, request: HttpRequest, data: Schema):
332
579
  """
@@ -396,37 +643,6 @@ class ModelUtil:
396
643
 
397
644
  return new_payload, customs
398
645
 
399
- async def parse_output_data(self, request: HttpRequest, data: Schema):
400
- """
401
- Post-process serialized output.
402
-
403
- For nested FK / OneToOne dicts:
404
- - Replace dict with authoritative related instance.
405
- - Rewrite nested FK keys to <name>_id for nested foreign keys.
406
-
407
- Parameters
408
- ----------
409
- request : HttpRequest
410
- data : Schema
411
- Schema (from_orm) instance.
412
-
413
- Returns
414
- -------
415
- dict
416
- Normalized output payload.
417
- """
418
- payload = data.model_dump(mode="json")
419
-
420
- for k, v in payload.items():
421
- field_obj = await self._extract_field_obj(k)
422
- if not self._should_process_nested(v, field_obj):
423
- continue
424
- rel_instance = await self._fetch_related_instance(request, field_obj, v)
425
- if isinstance(field_obj, models.ForeignKey):
426
- v = await self._rewrite_nested_foreign_keys(rel_instance, v)
427
- payload[k] = rel_instance
428
- return payload
429
-
430
646
  async def create_s(self, request: HttpRequest, data: Schema, obj_schema: Schema):
431
647
  """
432
648
  Create a new instance and return serialized output.
@@ -451,39 +667,167 @@ class ModelUtil:
451
667
  obj = await self.get_object(request, pk)
452
668
  if isinstance(self.model, ModelSerializerMeta):
453
669
  await asyncio.gather(obj.custom_actions(customs), obj.post_create())
454
- return await self.read_s(request, obj, obj_schema)
670
+ return await self.read_s(obj_schema, request, obj)
455
671
 
456
- async def read_s(
672
+ async def _read_s(
457
673
  self,
458
- request: HttpRequest,
459
- obj: type["ModelSerializer"],
460
- obj_schema: Schema,
674
+ schema: Schema,
675
+ request: HttpRequest = None,
676
+ instance: models.QuerySet[type["ModelSerializer"] | models.Model]
677
+ | type["ModelSerializer"]
678
+ | models.Model = None,
679
+ query_data: QuerySchema = None,
680
+ is_for_read: bool = False,
461
681
  ):
462
682
  """
463
- Serialize an existing instance with the provided read schema.
683
+ Internal serialization method handling both single instances and querysets.
464
684
 
465
685
  Parameters
466
686
  ----------
467
- request : HttpRequest
468
- obj : Model
469
- Target instance.
470
- obj_schema : Schema
471
- Read schema class.
687
+ schema : Schema
688
+ Read schema class for serialization.
689
+ request : HttpRequest, optional
690
+ HTTP request object, required when instance is None.
691
+ instance : QuerySet | Model, optional
692
+ Instance(s) to serialize. If None, fetches based on query_data.
693
+ query_data : QuerySchema, optional
694
+ Query parameters for fetching objects when instance is None.
695
+ is_for_read : bool, optional
696
+ Whether to apply read-specific query optimizations.
697
+
698
+ Returns
699
+ -------
700
+ dict | list[dict]
701
+ Serialized instance(s).
702
+
703
+ Raises
704
+ ------
705
+ SerializeError
706
+ If schema is None or validation fails.
707
+ """
708
+ if schema is None:
709
+ raise SerializeError({"schema": "must be provided"}, 400)
710
+
711
+ if instance is not None:
712
+ if isinstance(instance, models.QuerySet):
713
+ return [
714
+ await self._bump_object_from_schema(obj, schema)
715
+ async for obj in instance
716
+ ]
717
+ return await self._bump_object_from_schema(instance, schema)
718
+
719
+ self._validate_read_params(request, query_data)
720
+ return await self._handle_query_mode(request, query_data, schema, is_for_read)
721
+
722
+ async def read_s(
723
+ self,
724
+ schema: Schema,
725
+ request: HttpRequest = None,
726
+ instance: type["ModelSerializer"] = None,
727
+ query_data: ObjectQuerySchema = None,
728
+ is_for_read: bool = False,
729
+ ) -> dict:
730
+ """
731
+ Serialize a single model instance or fetch and serialize using query parameters.
732
+
733
+ This method handles single-object serialization. It can serialize a provided
734
+ instance directly or fetch and serialize a single object using query_data.getters.
735
+
736
+ Parameters
737
+ ----------
738
+ schema : Schema
739
+ Read schema class for serialization output.
740
+ request : HttpRequest, optional
741
+ HTTP request object, required when instance is None.
742
+ instance : ModelSerializer | Model, optional
743
+ Single instance to serialize. If None, fetched based on query_data.
744
+ query_data : ObjectQuerySchema, optional
745
+ Query parameters with getters for single object lookup.
746
+ Required when instance is None.
747
+ is_for_read : bool, optional
748
+ Whether to apply read-specific query optimizations. Defaults to False.
472
749
 
473
750
  Returns
474
751
  -------
475
752
  dict
476
- Serialized payload.
753
+ Serialized model instance as dictionary.
477
754
 
478
755
  Raises
479
756
  ------
480
757
  SerializeError
481
- If obj_schema not provided.
758
+ - If schema is None
759
+ - If instance is None and request or query_data is None
760
+ - If query_data validation fails
761
+ NotFoundError
762
+ If using getters and no matching object is found.
763
+
764
+ Notes
765
+ -----
766
+ - Uses Pydantic's from_orm() with mode="json" for serialization
767
+ - When instance is provided, request and query_data are ignored
768
+ - Query optimizations applied when is_for_read=True
769
+ """
770
+ return await self._read_s(
771
+ schema,
772
+ request,
773
+ instance,
774
+ query_data,
775
+ is_for_read,
776
+ )
777
+
778
+ async def list_read_s(
779
+ self,
780
+ schema: Schema,
781
+ request: HttpRequest = None,
782
+ instances: models.QuerySet[type["ModelSerializer"] | models.Model] = None,
783
+ query_data: ObjectsQuerySchema = None,
784
+ is_for_read: bool = False,
785
+ ) -> list[dict]:
482
786
  """
483
- if obj_schema is None:
484
- raise SerializeError({"obj_schema": "must be provided"}, 400)
485
- return await self.parse_output_data(
486
- request, await sync_to_async(obj_schema.from_orm)(obj)
787
+ Serialize multiple model instances or fetch and serialize using query parameters.
788
+
789
+ This method handles queryset serialization. It can serialize provided instances
790
+ directly or fetch and serialize multiple objects using query_data.filters.
791
+
792
+ Parameters
793
+ ----------
794
+ schema : Schema
795
+ Read schema class for serialization output.
796
+ request : HttpRequest, optional
797
+ HTTP request object, required when instances is None.
798
+ instances : QuerySet, optional
799
+ Queryset of instances to serialize. If None, fetched based on query_data.
800
+ query_data : ObjectsQuerySchema, optional
801
+ Query parameters with filters for multiple object lookup.
802
+ Required when instances is None.
803
+ is_for_read : bool, optional
804
+ Whether to apply read-specific query optimizations. Defaults to False.
805
+
806
+ Returns
807
+ -------
808
+ list[dict]
809
+ List of serialized model instances as dictionaries.
810
+
811
+ Raises
812
+ ------
813
+ SerializeError
814
+ - If schema is None
815
+ - If instances is None and request or query_data is None
816
+ - If query_data validation fails
817
+
818
+ Notes
819
+ -----
820
+ - Uses Pydantic's from_orm() with mode="json" for serialization
821
+ - When instances is provided, request and query_data are ignored
822
+ - Query optimizations applied when is_for_read=True
823
+ - Processes queryset asynchronously for efficiency
824
+ """
825
+ return await self._read_s(
826
+ schema,
827
+ request,
828
+ instances,
829
+ query_data,
830
+ is_for_read,
487
831
  )
488
832
 
489
833
  async def update_s(
@@ -518,7 +862,7 @@ class ModelUtil:
518
862
  await obj.custom_actions(customs)
519
863
  await obj.asave()
520
864
  updated_object = await self.get_object(request, pk)
521
- return await self.read_s(request, updated_object, obj_schema)
865
+ return await self.read_s(obj_schema, request, updated_object)
522
866
 
523
867
  async def delete_s(self, request: HttpRequest, pk: int | str):
524
868
  """
@@ -555,9 +899,39 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
555
899
  See inline docstrings for per-method behavior.
556
900
  """
557
901
 
902
+ util: ClassVar[ModelUtil]
903
+ query_util: ClassVar[QueryUtil]
904
+
558
905
  class Meta:
559
906
  abstract = True
560
907
 
908
+ def __init_subclass__(cls, **kwargs):
909
+ super().__init_subclass__(**kwargs)
910
+ # Bind a ModelUtil instance to the subclass for convenient access
911
+ cls.util = ModelUtil(cls)
912
+ cls.query_util = QueryUtil(cls)
913
+
914
+ class QuerySet:
915
+ """
916
+ Configuration container describing how to build query schemas for a model.
917
+ Purpose
918
+ -------
919
+ Describes which fields and extras are available when querying for model
920
+ instances. A factory/metaclass can read this configuration to generate
921
+ Pydantic / Ninja query schemas.
922
+ Attributes
923
+ ----------
924
+ read : ModelQuerySetSchema
925
+ Schema configuration for read operations.
926
+ queryset_request : ModelQuerySetSchema
927
+ Schema configuration for queryset_request hook.
928
+ extras : list[ModelQuerySetExtraSchema]
929
+ Additional computed / synthetic query parameters.
930
+ """
931
+ read = ModelQuerySetSchema()
932
+ queryset_request = ModelQuerySetSchema()
933
+ extras: list[ModelQuerySetExtraSchema] = []
934
+
561
935
  class CreateSerializer:
562
936
  """Configuration container describing how to build a create (input) schema for a model.
563
937
 
@@ -621,48 +995,6 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
621
995
  optionals: list[tuple[str, type]] = []
622
996
  excludes: list[str] = []
623
997
 
624
- @property
625
- def has_custom_fields_create(self):
626
- """
627
- Whether CreateSerializer declares custom fields.
628
- """
629
- return hasattr(self.CreateSerializer, "customs")
630
-
631
- @property
632
- def has_custom_fields_update(self):
633
- """
634
- Whether UpdateSerializer declares custom fields.
635
- """
636
- return hasattr(self.UpdateSerializer, "customs")
637
-
638
- @property
639
- def has_custom_fields(self):
640
- """
641
- Whether any serializer declares custom fields.
642
- """
643
- return self.has_custom_fields_create or self.has_custom_fields_update
644
-
645
- @property
646
- def has_optional_fields_create(self):
647
- """
648
- Whether CreateSerializer declares optional fields.
649
- """
650
- return hasattr(self.CreateSerializer, "optionals")
651
-
652
- @property
653
- def has_optional_fields_update(self):
654
- """
655
- Whether UpdateSerializer declares optional fields.
656
- """
657
- return hasattr(self.UpdateSerializer, "optionals")
658
-
659
- @property
660
- def has_optional_fields(self):
661
- """
662
- Whether any serializer declares optional fields.
663
- """
664
- return self.has_optional_fields_create or self.has_optional_fields_update
665
-
666
998
  @classmethod
667
999
  def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
668
1000
  """
@@ -823,7 +1155,10 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
823
1155
  -------
824
1156
  QuerySet
825
1157
  """
826
- return cls.objects.select_related().all()
1158
+ return cls.query_util.apply_queryset_optimizations(
1159
+ queryset=cls.objects.all(),
1160
+ scope=cls.query_util.SCOPES.QUERYSET_REQUEST,
1161
+ )
827
1162
 
828
1163
  async def post_create(self) -> None:
829
1164
  """
@@ -1179,11 +1514,12 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
1179
1514
  """
1180
1515
  Override save lifecycle to inject create/update hooks.
1181
1516
  """
1182
- if self._state.adding:
1517
+ state_adding = self._state.adding
1518
+ if state_adding:
1183
1519
  self.on_create_before_save()
1184
1520
  self.before_save()
1185
1521
  super().save(*args, **kwargs)
1186
- if self._state.adding:
1522
+ if state_adding:
1187
1523
  self.on_create_after_save()
1188
1524
  self.after_save()
1189
1525