django-ninja-aio-crud 1.0.0__tar.gz → 1.0.2__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-1.0.2/PKG-INFO +338 -0
- django_ninja_aio_crud-1.0.2/README.md +304 -0
- {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/ninja_aio/__init__.py +1 -1
- {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/ninja_aio/exceptions.py +1 -1
- django_ninja_aio_crud-1.0.2/ninja_aio/helpers/__init__.py +3 -0
- django_ninja_aio_crud-1.0.2/ninja_aio/helpers/api.py +432 -0
- django_ninja_aio_crud-1.0.2/ninja_aio/models.py +1203 -0
- django_ninja_aio_crud-1.0.2/ninja_aio/renders.py +47 -0
- {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/ninja_aio/schemas.py +16 -1
- {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/ninja_aio/views.py +19 -193
- {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/pyproject.toml +1 -1
- django_ninja_aio_crud-1.0.0/PKG-INFO +0 -527
- django_ninja_aio_crud-1.0.0/README.md +0 -493
- django_ninja_aio_crud-1.0.0/ninja_aio/models.py +0 -887
- django_ninja_aio_crud-1.0.0/ninja_aio/renders.py +0 -54
- {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/LICENSE +0 -0
- {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/ninja_aio/api.py +0 -0
- {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/ninja_aio/auth.py +0 -0
- {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/ninja_aio/decorators.py +0 -0
- {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-1.0.0 → django_ninja_aio_crud-1.0.2}/ninja_aio/types.py +0 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-ninja-aio-crud
|
|
3
|
+
Version: 1.0.2
|
|
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://sonarcloud.io/summary/new_code?id=caspel26_django-ninja-aio-crud)
|
|
38
|
+
[](https://codecov.io/gh/caspel26/django-ninja-aio-crud/)
|
|
39
|
+
[](https://pypi.org/project/django-ninja-aio-crud/)
|
|
40
|
+
[](LICENSE)
|
|
41
|
+
[](https://github.com/astral-sh/ruff)
|
|
42
|
+
|
|
43
|
+
> Lightweight async CRUD layer on top of **[Django Ninja](https://django-ninja.dev/)** with automatic schema generation, filtering, pagination, auth & Many‑to‑Many management.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## ✨ Features
|
|
48
|
+
|
|
49
|
+
- Async CRUD ViewSets (create, list, retrieve, update, delete)
|
|
50
|
+
- Automatic Pydantic schemas from `ModelSerializer` (read/create/update)
|
|
51
|
+
- Dynamic query params (runtime schema via `pydantic.create_model`)
|
|
52
|
+
- Per-method authentication (`auth`, `get_auth`, `post_auth`, etc.)
|
|
53
|
+
- Async pagination (customizable)
|
|
54
|
+
- M2M relation endpoints via `M2MRelationSchema` (add/remove/get + filters)
|
|
55
|
+
- Reverse relation serialization
|
|
56
|
+
- Hook methods (`query_params_handler`, `<related>_query_params_handler`, `custom_actions`, lifecycle hooks)
|
|
57
|
+
- ORJSON renderer through `NinjaAIO`
|
|
58
|
+
- Clean, minimal integration
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 📦 Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install django-ninja-aio-crud
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Add to your project’s dependencies and ensure Django Ninja is installed.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 🚀 Quick Start
|
|
73
|
+
|
|
74
|
+
models.py
|
|
75
|
+
```python
|
|
76
|
+
from django.db import models
|
|
77
|
+
from ninja_aio.models import ModelSerializer
|
|
78
|
+
|
|
79
|
+
class Book(ModelSerializer):
|
|
80
|
+
title = models.CharField(max_length=120)
|
|
81
|
+
published = models.BooleanField(default=True)
|
|
82
|
+
|
|
83
|
+
class ReadSerializer:
|
|
84
|
+
fields = ["id", "title", "published"]
|
|
85
|
+
|
|
86
|
+
class CreateSerializer:
|
|
87
|
+
fields = ["title", "published"]
|
|
88
|
+
|
|
89
|
+
class UpdateSerializer:
|
|
90
|
+
optionals = [("title", str), ("published", bool)]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
views.py
|
|
94
|
+
```python
|
|
95
|
+
from ninja_aio import NinjaAIO
|
|
96
|
+
from ninja_aio.views import APIViewSet
|
|
97
|
+
from .models import Book
|
|
98
|
+
|
|
99
|
+
api = NinjaAIO()
|
|
100
|
+
|
|
101
|
+
class BookViewSet(APIViewSet):
|
|
102
|
+
model = Book
|
|
103
|
+
api = api
|
|
104
|
+
|
|
105
|
+
BookViewSet().add_views_to_route()
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Visit `/docs` → CRUD endpoints ready.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 🔄 Query Filtering
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
class BookViewSet(APIViewSet):
|
|
116
|
+
model = Book
|
|
117
|
+
api = api
|
|
118
|
+
query_params = {"published": (bool, None), "title": (str, None)}
|
|
119
|
+
|
|
120
|
+
async def query_params_handler(self, queryset, filters):
|
|
121
|
+
if filters.get("published") is not None:
|
|
122
|
+
queryset = queryset.filter(published=filters["published"])
|
|
123
|
+
if filters.get("title"):
|
|
124
|
+
queryset = queryset.filter(title__icontains=filters["title"])
|
|
125
|
+
return queryset
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Request:
|
|
129
|
+
```
|
|
130
|
+
GET /book/?published=true&title=python
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 🤝 Many-to-Many Example (with filters)
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from ninja_aio.schemas import M2MRelationSchema
|
|
139
|
+
|
|
140
|
+
class Tag(ModelSerializer):
|
|
141
|
+
name = models.CharField(max_length=50)
|
|
142
|
+
class ReadSerializer:
|
|
143
|
+
fields = ["id", "name"]
|
|
144
|
+
|
|
145
|
+
class Article(ModelSerializer):
|
|
146
|
+
title = models.CharField(max_length=120)
|
|
147
|
+
tags = models.ManyToManyField(Tag, related_name="articles")
|
|
148
|
+
|
|
149
|
+
class ReadSerializer:
|
|
150
|
+
fields = ["id", "title", "tags"]
|
|
151
|
+
|
|
152
|
+
class ArticleViewSet(APIViewSet):
|
|
153
|
+
model = Article
|
|
154
|
+
api = api
|
|
155
|
+
m2m_relations = [
|
|
156
|
+
M2MRelationSchema(
|
|
157
|
+
model=Tag,
|
|
158
|
+
related_name="tags",
|
|
159
|
+
filters={"name": (str, "")}
|
|
160
|
+
)
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
async def tags_query_params_handler(self, queryset, filters):
|
|
164
|
+
n = filters.get("name")
|
|
165
|
+
if n:
|
|
166
|
+
queryset = queryset.filter(name__icontains=n)
|
|
167
|
+
return queryset
|
|
168
|
+
|
|
169
|
+
ArticleViewSet().add_views_to_route()
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Endpoints:
|
|
173
|
+
- `GET /article/{pk}/tag?name=dev`
|
|
174
|
+
- `POST /article/{pk}/tag/` body: `{"add":[1,2],"remove":[3]}`
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 🔐 Authentication (JWT example)
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from ninja_aio.auth import AsyncJwtBearer
|
|
182
|
+
from joserfc import jwk
|
|
183
|
+
from .models import Book
|
|
184
|
+
|
|
185
|
+
PUBLIC_KEY = "-----BEGIN PUBLIC KEY----- ..."
|
|
186
|
+
|
|
187
|
+
class JWTAuth(AsyncJwtBearer):
|
|
188
|
+
jwt_public = jwk.RSAKey.import_key(PUBLIC_KEY)
|
|
189
|
+
jwt_alg = "RS256"
|
|
190
|
+
claims = {"sub": {"essential": True}}
|
|
191
|
+
|
|
192
|
+
async def auth_handler(self, request):
|
|
193
|
+
book_id = self.dcd.claims.get("sub")
|
|
194
|
+
return await Book.objects.aget(id=book_id)
|
|
195
|
+
|
|
196
|
+
class SecureBookViewSet(APIViewSet):
|
|
197
|
+
model = Book
|
|
198
|
+
api = api
|
|
199
|
+
auth = [JWTAuth()]
|
|
200
|
+
get_auth = None # list/retrieve public
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 📑 Lifecycle Hooks (ModelSerializer)
|
|
206
|
+
|
|
207
|
+
Available on every save/delete:
|
|
208
|
+
- `on_create_before_save`
|
|
209
|
+
- `on_create_after_save`
|
|
210
|
+
- `before_save`
|
|
211
|
+
- `after_save`
|
|
212
|
+
- `on_delete`
|
|
213
|
+
- `custom_actions(payload)` (create/update custom field logic)
|
|
214
|
+
- `post_create()` (after create commit)
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 🧩 Adding Custom Endpoints
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
class BookViewSet(APIViewSet):
|
|
222
|
+
model = Book
|
|
223
|
+
api = api
|
|
224
|
+
|
|
225
|
+
def views(self):
|
|
226
|
+
@self.router.get("/stats/")
|
|
227
|
+
async def stats(request):
|
|
228
|
+
total = await Book.objects.acount()
|
|
229
|
+
return {"total": total}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 📄 Pagination
|
|
235
|
+
|
|
236
|
+
Default: `PageNumberPagination`. Override:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from ninja.pagination import PageNumberPagination
|
|
240
|
+
|
|
241
|
+
class LargePagination(PageNumberPagination):
|
|
242
|
+
page_size = 50
|
|
243
|
+
max_page_size = 200
|
|
244
|
+
|
|
245
|
+
class BookViewSet(APIViewSet):
|
|
246
|
+
model = Book
|
|
247
|
+
api = api
|
|
248
|
+
pagination_class = LargePagination
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 🛠 Project Structure & Docs
|
|
254
|
+
|
|
255
|
+
Documentation (MkDocs + Material):
|
|
256
|
+
```
|
|
257
|
+
docs/
|
|
258
|
+
getting_started/
|
|
259
|
+
tutorial/
|
|
260
|
+
api/
|
|
261
|
+
views/
|
|
262
|
+
models/
|
|
263
|
+
authentication.md
|
|
264
|
+
pagination.md
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Browse full reference:
|
|
268
|
+
- APIViewSet: `docs/api/views/api_view_set.md`
|
|
269
|
+
- APIView: `docs/api/views/api_view.md`
|
|
270
|
+
- ModelSerializer: `docs/api/models/model_serializer.md`
|
|
271
|
+
- Authentication: `docs/api/authentication.md`
|
|
272
|
+
- Pagination: `docs/api/pagination.md`
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## 🧪 Tests
|
|
277
|
+
|
|
278
|
+
Use Django test runner + async ORM patterns. Example async pattern:
|
|
279
|
+
```python
|
|
280
|
+
obj = await Book.objects.acreate(title="T1", published=True)
|
|
281
|
+
count = await Book.objects.acount()
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## 🚫 Disable Operations
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
class ReadOnlyBookViewSet(APIViewSet):
|
|
290
|
+
model = Book
|
|
291
|
+
api = api
|
|
292
|
+
disable = ["update", "delete"]
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## 📌 Performance Tips
|
|
298
|
+
|
|
299
|
+
- Use `queryset_request` classmethod to prefetch
|
|
300
|
+
- Index frequently filtered fields
|
|
301
|
+
- Keep pagination enabled
|
|
302
|
+
- Limit slices (`queryset = queryset[:1000]`) for heavy searches
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## 🤲 Contributing
|
|
307
|
+
|
|
308
|
+
1. Fork
|
|
309
|
+
2. Create branch
|
|
310
|
+
3. Add tests
|
|
311
|
+
4. Run lint (`ruff check .`)
|
|
312
|
+
5. Open PR
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## ⭐ Support
|
|
317
|
+
|
|
318
|
+
Star the repo or donate:
|
|
319
|
+
- [Buy me a coffee](https://buymeacoffee.com/caspel26)
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## 📜 License
|
|
324
|
+
|
|
325
|
+
MIT License. See [LICENSE](LICENSE).
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## 🔗 Quick Links
|
|
330
|
+
|
|
331
|
+
| Item | Link |
|
|
332
|
+
|------|------|
|
|
333
|
+
| PyPI | https://pypi.org/project/django-ninja-aio-crud/ |
|
|
334
|
+
| Docs | https://django-ninja-aio.com
|
|
335
|
+
| Issues | https://github.com/caspel26/django-ninja-aio-crud/issues |
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# 🥷 django-ninja-aio-crud
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
[](https://sonarcloud.io/summary/new_code?id=caspel26_django-ninja-aio-crud)
|
|
5
|
+
[](https://codecov.io/gh/caspel26/django-ninja-aio-crud/)
|
|
6
|
+
[](https://pypi.org/project/django-ninja-aio-crud/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://github.com/astral-sh/ruff)
|
|
9
|
+
|
|
10
|
+
> Lightweight async CRUD layer on top of **[Django Ninja](https://django-ninja.dev/)** with automatic schema generation, filtering, pagination, auth & Many‑to‑Many management.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## ✨ Features
|
|
15
|
+
|
|
16
|
+
- Async CRUD ViewSets (create, list, retrieve, update, delete)
|
|
17
|
+
- Automatic Pydantic schemas from `ModelSerializer` (read/create/update)
|
|
18
|
+
- Dynamic query params (runtime schema via `pydantic.create_model`)
|
|
19
|
+
- Per-method authentication (`auth`, `get_auth`, `post_auth`, etc.)
|
|
20
|
+
- Async pagination (customizable)
|
|
21
|
+
- M2M relation endpoints via `M2MRelationSchema` (add/remove/get + filters)
|
|
22
|
+
- Reverse relation serialization
|
|
23
|
+
- Hook methods (`query_params_handler`, `<related>_query_params_handler`, `custom_actions`, lifecycle hooks)
|
|
24
|
+
- ORJSON renderer through `NinjaAIO`
|
|
25
|
+
- Clean, minimal integration
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 📦 Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install django-ninja-aio-crud
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Add to your project’s dependencies and ensure Django Ninja is installed.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 🚀 Quick Start
|
|
40
|
+
|
|
41
|
+
models.py
|
|
42
|
+
```python
|
|
43
|
+
from django.db import models
|
|
44
|
+
from ninja_aio.models import ModelSerializer
|
|
45
|
+
|
|
46
|
+
class Book(ModelSerializer):
|
|
47
|
+
title = models.CharField(max_length=120)
|
|
48
|
+
published = models.BooleanField(default=True)
|
|
49
|
+
|
|
50
|
+
class ReadSerializer:
|
|
51
|
+
fields = ["id", "title", "published"]
|
|
52
|
+
|
|
53
|
+
class CreateSerializer:
|
|
54
|
+
fields = ["title", "published"]
|
|
55
|
+
|
|
56
|
+
class UpdateSerializer:
|
|
57
|
+
optionals = [("title", str), ("published", bool)]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
views.py
|
|
61
|
+
```python
|
|
62
|
+
from ninja_aio import NinjaAIO
|
|
63
|
+
from ninja_aio.views import APIViewSet
|
|
64
|
+
from .models import Book
|
|
65
|
+
|
|
66
|
+
api = NinjaAIO()
|
|
67
|
+
|
|
68
|
+
class BookViewSet(APIViewSet):
|
|
69
|
+
model = Book
|
|
70
|
+
api = api
|
|
71
|
+
|
|
72
|
+
BookViewSet().add_views_to_route()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Visit `/docs` → CRUD endpoints ready.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 🔄 Query Filtering
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
class BookViewSet(APIViewSet):
|
|
83
|
+
model = Book
|
|
84
|
+
api = api
|
|
85
|
+
query_params = {"published": (bool, None), "title": (str, None)}
|
|
86
|
+
|
|
87
|
+
async def query_params_handler(self, queryset, filters):
|
|
88
|
+
if filters.get("published") is not None:
|
|
89
|
+
queryset = queryset.filter(published=filters["published"])
|
|
90
|
+
if filters.get("title"):
|
|
91
|
+
queryset = queryset.filter(title__icontains=filters["title"])
|
|
92
|
+
return queryset
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Request:
|
|
96
|
+
```
|
|
97
|
+
GET /book/?published=true&title=python
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 🤝 Many-to-Many Example (with filters)
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from ninja_aio.schemas import M2MRelationSchema
|
|
106
|
+
|
|
107
|
+
class Tag(ModelSerializer):
|
|
108
|
+
name = models.CharField(max_length=50)
|
|
109
|
+
class ReadSerializer:
|
|
110
|
+
fields = ["id", "name"]
|
|
111
|
+
|
|
112
|
+
class Article(ModelSerializer):
|
|
113
|
+
title = models.CharField(max_length=120)
|
|
114
|
+
tags = models.ManyToManyField(Tag, related_name="articles")
|
|
115
|
+
|
|
116
|
+
class ReadSerializer:
|
|
117
|
+
fields = ["id", "title", "tags"]
|
|
118
|
+
|
|
119
|
+
class ArticleViewSet(APIViewSet):
|
|
120
|
+
model = Article
|
|
121
|
+
api = api
|
|
122
|
+
m2m_relations = [
|
|
123
|
+
M2MRelationSchema(
|
|
124
|
+
model=Tag,
|
|
125
|
+
related_name="tags",
|
|
126
|
+
filters={"name": (str, "")}
|
|
127
|
+
)
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
async def tags_query_params_handler(self, queryset, filters):
|
|
131
|
+
n = filters.get("name")
|
|
132
|
+
if n:
|
|
133
|
+
queryset = queryset.filter(name__icontains=n)
|
|
134
|
+
return queryset
|
|
135
|
+
|
|
136
|
+
ArticleViewSet().add_views_to_route()
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Endpoints:
|
|
140
|
+
- `GET /article/{pk}/tag?name=dev`
|
|
141
|
+
- `POST /article/{pk}/tag/` body: `{"add":[1,2],"remove":[3]}`
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 🔐 Authentication (JWT example)
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from ninja_aio.auth import AsyncJwtBearer
|
|
149
|
+
from joserfc import jwk
|
|
150
|
+
from .models import Book
|
|
151
|
+
|
|
152
|
+
PUBLIC_KEY = "-----BEGIN PUBLIC KEY----- ..."
|
|
153
|
+
|
|
154
|
+
class JWTAuth(AsyncJwtBearer):
|
|
155
|
+
jwt_public = jwk.RSAKey.import_key(PUBLIC_KEY)
|
|
156
|
+
jwt_alg = "RS256"
|
|
157
|
+
claims = {"sub": {"essential": True}}
|
|
158
|
+
|
|
159
|
+
async def auth_handler(self, request):
|
|
160
|
+
book_id = self.dcd.claims.get("sub")
|
|
161
|
+
return await Book.objects.aget(id=book_id)
|
|
162
|
+
|
|
163
|
+
class SecureBookViewSet(APIViewSet):
|
|
164
|
+
model = Book
|
|
165
|
+
api = api
|
|
166
|
+
auth = [JWTAuth()]
|
|
167
|
+
get_auth = None # list/retrieve public
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 📑 Lifecycle Hooks (ModelSerializer)
|
|
173
|
+
|
|
174
|
+
Available on every save/delete:
|
|
175
|
+
- `on_create_before_save`
|
|
176
|
+
- `on_create_after_save`
|
|
177
|
+
- `before_save`
|
|
178
|
+
- `after_save`
|
|
179
|
+
- `on_delete`
|
|
180
|
+
- `custom_actions(payload)` (create/update custom field logic)
|
|
181
|
+
- `post_create()` (after create commit)
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 🧩 Adding Custom Endpoints
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
class BookViewSet(APIViewSet):
|
|
189
|
+
model = Book
|
|
190
|
+
api = api
|
|
191
|
+
|
|
192
|
+
def views(self):
|
|
193
|
+
@self.router.get("/stats/")
|
|
194
|
+
async def stats(request):
|
|
195
|
+
total = await Book.objects.acount()
|
|
196
|
+
return {"total": total}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## 📄 Pagination
|
|
202
|
+
|
|
203
|
+
Default: `PageNumberPagination`. Override:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from ninja.pagination import PageNumberPagination
|
|
207
|
+
|
|
208
|
+
class LargePagination(PageNumberPagination):
|
|
209
|
+
page_size = 50
|
|
210
|
+
max_page_size = 200
|
|
211
|
+
|
|
212
|
+
class BookViewSet(APIViewSet):
|
|
213
|
+
model = Book
|
|
214
|
+
api = api
|
|
215
|
+
pagination_class = LargePagination
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 🛠 Project Structure & Docs
|
|
221
|
+
|
|
222
|
+
Documentation (MkDocs + Material):
|
|
223
|
+
```
|
|
224
|
+
docs/
|
|
225
|
+
getting_started/
|
|
226
|
+
tutorial/
|
|
227
|
+
api/
|
|
228
|
+
views/
|
|
229
|
+
models/
|
|
230
|
+
authentication.md
|
|
231
|
+
pagination.md
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Browse full reference:
|
|
235
|
+
- APIViewSet: `docs/api/views/api_view_set.md`
|
|
236
|
+
- APIView: `docs/api/views/api_view.md`
|
|
237
|
+
- ModelSerializer: `docs/api/models/model_serializer.md`
|
|
238
|
+
- Authentication: `docs/api/authentication.md`
|
|
239
|
+
- Pagination: `docs/api/pagination.md`
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## 🧪 Tests
|
|
244
|
+
|
|
245
|
+
Use Django test runner + async ORM patterns. Example async pattern:
|
|
246
|
+
```python
|
|
247
|
+
obj = await Book.objects.acreate(title="T1", published=True)
|
|
248
|
+
count = await Book.objects.acount()
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 🚫 Disable Operations
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
class ReadOnlyBookViewSet(APIViewSet):
|
|
257
|
+
model = Book
|
|
258
|
+
api = api
|
|
259
|
+
disable = ["update", "delete"]
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## 📌 Performance Tips
|
|
265
|
+
|
|
266
|
+
- Use `queryset_request` classmethod to prefetch
|
|
267
|
+
- Index frequently filtered fields
|
|
268
|
+
- Keep pagination enabled
|
|
269
|
+
- Limit slices (`queryset = queryset[:1000]`) for heavy searches
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## 🤲 Contributing
|
|
274
|
+
|
|
275
|
+
1. Fork
|
|
276
|
+
2. Create branch
|
|
277
|
+
3. Add tests
|
|
278
|
+
4. Run lint (`ruff check .`)
|
|
279
|
+
5. Open PR
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## ⭐ Support
|
|
284
|
+
|
|
285
|
+
Star the repo or donate:
|
|
286
|
+
- [Buy me a coffee](https://buymeacoffee.com/caspel26)
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## 📜 License
|
|
291
|
+
|
|
292
|
+
MIT License. See [LICENSE](LICENSE).
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## 🔗 Quick Links
|
|
297
|
+
|
|
298
|
+
| Item | Link |
|
|
299
|
+
|------|------|
|
|
300
|
+
| PyPI | https://pypi.org/project/django-ninja-aio-crud/ |
|
|
301
|
+
| Docs | https://django-ninja-aio.com
|
|
302
|
+
| Issues | https://github.com/caspel26/django-ninja-aio-crud/issues |
|
|
303
|
+
|
|
304
|
+
---
|
|
@@ -42,7 +42,7 @@ class NotFoundError(BaseException):
|
|
|
42
42
|
|
|
43
43
|
def __init__(self, model: Model, details=None):
|
|
44
44
|
super().__init__(
|
|
45
|
-
error={model._meta.verbose_name: self.error},
|
|
45
|
+
error={model._meta.verbose_name.replace(" ", "_"): self.error},
|
|
46
46
|
status_code=self.status_code,
|
|
47
47
|
details=details,
|
|
48
48
|
)
|