django-ninja-aio-crud 2.2.0__tar.gz → 2.3.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.
Potentially problematic release.
This version of django-ninja-aio-crud might be problematic. Click here for more details.
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/.github/workflows/docs.yml +2 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/PKG-INFO +20 -15
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/README.md +19 -14
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/api/views/api_view.md +56 -10
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/api/views/api_view_set.md +128 -29
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/api/views/decorators.md +38 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/__init__.py +1 -1
- django_ninja_aio_crud-2.3.0/ninja_aio/decorators/__init__.py +23 -0
- django_ninja_aio_crud-2.3.0/ninja_aio/decorators/operations.py +9 -0
- django_ninja_aio_crud-2.2.0/ninja_aio/decorators.py → django_ninja_aio_crud-2.3.0/ninja_aio/decorators/views.py +4 -3
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/exceptions.py +23 -1
- django_ninja_aio_crud-2.3.0/ninja_aio/factory/__init__.py +3 -0
- django_ninja_aio_crud-2.3.0/ninja_aio/factory/operations.py +296 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/helpers/api.py +16 -2
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/helpers/query.py +6 -1
- django_ninja_aio_crud-2.3.0/ninja_aio/schemas/helpers.py +170 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/views/api.py +20 -7
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/helpers/test_many_to_many_api.py +11 -10
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/views/test_views.py +45 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/views/test_viewset.py +50 -0
- django_ninja_aio_crud-2.2.0/ninja_aio/schemas/helpers.py +0 -90
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/.github/dependabot.yml +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/.github/workflows/coverage.yml +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/.github/workflows/publish.yml +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/.gitignore +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/.pre-commit-config.yaml +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/LICENSE +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/CNAME +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/api/authentication.md +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/api/models/model_serializer.md +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/api/models/model_util.md +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/api/pagination.md +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/api/renderers/orjson_renderer.md +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/api/views/mixins.md +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/auth.md +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/contributing.md +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/extra.css +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/getting_started/installation.md +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/getting_started/quick_start.md +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/images/bar-swagger.png +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/images/favicon.ico +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/images/foo-swagger.png +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/images/logo.png +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/index.md +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/release_notes.md +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/requirements.txt +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/tutorial/authentication.md +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/tutorial/crud.md +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/tutorial/filtering.md +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/tutorial/model.md +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/examples/ex_1/models.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/examples/ex_1/urls.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/examples/ex_1/views.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/examples/ex_2/auth.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/examples/ex_2/models.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/examples/ex_2/urls.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/examples/ex_2/views.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/main.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/mkdocs.yml +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/api.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/auth.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/models.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/renders.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/schemas/__init__.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/schemas/api.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/schemas/generics.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/types.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/views/mixins.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/pyproject.toml +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/requirements.dev.txt +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/run-local-coverage.sh +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/__init__.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/core/__init__.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/core/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/core/test_exceptions_api.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/core/test_renderer_parser.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/generics/__init__.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/generics/literals.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/generics/models.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/generics/request.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/generics/views.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/models/test_model_util.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/models/test_models_extra.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/test_app/__init__.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/test_app/models.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/test_app/schema.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/test_app/views.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/test_auth.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/test_exceptions.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/test_query_util.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/test_settings.py +0 -0
- {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/views/__init__.py +0 -0
|
@@ -13,6 +13,7 @@ on:
|
|
|
13
13
|
- stable
|
|
14
14
|
- "1.0"
|
|
15
15
|
- "2.0"
|
|
16
|
+
- "2.2"
|
|
16
17
|
make_latest:
|
|
17
18
|
description: 'Set as "latest" and default?'
|
|
18
19
|
type: boolean
|
|
@@ -29,6 +30,7 @@ on:
|
|
|
29
30
|
- stable
|
|
30
31
|
- "1.0"
|
|
31
32
|
- "2.0"
|
|
33
|
+
- "2.2"
|
|
32
34
|
delete_confirm:
|
|
33
35
|
description: 'Confirm deletion of the selected version'
|
|
34
36
|
type: boolean
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-ninja-aio-crud
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Django Ninja AIO CRUD - Rest Framework
|
|
5
5
|
Author: Giuseppe Casillo
|
|
6
6
|
Requires-Python: >=3.10, <=3.14
|
|
@@ -100,11 +100,10 @@ from .models import Book
|
|
|
100
100
|
|
|
101
101
|
api = NinjaAIO()
|
|
102
102
|
|
|
103
|
+
@api.viewset(Book)
|
|
103
104
|
class BookViewSet(APIViewSet):
|
|
104
|
-
|
|
105
|
-
api = api
|
|
105
|
+
pass
|
|
106
106
|
|
|
107
|
-
BookViewSet().add_views_to_route()
|
|
108
107
|
```
|
|
109
108
|
|
|
110
109
|
Visit `/docs` → CRUD endpoints ready.
|
|
@@ -114,9 +113,8 @@ Visit `/docs` → CRUD endpoints ready.
|
|
|
114
113
|
## 🔄 Query Filtering
|
|
115
114
|
|
|
116
115
|
```python
|
|
116
|
+
@api.viewset(Book)
|
|
117
117
|
class BookViewSet(APIViewSet):
|
|
118
|
-
model = Book
|
|
119
|
-
api = api
|
|
120
118
|
query_params = {"published": (bool, None), "title": (str, None)}
|
|
121
119
|
|
|
122
120
|
async def query_params_handler(self, queryset, filters):
|
|
@@ -151,9 +149,8 @@ class Article(ModelSerializer):
|
|
|
151
149
|
class ReadSerializer:
|
|
152
150
|
fields = ["id", "title", "tags"]
|
|
153
151
|
|
|
152
|
+
@api.viewset(Article)
|
|
154
153
|
class ArticleViewSet(APIViewSet):
|
|
155
|
-
model = Article
|
|
156
|
-
api = api
|
|
157
154
|
m2m_relations = [
|
|
158
155
|
M2MRelationSchema(
|
|
159
156
|
model=Tag,
|
|
@@ -168,7 +165,6 @@ class ArticleViewSet(APIViewSet):
|
|
|
168
165
|
queryset = queryset.filter(name__icontains=n)
|
|
169
166
|
return queryset
|
|
170
167
|
|
|
171
|
-
ArticleViewSet().add_views_to_route()
|
|
172
168
|
```
|
|
173
169
|
|
|
174
170
|
Endpoints:
|
|
@@ -195,9 +191,8 @@ class JWTAuth(AsyncJwtBearer):
|
|
|
195
191
|
book_id = self.dcd.claims.get("sub")
|
|
196
192
|
return await Book.objects.aget(id=book_id)
|
|
197
193
|
|
|
194
|
+
@api.viewset(Book)
|
|
198
195
|
class SecureBookViewSet(APIViewSet):
|
|
199
|
-
model = Book
|
|
200
|
-
api = api
|
|
201
196
|
auth = [JWTAuth()]
|
|
202
197
|
get_auth = None # list/retrieve public
|
|
203
198
|
```
|
|
@@ -220,10 +215,21 @@ Available on every save/delete:
|
|
|
220
215
|
## 🧩 Adding Custom Endpoints
|
|
221
216
|
|
|
222
217
|
```python
|
|
218
|
+
from ninja_aio.decorators import api_get
|
|
219
|
+
|
|
220
|
+
@api.viewset(Book)
|
|
223
221
|
class BookViewSet(APIViewSet):
|
|
224
|
-
|
|
225
|
-
|
|
222
|
+
@api_get("/stats/")
|
|
223
|
+
async def stats(self, request):
|
|
224
|
+
total = await Book.objects.acount()
|
|
225
|
+
return {"total": total}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Or
|
|
226
229
|
|
|
230
|
+
```python
|
|
231
|
+
@api.viewset(Book)
|
|
232
|
+
class BookViewSet(APIViewSet):
|
|
227
233
|
def views(self):
|
|
228
234
|
@self.router.get("/stats/")
|
|
229
235
|
async def stats(request):
|
|
@@ -288,9 +294,8 @@ count = await Book.objects.acount()
|
|
|
288
294
|
## 🚫 Disable Operations
|
|
289
295
|
|
|
290
296
|
```python
|
|
297
|
+
@api.viewset(Book)
|
|
291
298
|
class ReadOnlyBookViewSet(APIViewSet):
|
|
292
|
-
model = Book
|
|
293
|
-
api = api
|
|
294
299
|
disable = ["update", "delete"]
|
|
295
300
|
```
|
|
296
301
|
|
|
@@ -65,11 +65,10 @@ from .models import Book
|
|
|
65
65
|
|
|
66
66
|
api = NinjaAIO()
|
|
67
67
|
|
|
68
|
+
@api.viewset(Book)
|
|
68
69
|
class BookViewSet(APIViewSet):
|
|
69
|
-
|
|
70
|
-
api = api
|
|
70
|
+
pass
|
|
71
71
|
|
|
72
|
-
BookViewSet().add_views_to_route()
|
|
73
72
|
```
|
|
74
73
|
|
|
75
74
|
Visit `/docs` → CRUD endpoints ready.
|
|
@@ -79,9 +78,8 @@ Visit `/docs` → CRUD endpoints ready.
|
|
|
79
78
|
## 🔄 Query Filtering
|
|
80
79
|
|
|
81
80
|
```python
|
|
81
|
+
@api.viewset(Book)
|
|
82
82
|
class BookViewSet(APIViewSet):
|
|
83
|
-
model = Book
|
|
84
|
-
api = api
|
|
85
83
|
query_params = {"published": (bool, None), "title": (str, None)}
|
|
86
84
|
|
|
87
85
|
async def query_params_handler(self, queryset, filters):
|
|
@@ -116,9 +114,8 @@ class Article(ModelSerializer):
|
|
|
116
114
|
class ReadSerializer:
|
|
117
115
|
fields = ["id", "title", "tags"]
|
|
118
116
|
|
|
117
|
+
@api.viewset(Article)
|
|
119
118
|
class ArticleViewSet(APIViewSet):
|
|
120
|
-
model = Article
|
|
121
|
-
api = api
|
|
122
119
|
m2m_relations = [
|
|
123
120
|
M2MRelationSchema(
|
|
124
121
|
model=Tag,
|
|
@@ -133,7 +130,6 @@ class ArticleViewSet(APIViewSet):
|
|
|
133
130
|
queryset = queryset.filter(name__icontains=n)
|
|
134
131
|
return queryset
|
|
135
132
|
|
|
136
|
-
ArticleViewSet().add_views_to_route()
|
|
137
133
|
```
|
|
138
134
|
|
|
139
135
|
Endpoints:
|
|
@@ -160,9 +156,8 @@ class JWTAuth(AsyncJwtBearer):
|
|
|
160
156
|
book_id = self.dcd.claims.get("sub")
|
|
161
157
|
return await Book.objects.aget(id=book_id)
|
|
162
158
|
|
|
159
|
+
@api.viewset(Book)
|
|
163
160
|
class SecureBookViewSet(APIViewSet):
|
|
164
|
-
model = Book
|
|
165
|
-
api = api
|
|
166
161
|
auth = [JWTAuth()]
|
|
167
162
|
get_auth = None # list/retrieve public
|
|
168
163
|
```
|
|
@@ -185,10 +180,21 @@ Available on every save/delete:
|
|
|
185
180
|
## 🧩 Adding Custom Endpoints
|
|
186
181
|
|
|
187
182
|
```python
|
|
183
|
+
from ninja_aio.decorators import api_get
|
|
184
|
+
|
|
185
|
+
@api.viewset(Book)
|
|
188
186
|
class BookViewSet(APIViewSet):
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
@api_get("/stats/")
|
|
188
|
+
async def stats(self, request):
|
|
189
|
+
total = await Book.objects.acount()
|
|
190
|
+
return {"total": total}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Or
|
|
191
194
|
|
|
195
|
+
```python
|
|
196
|
+
@api.viewset(Book)
|
|
197
|
+
class BookViewSet(APIViewSet):
|
|
192
198
|
def views(self):
|
|
193
199
|
@self.router.get("/stats/")
|
|
194
200
|
async def stats(request):
|
|
@@ -253,9 +259,8 @@ count = await Book.objects.acount()
|
|
|
253
259
|
## 🚫 Disable Operations
|
|
254
260
|
|
|
255
261
|
```python
|
|
262
|
+
@api.viewset(Book)
|
|
256
263
|
class ReadOnlyBookViewSet(APIViewSet):
|
|
257
|
-
model = Book
|
|
258
|
-
api = api
|
|
259
264
|
disable = ["update", "delete"]
|
|
260
265
|
```
|
|
261
266
|
|
|
@@ -31,9 +31,54 @@ class APIView:
|
|
|
31
31
|
|
|
32
32
|
## Methods
|
|
33
33
|
|
|
34
|
-
###
|
|
34
|
+
### Recommended: decorator-based endpoints
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
Prefer class method decorators to define non-CRUD endpoints. Decorators lazily bind instance methods to the router and automatically remove `self` from the OpenAPI signature while preserving type hints.
|
|
37
|
+
|
|
38
|
+
Available decorators (from `ninja_aio.decorators`):
|
|
39
|
+
|
|
40
|
+
- `@api_get(path, ...)`
|
|
41
|
+
- `@api_post(path, ...)`
|
|
42
|
+
- `@api_put(path, ...)`
|
|
43
|
+
- `@api_patch(path, ...)`
|
|
44
|
+
- `@api_delete(path, ...)`
|
|
45
|
+
- `@api_options(path, ...)`
|
|
46
|
+
- `@api_head(path, ...)`
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from ninja_aio import NinjaAIO
|
|
52
|
+
from ninja_aio.views import APIView
|
|
53
|
+
from ninja_aio.decorators import api_get, api_post
|
|
54
|
+
from ninja import Schema
|
|
55
|
+
|
|
56
|
+
api = NinjaAIO(title="My API")
|
|
57
|
+
|
|
58
|
+
class StatsSchema(Schema):
|
|
59
|
+
total: int
|
|
60
|
+
active: int
|
|
61
|
+
|
|
62
|
+
@api.view(prefix="/analytics", tags=["Analytics"])
|
|
63
|
+
class AnalyticsView(APIView):
|
|
64
|
+
@api_get("/dashboard", response=StatsSchema)
|
|
65
|
+
async def dashboard(self, request):
|
|
66
|
+
return {"total": 1000, "active": 750}
|
|
67
|
+
|
|
68
|
+
@api_post("/track")
|
|
69
|
+
async def track_event(self, request, event: str):
|
|
70
|
+
return {"tracked": event}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Notes:
|
|
74
|
+
|
|
75
|
+
- Decorators support per-endpoint `auth`, `response`, `tags`, `summary`, `description`, throttling, and OpenAPI extras.
|
|
76
|
+
- Sync methods run via `sync_to_async` automatically.
|
|
77
|
+
- `self` is excluded from the exposed signature; parameter type hints are preserved.
|
|
78
|
+
|
|
79
|
+
### Legacy: `views()` (still supported)
|
|
80
|
+
|
|
81
|
+
You can still override `views()` to define endpoints imperatively.
|
|
37
82
|
|
|
38
83
|
**Example - Basic Views:**
|
|
39
84
|
|
|
@@ -81,7 +126,7 @@ Registers all defined views to the API instance.
|
|
|
81
126
|
|
|
82
127
|
**Returns:** The router instance
|
|
83
128
|
|
|
84
|
-
**Note:** When using `@api.view(prefix="/path", tags=[...])`, manual registration via `add_views_to_route()` is not required
|
|
129
|
+
**Note:** When using `@api.view(prefix="/path", tags=[...])`, the router is mounted automatically and decorator-based endpoints are registered lazily on instantiation; manual registration via `add_views_to_route()` is not required.
|
|
85
130
|
|
|
86
131
|
## Complete Example
|
|
87
132
|
|
|
@@ -90,6 +135,7 @@ Registers all defined views to the API instance.
|
|
|
90
135
|
```python
|
|
91
136
|
from ninja_aio import NinjaAIO
|
|
92
137
|
from ninja_aio.views import APIView
|
|
138
|
+
from ninja_aio.decorators import api_get, api_post
|
|
93
139
|
from ninja import Schema
|
|
94
140
|
|
|
95
141
|
api = NinjaAIO(title="My API")
|
|
@@ -100,14 +146,13 @@ class StatsSchema(Schema):
|
|
|
100
146
|
|
|
101
147
|
@api.view(prefix="/analytics", tags=["Analytics"])
|
|
102
148
|
class AnalyticsView(APIView):
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return {"total": 1000, "active": 750}
|
|
149
|
+
@api_get("/dashboard", response=StatsSchema)
|
|
150
|
+
async def dashboard(self, request):
|
|
151
|
+
return {"total": 1000, "active": 750}
|
|
107
152
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
153
|
+
@api_post("/track")
|
|
154
|
+
async def track_event(self, request, event: str):
|
|
155
|
+
return {"tracked": event}
|
|
111
156
|
```
|
|
112
157
|
|
|
113
158
|
**Alternative implementation:**
|
|
@@ -138,6 +183,7 @@ AnalyticsView().add_views_to_route()
|
|
|
138
183
|
- For CRUD operations, use [`APIViewSet`](api_view_set.md)
|
|
139
184
|
- All views are async-compatible
|
|
140
185
|
- Standard error codes are available via `self.error_codes`
|
|
186
|
+
- Decorator-based endpoints are preferred for clarity and better OpenAPI signatures.
|
|
141
187
|
|
|
142
188
|
Note:
|
|
143
189
|
|
|
@@ -14,10 +14,84 @@
|
|
|
14
14
|
|
|
15
15
|
Notes:
|
|
16
16
|
|
|
17
|
-
- Retrieve path
|
|
17
|
+
- Retrieve path typically includes a trailing slash by default (see settings below); update/delete include a trailing slash.
|
|
18
18
|
- `{base}` auto-resolves from model verbose name plural (lowercase) unless `api_route_path` is provided.
|
|
19
19
|
- Error responses may use a unified generic schema for codes: 400, 401, 404.
|
|
20
20
|
|
|
21
|
+
### Settings: trailing slash behavior
|
|
22
|
+
|
|
23
|
+
- NINJA_AIO_APPEND_SLASH (default: True)
|
|
24
|
+
- When True (default, for backward compatibility), retrieve and POST paths includes a trailing slash into CRUD: `/{base}/{pk}/`.
|
|
25
|
+
- When False, retrieve and post paths is generated without a trailing slash: `/{base}/{pk}`.
|
|
26
|
+
|
|
27
|
+
## Recommended: Decorator-based extra endpoints
|
|
28
|
+
|
|
29
|
+
Use class method decorators to add non-CRUD endpoints to your ViewSet. This is the preferred way to extend a ViewSet with custom routes. The decorators lazily bind instance methods to the router and ensure correct OpenAPI signatures (no `self` in parameters).
|
|
30
|
+
|
|
31
|
+
Available decorators (from `ninja_aio.decorators`):
|
|
32
|
+
|
|
33
|
+
- `@api_get(path, ...)`
|
|
34
|
+
- `@api_post(path, ...)`
|
|
35
|
+
- `@api_put(path, ...)`
|
|
36
|
+
- `@api_patch(path, ...)`
|
|
37
|
+
- `@api_delete(path, ...)`
|
|
38
|
+
- `@api_options(path, ...)`
|
|
39
|
+
- `@api_head(path, ...)`
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from ninja_aio import NinjaAIO
|
|
45
|
+
from ninja_aio.views import APIViewSet
|
|
46
|
+
from ninja_aio.decorators import api_get, api_post
|
|
47
|
+
from .models import Article
|
|
48
|
+
|
|
49
|
+
api = NinjaAIO(title="Blog API")
|
|
50
|
+
|
|
51
|
+
@api.viewset(model=Article)
|
|
52
|
+
class ArticleViewSet(APIViewSet):
|
|
53
|
+
@api_get("/stats/")
|
|
54
|
+
async def stats(self, request):
|
|
55
|
+
total = await self.model.objects.acount()
|
|
56
|
+
return {"total": total}
|
|
57
|
+
|
|
58
|
+
@api_post("/{pk}/publish/")
|
|
59
|
+
async def publish(self, request, pk: int):
|
|
60
|
+
obj = await self.model.objects.aget(pk=pk)
|
|
61
|
+
obj.is_published = True
|
|
62
|
+
await obj.asave()
|
|
63
|
+
return {"message": "published"}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Notes:
|
|
67
|
+
|
|
68
|
+
- Decorators support per-endpoint `auth`, `response`, `tags`, `summary`, `description`, and more.
|
|
69
|
+
- Sync methods are executed via `sync_to_async` automatically.
|
|
70
|
+
- Signatures and type hints are preserved for OpenAPI (excluding `self`).
|
|
71
|
+
|
|
72
|
+
## Legacy: views() method (still supported)
|
|
73
|
+
|
|
74
|
+
The previous pattern of injecting endpoints inside `views()` is still supported, but the decorator-based approach above is now recommended.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
class ArticleViewSet(APIViewSet):
|
|
78
|
+
model = Article
|
|
79
|
+
api = api
|
|
80
|
+
|
|
81
|
+
def views(self):
|
|
82
|
+
@self.router.get("/stats/")
|
|
83
|
+
async def stats(request):
|
|
84
|
+
total = await self.model.objects.acount()
|
|
85
|
+
return {"total": total}
|
|
86
|
+
|
|
87
|
+
@self.router.post("/{pk}/publish/")
|
|
88
|
+
async def publish(request, pk: int):
|
|
89
|
+
obj = await self.model.objects.aget(pk=pk)
|
|
90
|
+
obj.is_published = True
|
|
91
|
+
await obj.asave()
|
|
92
|
+
return {"message": "published"}
|
|
93
|
+
```
|
|
94
|
+
|
|
21
95
|
## Core Attributes
|
|
22
96
|
|
|
23
97
|
| Attribute | Type | Default | Description |
|
|
@@ -144,6 +218,7 @@ Relations are declared via `M2MRelationSchema` objects (not tuples). Each schema
|
|
|
144
218
|
- `get`: enable GET listing (bool)
|
|
145
219
|
- `filters`: dict of `{param_name: (type, default)}` for relation-level filtering
|
|
146
220
|
- `related_schema`: optional pre-built schema for the related model (auto-generated if the `model` is a `ModelSerializer`)
|
|
221
|
+
- `append_slash`: bool to control trailing slash for the GET relation endpoint path. Defaults to `False` (no trailing slash) for backward compatibility. When `True`, the GET path ends with a trailing slash.
|
|
147
222
|
|
|
148
223
|
If `path` is empty it falls back to the related model verbose name (lowercase plural).
|
|
149
224
|
If `filters` is provided, a per-relation filters schema is auto-generated and exposed on the GET relation endpoint:
|
|
@@ -242,12 +317,14 @@ class MyViewSet(APIViewSet):
|
|
|
242
317
|
|
|
243
318
|
### Endpoint paths and operation naming
|
|
244
319
|
|
|
245
|
-
- GET relation: `/{base}/{pk}/{rel_path}` (no trailing slash)
|
|
320
|
+
- GET relation: `/{base}/{pk}/{rel_path}` by default (no trailing slash). You can enable a trailing slash per relation with `append_slash=True`, resulting in `/{base}/{pk}/{rel_path}/`.
|
|
321
|
+
- POST relation: `/{base}/{pk}/{rel_path}/` (always with trailing slash).
|
|
246
322
|
|
|
247
|
-
|
|
323
|
+
Path normalization rules:
|
|
248
324
|
|
|
249
|
-
-
|
|
250
|
-
-
|
|
325
|
+
- Relation `path` is normalized internally; providing `path` with or without a leading slash produces the same final URL.
|
|
326
|
+
- Example: `path="tags"` or `path="/tags"` both yield `GET /{base}/{pk}/tags` (or `GET /{base}/{pk}/tags/` when `append_slash=True`) and `POST /{base}/{pk}/tags/`.
|
|
327
|
+
- If `path` is empty, it falls back to the related model verbose name.
|
|
251
328
|
|
|
252
329
|
### Request/Response and concurrency
|
|
253
330
|
|
|
@@ -291,9 +368,22 @@ class ArticleViewSet(APIViewSet):
|
|
|
291
368
|
m2m_auth = [JWTAuth()] # fallback for relations without custom auth
|
|
292
369
|
```
|
|
293
370
|
|
|
371
|
+
Example with trailing slash on GET relation:
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
M2MRelationSchema(
|
|
375
|
+
model=Tag,
|
|
376
|
+
related_name="tags",
|
|
377
|
+
filters={"name": (str, "")},
|
|
378
|
+
append_slash=True, # GET /{base}/{pk}/tags/
|
|
379
|
+
)
|
|
380
|
+
```
|
|
381
|
+
|
|
294
382
|
## Custom Views
|
|
295
383
|
|
|
296
|
-
|
|
384
|
+
Preferred (decorators): see the section above.
|
|
385
|
+
|
|
386
|
+
Legacy (still supported):
|
|
297
387
|
|
|
298
388
|
```python
|
|
299
389
|
def views(self):
|
|
@@ -329,37 +419,45 @@ All CRUD and M2M endpoints may respond with `GenericMessageSchema` for error cod
|
|
|
329
419
|
|
|
330
420
|
## Minimal Usage
|
|
331
421
|
|
|
332
|
-
Recommended
|
|
422
|
+
=== "Recommended"
|
|
423
|
+
````python
|
|
424
|
+
from ninja_aio import NinjaAIO
|
|
425
|
+
from ninja_aio.views import APIViewSet
|
|
426
|
+
from .models import User
|
|
427
|
+
from ninja_aio.decorators import api_get
|
|
333
428
|
|
|
334
|
-
|
|
335
|
-
from ninja_aio import NinjaAIO
|
|
336
|
-
from ninja_aio.views import APIViewSet
|
|
337
|
-
from .models import User
|
|
429
|
+
api = NinjaAIO(title="My API")
|
|
338
430
|
|
|
339
|
-
api
|
|
340
|
-
|
|
341
|
-
@
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
431
|
+
@api.viewset(model=User)
|
|
432
|
+
class UserViewSet(APIViewSet):
|
|
433
|
+
@api_get("/stats/")
|
|
434
|
+
async def stats(self, request):
|
|
435
|
+
total = await self.model.objects.acount()
|
|
436
|
+
return {"total": total}
|
|
437
|
+
```
|
|
345
438
|
|
|
346
|
-
|
|
439
|
+
=== "Alternative implementation"
|
|
440
|
+
```python
|
|
441
|
+
from ninja_aio import NinjaAIO
|
|
442
|
+
from ninja_aio.views import APIViewSet
|
|
443
|
+
from .models import User
|
|
347
444
|
|
|
348
|
-
|
|
445
|
+
api = NinjaAIO(title="My API")
|
|
349
446
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
from .models import User
|
|
447
|
+
class UserViewSet(APIViewSet):
|
|
448
|
+
model = User
|
|
449
|
+
api = api
|
|
354
450
|
|
|
355
|
-
|
|
451
|
+
def views(self):
|
|
452
|
+
@self.router.get("/stats/")
|
|
453
|
+
async def stats(request):
|
|
454
|
+
total = await self.model.objects.acount()
|
|
455
|
+
return {"total": total}
|
|
356
456
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
api = api
|
|
457
|
+
UserViewSet().add_views_to_route()
|
|
458
|
+
```
|
|
360
459
|
|
|
361
|
-
|
|
362
|
-
```
|
|
460
|
+
Note: prefix and tags are optional. If omitted, the base path is inferred from the model verbose name plural and tags default to the model verbose name.
|
|
363
461
|
|
|
364
462
|
## Disable Selected Views
|
|
365
463
|
|
|
@@ -387,6 +485,7 @@ Recommended:
|
|
|
387
485
|
from ninja_aio import NinjaAIO
|
|
388
486
|
from ninja_aio.views import APIViewSet
|
|
389
487
|
from ninja_aio.models import ModelSerializer
|
|
488
|
+
from ninja_aio.decorators import api_get
|
|
390
489
|
from django.db import models
|
|
391
490
|
|
|
392
491
|
api = NinjaAIO(title="My API")
|
|
@@ -63,3 +63,41 @@ class MyViewSet(APIViewSet):
|
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
These are applied in combination with built-ins (e.g., unique_view, paginate) using decorate_view in the implementation.
|
|
66
|
+
|
|
67
|
+
## ApiMethodFactory.decorators
|
|
68
|
+
|
|
69
|
+
Example: use api_get within a ViewSet with extra decorators:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from ninja.pagination import PageNumberPagination
|
|
73
|
+
from ninja_aio.decorators.operations import api_get
|
|
74
|
+
from ninja_aio.views import APIViewSet
|
|
75
|
+
from ninja_aio.models import ModelSerializer
|
|
76
|
+
from ninja_aio.decorators import unique_view
|
|
77
|
+
from ninja.pagination import paginate
|
|
78
|
+
|
|
79
|
+
from . import models
|
|
80
|
+
|
|
81
|
+
api = NinjaAIO()
|
|
82
|
+
|
|
83
|
+
@api.viewset(models.Book)
|
|
84
|
+
class BookAPI(APIViewSet):
|
|
85
|
+
query_params = {
|
|
86
|
+
"title": (str, None),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@api_get(
|
|
90
|
+
"/custom-get",
|
|
91
|
+
response={200: list[GenericMessageSchema]},
|
|
92
|
+
decorators=[paginate(PageNumberPagination), unique_view("test-unique-view")],
|
|
93
|
+
)
|
|
94
|
+
async def get_test(self, request):
|
|
95
|
+
return [{"message": "This is a custom GET method in BookAPI"}]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Notes:
|
|
99
|
+
|
|
100
|
+
- Provide decorators as a list; they are applied in reverse order internally.
|
|
101
|
+
- paginate(PageNumberPagination) enables async pagination on the handler.
|
|
102
|
+
- unique_view(name) marks the route as unique to avoid duplicate registration.
|
|
103
|
+
- Works with @api.viewset(Model) classes extending APIViewSet.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .views import decorate_view, aatomic, unique_view
|
|
2
|
+
from .operations import (
|
|
3
|
+
api_get,
|
|
4
|
+
api_post,
|
|
5
|
+
api_put,
|
|
6
|
+
api_delete,
|
|
7
|
+
api_patch,
|
|
8
|
+
api_options,
|
|
9
|
+
api_head,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"decorate_view",
|
|
14
|
+
"aatomic",
|
|
15
|
+
"unique_view",
|
|
16
|
+
"api_get",
|
|
17
|
+
"api_post",
|
|
18
|
+
"api_put",
|
|
19
|
+
"api_delete",
|
|
20
|
+
"api_patch",
|
|
21
|
+
"api_options",
|
|
22
|
+
"api_head",
|
|
23
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from ninja_aio.factory import ApiMethodFactory
|
|
2
|
+
|
|
3
|
+
api_get = ApiMethodFactory.make("get")
|
|
4
|
+
api_post = ApiMethodFactory.make("post")
|
|
5
|
+
api_put = ApiMethodFactory.make("put")
|
|
6
|
+
api_patch = ApiMethodFactory.make("patch")
|
|
7
|
+
api_delete = ApiMethodFactory.make("delete")
|
|
8
|
+
api_options = ApiMethodFactory.make("options")
|
|
9
|
+
api_head = ApiMethodFactory.make("head")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
from django.db.transaction import Atomic
|
|
2
1
|
from functools import wraps
|
|
2
|
+
|
|
3
|
+
from django.db.transaction import Atomic
|
|
3
4
|
from asgiref.sync import sync_to_async
|
|
4
5
|
|
|
5
6
|
|
|
@@ -189,7 +190,7 @@ def decorate_view(*decorators):
|
|
|
189
190
|
@decorate_view(authenticate, log_request)
|
|
190
191
|
async def some_view(request):
|
|
191
192
|
...
|
|
192
|
-
|
|
193
|
+
|
|
193
194
|
Conditional decoration (skips None):
|
|
194
195
|
class MyAPIViewSet(APIViewSet):
|
|
195
196
|
api = api
|
|
@@ -215,4 +216,4 @@ def decorate_view(*decorators):
|
|
|
215
216
|
wrapped = dec(wrapped)
|
|
216
217
|
return wrapped
|
|
217
218
|
|
|
218
|
-
return _decorator
|
|
219
|
+
return _decorator
|