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.
ninja_aio/models.py DELETED
@@ -1,549 +0,0 @@
1
- import asyncio
2
- import base64
3
- from typing import Any
4
-
5
- from ninja import Schema
6
- from ninja.orm import create_schema
7
-
8
- from django.db import models
9
- from django.http import HttpRequest
10
- from django.core.exceptions import ObjectDoesNotExist
11
- from asgiref.sync import sync_to_async
12
- from django.db.models.fields.related_descriptors import (
13
- ReverseManyToOneDescriptor,
14
- ReverseOneToOneDescriptor,
15
- ManyToManyDescriptor,
16
- ForwardManyToOneDescriptor,
17
- ForwardOneToOneDescriptor,
18
- )
19
-
20
- from .exceptions import SerializeError
21
- from .types import S_TYPES, F_TYPES, SCHEMA_TYPES, ModelSerializerMeta
22
-
23
-
24
- async def agetattr(obj, name: str, default=None):
25
- return await sync_to_async(getattr)(obj, name, default)
26
-
27
-
28
- class ModelUtil:
29
- def __init__(self, model: type["ModelSerializer"] | models.Model):
30
- self.model = model
31
-
32
- @property
33
- def serializable_fields(self):
34
- if isinstance(self.model, ModelSerializerMeta):
35
- return self.model.get_fields("read")
36
- return [field.name for field in self.model._meta.get_fields()]
37
-
38
- @property
39
- def model_name(self) -> str:
40
- return self.model._meta.model_name
41
-
42
- @property
43
- def model_pk_name(self) -> str:
44
- return self.model._meta.pk.attname
45
-
46
- @property
47
- def model_verbose_name_plural(self) -> str:
48
- return self.model._meta.verbose_name_plural
49
-
50
- def verbose_name_path_resolver(self) -> str:
51
- return "-".join(self.model_verbose_name_plural.split(" "))
52
-
53
- def verbose_name_view_resolver(self) -> str:
54
- return self.model_verbose_name_plural.replace(" ", "")
55
-
56
- async def get_object(
57
- self,
58
- request: HttpRequest,
59
- pk: int | str = None,
60
- filters: dict = None,
61
- getters: dict = None,
62
- with_qs_request=True,
63
- ) -> (
64
- type["ModelSerializer"]
65
- | models.Model
66
- | models.QuerySet[type["ModelSerializer"] | models.Model]
67
- ):
68
- get_q = {self.model_pk_name: pk} if pk is not None else {}
69
- if getters:
70
- get_q |= getters
71
-
72
- obj_qs = self.model.objects.select_related()
73
- if isinstance(self.model, ModelSerializerMeta) and with_qs_request:
74
- obj_qs = await self.model.queryset_request(request)
75
-
76
- obj_qs = obj_qs.prefetch_related(*self.get_reverse_relations())
77
- if filters:
78
- obj_qs = obj_qs.filter(**filters)
79
-
80
- if not get_q:
81
- return obj_qs
82
-
83
- try:
84
- obj = await obj_qs.aget(**get_q)
85
- except ObjectDoesNotExist:
86
- raise SerializeError({self.model_name: "not found"}, 404)
87
-
88
- return obj
89
-
90
- def get_reverse_relations(self) -> list[str]:
91
- reverse_rels = []
92
- for f in self.serializable_fields:
93
- field_obj = getattr(self.model, f)
94
- if isinstance(field_obj, ManyToManyDescriptor):
95
- reverse_rels.append(f)
96
- continue
97
- if isinstance(field_obj, ReverseManyToOneDescriptor):
98
- reverse_rels.append(field_obj.field._related_name)
99
- continue
100
- if isinstance(field_obj, ReverseOneToOneDescriptor):
101
- reverse_rels.append(field_obj.related.name)
102
- return reverse_rels
103
-
104
- async def parse_input_data(self, request: HttpRequest, data: Schema):
105
- payload = data.model_dump(mode="json")
106
- customs = {}
107
- optionals = []
108
- if isinstance(self.model, ModelSerializerMeta):
109
- customs = {k: v for k, v in payload.items() if self.model.is_custom(k)}
110
- optionals = [
111
- k for k, v in payload.items() if self.model.is_optional(k) and v is None
112
- ]
113
- for k, v in payload.items():
114
- if isinstance(self.model, ModelSerializerMeta):
115
- if self.model.is_custom(k):
116
- continue
117
- if self.model.is_optional(k) and v is None:
118
- continue
119
- field_obj = (await agetattr(self.model, k)).field
120
- if isinstance(field_obj, models.BinaryField):
121
- try:
122
- payload |= {k: base64.b64decode(v)}
123
- except Exception as exc:
124
- raise SerializeError({k: ". ".join(exc.args)}, 400)
125
- if isinstance(field_obj, models.ForeignKey):
126
- rel_util = ModelUtil(field_obj.related_model)
127
- rel: ModelSerializer = await rel_util.get_object(
128
- request, v, with_qs_request=False
129
- )
130
- payload |= {k: rel}
131
- new_payload = {
132
- k: v for k, v in payload.items() if k not in (customs.keys() or optionals)
133
- }
134
- return new_payload, customs
135
-
136
- async def parse_output_data(self, request: HttpRequest, data: Schema):
137
- olds_k: list[dict] = []
138
- payload = data.model_dump(mode="json")
139
- for k, v in payload.items():
140
- try:
141
- field_obj = (await agetattr(self.model, k)).field
142
- except AttributeError:
143
- try:
144
- field_obj = (await agetattr(self.model, k)).related
145
- except AttributeError:
146
- pass
147
- if isinstance(v, dict) and (
148
- isinstance(field_obj, models.ForeignKey)
149
- or isinstance(field_obj, models.OneToOneField)
150
- ):
151
- rel_util = ModelUtil(field_obj.related_model)
152
- rel: ModelSerializer = await rel_util.get_object(
153
- request, v.get(rel_util.model_pk_name)
154
- )
155
- if isinstance(field_obj, models.ForeignKey):
156
- for rel_k, rel_v in v.items():
157
- field_rel_obj = await agetattr(rel, rel_k)
158
- if isinstance(field_rel_obj, models.ForeignKey):
159
- olds_k.append({rel_k: rel_v})
160
- for obj in olds_k:
161
- for old_k, old_v in obj.items():
162
- v.pop(old_k)
163
- v |= {f"{old_k}_id": old_v}
164
- olds_k = []
165
- payload |= {k: rel}
166
- return payload
167
-
168
- async def create_s(self, request: HttpRequest, data: Schema, obj_schema: Schema):
169
- payload, customs = await self.parse_input_data(request, data)
170
- pk = (await self.model.objects.acreate(**payload)).pk
171
- obj = await self.get_object(request, pk)
172
- if isinstance(self.model, ModelSerializerMeta):
173
- await asyncio.gather(obj.custom_actions(customs), obj.post_create())
174
- return await self.read_s(request, obj, obj_schema)
175
-
176
- async def read_s(
177
- self,
178
- request: HttpRequest,
179
- obj: type["ModelSerializer"],
180
- obj_schema: Schema,
181
- ):
182
- if obj_schema is None:
183
- raise SerializeError({"obj_schema": "must be provided"}, 400)
184
- return await self.parse_output_data(
185
- request, await sync_to_async(obj_schema.from_orm)(obj)
186
- )
187
-
188
- async def update_s(
189
- self, request: HttpRequest, data: Schema, pk: int | str, obj_schema: Schema
190
- ):
191
- obj = await self.get_object(request, pk)
192
- payload, customs = await self.parse_input_data(request, data)
193
- for k, v in payload.items():
194
- if v is not None:
195
- setattr(obj, k, v)
196
- if isinstance(self.model, ModelSerializerMeta):
197
- await obj.custom_actions(customs)
198
- await obj.asave()
199
- updated_object = await self.get_object(request, pk)
200
- return await self.read_s(request, updated_object, obj_schema)
201
-
202
- async def delete_s(self, request: HttpRequest, pk: int | str):
203
- obj = await self.get_object(request, pk)
204
- await obj.adelete()
205
- return None
206
-
207
-
208
- class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
209
- class Meta:
210
- abstract = True
211
-
212
- class CreateSerializer:
213
- fields: list[str] = []
214
- customs: list[tuple[str, type, Any]] = []
215
- optionals: list[tuple[str, type]] = []
216
- excludes: list[str] = []
217
-
218
- class ReadSerializer:
219
- fields: list[str] = []
220
- excludes: list[str] = []
221
- customs: list[tuple[str, type, Any]] = []
222
-
223
- class UpdateSerializer:
224
- fields: list[str] = []
225
- customs: list[tuple[str, type, Any]] = []
226
- optionals: list[tuple[str, type]] = []
227
- excludes: list[str] = []
228
-
229
- @property
230
- def has_custom_fields_create(self):
231
- return hasattr(self.CreateSerializer, "customs")
232
-
233
- @property
234
- def has_custom_fields_update(self):
235
- return hasattr(self.UpdateSerializer, "customs")
236
-
237
- @property
238
- def has_custom_fields(self):
239
- return self.has_custom_fields_create or self.has_custom_fields_update
240
-
241
- @property
242
- def has_optional_fields_create(self):
243
- return hasattr(self.CreateSerializer, "optionals")
244
-
245
- @property
246
- def has_optional_fields_update(self):
247
- return hasattr(self.UpdateSerializer, "optionals")
248
-
249
- @property
250
- def has_optional_fields(self):
251
- return self.has_optional_fields_create or self.has_optional_fields_update
252
-
253
- @classmethod
254
- def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
255
- match s_type:
256
- case "create":
257
- fields = getattr(cls.CreateSerializer, f_type, [])
258
- case "update":
259
- fields = getattr(cls.UpdateSerializer, f_type, [])
260
- case "read":
261
- fields = getattr(cls.ReadSerializer, f_type, [])
262
- return fields
263
-
264
- @classmethod
265
- def _is_special_field(
266
- cls, s_type: type[S_TYPES], field: str, f_type: type[F_TYPES]
267
- ):
268
- special_fields = cls._get_fields(s_type, f_type)
269
- return any(field in special_f for special_f in special_fields)
270
-
271
- @classmethod
272
- def _generate_model_schema(
273
- cls,
274
- schema_type: type[SCHEMA_TYPES],
275
- depth: int = None,
276
- ) -> Schema:
277
- match schema_type:
278
- case "In":
279
- s_type = "create"
280
- case "Patch":
281
- s_type = "update"
282
- case "Out":
283
- fields, reverse_rels, excludes, customs = cls.get_schema_out_data()
284
- if not fields and not reverse_rels and not excludes and not customs:
285
- return None
286
- return create_schema(
287
- model=cls,
288
- name=f"{cls._meta.model_name}SchemaOut",
289
- depth=depth,
290
- fields=fields,
291
- custom_fields=reverse_rels + customs,
292
- exclude=excludes,
293
- )
294
- case "Related":
295
- fields, customs = cls.get_related_schema_data()
296
- if not fields and not customs:
297
- return None
298
- return create_schema(
299
- model=cls,
300
- name=f"{cls._meta.model_name}SchemaRelated",
301
- fields=fields,
302
- custom_fields=customs,
303
- )
304
-
305
- fields = cls.get_fields(s_type)
306
- optionals = cls.get_optional_fields(s_type)
307
- customs = cls.get_custom_fields(s_type) + optionals
308
- excludes = cls.get_excluded_fields(s_type)
309
- if not fields and not excludes:
310
- fields = [f[0] for f in optionals]
311
- return (
312
- create_schema(
313
- model=cls,
314
- name=f"{cls._meta.model_name}Schema{schema_type}",
315
- fields=fields,
316
- custom_fields=customs,
317
- exclude=excludes,
318
- )
319
- if fields or customs or excludes
320
- else None
321
- )
322
-
323
- @classmethod
324
- def verbose_name_path_resolver(cls) -> str:
325
- return "-".join(cls._meta.verbose_name_plural.split(" "))
326
-
327
- def has_changed(self, field: str) -> bool:
328
- """
329
- Check if a model field has changed
330
- """
331
- if not self.pk:
332
- return False
333
- old_value = (
334
- self.__class__._default_manager.filter(pk=self.pk)
335
- .values(field)
336
- .get()[field]
337
- )
338
- return getattr(self, field) != old_value
339
-
340
- @classmethod
341
- async def queryset_request(cls, request: HttpRequest):
342
- """
343
- Override this method to return a filtered queryset based
344
- on the request received
345
- """
346
- return cls.objects.select_related().all()
347
-
348
- async def post_create(self) -> None:
349
- """
350
- Override this method to execute code after the object
351
- has been created
352
- """
353
- pass
354
-
355
- async def custom_actions(self, payload: dict[str, Any]):
356
- """
357
- Override this method to execute custom actions based on
358
- custom given fields. It could be useful for post create method.
359
- """
360
- pass
361
-
362
- @classmethod
363
- def get_related_schema_data(cls):
364
- fields = cls.get_fields("read")
365
- custom_f = {
366
- name: (value, default)
367
- for name, value, default in cls.get_custom_fields("read")
368
- }
369
- _related_fields = []
370
- for f in fields + list(custom_f.keys()):
371
- field_obj = getattr(cls, f)
372
- if not isinstance(
373
- field_obj,
374
- (
375
- ManyToManyDescriptor,
376
- ReverseManyToOneDescriptor,
377
- ReverseOneToOneDescriptor,
378
- ForwardManyToOneDescriptor,
379
- ForwardOneToOneDescriptor,
380
- ),
381
- ):
382
- _related_fields.append(f)
383
-
384
- if not _related_fields:
385
- return None, None
386
-
387
- custom_related_fields = [
388
- (f, *custom_f[f]) for f in _related_fields if f in custom_f
389
- ]
390
- related_fields = [f for f in _related_fields if f not in custom_f]
391
- return related_fields, custom_related_fields
392
-
393
- @classmethod
394
- def get_schema_out_data(cls):
395
- fields = []
396
- reverse_rels = []
397
- rels = []
398
- for f in cls.get_fields("read"):
399
- field_obj = getattr(cls, f)
400
- if isinstance(
401
- field_obj,
402
- (
403
- ManyToManyDescriptor,
404
- ReverseManyToOneDescriptor,
405
- ReverseOneToOneDescriptor,
406
- ),
407
- ):
408
- if isinstance(field_obj, ManyToManyDescriptor):
409
- rel_obj: ModelSerializer = field_obj.field.related_model
410
- if field_obj.reverse:
411
- rel_obj = field_obj.field.model
412
- rel_type = "many"
413
- elif isinstance(field_obj, ReverseManyToOneDescriptor):
414
- rel_obj = field_obj.field.model
415
- rel_type = "many"
416
- else: # ReverseOneToOneDescriptor
417
- rel_obj = field_obj.related.related_model
418
- rel_type = "one"
419
- if not isinstance(rel_obj, ModelSerializerMeta):
420
- continue
421
- if not rel_obj.get_fields("read") and not rel_obj.get_custom_fields(
422
- "read"
423
- ):
424
- continue
425
- rel_schema = (
426
- rel_obj.generate_related_s()
427
- if rel_type != "many"
428
- else list[rel_obj.generate_related_s()]
429
- )
430
- rel_data = (f, rel_schema | None, None)
431
- reverse_rels.append(rel_data)
432
- continue
433
- if isinstance(
434
- field_obj, (ForwardOneToOneDescriptor, ForwardManyToOneDescriptor)
435
- ):
436
- rel_obj = field_obj.field.related_model
437
- if not isinstance(rel_obj, ModelSerializerMeta):
438
- fields.append(f)
439
- continue
440
- if not rel_obj.get_fields("read") and not rel_obj.get_custom_fields(
441
- "read"
442
- ):
443
- continue
444
- rel_data = (f, rel_obj.generate_related_s() | None, None)
445
- rels.append(rel_data)
446
- continue
447
- fields.append(f)
448
- return (
449
- fields,
450
- reverse_rels,
451
- cls.get_excluded_fields("read"),
452
- cls.get_custom_fields("read") + rels,
453
- )
454
-
455
- @classmethod
456
- def is_custom(cls, field: str):
457
- return cls._is_special_field(
458
- "create", field, "customs"
459
- ) or cls._is_special_field("update", field, "customs")
460
-
461
- @classmethod
462
- def is_optional(cls, field: str):
463
- return cls._is_special_field(
464
- "create", field, "optionals"
465
- ) or cls._is_special_field("update", field, "optionals")
466
-
467
- @classmethod
468
- def get_custom_fields(cls, s_type: type[S_TYPES]) -> list[tuple]:
469
- return cls._get_fields(s_type, "customs")
470
-
471
- @classmethod
472
- def get_optional_fields(cls, s_type: type[S_TYPES]):
473
- return [
474
- (field, field_type, None)
475
- for field, field_type in cls._get_fields(s_type, "optionals")
476
- ]
477
-
478
- @classmethod
479
- def get_excluded_fields(cls, s_type: type[S_TYPES]):
480
- return cls._get_fields(s_type, "excludes")
481
-
482
- @classmethod
483
- def get_fields(cls, s_type: type[S_TYPES]):
484
- return cls._get_fields(s_type, "fields")
485
-
486
- @classmethod
487
- def generate_read_s(cls, depth: int = 1) -> Schema:
488
- return cls._generate_model_schema("Out", depth)
489
-
490
- @classmethod
491
- def generate_create_s(cls) -> Schema:
492
- return cls._generate_model_schema("In")
493
-
494
- @classmethod
495
- def generate_update_s(cls) -> Schema:
496
- return cls._generate_model_schema("Patch")
497
-
498
- @classmethod
499
- def generate_related_s(cls) -> Schema:
500
- return cls._generate_model_schema("Related")
501
-
502
- def after_save(self):
503
- """
504
- Override this method to execute code after the object
505
- has been saved
506
- """
507
- pass
508
-
509
- def before_save(self):
510
- """
511
- Override this method to execute code before the object
512
- has been saved
513
- """
514
- pass
515
-
516
- def on_create_after_save(self):
517
- """
518
- Override this method to execute code after the object
519
- has been created
520
- """
521
- pass
522
-
523
- def on_create_before_save(self):
524
- """
525
- Override this method to execute code before the object
526
- has been created
527
- """
528
- pass
529
-
530
- def on_delete(self):
531
- """
532
- Override this method to execute code after the object
533
- has been deleted
534
- """
535
- pass
536
-
537
- def save(self, *args, **kwargs):
538
- if self._state.adding:
539
- self.on_create_before_save()
540
- self.before_save()
541
- super().save(*args, **kwargs)
542
- if self._state.adding:
543
- self.on_create_after_save()
544
- self.after_save()
545
-
546
- def delete(self, *args, **kwargs):
547
- res = super().delete(*args, **kwargs)
548
- self.on_delete()
549
- return res