django-ninja-aio-crud 0.6.1__tar.gz → 0.6.3__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.1
3
+ Version: 0.6.3
4
4
  Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
6
  Requires-Python: >=3.10
@@ -100,7 +100,7 @@ class Foo(ModelSerializer):
100
100
  fields = ["name", "bar"]
101
101
  ```
102
102
 
103
- - 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).
103
+ - 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.
104
104
 
105
105
  ```python
106
106
  # models.py
@@ -184,6 +184,35 @@ class Foo(ModelSerializer):
184
184
  excludes = ["id", "name"]
185
185
  optionals = [("bar", str), ("active", bool)]
186
186
  ```
187
+ - If you want to add into ReadSerializer model properties you must define them as customs.
188
+ ```python
189
+ # models.py
190
+ from django.db import models
191
+ from ninja_aio.models import ModelSerializer
192
+
193
+
194
+ class Foo(ModelSerializer):
195
+ name = models.CharField(max_length=30)
196
+ bar = models.CharField(max_length=30, default="")
197
+ active = models.BooleanField(default=False)
198
+
199
+ @property
200
+ def full_name(self):
201
+ return f"{self.name} example_full_name"
202
+
203
+ class ReadSerializer:
204
+ excludes = ["bar"]
205
+ customs = [("full_name", str, "")]
206
+
207
+ class CreateSerializer:
208
+ fields = ["name"]
209
+ optionals = [("bar", str), ("active", bool)]
210
+
211
+ class UpdateSerializer:
212
+ excludes = ["id", "name"]
213
+ optionals = [("bar", str), ("active", bool)]
214
+ ```
215
+
187
216
 
188
217
 
189
218
  ### APIViewSet
@@ -69,7 +69,7 @@ class Foo(ModelSerializer):
69
69
  fields = ["name", "bar"]
70
70
  ```
71
71
 
72
- - 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).
72
+ - 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.
73
73
 
74
74
  ```python
75
75
  # models.py
@@ -153,6 +153,35 @@ class Foo(ModelSerializer):
153
153
  excludes = ["id", "name"]
154
154
  optionals = [("bar", str), ("active", bool)]
155
155
  ```
156
+ - If you want to add into ReadSerializer model properties you must define them as customs.
157
+ ```python
158
+ # models.py
159
+ from django.db import models
160
+ from ninja_aio.models import ModelSerializer
161
+
162
+
163
+ class Foo(ModelSerializer):
164
+ name = models.CharField(max_length=30)
165
+ bar = models.CharField(max_length=30, default="")
166
+ active = models.BooleanField(default=False)
167
+
168
+ @property
169
+ def full_name(self):
170
+ return f"{self.name} example_full_name"
171
+
172
+ class ReadSerializer:
173
+ excludes = ["bar"]
174
+ customs = [("full_name", str, "")]
175
+
176
+ class CreateSerializer:
177
+ fields = ["name"]
178
+ optionals = [("bar", str), ("active", bool)]
179
+
180
+ class UpdateSerializer:
181
+ excludes = ["id", "name"]
182
+ optionals = [("bar", str), ("active", bool)]
183
+ ```
184
+
156
185
 
157
186
 
158
187
  ### APIViewSet
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "0.6.1"
3
+ __version__ = "0.6.3"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
@@ -1,8 +1,8 @@
1
- from joserfc import jwt, jwk, errors
1
+ from joserfc import jwt, jwk
2
2
  from django.http.request import HttpRequest
3
3
  from ninja.security.http import HttpBearer
4
4
 
5
- from .exceptions import AuthError, parse_jose_error
5
+ from .exceptions import AuthError
6
6
 
7
7
 
8
8
  class AsyncJwtBearer(HttpBearer):
@@ -16,15 +16,7 @@ class AsyncJwtBearer(HttpBearer):
16
16
 
17
17
  def validate_claims(self, claims: jwt.Claims):
18
18
  jwt_claims = self.get_claims()
