django-ninja-aio-crud 0.4.0__py3-none-any.whl → 0.6.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-ninja-aio-crud
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
6
  Requires-Python: >=3.10
@@ -25,9 +25,16 @@ Classifier: Topic :: Internet :: WWW/HTTP
25
25
  Requires-Dist: django-ninja >=1.3.0
26
26
  Requires-Dist: joserfc >=1.0.0
27
27
  Requires-Dist: orjson >= 3.10.7
28
+ Requires-Dist: coverage ; extra == "test"
28
29
  Project-URL: Repository, https://github.com/caspel26/django-ninja-aio-crud
30
+ Provides-Extra: test
29
31
 
30
32
  # 🥷 django-ninja-aio-crud
33
+ ![Test](https://github.com/caspel26/django-ninja-aio-crud/actions/workflows/coverage.yml/badge.svg)
34
+ [![codecov](https://codecov.io/gh/caspel26/django-ninja-aio-crud/graph/badge.svg?token=DZ5WDT3S20)](https://codecov.io/gh/caspel26/django-ninja-aio-crud)
35
+ [![PyPI - Version](https://img.shields.io/pypi/v/django-ninja-aio-crud?color=g&logo=pypi&logoColor=white)](https://pypi.org/project/django-ninja-aio-crud/)
36
+ [![PyPI - License](https://img.shields.io/pypi/l/django-ninja-aio-crud)](https://github.com/caspel26/django-ninja-aio-crud/blob/main/LICENSE)
37
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
31
38
  > [!NOTE]
32
39
  > Django ninja aio crud framework is based on **<a href="https://django-ninja.dev/">Django Ninja framework</a>**. It comes out with built-in views and models which are able to make automatic async CRUD operations and codes views class based making the developers' life easier and the code cleaner.
33
40
 
@@ -120,7 +127,7 @@ class Foo(ModelSerializer):
120
127
  if not payload.get("force_activation"):
121
128
  return
122
129
  setattr(self, "force_activation", True)
123
-
130
+
124
131
  async def post_create(self) -> None:
125
132
  if not hasattr(self, "force_activation") or not getattr(self, "force_activation"):
126
133
  return
@@ -153,6 +160,32 @@ class Foo(ModelSerializer):
153
160
  optionals = [[("bar", str), ("active", bool)]
154
161
  ```
155
162
 
163
+ - 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").
164
+
165
+ ```python
166
+ # models.py
167
+ from django.db import models
168
+ from ninja_aio.models import ModelSerializer
169
+
170
+
171
+ class Foo(ModelSerializer):
172
+ name = models.CharField(max_length=30)
173
+ bar = models.CharField(max_length=30, default="")
174
+ active = models.BooleanField(default=False)
175
+
176
+ class ReadSerializer:
177
+ excludes = ["bar"]
178
+
179
+ class CreateSerializer:
180
+ fields = ["name"]
181
+ optionals = [("bar", str), ("active", bool)]
182
+
183
+ class UpdateSerializer:
184
+ excludes = ["id", "name"]
185
+ optionals = [[("bar", str), ("active", bool)]
186
+ ```
187
+
188
+
156
189
  ### APIViewSet
157
190
 
158
191
  - 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.
@@ -161,8 +194,6 @@ class Foo(ModelSerializer):
161
194
  # views.py
162
195
  from ninja_aio import NinjaAIO
163
196
  from ninja_aio.views import APIViewSet
164
- from ninja_aio.parsers import ORJSONParser
165
- from ninja_aio.renders import ORJSONRender
166
197
 
167
198
  from .models import Foo
168
199
 
@@ -173,7 +204,7 @@ class FooAPI(APIViewSet):
173
204
  model = Foo
174
205
  api = api
175
206
 
176
-
207
+
177
208
  FooAPI().add_views_to_route()
178
209
  ```
179
210
 
@@ -184,8 +215,6 @@ FooAPI().add_views_to_route()
184
215
  from ninja import Schema
185
216
  from ninja_aio import NinjaAIO
186
217
  from ninja_aio.views import APIViewSet
187
- from ninja_aio.parsers import ORJSONParser
188
- from ninja_aio.renders import ORJSONRender
189
218
 
190
219
  from .models import Foo
191
220
 
@@ -211,6 +240,54 @@ class FooAPI(APIViewSet):
211
240
  return 200, {sum: data.n1 + data.n2}
212
241
 
213
242
 
243
+ FooAPI().add_views_to_route()
244
+ ```
245
+
246
+ - 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.
247
+
248
+ > [!TIP]
249
+ > You can exclude by default an endpoint without declaring the serializer.
250
+ > For example if you don't want to give update method to a CRUD just do not declare UpdateSerializer into model.
251
+
252
+ ```python
253
+ # views.py
254
+ from ninja_aio import NinjaAIO
255
+ from ninja_aio.views import APIViewSet
256
+
257
+ from .models import Foo
258
+
259
+ api = NinjaAIO()
260
+
261
+
262
+ class FooAPI(APIViewSet):
263
+ model = Foo
264
+ api = api
265
+ disable = ["retrieve", "update"]
266
+
267
+
268
+ FooAPI().add_views_to_route()
269
+ ```
270
+ For the list endpoint you can also set query params and handle them. They will be also visible into swagger.
271
+
272
+ ```python
273
+ # views.py
274
+ from ninja_aio import NinjaAIO
275
+ from ninja_aio.views import APIViewSet
276
+
277
+ from .models import Foo
278
+
279
+ api = NinjaAIO()
280
+
281
+
282
+ class FooAPI(APIViewSet):
283
+ model = Foo
284
+ api = api
285
+ query_params = {"name": (str, None), "active": (bool, None)}
286
+
287
+ async def query_params_handler(self, queryset, filters):
288
+ return queryset.filter(**{k: v for k, v in filters.items() if v is not None})
289
+
290
+
214
291
  FooAPI().add_views_to_route()
215
292
  ```
216
293
 
@@ -223,8 +300,6 @@ FooAPI().add_views_to_route()
223
300
  from ninja import Schema
224
301
  from ninja_aio import NinjaAIO
225
302
  from ninja_aio.views import APIView
226
- from ninja_aio.parsers import ORJSONParser
227
- from ninja_aio.renders import ORJSONRender
228
303
 
229
304
  api = NinjaAIO()
230
305
 
@@ -294,8 +369,6 @@ class Foo(ModelSerializer):
294
369
  # views.py
295
370
  from ninja_aio import NinjaAIO
296
371
  from ninja_aio.views import APIViewSet
297
- from ninja_aio.parsers import ORJSONParser
298
- from ninja_aio.renders import ORJSONRender
299
372
 
300
373
  from .models import Foo, Bar
301
374
 
@@ -332,7 +405,7 @@ BarAPI().add_views_to_route()
332
405
 
333
406
  ### Jwt
334
407
 
335
- - 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.
408
+ - 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.
336
409
 
337
410
  ```python
338
411
  from ninja_aio.auth import AsyncJWTBearer
@@ -361,8 +434,6 @@ class CustomJWTBearer(AsyncJWTBearer):
361
434
  from ninja import Schema
362
435
  from ninja_aio import NinjaAIO
363
436
  from ninja_aio.views import APIViewSet, APIView
364
- from ninja_aio.parsers import ORJSONParser
365
- from ninja_aio.renders import ORJSONRender
366
437
 
367
438
  from .models import Foo
368
439
 
@@ -0,0 +1,13 @@
1
+ ninja_aio/__init__.py,sha256=-m_l3-9_2Q1-PvkjWqAfSmu2jt2ILNb490jT2yC4sbk,119
2
+ ninja_aio/api.py,sha256=Fe6l3YCy7MW5TY4-Lbl80CFuK2NT2Y7tHfmqPk6Mqak,1735
3
+ ninja_aio/auth.py,sha256=z9gniLIgT8SjRqhGN7ZI0AGHjsALwgU6eyr2m46fwFY,1389
4
+ ninja_aio/exceptions.py,sha256=JGUvZyYevGnFYFk2JYnNqng1e9ilG8l325wA5YC1RUA,1364
5
+ ninja_aio/models.py,sha256=ddV2QGzWSJzwZ4vt-AJ9rRdiQCVSEtl6Bj3mxroSiQE,15096
6
+ ninja_aio/parsers.py,sha256=e_4lGCPV7zs-HTqtdJTc8yQD2KPAn9njbL8nF_Mmgkc,153
7
+ ninja_aio/renders.py,sha256=mHeKNJtmDhZmgFpS9B6SPn5uZFcyVXrsoMhr149LeW8,1555
8
+ ninja_aio/schemas.py,sha256=EgRkfhnzZqwGvdBmqlZixMtMcoD1ZxV_qzJ3fmaAy20,113
9
+ ninja_aio/types.py,sha256=EHznS-6KWLwSX5hLeXbAi7qHWla09_rGeQraiLpH-aY,491
10
+ ninja_aio/views.py,sha256=b-KLPkNhfMXsTx4egfi-nKeuKqYEUOLXJFNt0fYariE,9027
11
+ django_ninja_aio_crud-0.6.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
12
+ django_ninja_aio_crud-0.6.0.dist-info/METADATA,sha256=aRvvPaOFsyvqGwg_wCpoQJ0PiQJfo0obaBPlovRlXeA,13078
13
+ django_ninja_aio_crud-0.6.0.dist-info/RECORD,,
ninja_aio/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.6.0"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
ninja_aio/auth.py CHANGED
@@ -2,7 +2,7 @@ from joserfc import jwt, jwk, errors
2
2
  from django.http.request import HttpRequest
3
3
  from ninja.security.http import HttpBearer
4
4
 
5
- from .exceptions import AuthError
5
+ from .exceptions import AuthError, parse_jose_error
6
6
 
7
7
 
8
8
  class AsyncJwtBearer(HttpBearer):
@@ -23,8 +23,8 @@ class AsyncJwtBearer(HttpBearer):
23
23
  errors.InvalidClaimError,
24
24
  errors.MissingClaimError,
25
25
  errors.ExpiredTokenError,
26
- ):
27
- raise AuthError()
26
+ ) as exc:
27
+ raise AuthError(**parse_jose_error(exc), status_code=401)
28
28
 
29
29
  async def auth_handler(self, request: HttpRequest):
30
30
  """
@@ -35,15 +35,11 @@ class AsyncJwtBearer(HttpBearer):
35
35
  async def authenticate(self, request: HttpRequest, token: str):
36
36
  try:
37
37
  self.dcd = jwt.decode(token, self.jwt_public, algorithms=self.algorithms)
38
- except (
39
- errors.BadSignatureError,
40
- ValueError,
41
- ):
42
- return None
38
+ except errors.BadSignatureError as exc:
39
+ raise AuthError(**parse_jose_error(exc), status_code=401)
40
+ except ValueError as exc:
41
+ raise AuthError(", ".join(exc.args), 401)
43
42
 
44
- try:
45
- self.validate_claims(self.dcd.claims)
46
- except AuthError:
47
- return None
43
+ self.validate_claims(self.dcd.claims)
48
44
 
49
45
  return await self.auth_handler(request)
ninja_aio/exceptions.py CHANGED
@@ -1,4 +1,6 @@
1
1
  from functools import partial
2
+
3
+ from joserfc.errors import JoseError
2
4
  from ninja import NinjaAPI
3
5
  from django.http import HttpRequest, HttpResponse
4
6
 
@@ -11,11 +13,14 @@ class BaseException(Exception):
11
13
  self,
12
14
  error: str | dict = None,
13
15
  status_code: int | None = None,
14
- is_critical: bool = False,
16
+ details: str | None = None,
15
17
  ) -> None:
16
- self.error = error or self.error
18
+ if isinstance(error, str):
19
+ self.error = {"error": error}
20
+ if isinstance(error, dict):
21
+ self.error = error
22
+ self.error |= {"details": details} if details else {}
17
23
  self.status_code = status_code or self.status_code
18
- self.is_critical = is_critical
19
24
 
20
25
  def get_error(self):
21
26
  return self.error, self.status_code
@@ -29,13 +34,20 @@ class AuthError(BaseException):
29
34
  pass
30
35
 
31
36
 
32
- def _default_serialize_error(
33
- request: HttpRequest, exc: SerializeError, api: "NinjaAPI"
37
+ def _default_error(
38
+ request: HttpRequest, exc: BaseException, api: type[NinjaAPI]
34
39
  ) -> HttpResponse:
35
40
  return api.create_response(request, exc.error, status=exc.status_code)
36
41
 
37
42
 
38
43
  def set_api_exception_handlers(api: type[NinjaAPI]) -> None:
39
- api.add_exception_handler(
40
- SerializeError, partial(_default_serialize_error, api=api)
44
+ api.add_exception_handler(BaseException, partial(_default_error, api=api))
45
+
46
+
47
+ def parse_jose_error(jose_exc: JoseError) -> dict:
48
+ error_msg = {"error": jose_exc.error}
49
+ return (
50
+ error_msg | {"details": jose_exc.description}
51
+ if jose_exc.description
52
+ else error_msg
41
53
  )
ninja_aio/models.py CHANGED
@@ -1,11 +1,11 @@
1
1
  import base64
2
2
  from typing import Any
3
3
 
4
- from ninja.schema import Schema
4
+ from ninja 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, F_TYPES, ModelSerializerMeta
17
+ from .types import S_TYPES, REL_TYPES, F_TYPES, SCHEMA_TYPES, ModelSerializerMeta
18
18
 
19
19
 
20
20
  class ModelUtil:
@@ -24,21 +24,51 @@ 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
+ @property
31
+ def model_name(self) -> str:
32
+ return self.model._meta.model_name
33
+
34
+ @property
35
+ def model_pk_name(self) -> str:
36
+ return self.model._meta.pk.attname
37
+
38
+ @property
39
+ def model_verbose_name_plural(self) -> str:
40
+ return self.model._meta.verbose_name_plural
41
+
30
42
  def verbose_name_path_resolver(self) -> str:
31
- return "-".join(self.model._meta.verbose_name_plural.split(" "))
43
+ return "-".join(self.model_verbose_name_plural.split(" "))
44
+
45
+ def verbose_name_view_resolver(self) -> str:
46
+ return self.model_verbose_name_plural.replace(" ", "")
47
+
48
+ async def get_object(
49
+ self,
50
+ request: HttpRequest,
51
+ pk: int | str = None,
52
+ filters: dict = None,
53
+ getters: dict = None,
54
+ ):
55
+ get_q = {self.model_pk_name: pk} if pk is not None else {}
56
+ if getters:
57
+ get_q |= getters
32
58
 
33
- async def get_object(self, request: HttpRequest, pk: int | str):
34
- q = {self.model._meta.pk.attname: pk}
35
59
  obj_qs = self.model.objects.select_related()
36
60
  if isinstance(self.model, ModelSerializerMeta):
37
61
  obj_qs = await self.model.queryset_request(request)
62
+
63
+ obj_qs = obj_qs.prefetch_related(*self.get_reverse_relations())
64
+ if filters:
65
+ obj_qs = obj_qs.filter(**filters)
66
+
38
67
  try:
39
- obj = await obj_qs.prefetch_related(*self.get_reverse_relations()).aget(**q)
68
+ obj = await obj_qs.aget(**get_q)
40
69
  except ObjectDoesNotExist:
41
- raise SerializeError({self.model._meta.model_name: "not found"}, 404)
70
+ raise SerializeError({self.model_name: "not found"}, 404)
71
+
42
72
  return obj
43
73
 
44
74
  def get_reverse_relations(self) -> list[str]:
@@ -121,7 +151,7 @@ class ModelUtil:
121
151
  if isinstance(self.model, ModelSerializerMeta):
122
152
  await obj.custom_actions(customs)
123
153
  await obj.post_create()
124
- return 201, await self.read_s(request, obj, obj_schema)
154
+ return await self.read_s(request, obj, obj_schema)
125
155
 
126
156
  async def read_s(
127
157
  self,
@@ -150,7 +180,7 @@ class ModelUtil:
150
180
  async def delete_s(self, request: HttpRequest, pk: int | str):
151
181
  obj = await self.get_object(request, pk)
152
182
  await obj.adelete()
153
- return HttpResponse(status=204)
183
+ return None
154
184
 
155
185
 
156
186
  class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
@@ -160,15 +190,18 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
160
190
  class CreateSerializer:
161
191
  fields: list[str] = []
162
192
  customs: list[tuple[str, type, Any]] = []
163
- optionals: list[str] = []
193
+ optionals: list[tuple[str, type]] = []
194
+ excludes: list[str] = []
164
195
 
165
196
  class ReadSerializer:
166
197
  fields: list[str] = []
198
+ excludes: list[str] = []
167
199
 
168
200
  class UpdateSerializer:
169
201
  fields: list[str] = []
170
202
  customs: list[tuple[str, type, Any]] = []
171
- optionals: list[str] = []
203
+ optionals: list[tuple[str, type]] = []
204
+ excludes: list[str] = []
172
205
 
173
206
  @property
174
207
  def has_custom_fields_create(self):
@@ -195,24 +228,61 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
195
228
  return self.has_optional_fields_create or self.has_optional_fields_update
196
229
 
197
230
  @classmethod
198
- def _get_special_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
199
- try:
200
- match s_type:
201
- case "create":
202
- fields = getattr(cls.CreateSerializer, f_type)
203
- case "update":
204
- fields = getattr(cls.UpdateSerializer, f_type)
205
- except AttributeError:
206
- return []
231
+ def _get_fields(cls, s_type: type[S_TYPES], f_type: type[F_TYPES]):
232
+ match s_type:
233
+ case "create":
234
+ fields = getattr(cls.CreateSerializer, f_type, [])
235
+ case "update":
236
+ fields = getattr(cls.UpdateSerializer, f_type, [])
237
+ case "read":
238
+ fields = getattr(cls.ReadSerializer, f_type, [])
207
239
  return fields
208
240
 
209
241
  @classmethod
210
242
  def _is_special_field(
211
243
  cls, s_type: type[S_TYPES], field: str, f_type: type[F_TYPES]
212
244
  ):
213
- special_fields = cls._get_special_fields(s_type, f_type)
245
+ special_fields = cls._get_fields(s_type, f_type)
214
246
  return any(field in special_f for special_f in special_fields)
215
247
 
248
+ @classmethod
249
+ def _generate_model_schema(
250
+ cls,
251
+ schema_type: type[SCHEMA_TYPES],
252
+ depth: int = None,
253
+ ) -> Schema:
254
+ match schema_type:
255
+ case "In":
256
+ s_type = "create"
257
+ case "Patch":
258
+ s_type = "update"
259
+ case "Out":
260
+ fields, reverse_rels, excludes = cls.get_schema_out_data()
261
+ if not fields and not reverse_rels and not excludes:
262
+ return None
263
+ return create_schema(
264
+ model=cls,
265
+ name=f"{cls._meta.model_name}SchemaOut",
266
+ depth=depth,
267
+ fields=fields,
268
+ custom_fields=reverse_rels,
269
+ exclude=excludes,
270
+ )
271
+ fields = cls.get_fields(s_type)
272
+ customs = cls.get_custom_fields(s_type) + cls.get_optional_fields(s_type)
273
+ excludes = cls.get_excluded_fields(s_type)
274
+ return (
275
+ create_schema(
276
+ model=cls,
277
+ name=f"{cls._meta.model_name}Schema{schema_type}",
278
+ fields=fields,
279
+ custom_fields=customs,
280
+ exclude=excludes,
281
+ )
282
+ if fields or customs or excludes
283
+ else None
284
+ )
285
+
216
286
  @classmethod
217
287
  def verbose_name_path_resolver(cls) -> str:
218
288
  return "-".join(cls._meta.verbose_name_plural.split(" "))
@@ -292,7 +362,7 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
292
362
  def get_schema_out_data(cls):
293
363
  fields = []
294
364
  reverse_rels = []
295
- for f in cls.ReadSerializer.fields:
365
+ for f in cls.get_fields("read"):
296
366
  field_obj = getattr(cls, f)
297
367
  if isinstance(field_obj, ManyToManyDescriptor):
298
368
  rel_obj: ModelSerializer = field_obj.field.related_model
@@ -312,7 +382,7 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
312
382
  reverse_rels.append(rel_data)
313
383
  continue
314
384
  fields.append(f)
315
- return fields, reverse_rels
385
+ return fields, reverse_rels, cls.get_excluded_fields("read")
316
386
 
317
387
  @classmethod
318
388
  def is_custom(cls, field: str):
@@ -328,49 +398,31 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
328
398
 
329
399
  @classmethod
330
400
  def get_custom_fields(cls, s_type: type[S_TYPES]):
331
- return cls._get_special_fields(s_type, "customs")
401
+ return cls._get_fields(s_type, "customs")
332
402
 
333
403
  @classmethod
334
404
  def get_optional_fields(cls, s_type: type[S_TYPES]):
335
405
  return [
336
406
  (field, field_type, None)
337
- for field, field_type in cls._get_special_fields(s_type, "optionals")
407
+ for field, field_type in cls._get_fields(s_type, "optionals")
338
408
  ]
339
409
 
410
+ @classmethod
411
+ def get_excluded_fields(cls, s_type: type[S_TYPES]):
412
+ return cls._get_fields(s_type, "excludes")
413
+
414
+ @classmethod
415
+ def get_fields(cls, s_type: type[S_TYPES]):
416
+ return cls._get_fields(s_type, "fields")
417
+
340
418
  @classmethod
341
419
  def generate_read_s(cls, depth: int = 1) -> Schema:
342
- fields, reverse_rels = cls.get_schema_out_data()
343
- customs = [custom for custom in reverse_rels]
344
- return create_schema(
345
- model=cls,
346
- name=f"{cls._meta.model_name}SchemaOut",
347
- depth=depth,
348
- fields=fields,
349
- custom_fields=customs,
350
- )
420
+ return cls._generate_model_schema("Out", depth)
351
421
 
352
422
  @classmethod
353
423
  def generate_create_s(cls) -> Schema:
354
- fields = getattr(cls.CreateSerializer, "fields", []) + [
355
- field[0] for field in cls.get_optional_fields("create")
356
- ]
357
- customs = cls.get_custom_fields("create") + cls.get_optional_fields("create")
358
- return create_schema(
359
- model=cls,
360
- name=f"{cls._meta.model_name}SchemaIn",
361
- fields=fields,
362
- custom_fields=customs,
363
- )
424
+ return cls._generate_model_schema("In")
364
425
 
365
426
  @classmethod
366
427
  def generate_update_s(cls) -> Schema:
367
- fields = getattr(cls.UpdateSerializer, "fields", []) + [
368
- field[0] for field in cls.get_optional_fields("update")
369
- ]
370
- customs = cls.get_custom_fields("update") + cls.get_optional_fields("update")
371
- return create_schema(
372
- model=cls,
373
- name=f"{cls._meta.model_name}SchemaPatch",
374
- fields=fields,
375
- custom_fields=customs,
376
- )
428
+ return cls._generate_model_schema("Patch")
ninja_aio/types.py CHANGED
@@ -2,10 +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
- F_TYPES = Literal["customs", "optionals"]
8
-
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"]
9
10
 
10
11
  class ModelSerializerType(type):
11
12
  def __repr__(self):
ninja_aio/views.py CHANGED
@@ -1,14 +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, Query
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
+ from django.db.models import Model, QuerySet
8
+ from pydantic import create_model
8
9
 
9
10
  from .models import ModelSerializer, ModelUtil
10
11
  from .schemas import GenericMessageSchema
11
- from .types import ModelSerializerMeta
12
+ from .types import ModelSerializerMeta, VIEW_TYPES
12
13
 
13
14
  ERROR_CODES = frozenset({400, 401, 404, 428})
14
15
 
@@ -55,7 +56,6 @@ class APIView:
55
56
  async def some_method(request, *args, **kwargs):
56
57
  pass
57
58
  """
58
- pass
59
59
 
60
60
  def add_views(self):
61
61
  self.views()
@@ -73,23 +73,63 @@ class APIViewSet:
73
73
  schema_update: Schema | None = None
74
74
  auths: list | None = NOT_SET
75
75
  pagination_class: type[AsyncPaginationBase] = PageNumberPagination
76
+ query_params: dict[str, tuple[type, ...]] = {}
77
+ disable: list[type[VIEW_TYPES]] = []
76
78
 
77
79
  def __init__(self) -> None:
78
- self.router = Router(tags=[self.model._meta.model_name.capitalize()])
79
- self.path = "/"
80
- self.path_retrieve = f"{self.model._meta.pk.attname}/"
81
80
  self.error_codes = ERROR_CODES
82
81
  self.model_util = ModelUtil(self.model)
83
- self.schema_out, self.schema_update, self.schema_in = self.get_schemas()
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")
84
114
 
85
115
  def get_schemas(self):
86
116
  if isinstance(self.model, ModelSerializerMeta):
87
117
  return (
88
118
  self.model.generate_read_s(),
89
- self.model.generate_update_s(),
90
119
  self.model.generate_create_s(),
120
+ self.model.generate_update_s(),
91
121
  )
92
- return self.schema_out, self.schema_update, self.schema_in
122
+ return self.schema_out, self.schema_in, self.schema_update
123
+
124
+ async def query_params_handler(
125
+ self, queryset: QuerySet[ModelSerializer], filters: dict
126
+ ):
127
+ """
128
+ Override this method to handle request query params making queries to the database
129
+ based on filters or any other logic. This method should return a queryset. filters
130
+ are given already dumped by the schema.
131
+ """
132
+ return queryset
93
133
 
94
134
  def create_view(self):
95
135
  @self.router.post(
@@ -98,9 +138,10 @@ class APIViewSet:
98
138
  response={201: self.schema_out, self.error_codes: GenericMessageSchema},
99
139
  )
100
140
  async def create(request: HttpRequest, data: self.schema_in):
101
- return await self.model_util.create_s(request, data, self.schema_out)
141
+ return 201, await self.model_util.create_s(request, data, self.schema_out)
102
142
 
103
- create.__name__ = f"create_{self.model._meta.model_name}"
143
+ create.__name__ = f"create_{self.model_util.model_name}"
144
+ return create
104
145
 
105
146
  def list_view(self):
106
147
  @self.router.get(
@@ -112,21 +153,22 @@ class APIViewSet:
112
153
  },
113
154
  )
114
155
  @paginate(self.pagination_class)
115
- async def list(request: HttpRequest):
156
+ async def list(request: HttpRequest, filters: Query[self.filters_schema]):
116
157
  qs = self.model.objects.select_related()
117
158
  if isinstance(self.model, ModelSerializerMeta):
118
159
  qs = await self.model.queryset_request(request)
119
160
  rels = self.model_util.get_reverse_relations()
120
- print(rels)
121
161
  if len(rels) > 0:
122
162
  qs = qs.prefetch_related(*rels)
163
+ qs = await self.query_params_handler(qs, filters.model_dump())
123
164
  objs = [
124
165
  await self.model_util.read_s(request, obj, self.schema_out)
125
166
  async for obj in qs.all()
126
167
  ]
127
168
  return objs
128
169
 
129
- list.__name__ = f"list_{self.model._meta.verbose_name_plural}"
170
+ list.__name__ = f"list_{self.model_util.verbose_name_view_resolver()}"
171
+ return list
130
172
 
131
173
  def retrieve_view(self):
132
174
  @self.router.get(
@@ -134,11 +176,12 @@ class APIViewSet:
134
176
  auth=self.auths,
135
177
  response={200: self.schema_out, self.error_codes: GenericMessageSchema},
136
178
  )
137
- async def retrieve(request: HttpRequest, pk: int | str):
179
+ async def retrieve(request: HttpRequest, pk: Path[self.path_schema]):
138
180
  obj = await self.model_util.get_object(request, pk)
139
181
  return await self.model_util.read_s(request, obj, self.schema_out)
140
182
 
141
- retrieve.__name__ = f"retrieve_{self.model._meta.model_name}"
183
+ retrieve.__name__ = f"retrieve_{self.model_util.model_name}"
184
+ return retrieve
142
185
 
143
186
  def update_view(self):
144
187
  @self.router.patch(
@@ -146,10 +189,13 @@ class APIViewSet:
146
189
  auth=self.auths,
147
190
  response={200: self.schema_out, self.error_codes: GenericMessageSchema},
148
191
  )
149
- async def update(request: HttpRequest, data: self.schema_update, pk: int | str):
192
+ async def update(
193
+ request: HttpRequest, data: self.schema_update, pk: Path[self.path_schema]
194
+ ):
150
195
  return await self.model_util.update_s(request, data, pk, self.schema_out)
151
196
 
152
- update.__name__ = f"update_{self.model._meta.model_name}"
197
+ update.__name__ = f"update_{self.model_util.model_name}"
198
+ return update
153
199
 
154
200
  def delete_view(self):
155
201
  @self.router.delete(
@@ -157,10 +203,11 @@ class APIViewSet:
157
203
  auth=self.auths,
158
204
  response={204: None, self.error_codes: GenericMessageSchema},
159
205
  )
160
- async def delete(request: HttpRequest, pk: int | str):
161
- return await self.model_util.delete_s(request, pk)
206
+ async def delete(request: HttpRequest, pk: Path[self.path_schema]):
207
+ return 204, await self.model_util.delete_s(request, pk)
162
208
 
163
- delete.__name__ = f"delete_{self.model._meta.model_name}"
209
+ delete.__name__ = f"delete_{self.model_util.model_name}"
210
+ return delete
164
211
 
165
212
  def views(self):
166
213
  """
@@ -194,14 +241,18 @@ class APIViewSet:
194
241
  async def some_method(request, *args, **kwargs):
195
242
  pass
196
243
  """
197
- pass
198
244
 
199
245
  def add_views(self):
200
- self.create_view()
201
- self.list_view()
202
- self.retrieve_view()
203
- self.update_view()
204
- self.delete_view()
246
+ if "all" in self.disable:
247
+ self.views()
248
+ return self.router
249
+
250
+ for views_type, (schema, view) in self._crud_views.items():
251
+ if views_type not in self.disable and (
252
+ schema is not None or views_type == "delete"
253
+ ):
254
+ view()
255
+
205
256
  self.views()
206
257
  return self.router
207
258
 
@@ -1,13 +0,0 @@
1
- ninja_aio/__init__.py,sha256=GPvxFosuQ4fZ9LpbRClXqv8e7BecmVUtj2nvbJyalus,119
2
- ninja_aio/api.py,sha256=Fe6l3YCy7MW5TY4-Lbl80CFuK2NT2Y7tHfmqPk6Mqak,1735
3
- ninja_aio/auth.py,sha256=3Pr8llYoCN59ZH3J_2qWmzjXxsy-rpQzXrVfwLfY25Q,1299
4
- ninja_aio/exceptions.py,sha256=LP9GZpDk1fYMRRgGHSAstcCvgu5uSDLg8PPDANlZGTs,1008
5
- ninja_aio/models.py,sha256=mIZofuGOsRATYQki7GIuz50OQWfMes41cvOOAAonQA0,13768
6
- ninja_aio/parsers.py,sha256=e_4lGCPV7zs-HTqtdJTc8yQD2KPAn9njbL8nF_Mmgkc,153
7
- ninja_aio/renders.py,sha256=mHeKNJtmDhZmgFpS9B6SPn5uZFcyVXrsoMhr149LeW8,1555
8
- ninja_aio/schemas.py,sha256=EgRkfhnzZqwGvdBmqlZixMtMcoD1ZxV_qzJ3fmaAy20,113
9
- ninja_aio/types.py,sha256=ZhFqRDP5g2A2er3izx36QjrYJxL_jgusbH7w7mVHNrk,339
10
- ninja_aio/views.py,sha256=IT6rTCcjXcVYrCi4_uEwnGOGXeWlIB4ZuyXJp8Fsqqc,6971
11
- django_ninja_aio_crud-0.4.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
12
- django_ninja_aio_crud-0.4.0.dist-info/METADATA,sha256=0yQr4CWufJ7ZtXuhpTOchKQ-yOzG7o-wTaymDV0Tqvc,10832
13
- django_ninja_aio_crud-0.4.0.dist-info/RECORD,,