django-ninja-aio-crud 0.5.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.5.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
@@ -197,7 +204,7 @@ class FooAPI(APIViewSet):
197
204
  model = Foo
198
205
  api = api
199
206
 
200
-
207
+
201
208
  FooAPI().add_views_to_route()
202
209
  ```
203
210
 
@@ -238,6 +245,10 @@ FooAPI().add_views_to_route()
238
245
 
239
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.
240
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
+
241
252
  ```python
242
253
  # views.py
243
254
  from ninja_aio import NinjaAIO
@@ -254,6 +265,29 @@ class FooAPI(APIViewSet):
254
265
  disable = ["retrieve", "update"]
255
266
 
256
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
+
257
291
  FooAPI().add_views_to_route()
258
292
  ```
259
293
 
@@ -371,7 +405,7 @@ BarAPI().add_views_to_route()
371
405
 
372
406
  ### Jwt
373
407
 
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.
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.
375
409
 
376
410
  ```python
377
411
  from ninja_aio.auth import AsyncJWTBearer
@@ -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.5.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,7 +1,7 @@
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
@@ -27,21 +27,48 @@ class ModelUtil:
27
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(" "))
32
44
 
33
45
  def verbose_name_view_resolver(self) -> str:
34
- return self.model._meta.verbose_name_plural.replace(" ", "")
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
35
58
 
36
- async def get_object(self, request: HttpRequest, pk: int | str):
37
- q = {self.model._meta.pk.attname: pk}
38
59
  obj_qs = self.model.objects.select_related()
39
60
  if isinstance(self.model, ModelSerializerMeta):
40
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
+
41
67
  try:
42
- obj = await obj_qs.prefetch_related(*self.get_reverse_relations()).aget(**q)
68
+ obj = await obj_qs.aget(**get_q)
43
69
  except ObjectDoesNotExist:
44
- raise SerializeError({self.model._meta.model_name: "not found"}, 404)
70
+ raise SerializeError({self.model_name: "not found"}, 404)
71
+
45
72
  return obj
46
73
 
47
74
  def get_reverse_relations(self) -> list[str]:
@@ -379,11 +406,11 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
379
406
  (field, field_type, None)
380
407
  for field, field_type in cls._get_fields(s_type, "optionals")
381
408
  ]
382
-
409
+
383
410
  @classmethod
384
411
  def get_excluded_fields(cls, s_type: type[S_TYPES]):
385
412
  return cls._get_fields(s_type, "excludes")
386
-
413
+
387
414
  @classmethod
388
415
  def get_fields(cls, s_type: type[S_TYPES]):
389
416
  return cls._get_fields(s_type, "fields")
ninja_aio/views.py CHANGED
@@ -1,10 +1,10 @@
1
1
  from typing import List
2
2
 
3
- from ninja import NinjaAPI, Router, Schema, Path
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
8
  from pydantic import create_model
9
9
 
10
10
  from .models import ModelSerializer, ModelUtil
@@ -73,16 +73,19 @@ 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, ...]] = {}
76
77
  disable: list[type[VIEW_TYPES]] = []
77
78
 
78
79
  def __init__(self) -> None:
79
- self.router = Router(tags=[self.model._meta.model_name.capitalize()])
80
- self.path = "/"
81
- self.path_retrieve = f"{{{self.model._meta.pk.attname}}}/"
82
80
  self.error_codes = ERROR_CODES
83
81
  self.model_util = ModelUtil(self.model)
84
82
  self.schema_out, self.schema_in, self.schema_update = self.get_schemas()
85
- self.path_schema = self._create_path_schema()
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}}}/"
86
89
 
87
90
  @property
88
91
  def _crud_views(self):
@@ -98,11 +101,16 @@ class APIViewSet:
98
101
  "delete": (None, self.delete_view),
99
102
  }
100
103
 
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)
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")
106
114
 
107
115
  def get_schemas(self):
108
116
  if isinstance(self.model, ModelSerializerMeta):
