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.
- {django_ninja_aio_crud-0.4.0.dist-info → django_ninja_aio_crud-0.6.0.dist-info}/METADATA +85 -14
- django_ninja_aio_crud-0.6.0.dist-info/RECORD +13 -0
- ninja_aio/__init__.py +1 -1
- ninja_aio/auth.py +8 -12
- ninja_aio/exceptions.py +19 -7
- ninja_aio/models.py +108 -56
- ninja_aio/types.py +4 -3
- ninja_aio/views.py +79 -28
- django_ninja_aio_crud-0.4.0.dist-info/RECORD +0 -13
- {django_ninja_aio_crud-0.4.0.dist-info → django_ninja_aio_crud-0.6.0.dist-info}/WHEEL +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: django-ninja-aio-crud
|
|
3
|
-
Version: 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
|
+

|
|
34
|
+
[](https://codecov.io/gh/caspel26/django-ninja-aio-crud)
|
|
35
|
+
[](https://pypi.org/project/django-ninja-aio-crud/)
|
|
36
|
+
[](https://github.com/caspel26/django-ninja-aio-crud/blob/main/LICENSE)
|
|
37
|
+
[](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
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
+
details: str | None = None,
|
|
15
17
|
) -> None:
|
|
16
|
-
|
|
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
|
|
33
|
-
request: HttpRequest, exc:
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
68
|
+
obj = await obj_qs.aget(**get_q)
|
|
40
69
|
except ObjectDoesNotExist:
|
|
41
|
-
raise SerializeError({self.
|
|
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
|
|
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
|
|
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
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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.
|
|
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(
|
|
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.
|
|
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:
|
|
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.
|
|
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.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
self.
|
|
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,,
|
|
File without changes
|