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,738 @@
1
+ from typing import Any, ClassVar, 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
+ from ninja_aio.helpers.query import QueryUtil
23
+
24
+
25
+ class BaseSerializer:
26
+ """
27
+ BaseSerializer
28
+ --------------
29
+ Shared serializer utilities used by both ModelSerializer (model-bound) and
30
+ Serializer (Meta-driven). Centralizes common field normalization, relation
31
+ schema construction and schema generation helpers.
32
+
33
+ Subclasses must implement:
34
+ - _get_fields(s_type, f_type): source raw config for fields/optionals/customs/excludes
35
+ - _get_model(): return the Django model class associated with the serializer
36
+ - _get_relations_serializers(): optional mapping of relation field -> serializer (may be empty)
37
+ """
38
+
39
+ @classmethod
40
+ def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
41
+ # Subclasses provide implementation.
42
+ raise NotImplementedError
43
+
44
+ @classmethod
45
+ def _get_model(cls) -> models.Model:
46
+ # Subclasses provide implementation.
47
+ raise NotImplementedError
48
+
49
+ @classmethod
50
+ def _get_relations_serializers(cls) -> dict[str, "Serializer"]:
51
+ # Optional in subclasses. Default to no explicit relation serializers.
52
+ return {}
53
+
54
+ @classmethod
55
+ def _is_special_field(
56
+ cls, s_type: type[S_TYPES], field: str, f_type: type[F_TYPES]
57
+ ) -> bool:
58
+ """Return True if field appears in the given category for s_type."""
59
+ special_fields = cls._get_fields(s_type, f_type)
60
+ return any(field in special_f for special_f in special_fields)
61
+
62
+ @classmethod
63
+ def get_custom_fields(cls, s_type: type[S_TYPES]) -> list[tuple[str, type, Any]]:
64
+ """
65
+ Normalize declared custom field specs into (name, py_type, default).
66
+ Accepted tuple shapes:
67
+ - (name, py_type, default)
68
+ - (name, py_type) -> default Ellipsis (required)
69
+ """
70
+ raw_customs = cls._get_fields(s_type, "customs") or []
71
+ normalized: list[tuple[str, type, Any]] = []
72
+ for spec in raw_customs:
73
+ if not isinstance(spec, tuple):
74
+ raise ValueError(f"Custom field spec must be a tuple, got {type(spec)}")
75
+ match len(spec):
76
+ case 3:
77
+ name, py_type, default = spec
78
+ case 2:
79
+ name, py_type = spec
80
+ default = ...
81
+ case _:
82
+ raise ValueError(
83
+ f"Custom field tuple must have length 2 or 3 (name, type[, default]); got {len(spec)}"
84
+ )
85
+ normalized.append((name, py_type, default))
86
+ return normalized
87
+
88
+ @classmethod
89
+ def get_optional_fields(cls, s_type: type[S_TYPES]):
90
+ """Return optional field specs normalized to (name, type, None)."""
91
+ return [
92
+ (field, field_type, None)
93
+ for field, field_type in cls._get_fields(s_type, "optionals")
94
+ ]
95
+
96
+ @classmethod
97
+ def get_excluded_fields(cls, s_type: type[S_TYPES]):
98
+ """Return excluded field names for the serializer type."""
99
+ return cls._get_fields(s_type, "excludes")
100
+
101
+ @classmethod
102
+ def get_fields(cls, s_type: type[S_TYPES]):
103
+ """Return explicit declared fields for the serializer type."""
104
+ return cls._get_fields(s_type, "fields")
105
+
106
+ @classmethod
107
+ def is_custom(cls, field: str) -> bool:
108
+ """True if field is declared as a custom input in create or update."""
109
+ return cls._is_special_field(
110
+ "create", field, "customs"
111
+ ) or cls._is_special_field("update", field, "customs")
112
+
113
+ @classmethod
114
+ def is_optional(cls, field: str) -> bool:
115
+ """True if field is declared as optional in create or update."""
116
+ return cls._is_special_field(
117
+ "create", field, "optionals"
118
+ ) or cls._is_special_field("update", field, "optionals")
119
+
120
+ @classmethod
121
+ def _build_schema_reverse_rel(cls, field_name: str, descriptor: Any):
122
+ """
123
+ Build a reverse relation schema component for 'Out' schema generation.
124
+ Returns a custom field tuple or None to skip.
125
+ """
126
+ # Resolve related model and cardinality
127
+ if isinstance(descriptor, ManyToManyDescriptor):
128
+ # M2M uses the same descriptor on both sides; rely on .reverse to pick the model
129
+ rel_model: models.Model = (
130
+ descriptor.field.model
131
+ if descriptor.reverse
132
+ else descriptor.field.related_model
133
+ )
134
+ many = True
135
+ elif isinstance(descriptor, ReverseManyToOneDescriptor):
136
+ rel_model = descriptor.field.model
137
+ many = True
138
+ else: # ReverseOneToOneDescriptor
139
+ rel_model = descriptor.related.related_model
140
+ many = False
141
+
142
+ schema = None
143
+ if isinstance(rel_model, ModelSerializerMeta):
144
+ # Auto-include if related model exposes readable data
145
+ if rel_model.get_fields("read") or rel_model.get_custom_fields("read"):
146
+ schema = rel_model.generate_related_s()
147
+ else:
148
+ # Use explicit serializer when provided by subclasses
149
+ rel_serializers = cls._get_relations_serializers() or {}
150
+ serializer = rel_serializers.get(field_name)
151
+ if serializer:
152
+ schema = serializer.generate_related_s()
153
+
154
+ if not schema:
155
+ return None
156
+
157
+ rel_schema_type = schema if not many else list[schema]
158
+ return (field_name, rel_schema_type | None, None)
159
+
160
+ @classmethod
161
+ def _build_schema_forward_rel(cls, field_name: str, descriptor: Any):
162
+ """
163
+ Build a forward relation schema component for 'Out' schema generation.
164
+ Returns True to treat as plain field, a custom field tuple to include relation schema,
165
+ or None to skip entirely.
166
+ """
167
+ rel_model = descriptor.field.related_model
168
+
169
+ schema = None
170
+ if isinstance(rel_model, ModelSerializerMeta):
171
+ # Prefer auto-inclusion when the related model is a ModelSerializer
172
+ if rel_model.get_fields("read") or rel_model.get_custom_fields("read"):
173
+ schema = rel_model.generate_related_s()
174
+ else:
175
+ # Explicit ModelSerializer with no readable fields -> skip entirely
176
+ return None
177
+ else:
178
+ # Fall back to an explicitly provided serializer mapping
179
+ rel_serializers = cls._get_relations_serializers() or {}
180
+ serializer = rel_serializers.get(field_name)
181
+ if serializer:
182
+ schema = serializer.generate_related_s()
183
+
184
+ if not schema:
185
+ # Could not build a schema: treat as a plain field (serialize as-is)
186
+ return True
187
+
188
+ # Forward relations are single objects; allow nullability
189
+ return (field_name, schema | None, None)
190
+
191
+ @classmethod
192
+ def get_schema_out_data(cls):
193
+ """
194
+ Collect components for 'Out' read schema generation.
195
+ Returns (fields, reverse_rel_descriptors, excludes, custom_fields_with_forward_relations, optionals).
196
+ Enforces relation serializers only when provided by subclass via _get_relations_serializers.
197
+ """
198
+ fields: list[str] = []
199
+ reverse_rels: list[tuple] = []
200
+ rels: list[tuple] = []
201
+ relations_serializers = cls._get_relations_serializers() or {}
202
+
203
+ for f in cls.get_fields("read"):
204
+ field_obj = getattr(cls._get_model(), f)
205
+ is_reverse = isinstance(
206
+ field_obj,
207
+ (
208
+ ManyToManyDescriptor,
209
+ ReverseManyToOneDescriptor,
210
+ ReverseOneToOneDescriptor,
211
+ ),
212
+ )
213
+ is_forward = isinstance(
214
+ field_obj, (ForwardOneToOneDescriptor, ForwardManyToOneDescriptor)
215
+ )
216
+
217
+ # If explicit relation serializers are declared, require mapping presence.
218
+ if (
219
+ is_reverse
220
+ and not isinstance(cls._get_model(), ModelSerializerMeta)
221
+ and f not in relations_serializers
222
+ and not getattr(settings, "NINJA_AIO_TESTING", False)
223
+ ):
224
+ warnings.warn(
225
+ f"{cls.__name__}: reverse relation '{f}' is listed in read fields but has no entry in relations_serializers; "
226
+ "it will be auto-resolved only for ModelSerializer relations, otherwise skipped.",
227
+ UserWarning,
228
+ stacklevel=2,
229
+ )
230
+
231
+ # Reverse relations
232
+ if is_reverse:
233
+ rel_tuple = cls._build_schema_reverse_rel(f, field_obj)
234
+ if rel_tuple:
235
+ reverse_rels.append(rel_tuple)
236
+ continue
237
+
238
+ # Forward relations
239
+ if is_forward:
240
+ rel_tuple = cls._build_schema_forward_rel(f, field_obj)
241
+ if rel_tuple is True:
242
+ fields.append(f)
243
+ elif rel_tuple:
244
+ rels.append(rel_tuple)
245
+ # None -> skip entirely
246
+ continue
247
+
248
+ # Plain field
249
+ fields.append(f)
250
+
251
+ return (
252
+ fields,
253
+ reverse_rels,
254
+ cls.get_excluded_fields("read"),
255
+ cls.get_custom_fields("read") + rels,
256
+ cls.get_optional_fields("read"),
257
+ )
258
+
259
+ @classmethod
260
+ def _generate_model_schema(
261
+ cls,
262
+ schema_type: type[SCHEMA_TYPES],
263
+ depth: int = None,
264
+ ) -> Schema:
265
+ """
266
+ Core schema factory bridging configuration to ninja.orm.create_schema.
267
+ Handles In/Patch/Out/Related.
268
+ """
269
+ model = cls._get_model()
270
+ match schema_type:
271
+ case "In":
272
+ s_type = "create"
273
+ case "Patch":
274
+ s_type = "update"
275
+ case "Out":
276
+ fields, reverse_rels, excludes, customs, optionals = (
277
+ cls.get_schema_out_data()
278
+ )
279
+ if not fields and not reverse_rels and not excludes and not customs:
280
+ return None
281
+ return create_schema(
282
+ model=model,
283
+ name=f"{model._meta.model_name}SchemaOut",
284
+ depth=depth,
285
+ fields=fields,
286
+ custom_fields=reverse_rels + customs + optionals,
287
+ exclude=excludes,
288
+ )
289
+ case "Related":
290
+ # Related schema includes only non-relational declared fields + customs
291
+ fields, customs = cls.get_related_schema_data()
292
+ if not fields and not customs:
293
+ return None
294
+ return create_schema(
295
+ model=model,
296
+ name=f"{model._meta.model_name}SchemaRelated",
297
+ fields=fields,
298
+ custom_fields=customs,
299
+ )
300
+ fields = cls.get_fields(s_type)
301
+ optionals = cls.get_optional_fields(s_type)
302
+ customs = cls.get_custom_fields(s_type) + optionals
303
+ excludes = cls.get_excluded_fields(s_type)
304
+ if not fields and not excludes:
305
+ fields = [f[0] for f in optionals]
306
+ return (
307
+ create_schema(
308
+ model=model,
309
+ name=f"{model._meta.model_name}Schema{schema_type}",
310
+ fields=fields,
311
+ custom_fields=customs,
312
+ exclude=excludes,
313
+ )
314
+ if fields or customs or excludes
315
+ else None
316
+ )
317
+
318
+ @classmethod
319
+ def get_related_schema_data(cls):
320
+ """
321
+ Build field/custom lists for 'Related' schema, flattening non-relational fields.
322
+ """
323
+ fields = cls.get_fields("read")
324
+ custom_f = {
325
+ name: (value, default)
326
+ for name, value, default in cls.get_custom_fields("read")
327
+ }
328
+ _related_fields = []
329
+ model = cls._get_model()
330
+ for f in fields + list(custom_f.keys()):
331
+ field_obj = getattr(model, f)
332
+ if not isinstance(
333
+ field_obj,
334
+ (
335
+ ManyToManyDescriptor,
336
+ ReverseManyToOneDescriptor,
337
+ ReverseOneToOneDescriptor,
338
+ ForwardManyToOneDescriptor,
339
+ ForwardOneToOneDescriptor,
340
+ ),
341
+ ):
342
+ _related_fields.append(f)
343
+ if not _related_fields:
344
+ return None, None
345
+ custom_related_fields = [
346
+ (f, *custom_f[f]) for f in _related_fields if f in custom_f
347
+ ]
348
+ related_fields = [f for f in _related_fields if f not in custom_f]
349
+ return related_fields, custom_related_fields
350
+
351
+ @classmethod
352
+ def generate_read_s(cls, depth: int = 1) -> Schema:
353
+ """Generate read (Out) schema."""
354
+ return cls._generate_model_schema("Out", depth)
355
+
356
+ @classmethod
357
+ def generate_create_s(cls) -> Schema:
358
+ """Generate create (In) schema."""
359
+ return cls._generate_model_schema("In")
360
+
361
+ @classmethod
362
+ def generate_update_s(cls) -> Schema:
363
+ """Generate update (Patch) schema."""
364
+ return cls._generate_model_schema("Patch")
365
+
366
+ @classmethod
367
+ def generate_related_s(cls) -> Schema:
368
+ """Generate related (nested) schema."""
369
+ return cls._generate_model_schema("Related")
370
+
371
+ @classmethod
372
+ async def queryset_request(cls, request: HttpRequest):
373
+ """
374
+ Override to return a request-scoped filtered queryset.
375
+
376
+ Parameters
377
+ ----------
378
+ request : HttpRequest
379
+
380
+ Returns
381
+ -------
382
+ QuerySet
383
+ """
384
+ raise NotImplementedError
385
+
386
+
387
+ class ModelSerializer(models.Model, BaseSerializer, metaclass=ModelSerializerMeta):
388
+ """
389
+ ModelSerializer
390
+ =================
391
+ Model-bound serializer mixin centralizing declarative configuration directly
392
+ on the model class. Inherits common behavior from BaseSerializer and adds
393
+ lifecycle hooks and query utilities.
394
+ """
395
+
396
+ util: ClassVar
397
+ query_util: ClassVar[QueryUtil]
398
+
399
+ class Meta:
400
+ abstract = True
401
+
402
+ def __init_subclass__(cls, **kwargs):
403
+ super().__init_subclass__(**kwargs)
404
+ from ninja_aio.models.utils import ModelUtil
405
+
406
+ # Bind a ModelUtil instance to the subclass for convenient access
407
+ cls.util = ModelUtil(cls)
408
+ cls.query_util = QueryUtil(cls)
409
+
410
+ class QuerySet:
411
+ """
412
+ Configuration container describing how to build query schemas for a model.
413
+ Purpose
414
+ -------
415
+ Describes which fields and extras are available when querying for model
416
+ instances. A factory/metaclass can read this configuration to generate
417
+ Pydantic / Ninja query schemas.
418
+ Attributes
419
+ ----------
420
+ read : ModelQuerySetSchema
421
+ Schema configuration for read operations.
422
+ queryset_request : ModelQuerySetSchema
423
+ Schema configuration for queryset_request hook.
424
+ extras : list[ModelQuerySetExtraSchema]
425
+ Additional computed / synthetic query parameters.
426
+ """
427
+
428
+ read = ModelQuerySetSchema()
429
+ queryset_request = ModelQuerySetSchema()
430
+ extras: list[ModelQuerySetExtraSchema] = []
431
+
432
+ class CreateSerializer:
433
+ """Configuration container describing how to build a create (input) schema for a model.
434
+
435
+ Purpose
436
+ -------
437
+ Describes which fields are accepted (and in what form) when creating a new
438
+ instance. A factory/metaclass can read this configuration to generate a
439
+ Pydantic / Ninja input schema.
440
+
441
+ Attributes
442
+ ----------
443
+ fields : list[str]
444
+ REQUIRED model fields.
445
+ optionals : list[tuple[str, type]]
446
+ Optional model fields (nullable / patch-like).
447
+ customs : list[tuple[str, type, Any]]
448
+ Synthetic input fields (non-model).
449
+ excludes : list[str]
450
+ Disallowed model fields on create (e.g., id, timestamps).
451
+ """
452
+
453
+ fields: list[str] = []
454
+ customs: list[tuple[str, type, Any]] = []
455
+ optionals: list[tuple[str, type]] = []
456
+ excludes: list[str] = []
457
+
458
+ class ReadSerializer:
459
+ """Configuration describing how to build a read (output) schema.
460
+
461
+ Attributes
462
+ ----------
463
+ fields : list[str]
464
+ Explicit model fields to include.
465
+ excludes : list[str]
466
+ Fields to force exclude (safety).
467
+ customs : list[tuple[str, type, Any]]
468
+ Computed / synthetic output attributes.
469
+ optionals : list[tuple[str, type]]
470
+ Optional output fields.
471
+ """
472
+
473
+ fields: list[str] = []
474
+ customs: list[tuple[str, type, Any]] = []
475
+ optionals: list[tuple[str, type]] = []
476
+ excludes: list[str] = []
477
+
478
+ class UpdateSerializer:
479
+ """Configuration describing update (PATCH/PUT) schema.
480
+
481
+ Attributes
482
+ ----------
483
+ fields : list[str]
484
+ Required update fields (rare).
485
+ optionals : list[tuple[str, type]]
486
+ Editable optional fields.
487
+ customs : list[tuple[str, type, Any]]
488
+ Synthetic operational inputs.
489
+ excludes : list[str]
490
+ Immutable / blocked fields.
491
+ """
492
+
493
+ fields: list[str] = []
494
+ customs: list[tuple[str, type, Any]] = []
495
+ optionals: list[tuple[str, type]] = []
496
+ excludes: list[str] = []
497
+
498
+ @classmethod
499
+ def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
500
+ """
501
+ Internal accessor for raw configuration lists.
502
+
503
+ Parameters
504
+ ----------
505
+ s_type : str
506
+ Serializer type ("create" | "update" | "read").
507
+ f_type : str
508
+ Field category ("fields" | "optionals" | "customs" | "excludes").
509
+
510
+ Returns
511
+ -------
512
+ list
513
+ Raw configuration list or empty list.
514
+ """
515
+ match s_type:
516
+ case "create":
517
+ fields = getattr(cls.CreateSerializer, f_type, [])
518
+ case "update":
519
+ fields = getattr(cls.UpdateSerializer, f_type, [])
520
+ case "read":
521
+ fields = getattr(cls.ReadSerializer, f_type, [])
522
+ return fields
523
+
524
+ @classmethod
525
+ def _get_model(cls) -> "ModelSerializer":
526
+ """Return the model class itself."""
527
+ return cls
528
+
529
+ @classmethod
530
+ def verbose_name_path_resolver(cls) -> str:
531
+ """
532
+ Slugify plural verbose name for URL path segment.
533
+
534
+ Returns
535
+ -------
536
+ str
537
+ """
538
+ return "-".join(cls._meta.verbose_name_plural.split(" "))
539
+
540
+ def has_changed(self, field: str) -> bool:
541
+ """
542
+ Check if a model field has changed compared to the persisted value.
543
+
544
+ Parameters
545
+ ----------
546
+ field : str
547
+ Field name.
548
+
549
+ Returns
550
+ -------
551
+ bool
552
+ True if in-memory value differs from DB value.
553
+ """
554
+ if not self.pk:
555
+ return False
556
+ old_value = (
557
+ self.__class__._default_manager.filter(pk=self.pk)
558
+ .values(field)
559
+ .get()[field]
560
+ )
561
+ return getattr(self, field) != old_value
562
+
563
+ @classmethod
564
+ async def queryset_request(cls, request: HttpRequest):
565
+ return cls.query_util.apply_queryset_optimizations(
566
+ queryset=cls.objects.all(),
567
+ scope=cls.query_util.SCOPES.QUERYSET_REQUEST,
568
+ )
569
+
570
+ async def post_create(self) -> None:
571
+ """
572
+ Async hook executed after first persistence (create path).
573
+ """
574
+ pass
575
+
576
+ async def custom_actions(self, payload: dict[str, Any]):
577
+ """
578
+ Async hook for reacting to provided custom (synthetic) fields.
579
+
580
+ Parameters
581
+ ----------
582
+ payload : dict
583
+ Custom field name/value pairs.
584
+ """
585
+ pass
586
+
587
+ def after_save(self):
588
+ """
589
+ Sync hook executed after any save (create or update).
590
+ """
591
+ pass
592
+
593
+ def before_save(self):
594
+ """
595
+ Sync hook executed before any save (create or update).
596
+ """
597
+ pass
598
+
599
+ def on_create_after_save(self):
600
+ """
601
+ Sync hook executed only after initial creation save.
602
+ """
603
+ pass
604
+
605
+ def on_create_before_save(self):
606
+ """
607
+ Sync hook executed only before initial creation save.
608
+ """
609
+ pass
610
+
611
+ def on_delete(self):
612
+ """
613
+ Sync hook executed after delete.
614
+ """
615
+ pass
616
+
617
+ def save(self, *args, **kwargs):
618
+ """
619
+ Override save lifecycle to inject create/update hooks.
620
+ """
621
+ state_adding = self._state.adding
622
+ if state_adding:
623
+ self.on_create_before_save()
624
+ self.before_save()
625
+ super().save(*args, **kwargs)
626
+ if state_adding:
627
+ self.on_create_after_save()
628
+ self.after_save()
629
+
630
+ def delete(self, *args, **kwargs):
631
+ """
632
+ Override delete to inject on_delete hook.
633
+
634
+ Returns
635
+ -------
636
+ tuple(int, dict)
637
+ Django delete return signature.
638
+ """
639
+ res = super().delete(*args, **kwargs)
640
+ self.on_delete()
641
+ return res
642
+
643
+
644
+ class SchemaModelConfig(Schema):
645
+ """
646
+ SchemaModelConfig
647
+ -----------------
648
+ Configuration container for declarative schema definitions.
649
+ Attributes
650
+ ----------
651
+ fields : Optional[List[str]]
652
+ Explicit model fields to include.
653
+ optionals : Optional[List[tuple[str, type]]]
654
+ Optional model fields.
655
+ exclude : Optional[List[str]]
656
+ Model fields to exclude.
657
+ customs : Optional[List[tuple[str, type, Any]]]
658
+ Custom / synthetic fields.
659
+ """
660
+
661
+ fields: Optional[List[str]] = None
662
+ optionals: Optional[List[tuple[str, type]]] = None
663
+ exclude: Optional[List[str]] = None
664
+ customs: Optional[List[tuple[str, type, Any]]] = None
665
+
666
+
667
+ class Serializer(BaseSerializer):
668
+ """
669
+ Serializer
670
+ ----------
671
+ Meta-driven serializer for arbitrary Django models. Shares common behavior
672
+ from BaseSerializer but sources configuration from the nested Meta class.
673
+ Supports optional relations_serializers mapping to explicitly include related
674
+ schema components during read schema generation.
675
+ """
676
+
677
+ class Meta:
678
+ model: models.Model = None
679
+ schema_in: SchemaModelConfig = None
680
+ schema_out: SchemaModelConfig = None
681
+ schema_update: SchemaModelConfig = None
682
+ relations_serializers: dict[str, "Serializer"] = {}
683
+
684
+ def __init__(self):
685
+ self.model = self._validate_model()
686
+ self.schema_in = self.generate_create_s()
687
+ self.schema_out = self.generate_read_s()
688
+ self.schema_update = self.generate_update_s()
689
+ self.schema_related = self.generate_related_s()
690
+
691
+ @classmethod
692
+ def _get_meta_data(cls, attr_name: str) -> Any:
693
+ return getattr(cls.Meta, attr_name, None)
694
+
695
+ @classmethod
696
+ def _get_model(cls) -> models.Model:
697
+ return cls._validate_model()
698
+
699
+ @classmethod
700
+ def _get_relations_serializers(cls) -> dict[str, "Serializer"]:
701
+ relations_serializers = cls._get_meta_data("relations_serializers")
702
+ return relations_serializers or {}
703
+
704
+ @classmethod
705
+ def _get_schema_meta(cls, schema_type: str) -> SchemaModelConfig:
706
+ match schema_type:
707
+ case "in":
708
+ return cls._get_meta_data("schema_in")
709
+ case "out":
710
+ return cls._get_meta_data("schema_out")
711
+ case "update":
712
+ return cls._get_meta_data("schema_update")
713
+ return None
714
+
715
+ @classmethod
716
+ def _validate_model(cls):
717
+ model = cls._get_meta_data("model")
718
+ if not model:
719
+ raise ValueError("Meta.model must be defined for Serializer.")
720
+ if not issubclass(model, models.Model):
721
+ raise ValueError("Meta.model must be a Django model")
722
+ return model
723
+
724
+ @classmethod
725
+ def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
726
+ """Internal accessor for raw configuration lists from Meta schemas."""
727
+ schema = {
728
+ "create": cls._get_schema_meta("in"),
729
+ "update": cls._get_schema_meta("update"),
730
+ "read": cls._get_schema_meta("out"),
731
+ }.get(s_type)
732
+ if not schema:
733
+ return []
734
+ return getattr(schema, f_type, []) or []
735
+
736
+ @classmethod
737
+ async def queryset_request(cls, request: HttpRequest):
738
+ return cls._get_model()._default_manager.all()