django-ninja-aio-crud 1.0.0__py3-none-any.whl → 1.0.1__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-1.0.1.dist-info/METADATA +336 -0
- {django_ninja_aio_crud-1.0.0.dist-info → django_ninja_aio_crud-1.0.1.dist-info}/RECORD +6 -6
- ninja_aio/__init__.py +1 -1
- ninja_aio/views.py +127 -126
- django_ninja_aio_crud-1.0.0.dist-info/METADATA +0 -527
- {django_ninja_aio_crud-1.0.0.dist-info → django_ninja_aio_crud-1.0.1.dist-info}/WHEEL +0 -0
- {django_ninja_aio_crud-1.0.0.dist-info → django_ninja_aio_crud-1.0.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-ninja-aio-crud
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Django Ninja AIO CRUD - Rest Framework
|
|
5
|
+
Author: Giuseppe Casillo
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Topic :: Internet
|
|
10
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
13
|
+
Classifier: Topic :: Software Development
|
|
14
|
+
Classifier: Environment :: Web Environment
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Framework :: Django
|
|
22
|
+
Classifier: Framework :: AsyncIO
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
24
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: django-ninja >=1.3.0
|
|
27
|
+
Requires-Dist: joserfc >=1.0.0
|
|
28
|
+
Requires-Dist: orjson >= 3.10.7
|
|
29
|
+
Requires-Dist: coverage ; extra == "test"
|
|
30
|
+
Project-URL: Documentation, https://django-ninja-aio.com
|
|
31
|
+
Project-URL: Repository, https://github.com/caspel26/django-ninja-aio-crud
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
|
|
34
|
+
# 🥷 django-ninja-aio-crud
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+
[](https://codecov.io/gh/caspel26/django-ninja-aio-crud/)
|
|
38
|
+
[](https://pypi.org/project/django-ninja-aio-crud/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
[](https://github.com/astral-sh/ruff)
|
|
41
|
+
|
|
42
|
+
> Lightweight async CRUD layer on top of **[Django Ninja](https://django-ninja.dev/)** with automatic schema generation, filtering, pagination, auth & Many‑to‑Many management.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## ✨ Features
|
|
47
|
+
|
|
48
|
+
- Async CRUD ViewSets (create, list, retrieve, update, delete)
|
|
49
|
+
- Automatic Pydantic schemas from `ModelSerializer` (read/create/update)
|
|
50
|
+
- Dynamic query params (runtime schema via `pydantic.create_model`)
|
|
51
|
+
- Per-method authentication (`auth`, `get_auth`, `post_auth`, etc.)
|
|
52
|
+
- Async pagination (customizable)
|
|
53
|
+
- M2M relation endpoints via `M2MRelationSchema` (add/remove/get + filters)
|
|
54
|
+
- Reverse relation serialization
|
|
55
|
+
- Hook methods (`query_params_handler`, `<related>_query_params_handler`, `custom_actions`, lifecycle hooks)
|
|
56
|
+
- ORJSON renderer through `NinjaAIO`
|
|
57
|
+
- Clean, minimal integration
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 📦 Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install django-ninja-aio-crud
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Add to your project’s dependencies and ensure Django Ninja is installed.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 🚀 Quick Start
|
|
72
|
+
|
|
73
|
+
models.py
|
|
74
|
+
```python
|
|
75
|
+
from django.db import models
|
|
76
|
+
from ninja_aio.models import ModelSerializer
|
|
77
|
+
|
|
78
|
+
class Book(ModelSerializer):
|
|
79
|
+
title = models.CharField(max_length=120)
|
|
80
|
+
published = models.BooleanField(default=True)
|
|
81
|
+
|
|
82
|
+
class ReadSerializer:
|
|
83
|
+
fields = ["id", "title", "published"]
|
|
84
|
+
|
|
85
|
+
class CreateSerializer:
|
|
86
|
+
fields = ["title", "published"]
|
|
87
|
+
|
|
88
|
+
class UpdateSerializer:
|
|
89
|
+
optionals = [("title", str), ("published", bool)]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
views.py
|
|
93
|
+
```python
|
|
94
|
+
from ninja_aio import NinjaAIO
|
|
95
|
+
from ninja_aio.views import APIViewSet
|
|
96
|
+
from .models import Book
|
|
97
|
+
|
|
98
|
+
api = NinjaAIO()
|
|
99
|
+
|
|
100
|
+
class BookViewSet(APIViewSet):
|
|
101
|
+
model = Book
|
|
102
|
+
api = api
|
|
103
|
+
|
|
104
|
+
BookViewSet().add_views_to_route()
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Visit `/docs` → CRUD endpoints ready.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 🔄 Query Filtering
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
class BookViewSet(APIViewSet):
|
|
115
|
+
model = Book
|
|
116
|
+
api = api
|
|
117
|
+
query_params = {"published": (bool, None), "title": (str, None)}
|
|
118
|
+
|
|
119
|
+
async def query_params_handler(self, queryset, filters):
|
|
120
|
+
if filters.get("published") is not None:
|
|
121
|
+
queryset = queryset.filter(published=filters["published"])
|
|
122
|
+
if filters.get("title"):
|
|
123
|
+
queryset = queryset.filter(title__icontains=filters["title"])
|
|
124
|
+
return queryset
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Request:
|
|
128
|
+
```
|
|
129
|
+
GET /book/?published=true&title=python
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 🤝 Many-to-Many Example (with filters)
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from ninja_aio.schemas import M2MRelationSchema
|
|
138
|
+
|
|
139
|
+
class Tag(ModelSerializer):
|
|
140
|
+
name = models.CharField(max_length=50)
|
|
141
|
+
class ReadSerializer:
|
|
142
|
+
fields = ["id", "name"]
|
|
143
|
+
|
|
144
|
+
class Article(ModelSerializer):
|
|
145
|
+
title = models.CharField(max_length=120)
|
|
146
|
+
tags = models.ManyToManyField(Tag, related_name="articles")
|
|
147
|
+
|
|
148
|
+
class ReadSerializer:
|
|
149
|
+
fields = ["id", "title", "tags"]
|
|
150
|
+
|
|
151
|
+
class ArticleViewSet(APIViewSet):
|
|
152
|
+
model = Article
|
|
153
|
+
api = api
|
|
154
|
+
m2m_relations = [
|
|
155
|
+
M2MRelationSchema(
|
|
156
|
+
model=Tag,
|
|
157
|
+
related_name="tags",
|
|
158
|
+
filters={"name": (str, "")}
|
|
159
|
+
)
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
async def tags_query_params_handler(self, queryset, filters):
|
|
163
|
+
n = filters.get("name")
|
|
164
|
+
if n:
|
|
165
|
+
queryset = queryset.filter(name__icontains=n)
|
|
166
|
+
return queryset
|
|
167
|
+
|
|
168
|
+
ArticleViewSet().add_views_to_route()
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Endpoints:
|
|
172
|
+
- `GET /article/{pk}/tag?name=dev`
|
|
173
|
+
- `POST /article/{pk}/tag/` body: `{"add":[1,2],"remove":[3]}`
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 🔐 Authentication (JWT example)
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from ninja_aio.auth import AsyncJwtBearer
|
|
181
|
+
from joserfc import jwk
|
|
182
|
+
from .models import Book
|
|
183
|
+
|
|
184
|
+
PUBLIC_KEY = "-----BEGIN PUBLIC KEY----- ..."
|
|
185
|
+
|
|
186
|
+
class JWTAuth(AsyncJwtBearer):
|
|
187
|
+
jwt_public = jwk.RSAKey.import_key(PUBLIC_KEY)
|
|
188
|
+
jwt_alg = "RS256"
|
|
189
|
+
claims = {"sub": {"essential": True}}
|
|
190
|
+
|
|
191
|
+
async def auth_handler(self, request):
|
|
192
|
+
book_id = self.dcd.claims.get("sub")
|
|
193
|
+
return await Book.objects.aget(id=book_id)
|
|
194
|
+
|
|
195
|
+
class SecureBookViewSet(APIViewSet):
|
|
196
|
+
model = Book
|
|
197
|
+
api = api
|
|
198
|
+
auth = [JWTAuth()]
|
|
199
|
+
get_auth = None # list/retrieve public
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 📑 Lifecycle Hooks (ModelSerializer)
|
|
205
|
+
|
|
206
|
+
Available on every save/delete:
|
|
207
|
+
- `on_create_before_save`
|
|
208
|
+
- `on_create_after_save`
|
|
209
|
+
- `before_save`
|
|
210
|
+
- `after_save`
|
|
211
|
+
- `on_delete`
|
|
212
|
+
- `custom_actions(payload)` (create/update custom field logic)
|
|
213
|
+
- `post_create()` (after create commit)
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## 🧩 Adding Custom Endpoints
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
class BookViewSet(APIViewSet):
|
|
221
|
+
model = Book
|
|
222
|
+
api = api
|
|
223
|
+
|
|
224
|
+
def views(self):
|
|
225
|
+
@self.router.get("/stats/")
|
|
226
|
+
async def stats(request):
|
|
227
|
+
total = await Book.objects.acount()
|
|
228
|
+
return {"total": total}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## 📄 Pagination
|
|
234
|
+
|
|
235
|
+
Default: `PageNumberPagination`. Override:
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
from ninja.pagination import PageNumberPagination
|
|
239
|
+
|
|
240
|
+
class LargePagination(PageNumberPagination):
|
|
241
|
+
page_size = 50
|
|
242
|
+
max_page_size = 200
|
|
243
|
+
|
|
244
|
+
class BookViewSet(APIViewSet):
|
|
245
|
+
model = Book
|
|
246
|
+
api = api
|
|
247
|
+
pagination_class = LargePagination
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## 🛠 Project Structure & Docs
|
|
253
|
+
|
|
254
|
+
Documentation (MkDocs + Material):
|
|
255
|
+
```
|
|
256
|
+
docs/
|
|
257
|
+
getting_started/
|
|
258
|
+
tutorial/
|
|
259
|
+
api/
|
|
260
|
+
views/
|
|
261
|
+
models/
|
|
262
|
+
authentication.md
|
|
263
|
+
pagination.md
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Browse full reference:
|
|
267
|
+
- APIViewSet: `docs/api/views/api_view_set.md`
|
|
268
|
+
- APIView: `docs/api/views/api_view.md`
|
|
269
|
+
- ModelSerializer: `docs/api/models/model_serializer.md`
|
|
270
|
+
- Authentication: `docs/api/authentication.md`
|
|
271
|
+
- Pagination: `docs/api/pagination.md`
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 🧪 Tests
|
|
276
|
+
|
|
277
|
+
Use Django test runner + async ORM patterns. Example async pattern:
|
|
278
|
+
```python
|
|
279
|
+
obj = await Book.objects.acreate(title="T1", published=True)
|
|
280
|
+
count = await Book.objects.acount()
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## 🚫 Disable Operations
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
class ReadOnlyBookViewSet(APIViewSet):
|
|
289
|
+
model = Book
|
|
290
|
+
api = api
|
|
291
|
+
disable = ["update", "delete"]
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## 📌 Performance Tips
|
|
297
|
+
|
|
298
|
+
- Use `queryset_request` classmethod to prefetch
|
|
299
|
+
- Index frequently filtered fields
|
|
300
|
+
- Keep pagination enabled
|
|
301
|
+
- Limit slices (`queryset = queryset[:1000]`) for heavy searches
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## 🤲 Contributing
|
|
306
|
+
|
|
307
|
+
1. Fork
|
|
308
|
+
2. Create branch
|
|
309
|
+
3. Add tests
|
|
310
|
+
4. Run lint (`ruff check .`)
|
|
311
|
+
5. Open PR
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## ⭐ Support
|
|
316
|
+
|
|
317
|
+
Star the repo or donate:
|
|
318
|
+
- [Buy me a coffee](https://buymeacoffee.com/caspel26)
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## 📜 License
|
|
323
|
+
|
|
324
|
+
MIT License. See [LICENSE](LICENSE).
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## 🔗 Quick Links
|
|
329
|
+
|
|
330
|
+
| Item | Link |
|
|
331
|
+
|------|------|
|
|
332
|
+
| PyPI | https://pypi.org/project/django-ninja-aio-crud/ |
|
|
333
|
+
| Docs | https://django-ninja-aio.com
|
|
334
|
+
| Issues | https://github.com/caspel26/django-ninja-aio-crud/issues |
|
|
335
|
+
|
|
336
|
+
---
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
ninja_aio/__init__.py,sha256=
|
|
1
|
+
ninja_aio/__init__.py,sha256=hC9T2fsBJwd5QxeaoghQNrIN1i5mJfBAnK_Ky4lD7zs,119
|
|
2
2
|
ninja_aio/api.py,sha256=Fe6l3YCy7MW5TY4-Lbl80CFuK2NT2Y7tHfmqPk6Mqak,1735
|
|
3
3
|
ninja_aio/auth.py,sha256=zUwruKcz7MXuOnWp5k1CCSwEc8s2Lyqqk7Qm9kPbJ3o,5149
|
|
4
4
|
ninja_aio/decorators.py,sha256=LsvHbMxmw_So8NV0ey5NRRvSbfYkOZLeLQ4Fix7rQAY,5519
|
|
@@ -8,8 +8,8 @@ ninja_aio/parsers.py,sha256=e_4lGCPV7zs-HTqtdJTc8yQD2KPAn9njbL8nF_Mmgkc,153
|
|
|
8
8
|
ninja_aio/renders.py,sha256=0eYklRKd59aV4cZDom5vLZyA99Ob17OwkpMybsRXvyg,1970
|
|
9
9
|
ninja_aio/schemas.py,sha256=iv3VHCMlzez6Qs70zITYIwEz0EFOOaMPDVGRcTZCygA,1875
|
|
10
10
|
ninja_aio/types.py,sha256=TJSGlA7bt4g9fvPhJ7gzH5tKbLagPmZUzfgttEOp4xs,468
|
|
11
|
-
ninja_aio/views.py,sha256=
|
|
12
|
-
django_ninja_aio_crud-1.0.
|
|
13
|
-
django_ninja_aio_crud-1.0.
|
|
14
|
-
django_ninja_aio_crud-1.0.
|
|
15
|
-
django_ninja_aio_crud-1.0.
|
|
11
|
+
ninja_aio/views.py,sha256=EGOqybYR0Y0k7DVlMUwaruyEwF1LTJcM1EeFBU69EL0,23113
|
|
12
|
+
django_ninja_aio_crud-1.0.1.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
|
|
13
|
+
django_ninja_aio_crud-1.0.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
14
|
+
django_ninja_aio_crud-1.0.1.dist-info/METADATA,sha256=jIF-L1BHlBtCp8n7mTH1IK388J2FP_73567_ppirNUQ,8331
|
|
15
|
+
django_ninja_aio_crud-1.0.1.dist-info/RECORD,,
|
ninja_aio/__init__.py
CHANGED
ninja_aio/views.py
CHANGED
|
@@ -294,6 +294,7 @@ class APIViewSet:
|
|
|
294
294
|
"""
|
|
295
295
|
Register create endpoint.
|
|
296
296
|
"""
|
|
297
|
+
|
|
297
298
|
@self.router.post(
|
|
298
299
|
self.path,
|
|
299
300
|
auth=self.post_view_auth(),
|
|
@@ -311,6 +312,7 @@ class APIViewSet:
|
|
|
311
312
|
"""
|
|
312
313
|
Register list endpoint with pagination and optional filters.
|
|
313
314
|
"""
|
|
315
|
+
|
|
314
316
|
@self.router.get(
|
|
315
317
|
self.get_path,
|
|
316
318
|
auth=self.get_view_auth(),
|
|
@@ -347,6 +349,7 @@ class APIViewSet:
|
|
|
347
349
|
"""
|
|
348
350
|
Register retrieve endpoint.
|
|
349
351
|
"""
|
|
352
|
+
|
|
350
353
|
@self.router.get(
|
|
351
354
|
self.get_path_retrieve,
|
|
352
355
|
auth=self.get_view_auth(),
|
|
@@ -365,6 +368,7 @@ class APIViewSet:
|
|
|
365
368
|
"""
|
|
366
369
|
Register update endpoint.
|
|
367
370
|
"""
|
|
371
|
+
|
|
368
372
|
@self.router.patch(
|
|
369
373
|
self.path_retrieve,
|
|
370
374
|
auth=self.patch_view_auth(),
|
|
@@ -388,6 +392,7 @@ class APIViewSet:
|
|
|
388
392
|
"""
|
|
389
393
|
Register delete endpoint.
|
|
390
394
|
"""
|
|
395
|
+
|
|
391
396
|
@self.router.delete(
|
|
392
397
|
self.path_retrieve,
|
|
393
398
|
auth=self.delete_view_auth(),
|
|
@@ -440,140 +445,134 @@ class APIViewSet:
|
|
|
440
445
|
)
|
|
441
446
|
return errors, objs_detail, objs
|
|
442
447
|
|
|
443
|
-
def _m2m_views(self):
|
|
448
|
+
def _m2m_views(self, m2m_relation: M2MRelationSchema):
|
|
444
449
|
"""
|
|
445
450
|
Register M2M get/manage endpoints for each relation in m2m_relations.
|
|
446
451
|
Supports optional per-relation filters and custom query handler:
|
|
447
452
|
<related_name>_query_params_handler.
|
|
448
453
|
"""
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
454
|
+
model = m2m_relation.model
|
|
455
|
+
related_name = m2m_relation.related_name
|
|
456
|
+
m2m_auth = m2m_relation.auth or self.m2m_auth
|
|
457
|
+
rel_util = ModelUtil(model)
|
|
458
|
+
rel_path = (
|
|
459
|
+
rel_util.verbose_name_path_resolver()
|
|
460
|
+
if not m2m_relation.path
|
|
461
|
+
else m2m_relation.path
|
|
462
|
+
)
|
|
463
|
+
m2m_add = m2m_relation.add
|
|
464
|
+
m2m_remove = m2m_relation.remove
|
|
465
|
+
m2m_get = m2m_relation.get
|
|
466
|
+
filters_schema = self.m2m_filters_schemas.get(related_name)
|
|
467
|
+
if m2m_get:
|
|
468
|
+
|
|
469
|
+
@self.router.get(
|
|
470
|
+
f"{self.path_retrieve}{rel_path}",
|
|
471
|
+
response={
|
|
472
|
+
200: List[model.generate_related_s(),],
|
|
473
|
+
self.error_codes: GenericMessageSchema,
|
|
474
|
+
},
|
|
475
|
+
auth=m2m_auth,
|
|
476
|
+
summary=f"Get {rel_util.model._meta.verbose_name_plural.capitalize()}",
|
|
477
|
+
description=f"Get all related {rel_util.model._meta.verbose_name_plural.capitalize()}",
|
|
458
478
|
)
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
@paginate(self.pagination_class)
|
|
477
|
-
async def get_related(
|
|
478
|
-
request: HttpRequest,
|
|
479
|
-
pk: Path[self.path_schema], # type: ignore
|
|
480
|
-
filters: Query[filters_schema] = None # type: ignore
|
|
481
|
-
):
|
|
482
|
-
obj = await self.model_util.get_object(request, self._get_pk(pk))
|
|
483
|
-
related_manager = getattr(obj, related_name)
|
|
484
|
-
related_qs = related_manager.all()
|
|
485
|
-
if (
|
|
486
|
-
filters is not None
|
|
487
|
-
and (
|
|
488
|
-
query_handler := getattr(
|
|
489
|
-
self,
|
|
490
|
-
f"{m2m_data.related_name}_query_params_handler",
|
|
491
|
-
None,
|
|
492
|
-
)
|
|
493
|
-
)
|
|
494
|
-
is not None
|
|
495
|
-
):
|
|
496
|
-
related_qs = await query_handler(
|
|
497
|
-
related_qs, filters.model_dump()
|
|
498
|
-
)
|
|
499
|
-
related_objs = [
|
|
500
|
-
await rel_util.read_s(
|
|
501
|
-
request, rel_obj, model.generate_related_s()
|
|
479
|
+
@unique_view(f"get_{self.model_util.model_name}_{rel_path}")
|
|
480
|
+
@paginate(self.pagination_class)
|
|
481
|
+
async def get_related(
|
|
482
|
+
request: HttpRequest,
|
|
483
|
+
pk: Path[self.path_schema], # type: ignore
|
|
484
|
+
filters: Query[filters_schema] = None, # type: ignore
|
|
485
|
+
):
|
|
486
|
+
obj = await self.model_util.get_object(request, self._get_pk(pk))
|
|
487
|
+
related_manager = getattr(obj, related_name)
|
|
488
|
+
related_qs = related_manager.all()
|
|
489
|
+
if (
|
|
490
|
+
filters is not None
|
|
491
|
+
and (
|
|
492
|
+
query_handler := getattr(
|
|
493
|
+
self,
|
|
494
|
+
f"{m2m_relation.related_name}_query_params_handler",
|
|
495
|
+
None,
|
|
502
496
|
)
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
return related_objs
|
|
506
|
-
|
|
507
|
-
if m2m_add or m2m_remove:
|
|
508
|
-
summary = f"{'Add or Remove' if m2m_add and m2m_remove else 'Add' if m2m_add else 'Remove'} {rel_util.model._meta.verbose_name_plural.capitalize()}"
|
|
509
|
-
description = f"{'Add or remove' if m2m_add and m2m_remove else 'Add' if m2m_add else 'Remove'} {rel_util.model._meta.verbose_name_plural.capitalize()}"
|
|
510
|
-
schema_in = (
|
|
511
|
-
M2MSchemaIn
|
|
512
|
-
if m2m_add and m2m_remove
|
|
513
|
-
else M2MAddSchemaIn
|
|
514
|
-
if m2m_add
|
|
515
|
-
else M2MRemoveSchemaIn
|
|
516
|
-
)
|
|
517
|
-
|
|
518
|
-
@self.router.post(
|
|
519
|
-
f"{self.path_retrieve}{rel_path}/",
|
|
520
|
-
response={
|
|
521
|
-
200: M2MSchemaOut,
|
|
522
|
-
self.error_codes: GenericMessageSchema,
|
|
523
|
-
},
|
|
524
|
-
auth=m2m_auth,
|
|
525
|
-
summary=summary,
|
|
526
|
-
description=description,
|
|
527
|
-
)
|
|
528
|
-
@unique_view(f"manage_{self.model_util.model_name}_{rel_path}")
|
|
529
|
-
async def manage_related(
|
|
530
|
-
request: HttpRequest,
|
|
531
|
-
pk: Path[self.path_schema], # type: ignore
|
|
532
|
-
data: schema_in, # type: ignore
|
|
497
|
+
)
|
|
498
|
+
is not None
|
|
533
499
|
):
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
remove_objs,
|
|
552
|
-
) = await self._check_m2m_objs(
|
|
553
|
-
request,
|
|
554
|
-
data.remove,
|
|
555
|
-
model,
|
|
556
|
-
related_manager,
|
|
557
|
-
remove=True,
|
|
558
|
-
)
|
|
500
|
+
related_qs = await query_handler(related_qs, filters.model_dump())
|
|
501
|
+
related_objs = [
|
|
502
|
+
await rel_util.read_s(request, rel_obj, model.generate_related_s())
|
|
503
|
+
async for rel_obj in related_qs
|
|
504
|
+
]
|
|
505
|
+
return related_objs
|
|
506
|
+
|
|
507
|
+
if m2m_add or m2m_remove:
|
|
508
|
+
summary = f"{'Add or Remove' if m2m_add and m2m_remove else 'Add' if m2m_add else 'Remove'} {rel_util.model._meta.verbose_name_plural.capitalize()}"
|
|
509
|
+
description = f"{'Add or remove' if m2m_add and m2m_remove else 'Add' if m2m_add else 'Remove'} {rel_util.model._meta.verbose_name_plural.capitalize()}"
|
|
510
|
+
schema_in = (
|
|
511
|
+
M2MSchemaIn
|
|
512
|
+
if m2m_add and m2m_remove
|
|
513
|
+
else M2MAddSchemaIn
|
|
514
|
+
if m2m_add
|
|
515
|
+
else M2MRemoveSchemaIn
|
|
516
|
+
)
|
|
559
517
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
518
|
+
@self.router.post(
|
|
519
|
+
f"{self.path_retrieve}{rel_path}/",
|
|
520
|
+
response={
|
|
521
|
+
200: M2MSchemaOut,
|
|
522
|
+
self.error_codes: GenericMessageSchema,
|
|
523
|
+
},
|
|
524
|
+
auth=m2m_auth,
|
|
525
|
+
summary=summary,
|
|
526
|
+
description=description,
|
|
527
|
+
)
|
|
528
|
+
@unique_view(f"manage_{self.model_util.model_name}_{rel_path}")
|
|
529
|
+
async def manage_related(
|
|
530
|
+
request: HttpRequest,
|
|
531
|
+
pk: Path[self.path_schema], # type: ignore
|
|
532
|
+
data: schema_in, # type: ignore
|
|
533
|
+
):
|
|
534
|
+
obj = await self.model_util.get_object(request, self._get_pk(pk))
|
|
535
|
+
related_manager: QuerySet = getattr(obj, related_name)
|
|
536
|
+
add_errors, add_details, add_objs = [], [], []
|
|
537
|
+
remove_errors, remove_details, remove_objs = [], [], []
|
|
538
|
+
|
|
539
|
+
if m2m_add and hasattr(data, "add"):
|
|
540
|
+
(
|
|
541
|
+
add_errors,
|
|
542
|
+
add_details,
|
|
543
|
+
add_objs,
|
|
544
|
+
) = await self._check_m2m_objs(
|
|
545
|
+
request, data.add, model, related_manager
|
|
563
546
|
)
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
547
|
+
if m2m_remove and hasattr(data, "remove"):
|
|
548
|
+
(
|
|
549
|
+
remove_errors,
|
|
550
|
+
remove_details,
|
|
551
|
+
remove_objs,
|
|
552
|
+
) = await self._check_m2m_objs(
|
|
553
|
+
request,
|
|
554
|
+
data.remove,
|
|
555
|
+
model,
|
|
556
|
+
related_manager,
|
|
557
|
+
remove=True,
|
|
558
|
+
)
|
|
559
|
+
await asyncio.gather(
|
|
560
|
+
related_manager.aadd(*add_objs),
|
|
561
|
+
related_manager.aremove(*remove_objs),
|
|
562
|
+
)
|
|
563
|
+
results = add_details + remove_details
|
|
564
|
+
errors = add_errors + remove_errors
|
|
565
|
+
|
|
566
|
+
return {
|
|
567
|
+
"results": {
|
|
568
|
+
"count": len(results),
|
|
569
|
+
"details": results,
|
|
570
|
+
},
|
|
571
|
+
"errors": {
|
|
572
|
+
"count": len(errors),
|
|
573
|
+
"details": errors,
|
|
574
|
+
},
|
|
575
|
+
}
|
|
577
576
|
|
|
578
577
|
def _add_views(self):
|
|
579
578
|
"""
|
|
@@ -582,7 +581,8 @@ class APIViewSet:
|
|
|
582
581
|
"""
|
|
583
582
|
if "all" in self.disable:
|
|
584
583
|
if self.m2m_relations:
|
|
585
|
-
self.
|
|
584
|
+
for m2m_relation in self.m2m_relations:
|
|
585
|
+
self._m2m_views(m2m_relation)
|
|
586
586
|
self.views()
|
|
587
587
|
return self.router
|
|
588
588
|
|
|
@@ -594,7 +594,8 @@ class APIViewSet:
|
|
|
594
594
|
|
|
595
595
|
self.views()
|
|
596
596
|
if self.m2m_relations:
|
|
597
|
-
self.
|
|
597
|
+
for m2m_relation in self.m2m_relations:
|
|
598
|
+
self._m2m_views(m2m_relation)
|
|
598
599
|
return self.router
|
|
599
600
|
|
|
600
601
|
def add_views_to_route(self):
|
|
@@ -1,527 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: django-ninja-aio-crud
|
|
3
|
-
Version: 1.0.0
|
|
4
|
-
Summary: Django Ninja AIO CRUD - Rest Framework
|
|
5
|
-
Author: Giuseppe Casillo
|
|
6
|
-
Requires-Python: >=3.10
|
|
7
|
-
Description-Content-Type: text/markdown
|
|
8
|
-
Classifier: Operating System :: OS Independent
|
|
9
|
-
Classifier: Topic :: Internet
|
|
10
|
-
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
11
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
|
-
Classifier: Topic :: Software Development :: Libraries
|
|
13
|
-
Classifier: Topic :: Software Development
|
|
14
|
-
Classifier: Environment :: Web Environment
|
|
15
|
-
Classifier: Intended Audience :: Developers
|
|
16
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
-
Classifier: Framework :: Django
|
|
22
|
-
Classifier: Framework :: AsyncIO
|
|
23
|
-
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
24
|
-
Classifier: Topic :: Internet :: WWW/HTTP
|
|
25
|
-
License-File: LICENSE
|
|
26
|
-
Requires-Dist: django-ninja >=1.3.0
|
|
27
|
-
Requires-Dist: joserfc >=1.0.0
|
|
28
|
-
Requires-Dist: orjson >= 3.10.7
|
|
29
|
-
Requires-Dist: coverage ; extra == "test"
|
|
30
|
-
Project-URL: Documentation, https://caspel26.github.io/django-ninja-aio-crud/
|
|
31
|
-
Project-URL: Repository, https://github.com/caspel26/django-ninja-aio-crud
|
|
32
|
-
Provides-Extra: test
|
|
33
|
-
|
|
34
|
-
# 🥷 django-ninja-aio-crud
|
|
35
|
-

|
|
36
|
-
[](https://codecov.io/gh/caspel26/django-ninja-aio-crud)
|
|
37
|
-
[](https://pypi.org/project/django-ninja-aio-crud/)
|
|
38
|
-
[](https://github.com/caspel26/django-ninja-aio-crud/blob/main/LICENSE)
|
|
39
|
-
[](https://github.com/astral-sh/ruff)
|
|
40
|
-
> [!NOTE]
|
|
41
|
-
> 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.
|
|
42
|
-
|
|
43
|
-
## 📝 Instructions
|
|
44
|
-
|
|
45
|
-
### 📚 Prerequisites
|
|
46
|
-
|
|
47
|
-
- Install Python from the [official website](https://www.python.org/) (latest version) and ensure it is added to the system Path and environment variables.
|
|
48
|
-
|
|
49
|
-
### 💻 Setup your environment
|
|
50
|
-
|
|
51
|
-
- Create a virtual environment
|
|
52
|
-
```bash
|
|
53
|
-
python -m venv .venv
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### ✅ Activate it
|
|
57
|
-
|
|
58
|
-
- If you are from linux activate it with
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
. .venv/bin/activate
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
- If you are from windows activate it with
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
. .venv/Scripts/activate
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### 📥 Install package
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
pip install django-ninja-aio-crud
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## 🚀 Usage
|
|
77
|
-
|
|
78
|
-
> [!TIP]
|
|
79
|
-
> If you find **django ninja aio crud** useful, consider :star: this project
|
|
80
|
-
> and why not ... [Buy me a coffee](https://buymeacoffee.com/caspel26)
|
|
81
|
-
|
|
82
|
-
### ModelSerializer
|
|
83
|
-
|
|
84
|
-
- You can serialize your models using ModelSerializer and made them inherit from it. In your models.py import ModelSerializer
|
|
85
|
-
```python
|
|
86
|
-
# models.py
|
|
87
|
-
from django.db import models
|
|
88
|
-
from ninja_aio.models import ModelSerializer
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
class Foo(ModelSerializer):
|
|
92
|
-
name = models.CharField(max_length=30)
|
|
93
|
-
bar = models.CharField(max_length=30)
|
|
94
|
-
|
|
95
|
-
class ReadSerializer:
|
|
96
|
-
fields = ["id", "name", "bar"]
|
|
97
|
-
|
|
98
|
-
class CreateSerializer:
|
|
99
|
-
fields = ["name", "bar"]
|
|
100
|
-
|
|
101
|
-
class UpdateSerializer:
|
|
102
|
-
fields = ["name", "bar"]
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
- 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.
|
|
106
|
-
|
|
107
|
-
```python
|
|
108
|
-
# models.py
|
|
109
|
-
from django.db import models
|
|
110
|
-
from ninja_aio.models import ModelSerializer
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
class Foo(ModelSerializer):
|
|
114
|
-
name = models.CharField(max_length=30)
|
|
115
|
-
bar = models.CharField(max_length=30)
|
|
116
|
-
active = models.BooleanField(default=False)
|
|
117
|
-
|
|
118
|
-
class ReadSerializer:
|
|
119
|
-
fields = ["id", "name", "bar"]
|
|
120
|
-
|
|
121
|
-
class CreateSerializer:
|
|
122
|
-
customs = [("force_activation", bool, False)]
|
|
123
|
-
fields = ["name", "bar"]
|
|
124
|
-
|
|
125
|
-
class UpdateSerializer:
|
|
126
|
-
fields = ["name", "bar"]
|
|
127
|
-
|
|
128
|
-
async def custom_actions(self, payload: dict[str, Any]):
|
|
129
|
-
if not payload.get("force_activation"):
|
|
130
|
-
return
|
|
131
|
-
setattr(self, "force_activation", True)
|
|
132
|
-
|
|
133
|
-
async def post_create(self) -> None:
|
|
134
|
-
if not hasattr(self, "force_activation") or not getattr(self, "force_activation"):
|
|
135
|
-
return
|
|
136
|
-
self.active = True
|
|
137
|
-
await self.asave()
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
- 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.
|
|
141
|
-
|
|
142
|
-
- You can also define optional fields for you Create and Update serializers. To declare an optional fields you have to give the field type too.
|
|
143
|
-
```python
|
|
144
|
-
# models.py
|
|
145
|
-
from django.db import models
|
|
146
|
-
from ninja_aio.models import ModelSerializer
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
class Foo(ModelSerializer):
|
|
150
|
-
name = models.CharField(max_length=30)
|
|
151
|
-
bar = models.CharField(max_length=30, default="")
|
|
152
|
-
active = models.BooleanField(default=False)
|
|
153
|
-
|
|
154
|
-
class ReadSerializer:
|
|
155
|
-
fields = ["id", "name", "bar"]
|
|
156
|
-
|
|
157
|
-
class CreateSerializer:
|
|
158
|
-
fields = ["name"]
|
|
159
|
-
optionals = [("bar", str), ("active", bool)]
|
|
160
|
-
|
|
161
|
-
class UpdateSerializer:
|
|
162
|
-
optionals = [("bar", str), ("active", bool)]
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
- 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").
|
|
166
|
-
|
|
167
|
-
```python
|
|
168
|
-
# models.py
|
|
169
|
-
from django.db import models
|
|
170
|
-
from ninja_aio.models import ModelSerializer
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
class Foo(ModelSerializer):
|
|
174
|
-
name = models.CharField(max_length=30)
|
|
175
|
-
bar = models.CharField(max_length=30, default="")
|
|
176
|
-
active = models.BooleanField(default=False)
|
|
177
|
-
|
|
178
|
-
class ReadSerializer:
|
|
179
|
-
excludes = ["bar"]
|
|
180
|
-
|
|
181
|
-
class CreateSerializer:
|
|
182
|
-
fields = ["name"]
|
|
183
|
-
optionals = [("bar", str), ("active", bool)]
|
|
184
|
-
|
|
185
|
-
class UpdateSerializer:
|
|
186
|
-
excludes = ["id", "name"]
|
|
187
|
-
optionals = [("bar", str), ("active", bool)]
|
|
188
|
-
```
|
|
189
|
-
- If you want to add into ReadSerializer model properties you must define them as customs.
|
|
190
|
-
```python
|
|
191
|
-
# models.py
|
|
192
|
-
from django.db import models
|
|
193
|
-
from ninja_aio.models import ModelSerializer
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
class Foo(ModelSerializer):
|
|
197
|
-
name = models.CharField(max_length=30)
|
|
198
|
-
bar = models.CharField(max_length=30, default="")
|
|
199
|
-
active = models.BooleanField(default=False)
|
|
200
|
-
|
|
201
|
-
@property
|
|
202
|
-
def full_name(self):
|
|
203
|
-
return f"{self.name} example_full_name"
|
|
204
|
-
|
|
205
|
-
class ReadSerializer:
|
|
206
|
-
excludes = ["bar"]
|
|
207
|
-
customs = [("full_name", str, "")]
|
|
208
|
-
|
|
209
|
-
class CreateSerializer:
|
|
210
|
-
fields = ["name"]
|
|
211
|
-
optionals = [("bar", str), ("active", bool)]
|
|
212
|
-
|
|
213
|
-
class UpdateSerializer:
|
|
214
|
-
excludes = ["id", "name"]
|
|
215
|
-
optionals = [("bar", str), ("active", bool)]
|
|
216
|
-
```
|
|
217
|
-
- ModelSerializer comes out also with methods executed on object save and delete, them are:
|
|
218
|
-
|
|
219
|
-
1. on_create_before_save: code executed on object creation but before saving;
|
|
220
|
-
1. on_create_after_save: code executed on object creation but after saving;
|
|
221
|
-
1. before_save: code executed on every save but before saving;
|
|
222
|
-
1. after_save: code executed on every save but after saving;
|
|
223
|
-
1. on_delete: code executed after object delete;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
### APIViewSet
|
|
227
|
-
|
|
228
|
-
- 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.
|
|
229
|
-
|
|
230
|
-
```python
|
|
231
|
-
# views.py
|
|
232
|
-
from ninja_aio import NinjaAIO
|
|
233
|
-
from ninja_aio.views import APIViewSet
|
|
234
|
-
|
|
235
|
-
from .models import Foo
|
|
236
|
-
|
|
237
|
-
api = NinjaAIO()
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
class FooAPI(APIViewSet):
|
|
241
|
-
model = Foo
|
|
242
|
-
api = api
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
FooAPI().add_views_to_route()
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
- 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".
|
|
249
|
-
|
|
250
|
-
```python
|
|
251
|
-
# views.py
|
|
252
|
-
from ninja import Schema
|
|
253
|
-
from ninja_aio import NinjaAIO
|
|
254
|
-
from ninja_aio.views import APIViewSet
|
|
255
|
-
|
|
256
|
-
from .models import Foo
|
|
257
|
-
|
|
258
|
-
api = NinjaAIO()
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
class ExampleSchemaOut(Schema):
|
|
262
|
-
sum: float
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
class ExampleSchemaIn(Schema):
|
|
266
|
-
n1: float
|
|
267
|
-
n2: float
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
class FooAPI(APIViewSet):
|
|
271
|
-
model = Foo
|
|
272
|
-
api = api
|
|
273
|
-
|
|
274
|
-
def views(self):
|
|
275
|
-
@self.router.post("numbers-sum/", response={200: ExampleSchemaOut})
|
|
276
|
-
async def sum(request: HttpRequest, data: ExampleSchemaIn):
|
|
277
|
-
return 200, {sum: data.n1 + data.n2}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
FooAPI().add_views_to_route()
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
- 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.
|
|
284
|
-
|
|
285
|
-
> [!TIP]
|
|
286
|
-
> You can exclude by default an endpoint without declaring the serializer.
|
|
287
|
-
> For example if you don't want to give update method to a CRUD just do not declare UpdateSerializer into model.
|
|
288
|
-
|
|
289
|
-
```python
|
|
290
|
-
# views.py
|
|
291
|
-
from ninja_aio import NinjaAIO
|
|
292
|
-
from ninja_aio.views import APIViewSet
|
|
293
|
-
|
|
294
|
-
from .models import Foo
|
|
295
|
-
|
|
296
|
-
api = NinjaAIO()
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
class FooAPI(APIViewSet):
|
|
300
|
-
model = Foo
|
|
301
|
-
api = api
|
|
302
|
-
disable = ["retrieve", "update"]
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
FooAPI().add_views_to_route()
|
|
306
|
-
```
|
|
307
|
-
For the list endpoint you can also set query params and handle them. They will be also visible into swagger.
|
|
308
|
-
|
|
309
|
-
```python
|
|
310
|
-
# views.py
|
|
311
|
-
from ninja_aio import NinjaAIO
|
|
312
|
-
from ninja_aio.views import APIViewSet
|
|
313
|
-
|
|
314
|
-
from .models import Foo
|
|
315
|
-
|
|
316
|
-
api = NinjaAIO()
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
class FooAPI(APIViewSet):
|
|
320
|
-
model = Foo
|
|
321
|
-
api = api
|
|
322
|
-
query_params = {"name": (str, None), "active": (bool, None)}
|
|
323
|
-
|
|
324
|
-
async def query_params_handler(self, queryset, filters):
|
|
325
|
-
return queryset.filter(**{k: v for k, v in filters.items() if v is not None})
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
FooAPI().add_views_to_route()
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
### APIView
|
|
332
|
-
|
|
333
|
-
- View class to code generic views class based. In your views.py import APIView class.
|
|
334
|
-
|
|
335
|
-
```python
|
|
336
|
-
# views.py
|
|
337
|
-
from ninja import Schema
|
|
338
|
-
from ninja_aio import NinjaAIO
|
|
339
|
-
from ninja_aio.views import APIView
|
|
340
|
-
|
|
341
|
-
api = NinjaAIO()
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
class ExampleSchemaOut(Schema):
|
|
345
|
-
sum: float
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
class ExampleSchemaIn(Schema):
|
|
349
|
-
n1: float
|
|
350
|
-
n2: float
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
class SumView(APIView):
|
|
354
|
-
api = api
|
|
355
|
-
api_router_path = "numbers-sum/"
|
|
356
|
-
router_tag = "Sum"
|
|
357
|
-
|
|
358
|
-
def views(self):
|
|
359
|
-
@self.router.post("/", response={200: ExampleSchemaOut})
|
|
360
|
-
async def sum(request: HttpRequest, data: ExampleSchemaIn):
|
|
361
|
-
return 200, {sum: data.n1 + data.n2}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
SumView().add_views_to_route()
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
### Relations
|
|
368
|
-
- You can also set ForeignKey, OneToOne and ManyToMany relations into serialization(reverse relations are supported too). Django ninja aio crud will serialize every of these relation automatically.
|
|
369
|
-
|
|
370
|
-
- Define models:
|
|
371
|
-
|
|
372
|
-
```python
|
|
373
|
-
# models.py
|
|
374
|
-
class Bar(ModelSerializer):
|
|
375
|
-
name = models.CharField(max_length=30)
|
|
376
|
-
description = models.TextField(max_length=30)
|
|
377
|
-
|
|
378
|
-
# ReadSerializer with reverse OneToMany relation (foos)
|
|
379
|
-
class ReadSerializer:
|
|
380
|
-
fields = ["id", "name", "description", "foos"]
|
|
381
|
-
|
|
382
|
-
class CreateSerializer:
|
|
383
|
-
fields = ["name", "description"]
|
|
384
|
-
|
|
385
|
-
class UpdateSerializer:
|
|
386
|
-
fields = ["name", "description"]
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
class Foo(ModelSerializer):
|
|
390
|
-
name = models.CharField(max_length=30)
|
|
391
|
-
bar = models.ForeignKey(Bar, on_delete=models.CASCADE, related_name="foos")
|
|
392
|
-
|
|
393
|
-
class ReadSerializer:
|
|
394
|
-
fields = ["id", "name", "bar"]
|
|
395
|
-
|
|
396
|
-
class CreateSerializer:
|
|
397
|
-
fields = ["name", "bar"]
|
|
398
|
-
|
|
399
|
-
class UpdateSerializer:
|
|
400
|
-
fields = ["name"]
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
- Define views:
|
|
404
|
-
|
|
405
|
-
```python
|
|
406
|
-
# views.py
|
|
407
|
-
from ninja_aio import NinjaAIO
|
|
408
|
-
from ninja_aio.views import APIViewSet
|
|
409
|
-
|
|
410
|
-
from .models import Foo, Bar
|
|
411
|
-
|
|
412
|
-
api = NinjaAIO()
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
class FooAPI(APIViewSet):
|
|
416
|
-
model = Foo
|
|
417
|
-
api = api
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
class BarAPI(APIViewSet):
|
|
421
|
-
model = Bar
|
|
422
|
-
api = api
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
FooAPI().add_views_to_route()
|
|
426
|
-
BarAPI().add_views_to_route()
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
- Now run your server and go to /docs url:
|
|
430
|
-
|
|
431
|
-
### Docs
|
|
432
|
-
|
|
433
|
-
- Foo Schemas
|
|
434
|
-
|
|
435
|
-

|
|
436
|
-
|
|
437
|
-
- Bar Schemas with reverse relation
|
|
438
|
-
|
|
439
|
-

|
|
440
|
-
|
|
441
|
-
## 🔒 Authentication
|
|
442
|
-
|
|
443
|
-
### Jwt
|
|
444
|
-
|
|
445
|
-
- 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.
|
|
446
|
-
|
|
447
|
-
```python
|
|
448
|
-
from ninja_aio.auth import AsyncJWTBearer
|
|
449
|
-
from django.conf import settings
|
|
450
|
-
from django.http import HttpRequest
|
|
451
|
-
|
|
452
|
-
from .models import Foo
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
class CustomJWTBearer(AsyncJWTBearer):
|
|
456
|
-
jwt_public = settings.JWT_PUBLIC
|
|
457
|
-
claims = {"foo_id": {"essential": True}}
|
|
458
|
-
|
|
459
|
-
async def auth_handler(self, request: HttpRequest):
|
|
460
|
-
try:
|
|
461
|
-
request.user = await Foo.objects.aget(id=self.dcd.claims["foo_id"])
|
|
462
|
-
except Foo.DoesNotExist:
|
|
463
|
-
return None
|
|
464
|
-
return request.user
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
- Then add it to views.
|
|
468
|
-
|
|
469
|
-
```python
|
|
470
|
-
# views.py
|
|
471
|
-
from ninja import Schema
|
|
472
|
-
from ninja_aio import NinjaAIO
|
|
473
|
-
from ninja_aio.views import APIViewSet, APIView
|
|
474
|
-
|
|
475
|
-
from .models import Foo
|
|
476
|
-
|
|
477
|
-
api = NinjaAIO()
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
class FooAPI(APIViewSet):
|
|
481
|
-
model = Foo
|
|
482
|
-
api = api
|
|
483
|
-
auth = CustomJWTBearer()
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
class ExampleSchemaOut(Schema):
|
|
487
|
-
sum: float
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
class ExampleSchemaIn(Schema):
|
|
491
|
-
n1: float
|
|
492
|
-
n2: float
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
class SumView(APIView):
|
|
496
|
-
api = api
|
|
497
|
-
api_router_path = "numbers-sum/"
|
|
498
|
-
router_tag = "Sum"
|
|
499
|
-
auth = CustomJWTBearer()
|
|
500
|
-
|
|
501
|
-
def views(self):
|
|
502
|
-
@self.router.post("/", response={200: ExampleSchemaOut}, auth=self.auth)
|
|
503
|
-
async def sum(request: HttpRequest, data: ExampleSchemaIn):
|
|
504
|
-
return 200, {sum: data.n1 + data.n2}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
FooAPI().add_views_to_route()
|
|
508
|
-
SumView().add_views_to_route()
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
## 📝 Pagination
|
|
512
|
-
|
|
513
|
-
- 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>**.
|
|
514
|
-
|
|
515
|
-
```python
|
|
516
|
-
# views.py
|
|
517
|
-
|
|
518
|
-
class FooAPI(APIViewSet):
|
|
519
|
-
model = Foo
|
|
520
|
-
api = api
|
|
521
|
-
pagination_class = CustomPaginationClass
|
|
522
|
-
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
## 📌 Notes
|
|
526
|
-
- Feel free to contribute and improve the program. 🛠️
|
|
527
|
-
|
|
File without changes
|
{django_ninja_aio_crud-1.0.0.dist-info → django_ninja_aio_crud-1.0.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|