django-ninja-aio-crud 2.3.2__py3-none-any.whl → 2.5.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,888 @@
1
+ from typing import Any, List, Optional
2
+ import warnings
3
+
4
+ from django.conf import settings
5
+ from ninja import Schema
6
+ from ninja.orm import create_schema
7
+ from django.db import models
8
+ from django.http import HttpRequest
9
+ from django.db.models.fields.related_descriptors import (
10
+ ReverseManyToOneDescriptor,
11
+ ReverseOneToOneDescriptor,
12
+ ManyToManyDescriptor,
13
+ ForwardManyToOneDescriptor,
14
+ ForwardOneToOneDescriptor,
15
+ )
16
+
17
+ from ninja_aio.types import S_TYPES, F_TYPES, SCHEMA_TYPES, ModelSerializerMeta
18
+ from ninja_aio.schemas.helpers import (
19
+ ModelQuerySetSchema,
20
+ ModelQuerySetExtraSchema,
21
+ )
22
+
23
+
24
+ class BaseSerializer:
25
+ """
26
+ BaseSerializer
27
+ --------------
28
+ Shared serializer utilities used by both ModelSerializer (model-bound) and
29
+ Serializer (Meta-driven). Centralizes common field normalization, relation
30
+ schema construction and schema generation helpers.
31
+
32
+ Subclasses must implement:
33
+ - _get_fields(s_type, f_type): source raw config for fields/optionals/customs/excludes
34
+ - _get_model(): return the Django model class associated with the serializer
35
+ - _get_relations_serializers(): optional mapping of relation field -> serializer (may be empty)
36
+ """
37
+
38
+ class QuerySet:
39
+ """
40
+ Configuration container describing how to build query schemas for a model.
41
+ Purpose
42
+ -------
43
+ Describes which fields and extras are available when querying for model
44
+ instances. A factory/metaclass can read this configuration to generate
45
+ Pydantic / Ninja query schemas.
46
+ Attributes
47
+ ----------
48
+ read : ModelQuerySetSchema
49
+ Schema configuration for read operations.
50
+ queryset_request : ModelQuerySetSchema
51
+ Schema configuration for queryset_request hook.
52
+ extras : list[ModelQuerySetExtraSchema]
53
+ Additional computed / synthetic query parameters.
54
+ """
55
+
56
+ read = ModelQuerySetSchema()
57
+ queryset_request = ModelQuerySetSchema()
58
+ extras: list[ModelQuerySetExtraSchema] = []
59
+
60
+ @classmethod
61
+ def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
62
+ # Subclasses provide implementation.
63
+ raise NotImplementedError
64
+
65
+ @classmethod
66
+ def _get_model(cls) -> models.Model:
67
+ # Subclasses provide implementation.
68
+ raise NotImplementedError
69
+
70
+ @classmethod
71
+ def _get_relations_serializers(cls) -> dict[str, "Serializer"]:
72
+ # Optional in subclasses. Default to no explicit relation serializers.
73
+ return {}
74
+
75
+ @classmethod
76
+ def _is_special_field(
77
+ cls, s_type: type[S_TYPES], field: str, f_type: type[F_TYPES]
78
+ ) -> bool:
79
+ """Return True if field appears in the given category for s_type."""
80
+ special_fields = cls._get_fields(s_type, f_type)
81
+ return any(field in special_f for special_f in special_fields)
82
+
83
+ @classmethod
84
+ def get_custom_fields(cls, s_type: type[S_TYPES]) -> list[tuple[str, type, Any]]:
85
+ """
86
+ Normalize declared custom field specs into (name, py_type, default).
87
+ Accepted tuple shapes:
88
+ - (name, py_type, default)
89
+ - (name, py_type) -> default Ellipsis (required)
90
+ """
91
+ raw_customs = cls._get_fields(s_type, "customs") or []
92
+ normalized: list[tuple[str, type, Any]] = []
93
+ for spec in raw_customs:
94
+ if not isinstance(spec, tuple):
95
+ raise ValueError(f"Custom field spec must be a tuple, got {type(spec)}")
96
+ match len(spec):
97
+ case 3:
98
+ name, py_type, default = spec
99
+ case 2:
100
+ name, py_type = spec
101
+ default = ...
102
+ case _:
103
+ raise ValueError(
104
+ f"Custom field tuple must have length 2 or 3 (name, type[, default]); got {len(spec)}"
105
+ )
106
+ normalized.append((name, py_type, default))
107
+ return normalized
108
+
109
+ @classmethod
110
+ def get_optional_fields(cls, s_type: type[S_TYPES]):
111
+ """Return optional field specs normalized to (name, type, None)."""
112
+ return [
113
+ (field, field_type, None)
114
+ for field, field_type in cls._get_fields(s_type, "optionals")
115
+ ]
116
+
117
+ @classmethod
118
+ def get_excluded_fields(cls, s_type: type[S_TYPES]):
119
+ """Return excluded field names for the serializer type."""
120
+ return cls._get_fields(s_type, "excludes")
121
+
122
+ @classmethod
123
+ def get_fields(cls, s_type: type[S_TYPES]):
124
+ """Return explicit declared fields for the serializer type."""
125
+ return cls._get_fields(s_type, "fields")
126
+
127
+ @classmethod
128
+ def is_custom(cls, field: str) -> bool:
129
+ """True if field is declared as a custom input in create or update."""
130
+ return cls._is_special_field(
131
+ "create", field, "customs"
132
+ ) or cls._is_special_field("update", field, "customs")
133
+
134
+ @classmethod
135
+ def is_optional(cls, field: str) -> bool:
136
+ """True if field is declared as optional in create or update."""
137
+ return cls._is_special_field(
138
+ "create", field, "optionals"
139
+ ) or cls._is_special_field("update", field, "optionals")
140
+
141
+ @classmethod
142
+ def _build_schema_reverse_rel(cls, field_name: str, descriptor: Any):
143
+ """
144
+ Build a reverse relation schema component for 'Out' schema generation.
145
+ Returns a custom field tuple or None to skip.
146
+ """
147
+ # Resolve related model and cardinality
148
+ if isinstance(descriptor, ManyToManyDescriptor):
149
+ # M2M uses the same descriptor on both sides; rely on .reverse to pick the model
150
+ rel_model: models.Model = (
151
+ descriptor.field.model
152
+ if descriptor.reverse
153
+ else descriptor.field.related_model
154
+ )
155
+ many = True
156
+ elif isinstance(descriptor, ReverseManyToOneDescriptor):
157
+ rel_model = descriptor.field.model
158
+ many = True
159
+ else: # ReverseOneToOneDescriptor
160
+ rel_model = descriptor.related.related_model
161
+ many = False
162
+
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
+
175
+ if not schema:
176
+ return None
177
+
178
+ rel_schema_type = schema if not many else list[schema]
179
+ return (field_name, rel_schema_type | None, None)
180
+
181
+ @classmethod
182
+ def _build_schema_forward_rel(cls, field_name: str, descriptor: Any):
183
+ """
184
+ Build a forward relation schema component for 'Out' schema generation.
185
+ Returns True to treat as plain field, a custom field tuple to include relation schema,
186
+ or None to skip entirely.
187
+ """
188
+ rel_model = descriptor.field.related_model
189
+
190
+ schema = None
191
+ 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
197
+ 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
+
205
+ if not schema:
206
+ # Could not build a schema: treat as a plain field (serialize as-is)
207
+ return True
208
+
209
+ # Forward relations are single objects; allow nullability
210
+ return (field_name, schema | None, None)
211
+
212
+ @classmethod
213
+ def get_schema_out_data(cls):
214
+ """
215
+ Collect components for 'Out' read schema generation.
216
+ Returns (fields, reverse_rel_descriptors, excludes, custom_fields_with_forward_relations, optionals).
217
+ Enforces relation serializers only when provided by subclass via _get_relations_serializers.
218
+ """
219
+ fields: list[str] = []
220
+ reverse_rels: list[tuple] = []
221
+ rels: list[tuple] = []
222
+ relations_serializers = cls._get_relations_serializers() or {}
223
+ model = cls._get_model()
224
+
225
+ for f in cls.get_fields("read"):
226
+ field_obj = getattr(model, f)
227
+ is_reverse = isinstance(
228
+ field_obj,
229
+ (
230
+ ManyToManyDescriptor,
231
+ ReverseManyToOneDescriptor,
232
+ ReverseOneToOneDescriptor,
233
+ ),
234
+ )
235
+ is_forward = isinstance(
236
+ field_obj, (ForwardOneToOneDescriptor, ForwardManyToOneDescriptor)
237
+ )
238
+
239
+ # If explicit relation serializers are declared, require mapping presence.
240
+ if (
241
+ is_reverse
242
+ and not isinstance(model, ModelSerializerMeta)
243
+ and f not in relations_serializers
244
+ and not getattr(settings, "NINJA_AIO_TESTING", False)
245
+ ):
246
+ warnings.warn(
247
+ f"{cls.__name__}: reverse relation '{f}' is listed in read fields but has no entry in relations_serializers; "
248
+ "it will be auto-resolved only for ModelSerializer relations, otherwise skipped.",
249
+ UserWarning,
250
+ stacklevel=2,
251
+ )
252
+
253
+ # Reverse relations
254
+ if is_reverse:
255
+ rel_tuple = cls._build_schema_reverse_rel(f, field_obj)
256
+ if rel_tuple:
257
+ reverse_rels.append(rel_tuple)
258
+ continue
259
+
260
+ # Forward relations
261
+ if is_forward:
262
+ rel_tuple = cls._build_schema_forward_rel(f, field_obj)
263
+ if rel_tuple is True:
264
+ fields.append(f)
265
+ elif rel_tuple:
266
+ rels.append(rel_tuple)
267
+ # None -> skip entirely
268
+ continue
269
+
270
+ # Plain field
271
+ fields.append(f)
272
+
273
+ return (
274
+ fields,
275
+ reverse_rels,
276
+ cls.get_excluded_fields("read"),
277
+ cls.get_custom_fields("read") + rels,
278
+ cls.get_optional_fields("read"),
279
+ )
280
+
281
+ @classmethod
282
+ def _generate_model_schema(
283
+ cls,
284
+ schema_type: type[SCHEMA_TYPES],
285
+ depth: int = None,
286
+ ) -> Schema:
287
+ """
288
+ Core schema factory bridging configuration to ninja.orm.create_schema.
289
+ Handles In/Patch/Out/Related.
290
+ """
291
+ 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
+ )
322
+ fields = cls.get_fields(s_type)
323
+ optionals = cls.get_optional_fields(s_type)
324
+ customs = cls.get_custom_fields(s_type) + optionals
325
+ excludes = cls.get_excluded_fields(s_type)
326
+ if not fields and not excludes:
327
+ 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
338
+ )
339
+
340
+ @classmethod
341
+ def get_related_schema_data(cls):
342
+ """
343
+ Build field/custom lists for 'Related' schema, flattening non-relational fields.
344
+ """
345
+ fields = cls.get_fields("read")
346
+ custom_f = {
347
+ name: (value, default)
348
+ for name, value, default in cls.get_custom_fields("read")
349
+ }
350
+ _related_fields = []
351
+ model = cls._get_model()
352
+ for f in fields + list(custom_f.keys()):
353
+ field_obj = getattr(model, f)
354
+ if not isinstance(
355
+ field_obj,
356
+ (
357
+ ManyToManyDescriptor,
358
+ ReverseManyToOneDescriptor,
359
+ ReverseOneToOneDescriptor,
360
+ ForwardManyToOneDescriptor,
361
+ ForwardOneToOneDescriptor,
362
+ ),
363
+ ):
364
+ _related_fields.append(f)
365
+ if not _related_fields:
366
+ return None, None
367
+ custom_related_fields = [
368
+ (f, *custom_f[f]) for f in _related_fields if f in custom_f
369
+ ]
370
+ related_fields = [f for f in _related_fields if f not in custom_f]
371
+ return related_fields, custom_related_fields
372
+
373
+ @classmethod
374
+ def generate_read_s(cls, depth: int = 1) -> Schema:
375
+ """Generate read (Out) schema."""
376
+ return cls._generate_model_schema("Out", depth)
377
+
378
+ @classmethod
379
+ def generate_create_s(cls) -> Schema:
380
+ """Generate create (In) schema."""
381
+ return cls._generate_model_schema("In")
382
+
383
+ @classmethod
384
+ def generate_update_s(cls) -> Schema:
385
+ """Generate update (Patch) schema."""
386
+ return cls._generate_model_schema("Patch")
387
+
388
+ @classmethod
389
+ def generate_related_s(cls) -> Schema:
390
+ """Generate related (nested) schema."""
391
+ return cls._generate_model_schema("Related")
392
+
393
+ @classmethod
394
+ async def queryset_request(cls, request: HttpRequest):
395
+ """
396
+ Override to return a request-scoped filtered queryset.
397
+
398
+ Parameters
399
+ ----------
400
+ request : HttpRequest
401
+
402
+ Returns
403
+ -------
404
+ QuerySet
405
+ """
406
+ raise NotImplementedError
407
+
408
+
409
+ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMeta):
410
+ """
411
+ ModelSerializer
412
+ =================
413
+ Model-bound serializer mixin centralizing declarative configuration directly
414
+ on the model class. Inherits common behavior from BaseSerializer and adds
415
+ lifecycle hooks and query utilities.
416
+ """
417
+
418
+ def __init_subclass__(cls, **kwargs):
419
+ super().__init_subclass__(**kwargs)
420
+ from ninja_aio.models.utils import ModelUtil
421
+ from ninja_aio.helpers.query import QueryUtil
422
+
423
+ # Bind a ModelUtil instance to the subclass for convenient access
424
+ cls.util = ModelUtil(cls)
425
+ cls.query_util = QueryUtil(cls)
426
+
427
+ class Meta:
428
+ abstract = True
429
+
430
+ class CreateSerializer:
431
+ """Configuration container describing how to build a create (input) schema for a model.
432
+
433
+ Purpose
434
+ -------
435
+ Describes which fields are accepted (and in what form) when creating a new
436
+ instance. A factory/metaclass can read this configuration to generate a
437
+ Pydantic / Ninja input schema.
438
+
439
+ Attributes
440
+ ----------
441
+ fields : list[str]
442
+ REQUIRED model fields.
443
+ optionals : list[tuple[str, type]]
444
+ Optional model fields (nullable / patch-like).
445
+ customs : list[tuple[str, type, Any]]
446
+ Synthetic input fields (non-model).
447
+ excludes : list[str]
448
+ Disallowed model fields on create (e.g., id, timestamps).
449
+ """
450
+
451
+ fields: list[str] = []
452
+ customs: list[tuple[str, type, Any]] = []
453
+ optionals: list[tuple[str, type]] = []
454
+ excludes: list[str] = []
455
+
456
+ class ReadSerializer:
457
+ """Configuration describing how to build a read (output) schema.
458
+
459
+ Attributes
460
+ ----------
461
+ fields : list[str]
462
+ Explicit model fields to include.
463
+ excludes : list[str]
464
+ Fields to force exclude (safety).
465
+ customs : list[tuple[str, type, Any]]
466
+ Computed / synthetic output attributes.
467
+ optionals : list[tuple[str, type]]
468
+ Optional output fields.
469
+ """
470
+
471
+ fields: list[str] = []
472
+ customs: list[tuple[str, type, Any]] = []
473
+ optionals: list[tuple[str, type]] = []
474
+ excludes: list[str] = []
475
+
476
+ class UpdateSerializer:
477
+ """Configuration describing update (PATCH/PUT) schema.
478
+
479
+ Attributes
480
+ ----------
481
+ fields : list[str]
482
+ Required update fields (rare).
483
+ optionals : list[tuple[str, type]]
484
+ Editable optional fields.
485
+ customs : list[tuple[str, type, Any]]
486
+ Synthetic operational inputs.
487
+ excludes : list[str]
488
+ Immutable / blocked fields.
489
+ """
490
+
491
+ fields: list[str] = []
492
+ customs: list[tuple[str, type, Any]] = []
493
+ optionals: list[tuple[str, type]] = []
494
+ excludes: list[str] = []
495
+
496
+ @classmethod
497
+ def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
498
+ """
499
+ Internal accessor for raw configuration lists.
500
+
501
+ Parameters
502
+ ----------
503
+ s_type : str
504
+ Serializer type ("create" | "update" | "read").
505
+ f_type : str
506
+ Field category ("fields" | "optionals" | "customs" | "excludes").
507
+
508
+ Returns
509
+ -------
510
+ list
511
+ Raw configuration list or empty list.
512
+ """
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
521
+
522
+ @classmethod
523
+ def _get_model(cls) -> "ModelSerializer":
524
+ """Return the model class itself."""
525
+ return cls
526
+
527
+ @classmethod
528
+ def verbose_name_path_resolver(cls) -> str:
529
+ """
530
+ Slugify plural verbose name for URL path segment.
531
+
532
+ Returns
533
+ -------
534
+ str
535
+ """
536
+ return "-".join(cls._meta.verbose_name_plural.split(" "))
537
+
538
+ def has_changed(self, field: str) -> bool:
539
+ """
540
+ Check if a model field has changed compared to the persisted value.
541
+
542
+ Parameters
543
+ ----------
544
+ field : str
545
+ Field name.
546
+
547
+ Returns
548
+ -------
549
+ bool
550
+ True if in-memory value differs from DB value.
551
+ """
552
+ if not self.pk:
553
+ return False
554
+ old_value = (
555
+ self.__class__._default_manager.filter(pk=self.pk)
556
+ .values(field)
557
+ .get()[field]
558
+ )
559
+ return getattr(self, field) != old_value
560
+
561
+ @classmethod
562
+ async def queryset_request(cls, request: HttpRequest):
563
+ return cls.query_util.apply_queryset_optimizations(
564
+ queryset=cls.objects.all(),
565
+ scope=cls.query_util.SCOPES.QUERYSET_REQUEST,
566
+ )
567
+
568
+ async def post_create(self) -> None:
569
+ """
570
+ Async hook executed after first persistence (create path).
571
+ """
572
+ pass
573
+
574
+ async def custom_actions(self, payload: dict[str, Any]):
575
+ """
576
+ Async hook for reacting to provided custom (synthetic) fields.
577
+
578
+ Parameters
579
+ ----------
580
+ payload : dict
581
+ Custom field name/value pairs.
582
+ """
583
+ pass
584
+
585
+ def after_save(self):
586
+ """
587
+ Sync hook executed after any save (create or update).
588
+ """
589
+ pass
590
+
591
+ def before_save(self):
592
+ """
593
+ Sync hook executed before any save (create or update).
594
+ """
595
+ pass
596
+
597
+ def on_create_after_save(self):
598
+ """
599
+ Sync hook executed only after initial creation save.
600
+ """
601
+ pass
602
+
603
+ def on_create_before_save(self):
604
+ """
605
+ Sync hook executed only before initial creation save.
606
+ """
607
+ pass
608
+
609
+ def on_delete(self):
610
+ """
611
+ Sync hook executed after delete.
612
+ """
613
+ pass
614
+
615
+ def save(self, *args, **kwargs):
616
+ """
617
+ Override save lifecycle to inject create/update hooks.
618
+ """
619
+ state_adding = self._state.adding
620
+ if state_adding:
621
+ self.on_create_before_save()
622
+ self.before_save()
623
+ super().save(*args, **kwargs)
624
+ if state_adding:
625
+ self.on_create_after_save()
626
+ self.after_save()
627
+
628
+ def delete(self, *args, **kwargs):
629
+ """
630
+ Override delete to inject on_delete hook.
631
+
632
+ Returns
633
+ -------
634
+ tuple(int, dict)
635
+ Django delete return signature.
636
+ """
637
+ res = super().delete(*args, **kwargs)
638
+ self.on_delete()
639
+ return res
640
+
641
+
642
+ class SchemaModelConfig(Schema):
643
+ """
644
+ SchemaModelConfig
645
+ -----------------
646
+ Configuration container for declarative schema definitions.
647
+ Attributes
648
+ ----------
649
+ fields : Optional[List[str]]
650
+ Explicit model fields to include.
651
+ optionals : Optional[List[tuple[str, type]]]
652
+ Optional model fields.
653
+ exclude : Optional[List[str]]
654
+ Model fields to exclude.
655
+ customs : Optional[List[tuple[str, type, Any]]]
656
+ Custom / synthetic fields.
657
+ """
658
+
659
+ fields: Optional[List[str]] = None
660
+ optionals: Optional[List[tuple[str, type]]] = None
661
+ exclude: Optional[List[str]] = None
662
+ customs: Optional[List[tuple[str, type, Any]]] = None
663
+
664
+
665
+ class Serializer(BaseSerializer):
666
+ """
667
+ Serializer
668
+ ----------
669
+ Meta-driven serializer for arbitrary Django models. Shares common behavior
670
+ from BaseSerializer but sources configuration from the nested Meta class.
671
+ Supports optional relations_serializers mapping to explicitly include related
672
+ schema components during read schema generation.
673
+ """
674
+
675
+ def __init_subclass__(cls, **kwargs):
676
+ super().__init_subclass__(**kwargs)
677
+ from ninja_aio.models.utils import ModelUtil
678
+ from ninja_aio.helpers.query import QueryUtil
679
+
680
+ 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)
686
+ cls.query_util = QueryUtil(cls)
687
+
688
+ class Meta:
689
+ model: models.Model = None
690
+ schema_in: Optional[SchemaModelConfig] = None
691
+ schema_out: Optional[SchemaModelConfig] = None
692
+ schema_update: Optional[SchemaModelConfig] = None
693
+ relations_serializers: dict[str, "Serializer"] = {}
694
+
695
+ @classmethod
696
+ def _get_meta_data(cls, attr_name: str) -> Any:
697
+ return getattr(cls.Meta, attr_name, None)
698
+
699
+ @classmethod
700
+ def _get_model(cls) -> models.Model:
701
+ return cls._validate_model()
702
+
703
+ @classmethod
704
+ def _get_relations_serializers(cls) -> dict[str, "Serializer"]:
705
+ relations_serializers = cls._get_meta_data("relations_serializers")
706
+ return relations_serializers or {}
707
+
708
+ @classmethod
709
+ def _get_schema_meta(cls, schema_type: str) -> SchemaModelConfig | None:
710
+ match schema_type:
711
+ case "in":
712
+ return cls._get_meta_data("schema_in")
713
+ case "out":
714
+ return cls._get_meta_data("schema_out")
715
+ case "update":
716
+ return cls._get_meta_data("schema_update")
717
+ case _:
718
+ return None
719
+
720
+ @classmethod
721
+ def _validate_model(cls):
722
+ model = cls._get_meta_data("model")
723
+ if not model:
724
+ raise ValueError("Meta.model must be defined for Serializer.")
725
+ if not issubclass(model, models.Model):
726
+ raise ValueError("Meta.model must be a Django model")
727
+ return model
728
+
729
+ @classmethod
730
+ def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
731
+ """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)
737
+ if not schema:
738
+ return []
739
+ return getattr(schema, f_type, []) or []
740
+
741
+
742
+ @classmethod
743
+ async def queryset_request(cls, request: HttpRequest):
744
+ return cls.query_util.apply_queryset_optimizations(
745
+ queryset=cls.model._default_manager.all(),
746
+ scope=cls.query_util.SCOPES.QUERYSET_REQUEST,
747
+ )
748
+
749
+ async def post_create(self, instance: models.Model) -> None:
750
+ """
751
+ Async hook executed after first persistence (create path).
752
+ """
753
+ pass
754
+
755
+ async def custom_actions(self, payload: dict[str, Any], instance: models.Model):
756
+ """
757
+ Async hook for reacting to provided custom (synthetic) fields.
758
+
759
+ Parameters
760
+ ----------
761
+ payload : dict
762
+ Custom field name/value pairs.
763
+ """
764
+ pass
765
+
766
+ async def save(self, instance: models.Model) -> models.Model:
767
+ """
768
+ Async helper to save a model instance with lifecycle hooks.
769
+
770
+ Parameters
771
+ ----------
772
+ instance : models.Model
773
+ The model instance to save.
774
+ """
775
+ creation = instance._state.adding
776
+ if creation:
777
+ self.on_create_before_save(instance)
778
+ self.before_save(instance)
779
+ await instance.asave()
780
+ if creation:
781
+ self.on_create_after_save(instance)
782
+ self.after_save(instance)
783
+ return instance
784
+
785
+ async def create(self, payload: dict[str, Any]) -> models.Model:
786
+ """
787
+ Create a new model instance from the provided payload.
788
+
789
+ Parameters
790
+ ----------
791
+ payload : dict
792
+ Input data.
793
+
794
+ Returns
795
+ -------
796
+ models.Model
797
+ Created model instance.
798
+ """
799
+ instance: models.Model = self.model(**payload)
800
+ return await self.save(instance)
801
+
802
+ async def update(
803
+ self, instance: models.Model, payload: dict[str, Any]
804
+ ) -> models.Model:
805
+ """
806
+ Update an existing model instance with the provided payload.
807
+
808
+ Parameters
809
+ ----------
810
+ instance : models.Model
811
+ The model instance to update.
812
+ payload : dict
813
+ Input data.
814
+
815
+ Returns
816
+ -------
817
+ models.Model
818
+ Updated model instance.
819
+ """
820
+ for attr, value in payload.items():
821
+ setattr(instance, attr, value)
822
+ return await self.save(instance)
823
+
824
+ async def model_dump(self, instance: models.Model) -> dict[str, Any]:
825
+ """
826
+ Serialize a model instance to a dictionary using the Out schema.
827
+
828
+ Parameters
829
+ ----------
830
+ instance : models.Model
831
+ The model instance to serialize.
832
+
833
+ Returns
834
+ -------
835
+ dict
836
+ Serialized data.
837
+ """
838
+ return await self.model_util.read_s(schema=self.schema_out, instance=instance)
839
+
840
+ async def models_dump(
841
+ self, instances: models.QuerySet[models.Model]
842
+ ) -> list[dict[str, Any]]:
843
+ """
844
+ Serialize a list of model instances to a list of dictionaries using the Out schema.
845
+
846
+ Parameters
847
+ ----------
848
+ instances : list[models.Model]
849
+ The list of model instances to serialize.
850
+
851
+ Returns
852
+ -------
853
+ list[dict]
854
+ List of serialized data.
855
+ """
856
+ return await self.model_util.list_read_s(
857
+ schema=self.schema_out, instances=instances
858
+ )
859
+
860
+ def after_save(self, instance: models.Model):
861
+ """
862
+ Sync hook executed after any save (create or update).
863
+ """
864
+ pass
865
+
866
+ def before_save(self, instance: models.Model):
867
+ """
868
+ Sync hook executed before any save (create or update).
869
+ """
870
+ pass
871
+
872
+ def on_create_after_save(self, instance: models.Model):
873
+ """
874
+ Sync hook executed only after initial creation save.
875
+ """
876
+ pass
877
+
878
+ def on_create_before_save(self, instance: models.Model):
879
+ """
880
+ Sync hook executed only before initial creation save.
881
+ """
882
+ pass
883
+
884
+ def on_delete(self, instance: models.Model):
885
+ """
886
+ Sync hook executed after delete.
887
+ """
888
+ pass