django-ninja-aio-crud 0.3.1__tar.gz → 0.5.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,7 +1,7 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-ninja-aio-crud
3
- Version: 0.3.1
4
- Summary: Django Ninja AIO CRUD - Rest Framework
3
+ Version: 0.5.0
4
+ Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
6
  Requires-Python: >=3.10
7
7
  Description-Content-Type: text/markdown
@@ -73,7 +73,7 @@ pip install django-ninja-aio-crud
73
73
  ### ModelSerializer
74
74
 
75
75
  - You can serialize your models using ModelSerializer and made them inherit from it. In your models.py import ModelSerializer
76
- ```Python
76
+ ```python
77
77
  # models.py
78
78
  from django.db import models
79
79
  from ninja_aio.models import ModelSerializer
@@ -95,7 +95,7 @@ class Foo(ModelSerializer):
95
95
 
96
96
  - ReadSerializer, CreateSerializer, UpdateSerializer are used to define which fields would be included in runtime schemas creation. You can also specify custom fields and handle their function by overriding custom_actions ModelSerializer's method(custom fields are only available for Create and Update serializers).
97
97
 
98
- ```Python
98
+ ```python
99
99
  # models.py
100
100
  from django.db import models
101
101
  from ninja_aio.models import ModelSerializer
@@ -130,17 +130,63 @@ class Foo(ModelSerializer):
130
130
 
131
131
  - post create method is a custom method that comes out to handle actions which will be excuted after that the object is created. It can be used, indeed, for example to handle custom fields' actions.
132
132
 
133
+ - You can also define optional fields for you Create and Update serializers (remember to give your optional fields a default). To declare an optional fields you have to give the field type too.
134
+ ```python
135
+ # models.py
136
+ from django.db import models
137
+ from ninja_aio.models import ModelSerializer
138
+
139
+
140
+ class Foo(ModelSerializer):
141
+ name = models.CharField(max_length=30)
142
+ bar = models.CharField(max_length=30, default="")
143
+ active = models.BooleanField(default=False)
144
+
145
+ class ReadSerializer:
146
+ fields = ["id", "name", "bar"]
147
+
148
+ class CreateSerializer:
149
+ fields = ["name"]
150
+ optionals = [("bar", str), ("active", bool)]
151
+
152
+ class UpdateSerializer:
153
+ optionals = [[("bar", str), ("active", bool)]
154
+ ```
155
+
156
+ - Instead of declaring your fields maybe you want to exclude some of them. Declaring "excludes" attribute into serializers will exclude the given fields. (You can declare only one between "fields" and "excludes").
157
+
158
+ ```python
159
+ # models.py
160
+ from django.db import models
161
+ from ninja_aio.models import ModelSerializer
162
+
163
+
164
+ class Foo(ModelSerializer):
165
+ name = models.CharField(max_length=30)
166
+ bar = models.CharField(max_length=30, default="")
167
+ active = models.BooleanField(default=False)
168
+
169
+ class ReadSerializer:
170
+ excludes = ["bar"]
171
+
172
+ class CreateSerializer:
173
+ fields = ["name"]
174
+ optionals = [("bar", str), ("active", bool)]
175
+
176
+ class UpdateSerializer:
177
+ excludes = ["id", "name"]
178
+ optionals = [[("bar", str), ("active", bool)]
179
+ ```
180
+
133
181
 
134
182
  ### APIViewSet
135
183
 
136
184
  - View class used to automatically generate CRUD views. in your views.py import APIViewSet and define your api using NinjaAIO class. NinjaAIO class uses built-in parser and renderer which use orjson for data serialization.
137
185
 
138
- ```Python
186
+ ```python
139
187
  # views.py
140
188
  from ninja_aio import NinjaAIO
141
189
  from ninja_aio.views import APIViewSet
142
- from ninja_aio.parsers import ORJSONParser
143
- from ninja_aio.renders import ORJSONRender
144
190
 
145
191
  from .models import Foo
146
192
 
@@ -157,13 +203,11 @@ FooAPI().add_views_to_route()
157
203
 
158
204
  - and that's it, your model CRUD will be automatically created. You can also add custom views to CRUD overriding the built-in method "views".
159
205
 
160
- ```Python
206
+ ```python
161
207
  # views.py
162
208
  from ninja import Schema
163
209
  from ninja_aio import NinjaAIO
164
210
  from ninja_aio.views import APIViewSet
165
- from ninja_aio.parsers import ORJSONParser
166
- from ninja_aio.renders import ORJSONRender
167
211
 
168
212
  from .models import Foo
169
213
 
@@ -189,6 +233,27 @@ class FooAPI(APIViewSet):
189
233
  return 200, {sum: data.n1 + data.n2}
190
234
 
191
235
 
236
+ FooAPI().add_views_to_route()
237
+ ```
238
+
239
+ - You can also choose to disable any operation from crud by declaring "disbale" attribute. You can give "all" to disable every crud operation except for additional views.
240
+
241
+ ```python
242
+ # views.py
243
+ from ninja_aio import NinjaAIO
244
+ from ninja_aio.views import APIViewSet
245
+
246
+ from .models import Foo
247
+
248
+ api = NinjaAIO()
249
+
250
+
251
+ class FooAPI(APIViewSet):
252
+ model = Foo
253
+ api = api
254
+ disable = ["retrieve", "update"]
255
+
256
+
192
257
  FooAPI().add_views_to_route()
