django-ninja-aio-crud 0.2.2__tar.gz → 0.3.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-ninja-aio-crud
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
6
  Requires-Python: >=3.10
@@ -1,6 +1,6 @@
1
1
  """ Django Ninja AIO CRUD - Rest Framework """
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.3.0"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
@@ -1,5 +1,5 @@
1
1
  import base64
2
- from typing import Any, Literal
2
+ from typing import Any
3
3
 
4
4
  from ninja.schema import Schema
5
5
  from ninja.orm import create_schema
@@ -14,12 +14,147 @@ from django.db.models.fields.related_descriptors import (
14
14
  )
15
15
 
16
16
  from .exceptions import SerializeError
17
+ from .types import S_TYPES, REL_TYPES, ModelSerializerMeta
17
18
 
18
- S_TYPES = Literal["create", "update"]
19
- REL_TYPES = Literal["many", "one"]
20
19
 
20
+ class ModelUtil:
21
+ def __init__(self, model: type["ModelSerializer"] | models.Model):
22
+ self.model = model
21
23
 
22
- class ModelSerializer(models.Model):
24
+ @property
25
+ def serializable_fields(self):
26
+ if isinstance(self.model, ModelSerializerMeta):
27
+ return self.model.ReadSerializer.fields
28
+ return [field.name for field in self.model._meta.get_fields()]
29
+
30
+ def verbose_name_path_resolver(self) -> str:
31
+ return "-".join(self.model._meta.verbose_name_plural.split(" "))
32
+
33
+ async def get_object(self, request: HttpRequest, pk: int | str):
34
+ q = {self.model._meta.pk.attname: pk}
35
+ obj_qs = self.model.objects.select_related()
36
+ if isinstance(self.model, ModelSerializerMeta):
37
+ obj_qs = await self.model.queryset_request(request)
38
+ try:
39
+ obj = await obj_qs.prefetch_related(*self.get_reverse_relations()).aget(**q)
40
+ except ObjectDoesNotExist:
41
+ raise SerializeError({self.model._meta.model_name: "not found"}, 404)
42
+ return obj
43
+
44
+ def get_reverse_relations(self):
45
+ reverse_rels = []
46
+ for f in self.serializable_fields:
47
+ field_obj = getattr(self.model, f)
48
+ if isinstance(field_obj, ManyToManyDescriptor):
49
+ reverse_rels.append(f)
50
+ continue
51
+ if isinstance(field_obj, ReverseManyToOneDescriptor):
52
+ reverse_rels.append(field_obj.field._related_name)
53
+ continue
54
+ if isinstance(field_obj, ReverseOneToOneDescriptor):
55
+ reverse_rels.append(field_obj.related.name)
56
+ return reverse_rels
57
+
58
+ async def parse_input_data(self, request: HttpRequest, data: Schema):
59
+ payload = data.model_dump()
60
+ customs = {}
61
+ if isinstance(self.model, ModelSerializerMeta):
62
+ customs = {k: v for k, v in payload.items() if self.model.is_custom(k)}
63
+ for k, v in payload.items():
64
+ if isinstance(self.model, ModelSerializerMeta) and self.model.is_custom(k):
65
+ continue
66
+ field_obj = getattr(self.model, k).field
67
+ if isinstance(field_obj, models.BinaryField):
68
+ try:
69
+ payload |= {k: base64.b64decode(v)}
70
+ except Exception as exc:
71
+ raise SerializeError({k: ". ".join(exc.args)}, 400)
72
+ if isinstance(field_obj, models.ForeignKey):
73
+ rel_util = ModelUtil(field_obj.related_model)
74
+ rel: ModelSerializer = await rel_util.get_object(request, v)
75
+ payload |= {k: rel}
76
+ new_payload = {k: v for k, v in payload.items() if k not in customs}
77
+ return new_payload, customs
78
+
79
+ async def parse_output_data(self, request: HttpRequest, data: Schema):
80
+ olds_k: list[dict] = []
81
+ payload = data.model_dump()
82
+ for k, v in payload.items():
83
+ try:
84
+ field_obj = getattr(self.model, k).field
85
+ except AttributeError:
86
+ field_obj = getattr(self.model, k).related
87
+ if isinstance(v, dict) and (
88
+ isinstance(field_obj, models.ForeignKey)
89
+ or isinstance(field_obj, models.OneToOneField)
90
+ ):
91
+ rel_util = ModelUtil(field_obj.related_model)
92
+ rel: ModelSerializer = await rel_util.get_object(
93
+ request, list(v.values())[0]
94
+ )
95
+ if isinstance(field_obj, models.ForeignKey):
96
+ for rel_k, rel_v in v.items():
97
+ field_rel_obj = getattr(rel, rel_k)
98
+ if isinstance(field_rel_obj, models.ForeignKey):
99
+ olds_k.append({rel_k: rel_v})
100
+ for obj in olds_k:
101
+ for old_k, old_v in obj.items():
102
+ v.pop(old_k)
103
+ v |= {f"{old_k}_id": old_v}
104
+ olds_k = []
105
+ payload |= {k: rel}
106
+ return payload
107
+
108
+ async def create_s(self, request: HttpRequest, data: Schema, obj_schema: Schema):
109
+ try:
110
+ payload, customs = await self.parse_input_data(request, data)
111
+ pk = (await self.model.objects.acreate(**payload)).pk
112
+ obj = await self.get_object(request, pk)
113
+ except SerializeError as e:
114
+ return e.status_code, e.error
115
+ if isinstance(self.model, ModelSerializerMeta):
116
+ await obj.custom_actions(customs)
117
+ await obj.post_create()
118
+ return 201, await self.read_s(request, obj, obj_schema)
119
+
120
+ async def read_s(
121
+ self,
122
+ request: HttpRequest,
123
+ obj: type["ModelSerializer"],
124
+ obj_schema: Schema,
125
+ ):
126
+ if obj_schema is None:
127
+ raise SerializeError({"obj_schema": "must be provided"}, 400)
128
+ return await self.parse_output_data(request, obj_schema.from_orm(obj))
129
+
130
+ async def update_s(
131
+ self, request: HttpRequest, data: Schema, pk: int | str, obj_schema: Schema
132
+ ):
133
+ try:
134
+ obj = await self.get_object(request, pk)
135
+ except SerializeError as e:
136
+ return e.status_code, e.error
137
+
138
+ payload, customs = await self.parse_input_data(request, data)
139
+ for k, v in payload.items():
140
+ if v is not None:
141
+ setattr(obj, k, v)
142
+ if isinstance(self.model, ModelSerializerMeta):
143
+ await obj.custom_actions(customs)
144
+ await obj.asave()
145
+ updated_object = await self.get_object(request, pk)
146
+ return await self.read_s(request, updated_object, obj_schema)
147
+
148
+ async def delete_s(self, request: HttpRequest, pk: int | str):
149
+ try:
150
+ obj = await self.get_object(request, pk)
151
+ except SerializeError as e:
152
+ return e.status_code, e.error
153
+ await obj.adelete()
154
+ return HttpResponse(status=204)
155
+
156
+
157
+ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
23
158
  class Meta:
24
159
  abstract = True
25
160
 
@@ -85,21 +220,6 @@ class ModelSerializer(models.Model):
85
220
  """
86
221
  pass
87
222
 
88
- @classmethod
89
- def get_reverse_relations(cls):
90
- reverse_rels = []
91
- for f in cls.ReadSerializer.fields:
92
- field_obj = getattr(cls, f)
93
- if isinstance(field_obj, ManyToManyDescriptor):
94
- reverse_rels.append(f)
95
- continue
96
- if isinstance(field_obj, ReverseManyToOneDescriptor):
97
- reverse_rels.append(field_obj.field._related_name)
98
- continue
99
- if isinstance(field_obj, ReverseOneToOneDescriptor):
100
- reverse_rels.append(field_obj.related.name)
101
- return reverse_rels
102
-
103
223
  @classmethod
104
224
  def get_reverse_relation_schema(
105
225
  cls, obj: type["ModelSerializer"], rel_type: type[REL_TYPES], field: str
@@ -0,0 +1,15 @@
1
+ from typing import Literal
2
+
3
+ from django.db.models import Model
4
+
5
+ S_TYPES = Literal["create", "update"]
6
+ REL_TYPES = Literal["many", "one"]
7
+
8
+
9
+ class ModelSerializerType(type):
10
+ def __repr__(self):
11
+ return self.__name__
12
+
13
+
14
+ class ModelSerializerMeta(ModelSerializerType, type(Model)):
15
+ pass
@@ -1,13 +1,15 @@
1
1
  from typing import List
2
2
 
3
- from ninja import NinjaAPI, Router
3
+ from ninja import NinjaAPI, Router, Schema
4
4
  from ninja.constants import NOT_SET
5
5
  from ninja.pagination import paginate, AsyncPaginationBase, PageNumberPagination
6
6
  from django.http import HttpRequest
7
+ from django.db.models import Model
7
8
 
8
- from .models import ModelSerializer
9
+ from .models import ModelSerializer, ModelUtil
9
10
  from .schemas import GenericMessageSchema
10
11
  from .exceptions import SerializeError
12
+ from .types import ModelSerializerMeta
11
13
 
12
14
  ERROR_CODES = frozenset({400, 401, 404, 428})
13
15
 
@@ -65,28 +67,39 @@ class APIView:
65
67
 
66
68
 
67
69
  class APIViewSet:
68
- model: ModelSerializer
70
+ model: ModelSerializer | Model
69
71
  api: NinjaAPI
72
+ schema_in: Schema | None = None
73
+ schema_out: Schema | None = None
74
+ schema_update: Schema | None = None
70
75
  auths: list | None = NOT_SET
71
76
  pagination_class: type[AsyncPaginationBase] = PageNumberPagination
72
77
 
73
78
  def __init__(self) -> None:
74
79
  self.router = Router(tags=[self.model._meta.model_name.capitalize()])
75
- self.schema_in = self.model.generate_create_s()
76
- self.schema_out = self.model.generate_read_s()
77
- self.schema_update = self.model.generate_update_s()
78
80
  self.path = "/"
79
81
  self.path_retrieve = f"{self.model._meta.pk.attname}/"
80
82
  self.error_codes = ERROR_CODES
83
+ self.model_util = ModelUtil(self.model)
84
+ self.schema_out, self.schema_update, self.schema_in = self.get_schemas()
85
+
86
+ def get_schemas(self):
87
+ if isinstance(self.model, ModelSerializerMeta):
88
+ return (
89
+ self.model.generate_read_s(),
90
+ self.model.generate_update_s(),
91
+ self.model.generate_create_s(),
92
+ )
93
+ return self.schema_out, self.schema_update, self.schema_in
81
94
 
82
95
  def create_view(self):
83
96
  @self.router.post(
84
97
  self.path,
85
98
  auth=self.auths,
86
- response={200: self.schema_out, self.error_codes: GenericMessageSchema},
99
+ response={201: self.schema_out, self.error_codes: GenericMessageSchema},
87
100
  )
88
101
  async def create(request: HttpRequest, data: self.schema_in):
89
- return await self.model.create_s(request, data)
102
+ return await self.model_util.create_s(request, data, self.schema_out)
90
103
 
91
104
  create.__name__ = f"create_{self.model._meta.model_name}"
92
105
 
@@ -101,11 +114,17 @@ class APIViewSet:
101
114
  )
102
115
  @paginate(self.pagination_class)
103
116
  async def list(request: HttpRequest):
104
- qs = await self.model.queryset_request(request)
105
- rels = self.model.get_reverse_relations()
117
+ qs = self.model.objects.select_related()
118
+ if isinstance(self.model, ModelSerializerMeta):
119
+ qs = await self.model.queryset_request(request)
120
+ rels = self.model_util.get_reverse_relations()
121
+ print(rels)
106
122
  if len(rels) > 0:
107
123
  qs = qs.prefetch_related(*rels)
108
- objs = [await self.model.read_s(request, obj) async for obj in qs.all()]
124
+ objs = [
125
+ await self.model_util.read_s(request, obj, self.schema_out)
126
+ async for obj in qs.all()
127
+ ]
109
128
  return objs
110
129
 
111
130
  list.__name__ = f"list_{self.model._meta.verbose_name_plural}"
@@ -118,10 +137,10 @@ class APIViewSet:
118
137
  )
119
138
  async def retrieve(request: HttpRequest, pk: int | str):
120
139
  try:
121
- obj = await self.model.get_object(request, pk)
140
+ obj = await self.model_util.get_object(request, pk)
122
141
  except SerializeError as e:
123
142
  return e.status_code, e.error
124
- return await self.model.read_s(request, obj)
143
+ return await self.model_util.read_s(request, obj, self.schema_out)
125
144
 
126
145
  retrieve.__name__ = f"retrieve_{self.model._meta.model_name}"
127
146
 
@@ -132,7 +151,7 @@ class APIViewSet:
132
151
  response={200: self.schema_out, self.error_codes: GenericMessageSchema},
133
152
  )
134
153
  async def update(request: HttpRequest, data: self.schema_update, pk: int | str):
135
- return await self.model.update_s(request, data, pk)
154
+ return await self.model_util.update_s(request, data, pk, self.schema_out)
136
155
 
137
156
  update.__name__ = f"update_{self.model._meta.model_name}"
138
157
 
@@ -143,7 +162,7 @@ class APIViewSet:
143
162
  response={204: None, self.error_codes: GenericMessageSchema},
144
163
  )
145
164
  async def delete(request: HttpRequest, pk: int | str):
146
- return await self.model.delete_s(request, pk)
165
+ return await self.model_util.delete_s(request, pk)
147
166
 
148
167
  delete.__name__ = f"delete_{self.model._meta.model_name}"
149
168
 
@@ -192,6 +211,6 @@ class APIViewSet:
192
211
 
193
212
  def add_views_to_route(self):
194
213
  return self.api.add_router(
195
- f"{self.model.verbose_name_path_resolver()}/",
214
+ f"{self.model_util.verbose_name_path_resolver()}/",
196
215
  self.add_views(),
197
216
  )