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.
- {django_ninja_aio_crud-0.2.2 → django_ninja_aio_crud-0.3.0}/PKG-INFO +1 -1
- {django_ninja_aio_crud-0.2.2 → django_ninja_aio_crud-0.3.0}/ninja_aio/__init__.py +1 -1
- {django_ninja_aio_crud-0.2.2 → django_ninja_aio_crud-0.3.0}/ninja_aio/models.py +139 -19
- django_ninja_aio_crud-0.3.0/ninja_aio/types.py +15 -0
- {django_ninja_aio_crud-0.2.2 → django_ninja_aio_crud-0.3.0}/ninja_aio/views.py +35 -16
- {django_ninja_aio_crud-0.2.2 → django_ninja_aio_crud-0.3.0}/README.md +0 -0
- {django_ninja_aio_crud-0.2.2 → django_ninja_aio_crud-0.3.0}/ninja_aio/api.py +0 -0
- {django_ninja_aio_crud-0.2.2 → django_ninja_aio_crud-0.3.0}/ninja_aio/auth.py +0 -0
- {django_ninja_aio_crud-0.2.2 → django_ninja_aio_crud-0.3.0}/ninja_aio/exceptions.py +0 -0
- {django_ninja_aio_crud-0.2.2 → django_ninja_aio_crud-0.3.0}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-0.2.2 → django_ninja_aio_crud-0.3.0}/ninja_aio/renders.py +0 -0
- {django_ninja_aio_crud-0.2.2 → django_ninja_aio_crud-0.3.0}/ninja_aio/schemas.py +0 -0
- {django_ninja_aio_crud-0.2.2 → django_ninja_aio_crud-0.3.0}/pyproject.toml +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import base64
|
|
2
|
-
from typing import Any
|
|
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
|
-
|
|
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={
|
|
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.
|
|
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 =
|
|
105
|
-
|
|
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 = [
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
214
|
+
f"{self.model_util.verbose_name_path_resolver()}/",
|
|
196
215
|
self.add_views(),
|
|
197
216
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|