193
258
  ```
194
259
 
@@ -196,13 +261,11 @@ FooAPI().add_views_to_route()
196
261
 
197
262
  - View class to code generic views class based. In your views.py import APIView class.
198
263
 
199
- ```Python
264
+ ```python
200
265
  # views.py
201
266
  from ninja import Schema
202
267
  from ninja_aio import NinjaAIO
203
268
  from ninja_aio.views import APIView
204
- from ninja_aio.parsers import ORJSONParser
205
- from ninja_aio.renders import ORJSONRender
206
269
 
207
270
  api = NinjaAIO()
208
271
 
@@ -235,7 +298,7 @@ SumView().add_views_to_route()
235
298
 
236
299
  - Define models:
237
300
 
238
- ```Python
301
+ ```python
239
302
  # models.py
240
303
  class Bar(ModelSerializer):
241
304
  name = models.CharField(max_length=30)
@@ -268,12 +331,10 @@ class Foo(ModelSerializer):
268
331
 
269
332
  - Define views:
270
333
 
271
- ```Python
334
+ ```python
272
335
  # views.py
273
336
  from ninja_aio import NinjaAIO
274
337
  from ninja_aio.views import APIViewSet
275
- from ninja_aio.parsers import ORJSONParser
276
- from ninja_aio.renders import ORJSONRender
277
338
 
278
339
  from .models import Foo, Bar
279
340
 
@@ -312,7 +373,7 @@ BarAPI().add_views_to_route()
312
373
 
313
374
  - AsyncJWTBearer built-in class is an authenticator class which use joserfc module. It cames out with authenticate method which validate given claims. Override auth handler method to write your own authentication method. Default algorithms used is RS256. a jwt Token istance is set as class atribute so you can use it by self.dcd.
314
375
 
315
- ```Python
376
+ ```python
316
377
  from ninja_aio.auth import AsyncJWTBearer
317
378
  from django.conf import settings
318
379
  from django.http import HttpRequest
@@ -334,13 +395,11 @@ class CustomJWTBearer(AsyncJWTBearer):
334
395
 
335
396
  - Then add it to views.
336
397
 
337
- ```Python
398
+ ```python
338
399
  # views.py
339
400
  from ninja import Schema
340
401
  from ninja_aio import NinjaAIO
341
402
  from ninja_aio.views import APIViewSet, APIView
342
- from ninja_aio.parsers import ORJSONParser
343
- from ninja_aio.renders import ORJSONRender
344
403
 
345
404
  from .models import Foo
346
405
 
@@ -382,7 +441,7 @@ SumView().add_views_to_route()
382
441
 
383
442
  - By default APIViewSet list view uses Django Ninja built-in AsyncPagination class "PageNumberPagination". You can customize and assign it to APIViewSet class. To make your custom pagination consult **<a href="https://django-ninja.dev/guides/response/pagination/#async-pagination">Django Ninja pagination documentation</a>**.
384
443
 
385
- ```Python
444
+ ```python
386
445
  # views.py
387
446
 
388
447
  class FooAPI(APIViewSet):
@@ -44,7 +44,7 @@ pip install django-ninja-aio-crud
44
44
  ### ModelSerializer
45
45
 
46
46
  - You can serialize your models using ModelSerializer and made them inherit from it. In your models.py import ModelSerializer
47
- ```Python
47
+ ```python
48
48
  # models.py
49
49
  from django.db import models
50
50
  from ninja_aio.models import ModelSerializer
@@ -66,7 +66,7 @@ class Foo(ModelSerializer):
66
66
 
67
67
  - ReadSerializer, CreateSerializer, UpdateSerializer are used to define which fields would be included in runtime schemas creation. You can also specify custom fields and handle their function by overriding custom_actions ModelSerializer's method(custom fields are only available for Create and Update serializers).
68
68
 
69
- ```Python
69
+ ```python
70
70
  # models.py
71
71
  from django.db import models
72
72
  from ninja_aio.models import ModelSerializer
@@ -101,17 +101,63 @@ class Foo(ModelSerializer):
101
101
 
102
102
  - post create method is a custom method that comes out to handle actions which will be excuted after that the object is created. It can be used, indeed, for example to handle custom fields' actions.
103
103
 
104
+ - You can also define optional fields for you Create and Update serializers (remember to give your optional fields a default). To declare an optional fields you have to give the field type too.
105
+ ```python
106
+ # models.py
107
+ from django.db import models
108
+ from ninja_aio.models import ModelSerializer
109
+
110
+
111
+ class Foo(ModelSerializer):
112
+ name = models.CharField(max_length=30)
113
+ bar = models.CharField(max_length=30, default="")
114
+ active = models.BooleanField(default=False)
115
+
116
+ class ReadSerializer:
117
+ fields = ["id", "name", "bar"]
118
+
119
+ class CreateSerializer:
120
+ fields = ["name"]
121
+ optionals = [("bar", str), ("active", bool)]
122
+
123
+ class UpdateSerializer:
124
+ optionals = [[("bar", str), ("active", bool)]
125
+ ```
126
+
127
+ - Instead of declaring your fields maybe you want to exclude some of them. Declaring "excludes" attribute into serializers will exclude the given fields. (You can declare only one between "fields" and "excludes").
128
+
129
+ ```python
130
+ # models.py
131
+ from django.db import models
132
+ from ninja_aio.models import ModelSerializer
133
+
134
+
135
+ class Foo(ModelSerializer):
136
+ name = models.CharField(max_length=30)
137
+ bar = models.CharField(max_length=30, default="")
138
+ active = models.BooleanField(default=False)
139
+
140
+ class ReadSerializer:
141
+ excludes = ["bar"]
142
+
143
+ class CreateSerializer:
144
+ fields = ["name"]
145
+ optionals = [("bar", str), ("active", bool)]
146
+
147
+ class UpdateSerializer:
148
+ excludes = ["id", "name"]
149
+ optionals = [[("bar", str), ("active", bool)]
150
+ ```
151
+
104
152
 
105
153
  ### APIViewSet
106
154
 
107
155
  - View class used to automatically generate CRUD views. in your views.py import APIViewSet and define your api using NinjaAIO class. NinjaAIO class uses built-in parser and renderer which use orjson for data serialization.
108
156
 
109
- ```Python
157
+ ```python
110
158
  # views.py
111
159
  from ninja_aio import NinjaAIO
112
160
  from ninja_aio.views import APIViewSet
113
- from ninja_aio.parsers import ORJSONParser
114
- from ninja_aio.renders import ORJSONRender
115
161
 
116
162
  from .models import Foo
117
163
 
@@ -128,13 +174,11 @@ FooAPI().add_views_to_route()
128
174
 
129
175
  - and that's it, your model CRUD will be automatically created. You can also add custom views to CRUD overriding the built-in method "views".
130
176
 
131
- ```Python
177
+ ```python
132
178
  # views.py
133
179
  from ninja import Schema
134
180
  from ninja_aio import NinjaAIO
135
181
  from ninja_aio.views import APIViewSet
136
- from ninja_aio.parsers import ORJSONParser
137
- from ninja_aio.renders import ORJSONRender
138
182
 
139
183
  from .models import Foo
140
184
 
@@ -160,6 +204,27 @@ class FooAPI(APIViewSet):
160
204
  return 200, {sum: data.n1 + data.n2}
161
205
 
162
206
 
207
+ FooAPI().add_views_to_route()
208
+ ```
209
+
210
+ - You can also choose to disable any operation from crud by declaring "disbale" attribute. You can give "all" to disable every crud operation except for additional views.
211
+
212
+ ```python
213
+ # views.py
214
+ from ninja_aio import NinjaAIO
215
+ from ninja_aio.views import APIViewSet
216
+
217
+ from .models import Foo
218
+
219
+ api = NinjaAIO()
220
+
221
+
222
+ class FooAPI(APIViewSet):
223
+ model = Foo
224
+ api = api
225
+ disable = ["retrieve", "update"]
226
+
227
+
163
228
  FooAPI().add_views_to_route()
164
229
  ```
165
230
 
@@ -167,13 +232,11 @@ FooAPI().add_views_to_route()
167
232
 
168
233
  - View class to code generic views class based. In your views.py import APIView class.
169
234
 
170
- ```Python
235
+ ```python
171
236
  # views.py
172
237
  from ninja import Schema
173
238
  from ninja_aio import NinjaAIO
174
239
  from ninja_aio.views import APIView
175
- from ninja_aio.parsers import ORJSONParser
176
- from ninja_aio.renders import ORJSONRender
177
240
 
178
241
  api = NinjaAIO()
179
242
 
@@ -206,7 +269,7 @@ SumView().add_views_to_route()
206
269
 
207
270
  - Define models:
208
271
 
209
- ```Python
272
+ ```python
210
273
  # models.py
211
274
  class Bar(ModelSerializer):
212
275
  name = models.CharField(max_length=30)
@@ -239,12 +302,10 @@ class Foo(ModelSerializer):
239
302
 
240
303
  - Define views:
241
304
 
242
- ```Python
305
+ ```python
243
306
  # views.py
244
307
  from ninja_aio import NinjaAIO
245
308
  from ninja_aio.views import APIViewSet
246
- from ninja_aio.parsers import ORJSONParser
247
- from ninja_aio.renders import ORJSONRender
248
309
 
249
310
  from .models import Foo, Bar
250
311
 
@@ -283,7 +344,7 @@ BarAPI().add_views_to_route()
283
344
 
284
345
  - AsyncJWTBearer built-in class is an authenticator class which use joserfc module. It cames out with authenticate method which validate given claims. Override auth handler method to write your own authentication method. Default algorithms used is RS256. a jwt Token istance is set as class atribute so you can use it by self.dcd.
285
346
 
286
- ```Python
347
+ ```python
287
348
  from ninja_aio.auth import AsyncJWTBearer
288
349
  from django.conf import settings
289
350
  from django.http import HttpRequest
@@ -305,13 +366,11 @@ class CustomJWTBearer(AsyncJWTBearer):
305
366
 
306
367
  - Then add it to views.
307
368
 
308
- ```Python
369
+ ```python
309
370
  # views.py
310
371
  from ninja import Schema
311
372
  from ninja_aio import NinjaAIO
312
373
  from ninja_aio.views import APIViewSet, APIView
313
- from ninja_aio.parsers import ORJSONParser
314
- from ninja_aio.renders import ORJSONRender
315
374
 
316
375
  from .models import Foo
317
376
 
@@ -353,7 +412,7 @@ SumView().add_views_to_route()
353
412
 
354
413
  - By default APIViewSet list view uses Django Ninja built-in AsyncPagination class "PageNumberPagination". You can customize and assign it to APIViewSet class. To make your custom pagination consult **<a href="https://django-ninja.dev/guides/response/pagination/#async-pagination">Django Ninja pagination documentation</a>**.
355
414
 
356
- ```Python
415
+ ```python
357
416
  # views.py
358
417
 
359
418
  class FooAPI(APIViewSet):
@@ -0,0 +1,7 @@
1
+ """Django Ninja AIO CRUD - Rest Framework"""
2
+
3
+ __version__ = "0.5.0"
4
+
5
+ from .api import NinjaAIO
6
+
7
+ __all__ = ["NinjaAIO"]
@@ -8,6 +8,7 @@ from ninja.constants import NOT_SET, NOT_SET_TYPE
8
8
 
9
9
  from .parsers import ORJSONParser
10
10
  from .renders import ORJSONRenderer
11
+ from .exceptions import set_api_exception_handlers
11
12
 
12
13
 
13
14
  class NinjaAIO(NinjaAPI):
@@ -19,14 +20,14 @@ class NinjaAIO(NinjaAPI):
19
20
  openapi_url: str | None = "/openapi.json",
20
21
  docs: DocsBase = Swagger(),
21
22
  docs_url: str | None = "/docs",
22
- docs_decorator = None,
23
+ docs_decorator=None,
23
24
  servers: list[dict[str, Any]] | None = None,
24
25
  urls_namespace: str | None = None,
25
26
  csrf: bool = False,
26
- auth: Sequence[Any]| NOT_SET_TYPE = NOT_SET,
27
+ auth: Sequence[Any] | NOT_SET_TYPE = NOT_SET,
27
28
  throttle: BaseThrottle | list[BaseThrottle] | NOT_SET_TYPE = NOT_SET,
28
29
  default_router: Router | None = None,
29
- openapi_extra: dict[str, Any] | None = None
30
+ openapi_extra: dict[str, Any] | None = None,
30
31
  ):
31
32
  super().__init__(
32
33
  title=title,
@@ -45,4 +46,8 @@ class NinjaAIO(NinjaAPI):
45
46
  openapi_extra=openapi_extra,
46
47
  renderer=ORJSONRenderer(),
47
48
  parser=ORJSONParser(),
48
- )
49
+ )
50
+
51
+ def set_default_exception_handlers(self):
52
+ set_api_exception_handlers(self)
53
+ super().set_default_exception_handlers()
@@ -5,7 +5,6 @@ from ninja.security.http import HttpBearer
5
5
  from .exceptions import AuthError
