django-ninja-aio-crud 0.10.2__py3-none-any.whl → 2.4.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.
@@ -0,0 +1,894 @@
1
+ import asyncio
2
+ import base64
3
+ from typing import Any
4
+
5
+ from ninja import Schema
6
+ from ninja.orm import fields
7
+ from ninja.errors import ConfigError
8
+
9
+ from django.db import models
10
+ from django.http import HttpRequest
11
+ from django.core.exceptions import ObjectDoesNotExist
12
+ from asgiref.sync import sync_to_async
13
+ from django.db.models.fields.related_descriptors import (
14
+ ReverseManyToOneDescriptor,
15
+ ReverseOneToOneDescriptor,
16
+ ManyToManyDescriptor,
17
+ ForwardManyToOneDescriptor,
18
+ ForwardOneToOneDescriptor,
19
+ )
20
+
21
+ from ninja_aio.exceptions import SerializeError, NotFoundError
22
+ from ninja_aio.models.serializers import ModelSerializer
23
+ from ninja_aio.types import ModelSerializerMeta
24
+ from ninja_aio.schemas.helpers import (
25
+ QuerySchema,
26
+ ObjectQuerySchema,
27
+ ObjectsQuerySchema,
28
+ )
29
+
30
+
31
+ async def agetattr(obj, name: str, default=None):
32
+ """
33
+ Async wrapper around getattr using sync_to_async.
34
+
35
+ Parameters
36
+ ----------
37
+ obj : Any
38
+ Object from which to retrieve the attribute.
39
+ name : str
40
+ Attribute name.
41
+ default : Any, optional
42
+ Default value if attribute is missing.
43
+
44
+ Returns
45
+ -------
46
+ Any
47
+ Attribute value (or default).
48
+ """
49
+ return await sync_to_async(getattr)(obj, name, default)
50
+
51
+
52
+ class ModelUtil:
53
+ """
54
+ ModelUtil
55
+ =========
56
+ Async utility bound to a Django model class (or a ModelSerializer subclass)
57
+ providing high‑level CRUD helpers plus (de)serialization glue for Django Ninja.
58
+
59
+ Overview
60
+ --------
61
+ Central responsibilities:
62
+ - Introspect model metadata (field list, pk name, verbose names).
63
+ - Normalize inbound payloads (custom / optional fields, FK resolution, base64 decoding).
64
+ - Normalize outbound payloads (resolve nested relation dicts into model instances).
65
+ - Prefetch reverse relations to mitigate N+1 issues.
66
+ - Invoke optional serializer hooks: custom_actions(), post_create(), queryset_request().
67
+
68
+ Compatible With
69
+ ---------------
70
+ - Plain Django models.
71
+ - Models using ModelSerializerMeta exposing:
72
+ get_fields(mode), is_custom(name), is_optional(name),
73
+ queryset_request(request), custom_actions(payload), post_create().
74
+
75
+ Key Methods
76
+ -----------
77
+ - get_object()
78
+ - parse_input_data()
79
+ - parse_output_data()
80
+ - create_s / read_s / update_s / delete_s
81
+
82
+ Error Handling
83
+ --------------
84
+ - Missing objects -> NotFoundError(...)
85
+ - Bad base64 -> SerializeError({...}, 400)
86
+
87
+ Performance Notes
88
+ -----------------
89
+ - Each FK resolution is an async DB hit; batch when necessary externally.
90
+
91
+ Design
92
+ ------
93
+ - Stateless wrapper; safe per-request instantiation.
94
+ """
95
+
96
+ def __init__(
97
+ self, model: type["ModelSerializer"] | models.Model, serializer_class=None
98
+ ):
99
+ """
100
+ Initialize with a Django model or ModelSerializer subclass.
101
+
102
+ Parameters
103
+ ----------
104
+ model : Model | ModelSerializerMeta
105
+ Target model class.
106
+ """
107
+ from ninja_aio.models.serializers import Serializer
108
+
109
+ self.model = model
110
+ self.serializer_class: Serializer = serializer_class
111
+ if serializer_class is not None and isinstance(model, ModelSerializerMeta):
112
+ raise ConfigError(
113
+ "ModelUtil cannot accept both model and serializer_class if the model is a ModelSerializer."
114
+ )
115
+
116
+ @property
117
+ def pk_field_type(self):
118
+ """
119
+ Python type corresponding to the model's primary key field.
120
+
121
+ Resolution
122
+ ----------
123
+ Uses the Django field's internal type and ninja.orm.fields.TYPES mapping.
124
+ If the internal type is unknown, instructs how to register a custom mapping.
125
+
126
+ Returns
127
+ -------
128
+ type
129
+ Native Python type for the PK suitable for schema generation.
130
+
131
+ Raises
132
+ ------
133
+ ConfigError
134
+ If the internal type is not registered in ninja.orm.fields.TYPES.
135
+ """
136
+ try:
137
+ internal_type = self.model._meta.pk.get_internal_type()
138
+ return fields.TYPES[internal_type]
139
+ except KeyError as e:
140
+ msg = [
141
+ f"Do not know how to convert django field '{internal_type}'.",
142
+ "Try: from ninja.orm import register_field",
143
+ "register_field('{internal_type}', <your-python-type>)",
144
+ ]
145
+ raise ConfigError("\n".join(msg)) from e
146
+
147
+ @property
148
+ def serializable_fields(self):
149
+ """
150
+ List of fields considered serializable for read operations.
151
+
152
+ Returns
153
+ -------
154
+ list[str]
155
+ Explicit read fields if ModelSerializerMeta, otherwise all model fields.
156
+ """
157
+ if isinstance(self.model, ModelSerializerMeta):
158
+ return self.model.get_fields("read")
159
+ return self.model_fields
160
+
161
+ @property
162
+ def model_fields(self):
163
+ """
164
+ Raw model field names (including forward relations).
165
+
166
+ Returns
167
+ -------
168
+ list[str]
169
+ """
170
+ return [field.name for field in self.model._meta.get_fields()]
171
+
172
+ @property
173
+ def model_name(self) -> str:
174
+ """
175
+ Django internal model name.
176
+
177
+ Returns
178
+ -------
179
+ str
180
+ """
181
+ return self.model._meta.model_name
182
+
183
+ @property
184
+ def model_pk_name(self) -> str:
185
+ """
186
+ Primary key attribute name (attname).
187
+
188
+ Returns
189
+ -------
190
+ str
191
+ """
192
+ return self.model._meta.pk.attname
193
+
194
+ @property
195
+ def model_verbose_name_plural(self) -> str:
196
+ """
197
+ Human readable plural verbose name.
198
+
199
+ Returns
200
+ -------
201
+ str
202
+ """
203
+ return self.model._meta.verbose_name_plural
204
+
205
+ def verbose_name_path_resolver(self) -> str:
206
+ """
207
+ Slugify plural verbose name for URL path usage.
208
+
209
+ Returns
210
+ -------
211
+ str
212
+ """
213
+ return "-".join(self.model_verbose_name_plural.split(" "))
214
+
215
+ def verbose_name_view_resolver(self) -> str:
216
+ """
217
+ Camel-case plural verbose name for view name usage.
218
+
219
+ Returns
220
+ -------
221
+ str
222
+ """
223
+ return self.model_verbose_name_plural.replace(" ", "")
224
+
225
+ async def _get_base_queryset(
226
+ self,
227
+ request: HttpRequest,
228
+ query_data: QuerySchema,
229
+ with_qs_request: bool,
230
+ is_for_read: bool,
231
+ ) -> models.QuerySet[type["ModelSerializer"] | models.Model]:
232
+ """
233
+ Build base queryset with optimizations and filters.
234
+
235
+ Parameters
236
+ ----------
237
+ request : HttpRequest
238
+ The HTTP request object.
239
+ query_data : QuerySchema
240
+ Query configuration with filters and optimizations.
241
+ with_qs_request : bool
242
+ Whether to apply queryset_request hook.
243
+ is_for_read : bool
244
+ Whether this is a read operation.
245
+
246
+ Returns
247
+ -------
248
+ models.QuerySet
249
+ Optimized and filtered queryset.
250
+ """
251
+ # Start with base queryset
252
+ obj_qs = (
253
+ self.model.objects.all()
254
+ if self.serializer_class is None
255
+ else await self.serializer_class.queryset_request(request)
256
+ )
257
+
258
+ # Apply query optimizations
259
+ obj_qs = self._apply_query_optimizations(obj_qs, query_data, is_for_read)
260
+
261
+ # Apply queryset_request hook if available
262
+ if isinstance(self.model, ModelSerializerMeta) and with_qs_request:
263
+ obj_qs = await self.model.queryset_request(request)
264
+
265
+ # Apply filters if present
266
+ if hasattr(query_data, "filters") and query_data.filters:
267
+ obj_qs = obj_qs.filter(**query_data.filters)
268
+
269
+ return obj_qs
270
+
271
+ async def get_objects(
272
+ self,
273
+ request: HttpRequest,
274
+ query_data: ObjectsQuerySchema = None,
275
+ with_qs_request=True,
276
+ is_for_read: bool = False,
277
+ ) -> models.QuerySet[type["ModelSerializer"] | models.Model]:
278
+ """
279
+ Retrieve a queryset with optimized database queries.
280
+
281
+ This method fetches a queryset applying query optimizations including
282
+ select_related and prefetch_related based on the model's relationships
283
+ and the query parameters.
284
+
285
+ Parameters
286
+ ----------
287
+ request : HttpRequest
288
+ The HTTP request object, used for queryset_request hooks.
289
+ query_data : ObjectsQuerySchema, optional
290
+ Schema containing filters and query optimization parameters.
291
+ Defaults to an empty ObjectsQuerySchema instance.
292
+ with_qs_request : bool, optional
293
+ Whether to apply the model's queryset_request hook if available.
294
+ Defaults to True.
295
+ is_for_read : bool, optional
296
+ Flag indicating if the query is for read operations, which may affect
297
+ query optimization strategies. Defaults to False.
298
+
299
+ Returns
300
+ -------
301
+ models.QuerySet[type["ModelSerializer"] | models.Model]
302
+ A QuerySet of model instances.
303
+
304
+ Notes
305
+ -----
306
+ - Query optimizations are automatically applied based on discovered relationships
307
+ - The queryset_request hook is called if the model implements ModelSerializerMeta
308
+ """
309
+ if query_data is None:
310
+ query_data = ObjectsQuerySchema()
311
+
312
+ return await self._get_base_queryset(
313
+ request, query_data, with_qs_request, is_for_read
314
+ )
315
+
316
+ async def get_object(
317
+ self,
318
+ request: HttpRequest,
319
+ pk: int | str = None,
320
+ query_data: ObjectQuerySchema = None,
321
+ with_qs_request=True,
322
+ is_for_read: bool = False,
323
+ ) -> type["ModelSerializer"] | models.Model:
324
+ """
325
+ Retrieve a single object with optimized database queries.
326
+
327
+ This method handles single-object retrieval with automatic query optimizations
328
+ including select_related and prefetch_related based on the model's relationships
329
+ and the query parameters.
330
+
331
+ Parameters
332
+ ----------
333
+ request : HttpRequest
334
+ The HTTP request object, used for queryset_request hooks.
335
+ pk : int | str, optional
336
+ Primary key value for single object lookup. Defaults to None.
337
+ query_data : ObjectQuerySchema, optional
338
+ Schema containing getters and query optimization parameters.
339
+ Defaults to an empty ObjectQuerySchema instance.
340
+ with_qs_request : bool, optional
341
+ Whether to apply the model's queryset_request hook if available.
342
+ Defaults to True.
343
+ is_for_read : bool, optional
344
+ Flag indicating if the query is for read operations, which may affect
345
+ query optimization strategies. Defaults to False.
346
+
347
+ Returns
348
+ -------
349
+ type["ModelSerializer"] | models.Model
350
+ A single model instance.
351
+
352
+ Raises
353
+ ------
354
+ ValueError
355
+ If neither pk nor getters are provided.
356
+ NotFoundError
357
+ If no matching object exists in the database.
358
+
359
+ Notes
360
+ -----
361
+ - Query optimizations are automatically applied based on discovered relationships
362
+ - The queryset_request hook is called if the model implements ModelSerializerMeta
363
+ """
364
+ if query_data is None:
365
+ query_data = ObjectQuerySchema()
366
+
367
+ if not query_data.getters and pk is None:
368
+ raise ValueError(
369
+ "Either pk or getters must be provided for single object retrieval."
370
+ )
371
+
372
+ # Build lookup query and get optimized queryset
373
+ get_q = self._build_lookup_query(pk, query_data.getters)
374
+ obj_qs = await self._get_base_queryset(
375
+ request, query_data, with_qs_request, is_for_read
376
+ )
377
+
378
+ # Perform lookup
379
+ try:
380
+ obj = await obj_qs.aget(**get_q)
381
+ except ObjectDoesNotExist:
382
+ raise NotFoundError(self.model)
383
+
384
+ return obj
385
+
386
+ def _build_lookup_query(self, pk: int | str = None, getters: dict = None) -> dict:
387
+ """
388
+ Build lookup query dict from pk and additional getters.
389
+
390
+ Parameters
391
+ ----------
392
+ pk : int | str, optional
393
+ Primary key value.
394
+ getters : dict, optional
395
+ Additional field lookups.
396
+
397
+ Returns
398
+ -------
399
+ dict
400
+ Combined lookup criteria.
401
+ """
402
+ get_q = {self.model_pk_name: pk} if pk is not None else {}
403
+ if getters:
404
+ get_q |= getters
405
+ return get_q
406
+
407
+ def _apply_query_optimizations(
408
+ self,
409
+ queryset: models.QuerySet,
410
+ query_data: QuerySchema,
411
+ is_for_read: bool,
412
+ ) -> models.QuerySet:
413
+ """
414
+ Apply select_related and prefetch_related optimizations to queryset.
415
+
416
+ Parameters
417
+ ----------
418
+ queryset : QuerySet
419
+ Base queryset to optimize.
420
+ query_data : ModelQuerySchema
421
+ Query configuration with select_related/prefetch_related lists.
422
+ is_for_read : bool
423
+ Whether to include model-level relation discovery.
424
+
425
+ Returns
426
+ -------
427
+ QuerySet
428
+ Optimized queryset.
429
+ """
430
+ select_related = (
431
+ query_data.select_related + self.get_select_relateds()
432
+ if is_for_read
433
+ else query_data.select_related
434
+ )
435
+ prefetch_related = (
436
+ query_data.prefetch_related + self.get_reverse_relations()
437
+ if is_for_read
438
+ else query_data.prefetch_related
439
+ )
440
+
441
+ if select_related:
442
+ queryset = queryset.select_related(*select_related)
443
+ if prefetch_related:
444
+ queryset = queryset.prefetch_related(*prefetch_related)
445
+
446
+ return queryset
447
+
448
+ def get_reverse_relations(self) -> list[str]:
449
+ """
450
+ Discover reverse relation names for safe prefetching.
451
+
452
+ Returns
453
+ -------
454
+ list[str]
455
+ Relation attribute names.
456
+ """
457
+ reverse_rels = []
458
+ for f in self.serializable_fields:
459
+ field_obj = getattr(self.model, f)
460
+ if isinstance(field_obj, ManyToManyDescriptor):
461
+ reverse_rels.append(f)
462
+ continue
463
+ if isinstance(field_obj, ReverseManyToOneDescriptor):
464
+ reverse_rels.append(field_obj.field._related_name)
465
+ continue
466
+ if isinstance(field_obj, ReverseOneToOneDescriptor):
467
+ reverse_rels.append(field_obj.related.name)
468
+ return reverse_rels
469
+
470
+ def get_select_relateds(self) -> list[str]:
471
+ """
472
+ Discover forward relation names for safe select_related.
473
+
474
+ Returns
475
+ -------
476
+ list[str]
477
+ Relation attribute names.
478
+ """
479
+ select_rels = []
480
+ for f in self.serializable_fields:
481
+ field_obj = getattr(self.model, f)
482
+ if isinstance(field_obj, ForwardManyToOneDescriptor):
483
+ select_rels.append(f)
484
+ continue
485
+ if isinstance(field_obj, ForwardOneToOneDescriptor):
486
+ select_rels.append(f)
487
+ return select_rels
488
+
489
+ async def _get_field(self, k: str):
490
+ return (await agetattr(self.model, k)).field
491
+
492
+ def _decode_binary(self, payload: dict, k: str, v: Any, field_obj: models.Field):
493
+ if not isinstance(field_obj, models.BinaryField):
494
+ return
495
+ try:
496
+ payload[k] = base64.b64decode(v)
497
+ except Exception as exc:
498
+ raise SerializeError({k: ". ".join(exc.args)}, 400)
499
+
500
+ async def _resolve_fk(
501
+ self,
502
+ request: HttpRequest,
503
+ payload: dict,
504
+ k: str,
505
+ v: Any,
506
+ field_obj: models.Field,
507
+ ):
508
+ if not isinstance(field_obj, models.ForeignKey):
509
+ return
510
+ rel_util = ModelUtil(field_obj.related_model)
511
+ rel = await rel_util.get_object(request, v, with_qs_request=False)
512
+ payload[k] = rel
513
+
514
+ async def _bump_object_from_schema(
515
+ self, obj: type["ModelSerializer"] | models.Model, schema: Schema
516
+ ):
517
+ return (await sync_to_async(schema.from_orm)(obj)).model_dump(mode="json")
518
+
519
+ def _validate_read_params(self, request: HttpRequest, query_data: QuerySchema):
520
+ """Validate required parameters for read operations."""
521
+ if request is None:
522
+ raise SerializeError(
523
+ {"request": "must be provided when object is not given"}, 400
524
+ )
525
+
526
+ if query_data is None:
527
+ raise SerializeError(
528
+ {"query_data": "must be provided when object is not given"}, 400
529
+ )
530
+
531
+ if (
532
+ hasattr(query_data, "filters")
533
+ and hasattr(query_data, "getters")
534
+ and query_data.filters
535
+ and query_data.getters
536
+ ):
537
+ raise SerializeError(
538
+ {"query_data": "cannot contain both filters and getters"}, 400
539
+ )
540
+
541
+ async def _handle_query_mode(
542
+ self,
543
+ request: HttpRequest,
544
+ query_data: QuerySchema,
545
+ schema: Schema,
546
+ is_for_read: bool,
547
+ ):
548
+ """Handle different query modes (filters vs getters)."""
549
+ if hasattr(query_data, "filters") and query_data.filters:
550
+ return await self._serialize_queryset(
551
+ request, query_data, schema, is_for_read
552
+ )
553
+
554
+ if hasattr(query_data, "getters") and query_data.getters:
555
+ return await self._serialize_single_object(
556
+ request, query_data, schema, is_for_read
557
+ )
558
+
559
+ raise SerializeError(
560
+ {"query_data": "must contain either filters or getters"}, 400
561
+ )
562
+
563
+ async def _serialize_queryset(
564
+ self,
565
+ request: HttpRequest,
566
+ query_data: QuerySchema,
567
+ schema: Schema,
568
+ is_for_read: bool,
569
+ ):
570
+ """Serialize a queryset of objects."""
571
+ objs = await self.get_objects(
572
+ request, query_data=query_data, is_for_read=is_for_read
573
+ )
574
+ return [await self._bump_object_from_schema(obj, schema) async for obj in objs]
575
+
576
+ async def _serialize_single_object(
577
+ self,
578
+ request: HttpRequest,
579
+ query_data: QuerySchema,
580
+ obj_schema: Schema,
581
+ is_for_read: bool,
582
+ ):
583
+ """Serialize a single object."""
584
+ obj = await self.get_object(
585
+ request, query_data=query_data, is_for_read=is_for_read
586
+ )
587
+ return await self._bump_object_from_schema(obj, obj_schema)
588
+
589
+ async def parse_input_data(self, request: HttpRequest, data: Schema):
590
+ """
591
+ Transform inbound schema data to a model-ready payload.
592
+
593
+ Steps
594
+ -----
595
+ - Strip custom fields (retain separately).
596
+ - Drop optional fields with None (ModelSerializer only).
597
+ - Decode BinaryField base64 values.
598
+ - Resolve ForeignKey ids to model instances.
599
+
600
+ Parameters
601
+ ----------
602
+ request : HttpRequest
603
+ data : Schema
604
+ Incoming validated schema instance.
605
+
606
+ Returns
607
+ -------
608
+ tuple[dict, dict]
609
+ (payload_without_customs, customs_dict)
610
+
611
+ Raises
612
+ ------
613
+ SerializeError
614
+ On base64 decoding failure.
615
+ """
616
+ payload = data.model_dump(mode="json")
617
+
618
+ is_serializer = isinstance(self.model, ModelSerializerMeta)
619
+
620
+ # Collect custom and optional fields (only if ModelSerializerMeta)
621
+ customs: dict[str, Any] = {}
622
+ optionals: list[str] = []
623
+ if is_serializer:
624
+ customs = {
625
+ k: v
626
+ for k, v in payload.items()
627
+ if self.model.is_custom(k) and k not in self.model_fields
628
+ }
629
+ optionals = [
630
+ k for k, v in payload.items() if self.model.is_optional(k) and v is None
631
+ ]
632
+
633
+ skip_keys = set()
634
+ if is_serializer:
635
+ # Keys to skip during model field processing
636
+ skip_keys = {
637
+ k
638
+ for k, v in payload.items()
639
+ if (self.model.is_custom(k) and k not in self.model_fields)
640
+ or (self.model.is_optional(k) and v is None)
641
+ }
642
+
643
+ # Process payload fields
644
+ for k, v in payload.items():
645
+ if k in skip_keys:
646
+ continue
647
+ field_obj = await self._get_field(k)
648
+ self._decode_binary(payload, k, v, field_obj)
649
+ await self._resolve_fk(request, payload, k, v, field_obj)
650
+
651
+ # Preserve original exclusion semantics (customs if present else optionals)
652
+ exclude_keys = customs.keys() or optionals
653
+ new_payload = {k: v for k, v in payload.items() if k not in exclude_keys}
654
+
655
+ return new_payload, customs
656
+
657
+ async def create_s(self, request: HttpRequest, data: Schema, obj_schema: Schema):
658
+ """
659
+ Create a new instance and return serialized output.
660
+
661
+ Applies custom_actions + post_create hooks if available.
662
+
663
+ Parameters
664
+ ----------
665
+ request : HttpRequest
666
+ data : Schema
667
+ Input schema instance.
668
+ obj_schema : Schema
669
+ Read schema class for output.
670
+
671
+ Returns
672
+ -------
673
+ dict
674
+ Serialized created object.
675
+ """
676
+ payload, customs = await self.parse_input_data(request, data)
677
+ pk = (await self.model.objects.acreate(**payload)).pk
678
+ obj = await self.get_object(request, pk)
679
+ if isinstance(self.model, ModelSerializerMeta):
680
+ await asyncio.gather(obj.custom_actions(customs), obj.post_create())
681
+ return await self.read_s(obj_schema, request, obj)
682
+
683
+ async def _read_s(
684
+ self,
685
+ schema: Schema,
686
+ request: HttpRequest = None,
687
+ instance: models.QuerySet[type["ModelSerializer"] | models.Model]
688
+ | type["ModelSerializer"]
689
+ | models.Model = None,
690
+ query_data: QuerySchema = None,
691
+ is_for_read: bool = False,
692
+ ):
693
+ """
694
+ Internal serialization method handling both single instances and querysets.
695
+
696
+ Parameters
697
+ ----------
698
+ schema : Schema
699
+ Read schema class for serialization.
700
+ request : HttpRequest, optional
701
+ HTTP request object, required when instance is None.
702
+ instance : QuerySet | Model, optional
703
+ Instance(s) to serialize. If None, fetches based on query_data.
704
+ query_data : QuerySchema, optional
705
+ Query parameters for fetching objects when instance is None.
706
+ is_for_read : bool, optional
707
+ Whether to apply read-specific query optimizations.
708
+
709
+ Returns
710
+ -------
711
+ dict | list[dict]
712
+ Serialized instance(s).
713
+
714
+ Raises
715
+ ------
716
+ SerializeError
717
+ If schema is None or validation fails.
718
+ """
719
+ if schema is None:
720
+ raise SerializeError({"schema": "must be provided"}, 400)
721
+
722
+ if instance is not None:
723
+ if isinstance(instance, models.QuerySet):
724
+ return [
725
+ await self._bump_object_from_schema(obj, schema)
726
+ async for obj in instance
727
+ ]
728
+ return await self._bump_object_from_schema(instance, schema)
729
+
730
+ self._validate_read_params(request, query_data)
731
+ return await self._handle_query_mode(request, query_data, schema, is_for_read)
732
+
733
+ async def read_s(
734
+ self,
735
+ schema: Schema,
736
+ request: HttpRequest = None,
737
+ instance: type["ModelSerializer"] = None,
738
+ query_data: ObjectQuerySchema = None,
739
+ is_for_read: bool = False,
740
+ ) -> dict:
741
+ """
742
+ Serialize a single model instance or fetch and serialize using query parameters.
743
+
744
+ This method handles single-object serialization. It can serialize a provided
745
+ instance directly or fetch and serialize a single object using query_data.getters.
746
+
747
+ Parameters
748
+ ----------
749
+ schema : Schema
750
+ Read schema class for serialization output.
751
+ request : HttpRequest, optional
752
+ HTTP request object, required when instance is None.
753
+ instance : ModelSerializer | Model, optional
754
+ Single instance to serialize. If None, fetched based on query_data.
755
+ query_data : ObjectQuerySchema, optional
756
+ Query parameters with getters for single object lookup.
757
+ Required when instance is None.
758
+ is_for_read : bool, optional
759
+ Whether to apply read-specific query optimizations. Defaults to False.
760
+
761
+ Returns
762
+ -------
763
+ dict
764
+ Serialized model instance as dictionary.
765
+
766
+ Raises
767
+ ------
768
+ SerializeError
769
+ - If schema is None
770
+ - If instance is None and request or query_data is None
771
+ - If query_data validation fails
772
+ NotFoundError
773
+ If using getters and no matching object is found.
774
+
775
+ Notes
776
+ -----
777
+ - Uses Pydantic's from_orm() with mode="json" for serialization
778
+ - When instance is provided, request and query_data are ignored
779
+ - Query optimizations applied when is_for_read=True
780
+ """
781
+ return await self._read_s(
782
+ schema,
783
+ request,
784
+ instance,
785
+ query_data,
786
+ is_for_read,
787
+ )
788
+
789
+ async def list_read_s(
790
+ self,
791
+ schema: Schema,
792
+ request: HttpRequest = None,
793
+ instances: models.QuerySet[type["ModelSerializer"] | models.Model] = None,
794
+ query_data: ObjectsQuerySchema = None,
795
+ is_for_read: bool = False,
796
+ ) -> list[dict]:
797
+ """
798
+ Serialize multiple model instances or fetch and serialize using query parameters.
799
+
800
+ This method handles queryset serialization. It can serialize provided instances
801
+ directly or fetch and serialize multiple objects using query_data.filters.
802
+
803
+ Parameters
804
+ ----------
805
+ schema : Schema
806
+ Read schema class for serialization output.
807
+ request : HttpRequest, optional
808
+ HTTP request object, required when instances is None.
809
+ instances : QuerySet, optional
810
+ Queryset of instances to serialize. If None, fetched based on query_data.
811
+ query_data : ObjectsQuerySchema, optional
812
+ Query parameters with filters for multiple object lookup.
813
+ Required when instances is None.
814
+ is_for_read : bool, optional
815
+ Whether to apply read-specific query optimizations. Defaults to False.
816
+
817
+ Returns
818
+ -------
819
+ list[dict]
820
+ List of serialized model instances as dictionaries.
821
+
822
+ Raises
823
+ ------
824
+ SerializeError
825
+ - If schema is None
826
+ - If instances is None and request or query_data is None
827
+ - If query_data validation fails
828
+
829
+ Notes
830
+ -----
831
+ - Uses Pydantic's from_orm() with mode="json" for serialization
832
+ - When instances is provided, request and query_data are ignored
833
+ - Query optimizations applied when is_for_read=True
834
+ - Processes queryset asynchronously for efficiency
835
+ """
836
+ return await self._read_s(
837
+ schema,
838
+ request,
839
+ instances,
840
+ query_data,
841
+ is_for_read,
842
+ )
843
+
844
+ async def update_s(
845
+ self, request: HttpRequest, data: Schema, pk: int | str, obj_schema: Schema
846
+ ):
847
+ """
848
+ Update an existing instance and return serialized output.
849
+
850
+ Only non-null fields are applied to the instance.
851
+
852
+ Parameters
853
+ ----------
854
+ request : HttpRequest
855
+ data : Schema
856
+ Input update schema instance.
857
+ pk : int | str
858
+ Primary key of target object.
859
+ obj_schema : Schema
860
+ Read schema class for output.
861
+
862
+ Returns
863
+ -------
864
+ dict
865
+ Serialized updated object.
866
+ """
867
+ obj = await self.get_object(request, pk)
868
+ payload, customs = await self.parse_input_data(request, data)
869
+ for k, v in payload.items():
870
+ if v is not None:
871
+ setattr(obj, k, v)
872
+ if isinstance(self.model, ModelSerializerMeta):
873
+ await obj.custom_actions(customs)
874
+ await obj.asave()
875
+ updated_object = await self.get_object(request, pk)
876
+ return await self.read_s(obj_schema, request, updated_object)
877
+
878
+ async def delete_s(self, request: HttpRequest, pk: int | str):
879
+ """
880
+ Delete an instance by primary key.
881
+
882
+ Parameters
883
+ ----------
884
+ request : HttpRequest
885
+ pk : int | str
886
+ Primary key.
887
+
888
+ Returns
889
+ -------
890
+ None
891
+ """
892
+ obj = await self.get_object(request, pk)
893
+ await obj.adelete()
894
+ return None