19
-
20
- try:
21
- jwt_claims.validate(claims)
22
- except (
23
- errors.InvalidClaimError,
24
- errors.MissingClaimError,
25
- errors.ExpiredTokenError,
26
- ) as exc:
27
- raise AuthError(**parse_jose_error(exc), status_code=401)
19
+ jwt_claims.validate(claims)
28
20
 
29
21
  async def auth_handler(self, request: HttpRequest):
30
22
  """
@@ -35,8 +27,6 @@ class AsyncJwtBearer(HttpBearer):
35
27
  async def authenticate(self, request: HttpRequest, token: str):
36
28
  try:
37
29
  self.dcd = jwt.decode(token, self.jwt_public, algorithms=self.algorithms)
38
- except errors.BadSignatureError as exc:
39
- raise AuthError(**parse_jose_error(exc), status_code=401)
40
30
  except ValueError as exc:
41
31
  raise AuthError(", ".join(exc.args), 401)
42
32
 
@@ -53,8 +53,16 @@ def _pydantic_validation_error(
53
53
  return api.create_response(request, error.error, status=error.status_code)
54
54
 
55
55
 
56
+ def _jose_error(
57
+ request: HttpRequest, exc: JoseError, api: type[NinjaAPI]
58
+ ) -> HttpResponse:
59
+ error = BaseException(**parse_jose_error(exc), status_code=401)
60
+ return api.create_response(request, error.error, status=error.status_code)
61
+
62
+
56
63
  def set_api_exception_handlers(api: type[NinjaAPI]) -> None:
57
64
  api.add_exception_handler(BaseException, partial(_default_error, api=api))
65
+ api.add_exception_handler(JoseError, partial(_jose_error, api=api))
58
66
  api.add_exception_handler(
59
67
  ValidationError, partial(_pydantic_validation_error, api=api)
60
68
  )
@@ -51,13 +51,14 @@ class ModelUtil:
51
51
  pk: int | str = None,
52
52
  filters: dict = None,
53
53
  getters: dict = None,
54
+ with_qs_request=True,
54
55
  ):
55
56
  get_q = {self.model_pk_name: pk} if pk is not None else {}
56
57
  if getters:
57
58
  get_q |= getters
58
59
 
59
60
  obj_qs = self.model.objects.select_related()
60
- if isinstance(self.model, ModelSerializerMeta):
61
+ if isinstance(self.model, ModelSerializerMeta) and with_qs_request:
61
62
  obj_qs = await self.model.queryset_request(request)
62
63
 
63
64
  obj_qs = obj_qs.prefetch_related(*self.get_reverse_relations())
@@ -108,7 +109,9 @@ class ModelUtil:
108
109
  raise SerializeError({k: ". ".join(exc.args)}, 400)
109
110
  if isinstance(field_obj, models.ForeignKey):
110
111
  rel_util = ModelUtil(field_obj.related_model)
111
- rel: ModelSerializer = await rel_util.get_object(request, v)
112
+ rel: ModelSerializer = await rel_util.get_object(
113
+ request, v, with_qs_request=False
114
+ )
112
115
  payload |= {k: rel}
113
116
  new_payload = {
114
117
  k: v for k, v in payload.items() if k not in (customs.keys() or optionals)
@@ -122,14 +125,17 @@ class ModelUtil:
122
125
  try:
123
126
  field_obj = getattr(self.model, k).field
124
127
  except AttributeError:
125
- field_obj = getattr(self.model, k).related
128
+ try:
129
+ field_obj = getattr(self.model, k).related
130
+ except AttributeError:
131
+ pass
126
132
  if isinstance(v, dict) and (
127
133
  isinstance(field_obj, models.ForeignKey)
128
134
  or isinstance(field_obj, models.OneToOneField)
129
135
  ):
130
136
  rel_util = ModelUtil(field_obj.related_model)
131
137
  rel: ModelSerializer = await rel_util.get_object(
132
- request, list(v.values())[0]
138
+ request, list(v.values())[0], with_qs_request=False
133
139
  )
134
140
  if isinstance(field_obj, models.ForeignKey):
135
141
  for rel_k, rel_v in v.items():
@@ -196,6 +202,7 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
196
202
  class ReadSerializer:
197
203
  fields: list[str] = []
198
204
  excludes: list[str] = []
205
+ customs: list[tuple[str, type, Any]] = []
199
206
 
200
207
  class UpdateSerializer:
201
208
  fields: list[str] = []
@@ -257,15 +264,15 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
257
264
  case "Patch":
258
265
  s_type = "update"
259
266
  case "Out":
260
- fields, reverse_rels, excludes = cls.get_schema_out_data()
261
- if not fields and not reverse_rels and not excludes:
267
+ fields, reverse_rels, excludes, customs = cls.get_schema_out_data()
268
+ if not fields and not reverse_rels and not excludes and not customs:
262
269
  return None
263
270
  return create_schema(
264
271
  model=cls,
265
272
  name=f"{cls._meta.model_name}SchemaOut",
266
273
  depth=depth,
267
274
  fields=fields,
268
- custom_fields=reverse_rels,
275
+ custom_fields=reverse_rels + customs,
269
276
  exclude=excludes,
270
277
  )
271
278
  fields = cls.get_fields(s_type)
@@ -367,25 +374,36 @@ class ModelSerializer(models.Model, metaclass=ModelSerializerMeta):
367
374
  reverse_rels = []
368
375
  for f in cls.get_fields("read"):
369
376
  field_obj = getattr(cls, f)
370
- if isinstance(field_obj, ManyToManyDescriptor):
371
- rel_obj: ModelSerializer = field_obj.field.related_model
372
- if field_obj.reverse:
373
- rel_obj: ModelSerializer = field_obj.field.model
374
- rel_data = cls.get_reverse_relation_schema(rel_obj, "many", f)
375
- reverse_rels.append(rel_data)
376
- continue
377
- if isinstance(field_obj, ReverseManyToOneDescriptor):
378
- rel_obj: ModelSerializer = field_obj.field.model
379
- rel_data = cls.get_reverse_relation_schema(rel_obj, "many", f)
380
- reverse_rels.append(rel_data)
381
- continue
382
- if isinstance(field_obj, ReverseOneToOneDescriptor):
383
- rel_obj: ModelSerializer = field_obj.related.related_model
384
- rel_data = cls.get_reverse_relation_schema(rel_obj, "one", f)
377
+ if isinstance(
378
+ field_obj,
379
+ (
380
+ ManyToManyDescriptor,
381
+ ReverseManyToOneDescriptor,
382
+ ReverseOneToOneDescriptor,
383
+ ),
384
+ ):
385
+ if isinstance(field_obj, ManyToManyDescriptor):
386
+ rel_obj: ModelSerializer = field_obj.field.related_model
387
+ if field_obj.reverse:
388
+ rel_obj = field_obj.field.model
389
+ rel_type = "many"
390
+ elif isinstance(field_obj, ReverseManyToOneDescriptor):
391
+ rel_obj = field_obj.field.model
392
+ rel_type = "many"
393
+ else: # ReverseOneToOneDescriptor
394
+ rel_obj = field_obj.related.related_model
395
+ rel_type = "one"
396
+
397
+ rel_data = cls.get_reverse_relation_schema(rel_obj, rel_type, f)
385
398
  reverse_rels.append(rel_data)
386
399
  continue
387
400
  fields.append(f)
388
- return fields, reverse_rels, cls.get_excluded_fields("read")
401
+ return (
402
+ fields,
403
+ reverse_rels,
404
+ cls.get_excluded_fields("read"),
405
+ cls.get_custom_fields("read"),
406
+ )
389
407
 
390
408
  @classmethod
391
409
  def is_custom(cls, field: str):
@@ -1,271 +0,0 @@
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
- auths: 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
- auths: 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.auths,
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.auths,
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.auths,
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.auths,
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.auths,
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
- )