6
6
 
7
7
 
8
-
9
8
  class AsyncJwtBearer(HttpBearer):
10
9
  jwt_public: jwk.RSAKey
11
10
  claims: dict[str, dict]
@@ -1,3 +1,8 @@
1
+ from functools import partial
2
+ from ninja import NinjaAPI
3
+ from django.http import HttpRequest, HttpResponse
4
+
5
+
1
6
  class BaseException(Exception):
2
7
  error: str | dict = ""
3
8
  status_code: int = 400
@@ -22,3 +27,15 @@ class SerializeError(BaseException):
22
27
 
23
28
  class AuthError(BaseException):
24
29
  pass
30
+
31
+
32
+ def _default_serialize_error(
33
+ request: HttpRequest, exc: SerializeError, api: "NinjaAPI"
34
+ ) -> HttpResponse:
35
+ return api.create_response(request, exc.error, status=exc.status_code)
36
+
37
+
38
+ def set_api_exception_handlers(api: type[NinjaAPI]) -> None:
39
+ api.add_exception_handler(
40
+ SerializeError, partial(_default_serialize_error, api=api)
41
+ )
@@ -5,7 +5,7 @@ from ninja.schema import Schema
5
5
  from ninja.orm import create_schema
6
6
 
7
7
  from django.db import models
