django-ninja-aio-crud 0.6.3__py3-none-any.whl → 0.7.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.
- {django_ninja_aio_crud-0.6.3.dist-info → django_ninja_aio_crud-0.7.0.dist-info}/METADATA +4 -4
- {django_ninja_aio_crud-0.6.3.dist-info → django_ninja_aio_crud-0.7.0.dist-info}/RECORD +6 -5
- ninja_aio/__init__.py +1 -1
- ninja_aio/models.py +51 -2
- ninja_aio/views.py +271 -0
- {django_ninja_aio_crud-0.6.3.dist-info → django_ninja_aio_crud-0.7.0.dist-info}/WHEEL +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: django-ninja-aio-crud
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Django Ninja AIO CRUD - Rest Framework
|
|
5
5
|
Author: Giuseppe Casillo
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -472,7 +472,7 @@ api = NinjaAIO()
|
|
|
472
472
|
class FooAPI(APIViewSet):
|
|
473
473
|
model = Foo
|
|
474
474
|
api = api
|
|
475
|
-
|
|
475
|
+
auth = CustomJWTBearer()
|
|
476
476
|
|
|
477
477
|
|
|
478
478
|
class ExampleSchemaOut(Schema):
|
|
@@ -488,10 +488,10 @@ class SumView(APIView):
|
|
|
488
488
|
api = api
|
|
489
489
|
api_router_path = "numbers-sum/"
|
|
490
490
|
router_tag = "Sum"
|
|
491
|
-
|
|
491
|
+
auth = CustomJWTBearer()
|
|
492
492
|
|
|
493
493
|
def views(self):
|
|
494
|
-
@self.router.post("/", response={200: ExampleSchemaOut}, auth=self.
|
|
494
|
+
@self.router.post("/", response={200: ExampleSchemaOut}, auth=self.auth)
|
|
495
495
|
async def sum(request: HttpRequest, data: ExampleSchemaIn):
|
|
496
496
|
return 200, {sum: data.n1 + data.n2}
|
|
497
497
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
ninja_aio/__init__.py,sha256=
|
|
1
|
+
ninja_aio/__init__.py,sha256=0hUbl6e46JSJW76-kB74s2Vu6nXYrv6r9Jaq_ihI3SQ,119
|
|
2
2
|
ninja_aio/api.py,sha256=Fe6l3YCy7MW5TY4-Lbl80CFuK2NT2Y7tHfmqPk6Mqak,1735
|
|
3
3
|
ninja_aio/auth.py,sha256=fKboioU4sezPukKJukIwiboxml_KV7irhCH3vGYt5pU,1008
|
|
4
4
|
ninja_aio/exceptions.py,sha256=gPnZX1Do2GXudbU8wDYkwhO70Qj0ZNrIJJ2UXRs9vYk,2241
|
|
5
|
-
ninja_aio/models.py,sha256=
|
|
5
|
+
ninja_aio/models.py,sha256=R_CrpLlwEQjR10VBTIt8YStzOwBHfhuo1E_xFSNxlrE,17628
|
|
6
6
|
ninja_aio/parsers.py,sha256=e_4lGCPV7zs-HTqtdJTc8yQD2KPAn9njbL8nF_Mmgkc,153
|
|
7
7
|
ninja_aio/renders.py,sha256=mHeKNJtmDhZmgFpS9B6SPn5uZFcyVXrsoMhr149LeW8,1555
|
|
8
8
|
ninja_aio/schemas.py,sha256=EgRkfhnzZqwGvdBmqlZixMtMcoD1ZxV_qzJ3fmaAy20,113
|
|
9
9
|
ninja_aio/types.py,sha256=EHznS-6KWLwSX5hLeXbAi7qHWla09_rGeQraiLpH-aY,491
|
|
10
|
-
|
|
11
|
-
django_ninja_aio_crud-0.
|
|
12
|
-
django_ninja_aio_crud-0.
|
|
10
|
+
ninja_aio/views.py,sha256=qROag0OsMeVUXcnUYKV7ZmWj4E0F-QH52PI2-56zVRs,9263
|
|
11
|
+
django_ninja_aio_crud-0.7.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
|
|
12
|
+
django_ninja_aio_crud-0.7.0.dist-info/METADATA,sha256=bo1TL2NTpz3T3vsh0aPw_fpcrR5GUx1qqB36S6-HECY,13737
|
|
13
|
+
django_ninja_aio_crud-0.7.0.dist-info/RECORD,,
|
ninja_aio/__init__.py
CHANGED
ninja_aio/models.py
CHANGED
|
@@ -11,6 +11,8 @@ from django.db.models.fields.related_descriptors import (
|
|
|
11
11
|
ReverseManyToOneDescriptor,
|
|
12
12
|
ReverseOneToOneDescriptor,
|
|
13
13
|
ManyToManyDescriptor,
|
|
14
|
+
ForwardManyToOneDescriptor,
|
|
15
|
+
ForwardOneToOneDescriptor,
|
|
14
16
|
)
|
|
15
17
|
|
|
16
18
|
from .exceptions import SerializeError
|
|
@@ -99,7 +101,7 @@ class ModelUtil:
|
|
|
99
101
|
if isinstance(self.model, ModelSerializerMeta):
|
|
100
102
|
if self.model.is_custom(k):
|
|
101
103
|
continue
|
|
102
|
-
if self.model.is_optional(k) and
|
|
104
|
+
if self.model.is_optional(k) and v is None:
|
|
103
105
|
continue
|
|
104
106
|
field_obj = getattr(self.model, k).field
|
|
105
107
|
if isinstance(field_obj, models.BinaryField):
|
|
@@ -332,6 +334,41 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
|
|
|
332
334
|
"""
|
|
333
335
|
pass
|
|
334
336
|
|
|
337
|
+
@classmethod
|
|
338
|
+
def get_forward_relation_schema(cls, obj: type["ModelSerializer"], field: str):
|
|
339
|
+
cls_f = []
|
|
340
|
+
for rel_f in obj.ReadSerializer.fields:
|
|
341
|
+
rel_f_obj = getattr(obj, rel_f)
|
|
342
|
+
if isinstance(
|
|
343
|
+
rel_f_obj,
|
|
344
|
+
(
|
|
345
|
+
ManyToManyDescriptor,
|
|
346
|
+
ReverseManyToOneDescriptor,
|
|
347
|
+
ReverseOneToOneDescriptor,
|
|
348
|
+
),
|
|
349
|
+
):
|
|
350
|
+
if isinstance(rel_f_obj, ManyToManyDescriptor):
|
|
351
|
+
rel_obj: ModelSerializer = rel_f_obj.field.related_model
|
|
352
|
+
if rel_f_obj.reverse:
|
|
353
|
+
rel_obj = rel_f_obj.field.model
|
|
354
|
+
elif isinstance(rel_f_obj, ReverseManyToOneDescriptor):
|
|
355
|
+
rel_obj = rel_f_obj.field.model
|
|
356
|
+
else: # ReverseOneToOneDescriptor
|
|
357
|
+
rel_obj = rel_f_obj.related.related_model
|
|
358
|
+
if not rel_obj == cls:
|
|
359
|
+
continue
|
|
360
|
+
cls_f.append(rel_f)
|
|
361
|
+
obj.ReadSerializer.fields.remove(rel_f)
|
|
362
|
+
rel_schema = obj.generate_read_s(depth=0)
|
|
363
|
+
rel_data = (
|
|
364
|
+
field,
|
|
365
|
+
rel_schema | None,
|
|
366
|
+
None,
|
|
367
|
+
)
|
|
368
|
+
if len(cls_f) > 0:
|
|
369
|
+
obj.ReadSerializer.fields.append(*cls_f)
|
|
370
|
+
return rel_data
|
|
371
|
+
|
|
335
372
|
@classmethod
|
|
336
373
|
def get_reverse_relation_schema(
|
|
337
374
|
cls, obj: type["ModelSerializer"], rel_type: type[REL_TYPES], field: str
|
|
@@ -372,6 +409,7 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
|
|
|
372
409
|
def get_schema_out_data(cls):
|
|
373
410
|
fields = []
|
|
374
411
|
reverse_rels = []
|
|
412
|
+
rels = []
|
|
375
413
|
for f in cls.get_fields("read"):
|
|
376
414
|
field_obj = getattr(cls, f)
|
|
377
415
|
if isinstance(
|
|
@@ -394,15 +432,26 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
|
|
|
394
432
|
rel_obj = field_obj.related.related_model
|
|
395
433
|
rel_type = "one"
|
|
396
434
|
|
|
435
|
+
if not rel_obj.get_fields("read") or not rel_obj.get_custom_fields("read"):
|
|
436
|
+
continue
|
|
397
437
|
rel_data = cls.get_reverse_relation_schema(rel_obj, rel_type, f)
|
|
398
438
|
reverse_rels.append(rel_data)
|
|
399
439
|
continue
|
|
440
|
+
if isinstance(
|
|
441
|
+
field_obj, (ForwardOneToOneDescriptor, ForwardManyToOneDescriptor)
|
|
442
|
+
):
|
|
443
|
+
rel_obj = field_obj.field.related_model
|
|
444
|
+
if not rel_obj.get_fields("read") or not rel_obj.get_custom_fields("read"):
|
|
445
|
+
continue
|
|
446
|
+
rel_data = cls.get_forward_relation_schema(rel_obj, f)
|
|
447
|
+
rels.append(rel_data)
|
|
448
|
+
continue
|
|
400
449
|
fields.append(f)
|
|
401
450
|
return (
|
|
402
451
|
fields,
|
|
403
452
|
reverse_rels,
|
|
404
453
|
cls.get_excluded_fields("read"),
|
|
405
|
-
cls.get_custom_fields("read"),
|
|
454
|
+
cls.get_custom_fields("read") + rels,
|
|
406
455
|
)
|
|
407
456
|
|
|
408
457
|
@classmethod
|
ninja_aio/views.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from ninja import NinjaAPI, Router, Schema, Path, Query
|
|
4
|
+
from ninja.constants import NOT_SET
|
|
5
|
+
from ninja.pagination import paginate, AsyncPaginationBase, PageNumberPagination
|
|
6
|
+
from django.http import HttpRequest
|
|
7
|
+
from django.db.models import Model, QuerySet
|
|
8
|
+
from pydantic import create_model
|
|
9
|
+
|
|
10
|
+
from .models import ModelSerializer, ModelUtil
|
|
11
|
+
from .schemas import GenericMessageSchema
|
|
12
|
+
from .types import ModelSerializerMeta, VIEW_TYPES
|
|
13
|
+
|
|
14
|
+
ERROR_CODES = frozenset({400, 401, 404, 428})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class APIView:
|
|
18
|
+
api: NinjaAPI
|
|
19
|
+
router_tag: str
|
|
20
|
+
api_route_path: str
|
|
21
|
+
auth: list | None = NOT_SET
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self.router = Router(tags=[self.router_tag])
|
|
25
|
+
self.error_codes = ERROR_CODES
|
|
26
|
+
|
|
27
|
+
def views(self):
|
|
28
|
+
"""
|
|
29
|
+
Override this method to add your custom views. For example:
|
|
30
|
+
@self.router.get(some_path, response=some_schema)
|
|
31
|
+
async def some_method(request, *args, **kwargs):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
You can add multilple views just doing:
|
|
35
|
+
|
|
36
|
+
@self.router.get(some_path, response=some_schema)
|
|
37
|
+
async def some_method(request, *args, **kwargs):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@self.router.post(some_path, response=some_schema)
|
|
41
|
+
async def some_method(request, *args, **kwargs):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
If you provided a list of auths you can chose which of your views
|
|
45
|
+
should be authenticated:
|
|
46
|
+
|
|
47
|
+
AUTHENTICATED VIEW:
|
|
48
|
+
|
|
49
|
+
@self.router.get(some_path, response=some_schema, auth=self.auths)
|
|
50
|
+
async def some_method(request, *args, **kwargs):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
NOT AUTHENTICATED VIEW:
|
|
54
|
+
|
|
55
|
+
@self.router.post(some_path, response=some_schema)
|
|
56
|
+
async def some_method(request, *args, **kwargs):
|
|
57
|
+
pass
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def add_views(self):
|
|
61
|
+
self.views()
|
|
62
|
+
return self.router
|
|
63
|
+
|
|
64
|
+
def add_views_to_route(self):
|
|
65
|
+
return self.api.add_router(f"{self.api_route_path}/", self.add_views())
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class APIViewSet:
|
|
69
|
+
model: ModelSerializer | Model
|
|
70
|
+
api: NinjaAPI
|
|
71
|
+
schema_in: Schema | None = None
|
|
72
|
+
schema_out: Schema | None = None
|
|
73
|
+
schema_update: Schema | None = None
|
|
74
|
+
auth: list | None = NOT_SET
|
|
75
|
+
pagination_class: type[AsyncPaginationBase] = PageNumberPagination
|
|
76
|
+
query_params: dict[str, tuple[type, ...]] = {}
|
|
77
|
+
disable: list[type[VIEW_TYPES]] = []
|
|
78
|
+
|
|
79
|
+
def __init__(self) -> None:
|
|
80
|
+
self.error_codes = ERROR_CODES
|
|
81
|
+
self.model_util = ModelUtil(self.model)
|
|
82
|
+
self.schema_out, self.schema_in, self.schema_update = self.get_schemas()
|
|
83
|
+
self.path_schema = self._generate_path_schema()
|
|
84
|
+
self.filters_schema = self._generate_filters_schema()
|
|
85
|
+
self.router_tag = self.model_util.model_name.capitalize()
|
|
86
|
+
self.router = Router(tags=[self.router_tag])
|
|
87
|
+
self.path = "/"
|
|
88
|
+
self.path_retrieve = f"{{{self.model_util.model_pk_name}}}/"
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def _crud_views(self):
|
|
92
|
+
"""
|
|
93
|
+
key: view type (create, list, retrieve, update, delete or all)
|
|
94
|
+
value: tuple with schema and view method
|
|
95
|
+
"""
|
|
96
|
+
return {
|
|
97
|
+
"create": (self.schema_in, self.create_view),
|
|
98
|
+
"list": (self.schema_out, self.list_view),
|
|
99
|
+
"retrieve": (self.schema_out, self.retrieve_view),
|
|
100
|
+
"update": (self.schema_update, self.update_view),
|
|
101
|
+
"delete": (None, self.delete_view),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
def _generate_schema(self, fields: dict, name: str) -> Schema:
|
|
105
|
+
return create_model(f"{self.model_util.model_name}{name}", **fields)
|
|
106
|
+
|
|
107
|
+
def _generate_path_schema(self):
|
|
108
|
+
return self._generate_schema(
|
|
109
|
+
{self.model_util.model_pk_name: (int | str, ...)}, "PathSchema"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _generate_filters_schema(self):
|
|
113
|
+
return self._generate_schema(self.query_params, "FiltersSchema")
|
|
114
|
+
|
|
115
|
+
def _get_pk(self, data: Schema):
|
|
116
|
+
return data.model_dump()[self.model_util.model_pk_name]
|
|
117
|
+
|
|
118
|
+
def get_schemas(self):
|
|
119
|
+
if isinstance(self.model, ModelSerializerMeta):
|
|
120
|
+
return (
|
|
121
|
+
self.model.generate_read_s(),
|
|
122
|
+
self.model.generate_create_s(),
|
|
123
|
+
self.model.generate_update_s(),
|
|
124
|
+
)
|
|
125
|
+
return self.schema_out, self.schema_in, self.schema_update
|
|
126
|
+
|
|
127
|
+
async def query_params_handler(
|
|
128
|
+
self, queryset: QuerySet[ModelSerializer], filters: dict
|
|
129
|
+
):
|
|
130
|
+
"""
|
|
131
|
+
Override this method to handle request query params making queries to the database
|
|
132
|
+
based on filters or any other logic. This method should return a queryset. filters
|
|
133
|
+
are given already dumped by the schema.
|
|
134
|
+
"""
|
|
135
|
+
return queryset
|
|
136
|
+
|
|
137
|
+
def create_view(self):
|
|
138
|
+
@self.router.post(
|
|
139
|
+
self.path,
|
|
140
|
+
auth=self.auth,
|
|
141
|
+
response={201: self.schema_out, self.error_codes: GenericMessageSchema},
|
|
142
|
+
)
|
|
143
|
+
async def create(request: HttpRequest, data: self.schema_in):
|
|
144
|
+
return 201, await self.model_util.create_s(request, data, self.schema_out)
|
|
145
|
+
|
|
146
|
+
create.__name__ = f"create_{self.model_util.model_name}"
|
|
147
|
+
return create
|
|
148
|
+
|
|
149
|
+
def list_view(self):
|
|
150
|
+
@self.router.get(
|
|
151
|
+
self.path,
|
|
152
|
+
auth=self.auth,
|
|
153
|
+
response={
|
|
154
|
+
200: List[self.schema_out],
|
|
155
|
+
self.error_codes: GenericMessageSchema,
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
@paginate(self.pagination_class)
|
|
159
|
+
async def list(
|
|
160
|
+
request: HttpRequest, filters: Query[self.filters_schema] = None
|
|
161
|
+
):
|
|
162
|
+
qs = self.model.objects.select_related()
|
|
163
|
+
if isinstance(self.model, ModelSerializerMeta):
|
|
164
|
+
qs = await self.model.queryset_request(request)
|
|
165
|
+
rels = self.model_util.get_reverse_relations()
|
|
166
|
+
if len(rels) > 0:
|
|
167
|
+
qs = qs.prefetch_related(*rels)
|
|
168
|
+
if filters is not None:
|
|
169
|
+
qs = await self.query_params_handler(qs, filters.model_dump())
|
|
170
|
+
objs = [
|
|
171
|
+
await self.model_util.read_s(request, obj, self.schema_out)
|
|
172
|
+
async for obj in qs.all()
|
|
173
|
+
]
|
|
174
|
+
return objs
|
|
175
|
+
|
|
176
|
+
list.__name__ = f"list_{self.model_util.verbose_name_view_resolver()}"
|
|
177
|
+
return list
|
|
178
|
+
|
|
179
|
+
def retrieve_view(self):
|
|
180
|
+
@self.router.get(
|
|
181
|
+
self.path_retrieve,
|
|
182
|
+
auth=self.auth,
|
|
183
|
+
response={200: self.schema_out, self.error_codes: GenericMessageSchema},
|
|
184
|
+
)
|
|
185
|
+
async def retrieve(request: HttpRequest, pk: Path[self.path_schema]):
|
|
186
|
+
obj = await self.model_util.get_object(request, self._get_pk(pk))
|
|
187
|
+
return await self.model_util.read_s(request, obj, self.schema_out)
|
|
188
|
+
|
|
189
|
+
retrieve.__name__ = f"retrieve_{self.model_util.model_name}"
|
|
190
|
+
return retrieve
|
|
191
|
+
|
|
192
|
+
def update_view(self):
|
|
193
|
+
@self.router.patch(
|
|
194
|
+
self.path_retrieve,
|
|
195
|
+
auth=self.auth,
|
|
196
|
+
response={200: self.schema_out, self.error_codes: GenericMessageSchema},
|
|
197
|
+
)
|
|
198
|
+
async def update(
|
|
199
|
+
request: HttpRequest, data: self.schema_update, pk: Path[self.path_schema]
|
|
200
|
+
):
|
|
201
|
+
return await self.model_util.update_s(
|
|
202
|
+
request, data, self._get_pk(pk), self.schema_out
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
update.__name__ = f"update_{self.model_util.model_name}"
|
|
206
|
+
return update
|
|
207
|
+
|
|
208
|
+
def delete_view(self):
|
|
209
|
+
@self.router.delete(
|
|
210
|
+
self.path_retrieve,
|
|
211
|
+
auth=self.auth,
|
|
212
|
+
response={204: None, self.error_codes: GenericMessageSchema},
|
|
213
|
+
)
|
|
214
|
+
async def delete(request: HttpRequest, pk: Path[self.path_schema]):
|
|
215
|
+
return 204, await self.model_util.delete_s(request, self._get_pk(pk))
|
|
216
|
+
|
|
217
|
+
delete.__name__ = f"delete_{self.model_util.model_name}"
|
|
218
|
+
return delete
|
|
219
|
+
|
|
220
|
+
def views(self):
|
|
221
|
+
"""
|
|
222
|
+
Override this method to add your custom views. For example:
|
|
223
|
+
@self.router.get(some_path, response=some_schema)
|
|
224
|
+
async def some_method(request, *args, **kwargs):
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
You can add multilple views just doing:
|
|
228
|
+
|
|
229
|
+
@self.router.get(some_path, response=some_schema)
|
|
230
|
+
async def some_method(request, *args, **kwargs):
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
@self.router.post(some_path, response=some_schema)
|
|
234
|
+
async def some_method(request, *args, **kwargs):
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
If you provided a list of auths you can chose which of your views
|
|
238
|
+
should be authenticated:
|
|
239
|
+
|
|
240
|
+
AUTHENTICATED VIEW:
|
|
241
|
+
|
|
242
|
+
@self.router.get(some_path, response=some_schema, auth=self.auths)
|
|
243
|
+
async def some_method(request, *args, **kwargs):
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
NOT AUTHENTICATED VIEW:
|
|
247
|
+
|
|
248
|
+
@self.router.post(some_path, response=some_schema)
|
|
249
|
+
async def some_method(request, *args, **kwargs):
|
|
250
|
+
pass
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
def add_views(self):
|
|
254
|
+
if "all" in self.disable:
|
|
255
|
+
self.views()
|
|
256
|
+
return self.router
|
|
257
|
+
|
|
258
|
+
for views_type, (schema, view) in self._crud_views.items():
|
|
259
|
+
if views_type not in self.disable and (
|
|
260
|
+
schema is not None or views_type == "delete"
|
|
261
|
+
):
|
|
262
|
+
view()
|
|
263
|
+
|
|
264
|
+
self.views()
|
|
265
|
+
return self.router
|
|
266
|
+
|
|
267
|
+
def add_views_to_route(self):
|
|
268
|
+
return self.api.add_router(
|
|
269
|
+
f"{self.model_util.verbose_name_path_resolver()}/",
|
|
270
|
+
self.add_views(),
|
|
271
|
+
)
|
|
File without changes
|