django-ninja-aio-crud 0.6.3__tar.gz → 0.7.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.3
2
2
  Name: django-ninja-aio-crud
3
- Version: 0.6.3
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
- auths = CustomJWTBearer()
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
- auths = CustomJWTBearer()
491
+ auth = CustomJWTBearer()
492
492
 
493
493
  def views(self):
494
- @self.router.post("/", response={200: ExampleSchemaOut}, auth=self.auths)
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
 
@@ -441,7 +441,7 @@ api = NinjaAIO()
441
441
  class FooAPI(APIViewSet):
442
442
  model = Foo
443
443
  api = api
444
- auths = CustomJWTBearer()
444
+ auth = CustomJWTBearer()
445
445
 
446
446
 
447
447
  class ExampleSchemaOut(Schema):
@@ -457,10 +457,10 @@ class SumView(APIView):
457
457
  api = api
458
458
  api_router_path = "numbers-sum/"
459
459
  router_tag = "Sum"
460
- auths = CustomJWTBearer()
460
+ auth = CustomJWTBearer()
461
461
 
462
462
  def views(self):
463
- @self.router.post("/", response={200: ExampleSchemaOut}, auth=self.auths)
463
+ @self.router.post("/", response={200: ExampleSchemaOut}, auth=self.auth)
464
464
  async def sum(request: HttpRequest, data: ExampleSchemaIn):
465
465
  return 200, {sum: data.n1 + data.n2}
466
466
 
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "0.6.3"
3
+ __version__ = "0.7.0"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
@@ -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 k is None:
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
@@ -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
+ )