8
- from django.http import HttpResponse, HttpRequest
8
+ from django.http import HttpRequest
9
9
  from django.core.exceptions import ObjectDoesNotExist
10
10
  from django.db.models.fields.related_descriptors import (
11
11
  ReverseManyToOneDescriptor,
@@ -14,7 +14,7 @@ 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
+ from .types import S_TYPES, REL_TYPES, F_TYPES, SCHEMA_TYPES, ModelSerializerMeta
18
18
 
19
19
 
20
20
  class ModelUtil:
@@ -24,12 +24,15 @@ class ModelUtil:
24
24
  @property
25
25
  def serializable_fields(self):
26
26
  if isinstance(self.model, ModelSerializerMeta):
27
- return self.model.ReadSerializer.fields
27
+ return self.model.get_fields("read")
28
28
  return [field.name for field in self.model._meta.get_fields()]
29
29
 
30
30
  def verbose_name_path_resolver(self) -> str:
31
31
  return "-".join(self.model._meta.verbose_name_plural.split(" "))
32
32
 
33
+ def verbose_name_view_resolver(self) -> str:
34
+ return self.model._meta.verbose_name_plural.replace(" ", "")
35
+
33
36
  async def get_object(self, request: HttpRequest, pk: int | str):
34
37
  q = {self.model._meta.pk.attname: pk}
35
38
  obj_qs = self.model.objects.select_related()
@@ -58,11 +61,18 @@ class ModelUtil:
58
61
  async def parse_input_data(self, request: HttpRequest, data: Schema):
59
62
  payload = data.model_dump()
60
63
  customs = {}
64
+ optionals = []
61
65
  if isinstance(self.model, ModelSerializerMeta):
62
66
  customs = {k: v for k, v in payload.items() if self.model.is_custom(k)}
67
+ optionals = [
68
+ k for k, v in payload.items() if self.model.is_optional(k) and v is None
69
+ ]
63
70
  for k, v in payload.items():
64
- if isinstance(self.model, ModelSerializerMeta) and self.model.is_custom(k):
65
- continue
71
+ if isinstance(self.model, ModelSerializerMeta):
72
+ if self.model.is_custom(k):
73
+ continue
74
+ if self.model.is_optional(k) and k is None:
75
+ continue
66
76
  field_obj = getattr(self.model, k).field
67
77
  if isinstance(field_obj, models.BinaryField):
68
78
  try:
@@ -73,7 +83,9 @@ class ModelUtil:
73
83
  rel_util = ModelUtil(field_obj.related_model)
74
84
  rel: ModelSerializer = await rel_util.get_object(request, v)
75
85
  payload |= {k: rel}
76
- new_payload = {k: v for k, v in payload.items() if k not in customs}
86
+ new_payload = {
87
+ k: v for k, v in payload.items() if k not in (customs.keys() or optionals)
88
+ }
77
89
  return new_payload, customs
78
90
 
79
91
  async def parse_output_data(self, request: HttpRequest, data: Schema):
@@ -106,16 +118,13 @@ class ModelUtil:
106
118
  return payload
107
119
 
108
120
  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
121
+ payload, customs = await self.parse_input_data(request, data)
122
+ pk = (await self.model.objects.acreate(**payload)).pk
123
+ obj = await self.get_object(request, pk)
115
124
  if isinstance(self.model, ModelSerializerMeta):
116
125
  await obj.custom_actions(customs)
117
126
  await obj.post_create()
118
- return 201, await self.read_s(request, obj, obj_schema)
127
+ return await self.read_s(request, obj, obj_schema)
119
128
 
120
129
  async def read_s(
121
130
  self,
@@ -130,11 +139,7 @@ class ModelUtil:
130
139
  async def update_s(
131
140
  self, request: HttpRequest, data: Schema, pk: int | str, obj_schema: Schema
132
141
  ):
133
- try:
134
- obj = await self.get_object(request, pk)
135
- except SerializeError as e:
136
- return e.status_code, e.error
137
-
142
+ obj = await self.get_object(request, pk)
138
143
  payload, customs = await self.parse_input_data(request, data)
139
144
  for k, v in payload.items():
140
145
  if v is not None:
@@ -146,12 +151,9 @@ class ModelUtil:
146
151
  return await self.read_s(request, updated_object, obj_schema)
147
152
 
148
153
  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
154
+ obj = await self.get_object(request, pk)
153
155
  await obj.adelete()
154
- return HttpResponse(status=204)
156
+ return None
155
157
 
156
158
 
157
159
  class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
@@ -161,13 +163,18 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
161
163
  class CreateSerializer:
162
164
  fields: list[str] = []
163
165
  customs: list[tuple[str, type, Any]] = []
166
+ optionals: list[tuple[str, type]] = []
167
+ excludes: list[str] = []
164
168
 
165
169
  class ReadSerializer:
166
170
  fields: list[str] = []
171
+ excludes: list[str] = []
167
172
 
168
173
  class UpdateSerializer:
169
174
  fields: list[str] = []
170
175
  customs: list[tuple[str, type, Any]] = []
176
+ optionals: list[tuple[str, type]] = []
177
+ excludes: list[str] = []
171
178
 
172
179
  @property
173
180
  def has_custom_fields_create(self):
@@ -181,6 +188,74 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
181
188
  def has_custom_fields(self):
182
189
  return self.has_custom_fields_create or self.has_custom_fields_update
183
190
 
191
+ @property
192
+ def has_optional_fields_create(self):
193
+ return hasattr(self.CreateSerializer, "optionals")
194
+
195
+ @property
196
+ def has_optional_fields_update(self):
197
+ return hasattr(self.UpdateSerializer, "optionals")
198
+
199
+ @property
200
+ def has_optional_fields(self):
201
+ return self.has_optional_fields_create or self.has_optional_fields_update
202
+
203
+ @classmethod
204
+ def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
205
+ match s_type:
206
+ case "create":
207
+ fields = getattr(cls.CreateSerializer, f_type, [])
208
+ case "update":
209
+ fields = getattr(cls.UpdateSerializer, f_type, [])
210
+ case "read":
211
+ fields = getattr(cls.ReadSerializer, f_type, [])
212
+ return fields
213
+
214
+ @classmethod
215
+ def _is_special_field(
216
+ cls, s_type: type[S_TYPES], field: str, f_type: type[F_TYPES]
217
+ ):
218
+ special_fields = cls._get_fields(s_type, f_type)
219
+ return any(field in special_f for special_f in special_fields)
220
+
221
+ @classmethod
222
+ def _generate_model_schema(
223
+ cls,
224
+ schema_type: type[SCHEMA_TYPES],
225
+ depth: int = None,
226
+ ) -> Schema:
227
+ match schema_type:
228
+ case "In":
229
+ s_type = "create"
230
+ case "Patch":
231
+ s_type = "update"
232
+ case "Out":
233
+ fields, reverse_rels, excludes = cls.get_schema_out_data()
234
+ if not fields and not reverse_rels and not excludes:
235
+ return None
236
+ return create_schema(
237
+ model=cls,
238
+ name=f"{cls._meta.model_name}SchemaOut",
239
+ depth=depth,
240
+ fields=fields,
241
+ custom_fields=reverse_rels,
242
+ exclude=excludes,
243
+ )
244
+ fields = cls.get_fields(s_type)
245
+ customs = cls.get_custom_fields(s_type) + cls.get_optional_fields(s_type)
246
+ excludes = cls.get_excluded_fields(s_type)
247
+ return (
248
+ create_schema(
249
+ model=cls,
250
+ name=f"{cls._meta.model_name}Schema{schema_type}",
251
+ fields=fields,
252
+ custom_fields=customs,
253
+ exclude=excludes,
254
+ )
255
+ if fields or customs or excludes
256
+ else None
257
+ )
258
+
184
259
  @classmethod
185
260
  def verbose_name_path_resolver(cls) -> str:
186
261
  return "-".join(cls._meta.verbose_name_plural.split(" "))
@@ -260,7 +335,7 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
260
335
  def get_schema_out_data(cls):
261
336
  fields = []
262
337
  reverse_rels = []
263
- for f in cls.ReadSerializer.fields:
338
+ for f in cls.get_fields("read"):
264
339
  field_obj = getattr(cls, f)
265
340
  if isinstance(field_obj, ManyToManyDescriptor):
266
341
  rel_obj: ModelSerializer = field_obj.field.related_model
@@ -280,162 +355,47 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
280
355
  reverse_rels.append(rel_data)
281
356
  continue
282
357
  fields.append(f)
283
- return fields, reverse_rels
358
+ return fields, reverse_rels, cls.get_excluded_fields("read")
284
359
 
285
360
  @classmethod
286
361
  def is_custom(cls, field: str):
287
- customs = cls.get_custom_fields("create") or []
288
- customs.extend(cls.get_custom_fields("update") or [])
289
- return any(field in custom_f for custom_f in customs)
362
+ return cls._is_special_field(
363
+ "create", field, "customs"
364
+ ) or cls._is_special_field("update", field, "customs")
290
365
 
291
366
  @classmethod
292
- async def parse_input_data(cls, request: HttpRequest, data: Schema):
293
- payload = data.model_dump()
294
- customs = {k: v for k, v in payload.items() if cls.is_custom(k)}
295
- for k, v in payload.items():
296
- if cls.is_custom(k):
297
- continue
298
- field_obj = getattr(cls, k).field
299
- if isinstance(field_obj, models.BinaryField):
300
- try:
301
- payload |= {k: base64.b64decode(v)}
302
- except Exception as exc:
303
- raise SerializeError({k: ". ".join(exc.args)}, 400)
304
- if isinstance(field_obj, models.ForeignKey):
305
- rel: ModelSerializer = await field_obj.related_model.get_object(
306
- request, v
307
- )
308
- payload |= {k: rel}
309
- new_payload = {k: v for k, v in payload.items() if k not in customs}
310
- return new_payload, customs
311
-
312
- @classmethod
313
- async def parse_output_data(cls, request: HttpRequest, data: Schema):
314
- olds_k: list[dict] = []
315
- payload = data.model_dump()
316
- for k, v in payload.items():
317
- try:
318
- field_obj = getattr(cls, k).field
319
- except AttributeError:
320
- field_obj = getattr(cls, k).related
321
- if isinstance(v, dict) and (
322
- isinstance(field_obj, models.ForeignKey)
323
- or isinstance(field_obj, models.OneToOneField)
324
- ):
325
- rel: ModelSerializer = await field_obj.related_model.get_object(
326
- request, list(v.values())[0]
327
- )
328
- if isinstance(field_obj, models.ForeignKey):
329
- for rel_k, rel_v in v.items():
330
- field_rel_obj = getattr(rel, rel_k)
331
- if isinstance(field_rel_obj, models.ForeignKey):
332
- olds_k.append({rel_k: rel_v})
333
- for obj in olds_k:
334
- for old_k, old_v in obj.items():
335
- v.pop(old_k)
336
- v |= {f"{old_k}_id": old_v}
337
- olds_k = []
338
- payload |= {k: rel}
339
- return payload
367
+ def is_optional(cls, field: str):
368
+ return cls._is_special_field(
369
+ "create", field, "optionals"
370
+ ) or cls._is_special_field("update", field, "optionals")
340
371
 
341
372
  @classmethod
342
373
  def get_custom_fields(cls, s_type: type[S_TYPES]):
343
- try:
344
- match s_type:
345
- case "create":
346
- customs = cls.CreateSerializer.customs
347
- case "update":
348
- customs = cls.UpdateSerializer.customs
349
- except AttributeError:
350
- return None
351
- return customs
374
+ return cls._get_fields(s_type, "customs")
352
375
 
353
376
  @classmethod
354
- def generate_read_s(cls, depth: int = 1) -> Schema:
355
- fields, reverse_rels = cls.get_schema_out_data()
356
- customs = [custom for custom in reverse_rels]
357
- return create_schema(
358
- model=cls,
359
- name=f"{cls._meta.model_name}SchemaOut",
360
- depth=depth,
361
- fields=fields,
362
- custom_fields=customs,
363
- )
364
-
377
+ def get_optional_fields(cls, s_type: type[S_TYPES]):
378
+ return [
379
+ (field, field_type, None)
380
+ for field, field_type in cls._get_fields(s_type, "optionals")
381
+ ]
382
+
365
383
  @classmethod
366
- def generate_create_s(cls) -> Schema:
367
- return create_schema(
368
- model=cls,
369
- name=f"{cls._meta.model_name}SchemaIn",
370
- fields=cls.CreateSerializer.fields,
371
- custom_fields=cls.get_custom_fields("create"),
372
- )
373
-
384
+ def get_excluded_fields(cls, s_type: type[S_TYPES]):
385
+ return cls._get_fields(s_type, "excludes")
386
+
374
387
  @classmethod
375
- def generate_update_s(cls) -> Schema:
376
- return create_schema(
377
- model=cls,
378
- name=f"{cls._meta.model_name}SchemaPatch",
379
- fields=cls.UpdateSerializer.fields,
380
- custom_fields=cls.get_custom_fields("update"),
381
- )
388
+ def get_fields(cls, s_type: type[S_TYPES]):
389
+ return cls._get_fields(s_type, "fields")
382
390
 
383
391
  @classmethod
384
- async def get_object(cls, request: HttpRequest, pk: int | str):
385
- q = {cls._meta.pk.attname: pk}
386
- try:
387
- obj = (
388
- await (await cls.queryset_request(request))
389
- .prefetch_related(*cls.get_reverse_relations())
390
- .aget(**q)
391
- )
392
- except ObjectDoesNotExist:
393
- raise SerializeError({cls._meta.model_name: "not found"}, 404)
394
- return obj
395
-
396
- @classmethod
397
- async def create_s(cls, request: HttpRequest, data: Schema):
398
- try:
399
- payload, customs = await cls.parse_input_data(request, data)
400
- pk = (await cls.objects.acreate(**payload)).pk
401
- obj = await cls.get_object(request, pk)
402
- except SerializeError as e:
403
- return e.status_code, e.error
404
- payload |= customs
405
- await obj.custom_actions(payload)
406
- await obj.post_create()
407
- return await cls.read_s(request, obj)
408
-
409
- @classmethod
410
- async def read_s(cls, request: HttpRequest, obj: type["ModelSerializer"]):
411
- schema = cls.generate_read_s().from_orm(obj)
412
- try:
413
- data = await cls.parse_output_data(request, schema)
414
- except SerializeError as e:
415
- return e.status_code, e.error
416
- return data
392
+ def generate_read_s(cls, depth: int = 1) -> Schema:
393
+ return cls._generate_model_schema("Out", depth)
417
394
 
418
395
  @classmethod
419
- async def update_s(cls, request: HttpRequest, data: Schema, pk: int | str):
420
- try:
421
- obj = await cls.get_object(request, pk)
422
- except SerializeError as e:
423
- return e.status_code, e.error
424
-
425
- payload, customs = await cls.parse_input_data(request, data)
426
- for k, v in payload.items():
427
- if v is not None:
428
- setattr(obj, k, v)
429
- await obj.custom_actions(customs)
430
- await obj.asave()
431
- updated_object = await cls.get_object(request, pk)
432
- return await cls.read_s(request, updated_object)
396
+ def generate_create_s(cls) -> Schema:
397
+ return cls._generate_model_schema("In")
433
398
 
434
399
  @classmethod
435
- async def delete_s(cls, request: HttpRequest, pk: int | str):
436
- try:
437
- obj = await cls.get_object(request, pk)
438
- except SerializeError as e:
439
- return e.status_code, e.error
440
- await obj.adelete()
441
- return HttpResponse(status=204)
400
+ def generate_update_s(cls) -> Schema:
401
+ return cls._generate_model_schema("Patch")
@@ -2,9 +2,11 @@ from typing import Literal
2
2
 
3
3
  from django.db.models import Model
4
4
 
5
- S_TYPES = Literal["create", "update"]
5
+ S_TYPES = Literal["read", "create", "update"]
6
6
  REL_TYPES = Literal["many", "one"]
7
-
7
+ F_TYPES = Literal["fields", "customs", "optionals", "excludes"]
8
+ SCHEMA_TYPES = Literal["In", "Out", "Patch"]
9
+ VIEW_TYPES = Literal["list", "retrieve", "create", "update", "delete", "all"]
8
10
 
9
11
  class ModelSerializerType(type):
10
12
  def __repr__(self):
@@ -1,15 +1,15 @@
1
1
  from typing import List
2
2
 
3
- from ninja import NinjaAPI, Router, Schema
3
+ from ninja import NinjaAPI, Router, Schema, Path
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
7
  from django.db.models import Model
8
+ from pydantic import create_model
8
9
 
9
10
  from .models import ModelSerializer, ModelUtil
10
11
  from .schemas import GenericMessageSchema
11
- from .exceptions import SerializeError
12
- from .types import ModelSerializerMeta
12
+ from .types import ModelSerializerMeta, VIEW_TYPES
13
13
 
14
14
  ERROR_CODES = frozenset({400, 401, 404, 428})
15
15
 
@@ -56,7 +56,6 @@ class APIView:
56
56
  async def some_method(request, *args, **kwargs):
57
57
  pass
58
58
  """
59
- pass
60
59
 
61
60
  def add_views(self):
62
61
  self.views()
@@ -74,23 +73,45 @@ class APIViewSet:
74
73
  schema_update: Schema | None = None
75
74
  auths: list | None = NOT_SET
76
75
  pagination_class: type[AsyncPaginationBase] = PageNumberPagination
76
+ disable: list[type[VIEW_TYPES]] = []
77
77
 
78
78
  def __init__(self) -> None:
79
79
  self.router = Router(tags=[self.model._meta.model_name.capitalize()])
80
80
  self.path = "/"
81
- self.path_retrieve = f"{self.model._meta.pk.attname}/"
81
+ self.path_retrieve = f"{{{self.model._meta.pk.attname}}}/"
82
82
  self.error_codes = ERROR_CODES
83
83
  self.model_util = ModelUtil(self.model)
84
- self.schema_out, self.schema_update, self.schema_in = self.get_schemas()
84
+ self.schema_out, self.schema_in, self.schema_update = self.get_schemas()
85
+ self.path_schema = self._create_path_schema()
86
+
87
+ @property
88
+ def _crud_views(self):
89
+ """
90
+ key: view type (create, list, retrieve, update, delete or all)
91
+ value: tuple with schema and view method
92
+ """
93
+ return {
94
+ "create": (self.schema_in, self.create_view),
95
+ "list": (self.schema_out, self.list_view),
96
+ "retrieve": (self.schema_out, self.retrieve_view),
97
+ "update": (self.schema_update, self.update_view),
98
+ "delete": (None, self.delete_view),
99
+ }
100
+
101
+ def _create_path_schema(self):
102
+ fields = {
103
+ self.model._meta.pk.attname: (str | int , ...),
104
+ }
105
+ return create_model(f"{self.model._meta.model_name}PathSchema", **fields)
85
106
 
86
107
  def get_schemas(self):
87
108
  if isinstance(self.model, ModelSerializerMeta):
88
109
  return (
89
110
  self.model.generate_read_s(),
90
- self.model.generate_update_s(),
91
111
  self.model.generate_create_s(),
112
+ self.model.generate_update_s(),
92
113
  )
93
- return self.schema_out, self.schema_update, self.schema_in
114
+ return self.schema_out, self.schema_in, self.schema_update
94
115
 
95
116
  def create_view(self):
96
117
  @self.router.post(
@@ -99,9 +120,10 @@ class APIViewSet:
99
120
  response={201: self.schema_out, self.error_codes: GenericMessageSchema},
100
121
  )
101
122
  async def create(request: HttpRequest, data: self.schema_in):
102
- return await self.model_util.create_s(request, data, self.schema_out)
123
+ return 201, await self.model_util.create_s(request, data, self.schema_out)
103
124
 
104
125
  create.__name__ = f"create_{self.model._meta.model_name}"
126
+ return create
105
127
 
106
128
  def list_view(self):
107
129
  @self.router.get(
@@ -118,7 +140,6 @@ class APIViewSet:
118
140
  if isinstance(self.model, ModelSerializerMeta):
119
141
  qs = await self.model.queryset_request(request)
120
142
  rels = self.model_util.get_reverse_relations()
121
- print(rels)
122
143
  if len(rels) > 0:
123
144
  qs = qs.prefetch_related(*rels)
124
145
  objs = [
@@ -127,7 +148,8 @@ class APIViewSet:
127
148
  ]
128
149
  return objs
129
150
 
130
- list.__name__ = f"list_{self.model._meta.verbose_name_plural}"
151
+ list.__name__ = f"list_{self.model_util.verbose_name_view_resolver()}"
152
+ return list
131
153
 
132
154
  def retrieve_view(self):
133
155
  @self.router.get(
@@ -135,14 +157,12 @@ class APIViewSet:
135
157
  auth=self.auths,
136
158
  response={200: self.schema_out, self.error_codes: GenericMessageSchema},
137
159
  )
138
- async def retrieve(request: HttpRequest, pk: int | str):
139
- try:
140
- obj = await self.model_util.get_object(request, pk)
141
- except SerializeError as e:
142
- return e.status_code, e.error
160
+ async def retrieve(request: HttpRequest, pk: Path[self.path_schema]):
161
+ obj = await self.model_util.get_object(request, pk)
143
162
  return await self.model_util.read_s(request, obj, self.schema_out)
144
163
 
145
164
  retrieve.__name__ = f"retrieve_{self.model._meta.model_name}"
165
+ return retrieve
146
166
 
147
167
  def update_view(self):
148
168
  @self.router.patch(
@@ -150,10 +170,11 @@ class APIViewSet:
150
170
  auth=self.auths,
151
171
  response={200: self.schema_out, self.error_codes: GenericMessageSchema},
152
172
  )
153
- async def update(request: HttpRequest, data: self.schema_update, pk: int | str):
173
+ async def update(request: HttpRequest, data: self.schema_update, pk: Path[self.path_schema]):
154
174
  return await self.model_util.update_s(request, data, pk, self.schema_out)
155
175
 
156
176
  update.__name__ = f"update_{self.model._meta.model_name}"
177
+ return update
157
178
 
158
179
  def delete_view(self):
159
180
  @self.router.delete(
@@ -161,10 +182,11 @@ class APIViewSet:
161
182
  auth=self.auths,
162
183
  response={204: None, self.error_codes: GenericMessageSchema},
163
184
  )
164
- async def delete(request: HttpRequest, pk: int | str):
165
- return await self.model_util.delete_s(request, pk)
185
+ async def delete(request: HttpRequest, pk: Path[self.path_schema]):
186
+ return 204, await self.model_util.delete_s(request, pk)
166
187
 
167
188
  delete.__name__ = f"delete_{self.model._meta.model_name}"
189
+ return delete
168
190
 
169
191
  def views(self):
170
192
  """
@@ -198,14 +220,18 @@ class APIViewSet:
198
220
  async def some_method(request, *args, **kwargs):
199
221
  pass
200
222
  """
201
- pass
202
223
 
203
224
  def add_views(self):
204
- self.create_view()
205
- self.list_view()
206
- self.retrieve_view()
207
- self.update_view()
208
- self.delete_view()
225
+ if "all" in self.disable:
226
+ self.views()
227
+ return self.router
228
+
229
+ for views_type, (schema, view) in self._crud_views.items():
230
+ if views_type not in self.disable and (
231
+ schema is not None or views_type == "delete"
232
+ ):
233
+ view()
234
+
209
235
  self.views()
210
236
  return self.router
211
237
 
@@ -1,7 +0,0 @@
1
- """ Django Ninja AIO CRUD - Rest Framework """
2
-
3
- __version__ = "0.3.1"
4
-
5
- from .api import NinjaAIO
6
-
7
- __all__ = ["NinjaAIO"]