django-ninja-aio-crud 0.11.4__tar.gz → 1.0.1__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.1/PKG-INFO +336 -0
- django_ninja_aio_crud-1.0.1/README.md +303 -0
- {django_ninja_aio_crud-0.11.4 → django_ninja_aio_crud-1.0.1}/ninja_aio/__init__.py +1 -1
- django_ninja_aio_crud-1.0.1/ninja_aio/schemas.py +67 -0
- django_ninja_aio_crud-1.0.1/ninja_aio/views.py +605 -0
- {django_ninja_aio_crud-0.11.4 → django_ninja_aio_crud-1.0.1}/pyproject.toml +1 -1
- django_ninja_aio_crud-0.11.4/PKG-INFO +0 -527
- django_ninja_aio_crud-0.11.4/README.md +0 -493
- django_ninja_aio_crud-0.11.4/ninja_aio/schemas.py +0 -29
- django_ninja_aio_crud-0.11.4/ninja_aio/views.py +0 -533
- {django_ninja_aio_crud-0.11.4 → django_ninja_aio_crud-1.0.1}/LICENSE +0 -0
- {django_ninja_aio_crud-0.11.4 → django_ninja_aio_crud-1.0.1}/ninja_aio/api.py +0 -0
- {django_ninja_aio_crud-0.11.4 → django_ninja_aio_crud-1.0.1}/ninja_aio/auth.py +0 -0
- {django_ninja_aio_crud-0.11.4 → django_ninja_aio_crud-1.0.1}/ninja_aio/decorators.py +0 -0
- {django_ninja_aio_crud-0.11.4 → django_ninja_aio_crud-1.0.1}/ninja_aio/exceptions.py +0 -0
- {django_ninja_aio_crud-0.11.4 → django_ninja_aio_crud-1.0.1}/ninja_aio/models.py +0 -0
- {django_ninja_aio_crud-0.11.4 → django_ninja_aio_crud-1.0.1}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-0.11.4 → django_ninja_aio_crud-1.0.1}/ninja_aio/renders.py +0 -0
- {django_ninja_aio_crud-0.11.4 → django_ninja_aio_crud-1.0.1}/ninja_aio/types.py +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
|
+
---
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# 🥷 django-ninja-aio-crud
|
|
2
|
+
|
|
3
|
+

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