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