@@ -113,6 +121,16 @@ class APIViewSet:
113
121
  )
114
122
  return self.schema_out, self.schema_in, self.schema_update
115
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
133
+
116
134
  def create_view(self):
117
135
  @self.router.post(
118
136
  self.path,
@@ -122,7 +140,7 @@ class APIViewSet:
122
140
  async def create(request: HttpRequest, data: self.schema_in):
123
141
  return 201, await self.model_util.create_s(request, data, self.schema_out)
124
142
 
125
- create.__name__ = f"create_{self.model._meta.model_name}"
143
+ create.__name__ = f"create_{self.model_util.model_name}"
126
144
  return create
127
145
 
128
146
  def list_view(self):
@@ -135,13 +153,14 @@ class APIViewSet:
135
153
  },
136
154
  )
137
155
  @paginate(self.pagination_class)
138
- async def list(request: HttpRequest):
156
+ async def list(request: HttpRequest, filters: Query[self.filters_schema]):
139
157
  qs = self.model.objects.select_related()
140
158
  if isinstance(self.model, ModelSerializerMeta):
141
159
  qs = await self.model.queryset_request(request)
142
160
  rels = self.model_util.get_reverse_relations()
143
161
  if len(rels) > 0:
144
162
  qs = qs.prefetch_related(*rels)
163
+ qs = await self.query_params_handler(qs, filters.model_dump())
145
164
  objs = [
146
165
  await self.model_util.read_s(request, obj, self.schema_out)
147
166
  async for obj in qs.all()
@@ -161,7 +180,7 @@ class APIViewSet:
161
180
  obj = await self.model_util.get_object(request, pk)
162
181
  return await self.model_util.read_s(request, obj, self.schema_out)
163
182
 
164
- retrieve.__name__ = f"retrieve_{self.model._meta.model_name}"
183
+ retrieve.__name__ = f"retrieve_{self.model_util.model_name}"
165
184
  return retrieve
166
185
 
167
186
  def update_view(self):
@@ -170,10 +189,12 @@ class APIViewSet:
170
189
  auth=self.auths,
171
190
  response={200: self.schema_out, self.error_codes: GenericMessageSchema},
172
191
  )
173
- async def update(request: HttpRequest, data: self.schema_update, pk: Path[self.path_schema]):
192
+ async def update(
193
+ request: HttpRequest, data: self.schema_update, pk: Path[self.path_schema]
194
+ ):
174
195
  return await self.model_util.update_s(request, data, pk, self.schema_out)
175
196
 
176
- update.__name__ = f"update_{self.model._meta.model_name}"
197
+ update.__name__ = f"update_{self.model_util.model_name}"
177
198
  return update
178
199
 
179
200
  def delete_view(self):
@@ -185,7 +206,7 @@ class APIViewSet:
185
206
  async def delete(request: HttpRequest, pk: Path[self.path_schema]):
186
207
  return 204, await self.model_util.delete_s(request, pk)
187
208
 
188
- delete.__name__ = f"delete_{self.model._meta.model_name}"
209
+ delete.__name__ = f"delete_{self.model_util.model_name}"
189
210
  return delete
190
211
 
191
212
  def views(self):
@@ -1,13 +0,0 @@
1
- ninja_aio/__init__.py,sha256=ZragxlHPdmlSQb8pX6GdLzlHSdO-6qs4EelxyzrSMGw,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=wCEQfzU3Q96rMwbkqBMjuaPipILRoX2eorId1hfsiR4,14569
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=p0wWVMw1TRtdCSHKfIWHPINGn3rkGWFPsRwMyg4HUOs,8121
11
- django_ninja_aio_crud-0.5.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
12
- django_ninja_aio_crud-0.5.0.dist-info/METADATA,sha256=FdZKHgHjgaZqqrv9BNB801Q1lHjSlYaMFUA-cBA_SQs,11573
13
- django_ninja_aio_crud-0.5.0.dist-info/RECORD,,