django-ninja-aio-crud 2.1.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.1.0 → django_ninja_aio_crud-2.3.0}/.github/workflows/docs.yml +3 -1
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/PKG-INFO +21 -16
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/README.md +19 -14
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/api/authentication.md +1 -178
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/api/views/api_view.md +72 -17
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/api/views/api_view_set.md +188 -58
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/api/views/decorators.md +38 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/index.md +11 -15
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/mkdocs.yml +1 -2
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/__init__.py +1 -1
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/api.py +24 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/auth.py +3 -3
- 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.1.0/ninja_aio/decorators.py → django_ninja_aio_crud-2.3.0/ninja_aio/decorators/views.py +4 -3
- {django_ninja_aio_crud-2.1.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.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/helpers/api.py +16 -2
- {django_ninja_aio_crud-2.1.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.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/types.py +1 -1
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/views/api.py +114 -34
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/pyproject.toml +1 -1
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/helpers/test_many_to_many_api.py +12 -12
- django_ninja_aio_crud-2.3.0/tests/views/test_views.py +144 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/views/test_viewset.py +104 -1
- django_ninja_aio_crud-2.1.0/ninja_aio/schemas/helpers.py +0 -90
- django_ninja_aio_crud-2.1.0/tests/views/test_views.py +0 -57
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/.github/dependabot.yml +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/.github/workflows/coverage.yml +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/.github/workflows/publish.yml +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/.gitignore +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/.pre-commit-config.yaml +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/LICENSE +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/CNAME +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/api/models/model_serializer.md +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/api/models/model_util.md +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/api/pagination.md +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/api/renderers/orjson_renderer.md +0 -0
- {django_ninja_aio_crud-2.1.0/docs → django_ninja_aio_crud-2.3.0/docs/api/views}/mixins.md +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/auth.md +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/contributing.md +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/extra.css +0 -0
- {django_ninja_aio_crud-2.1.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.1.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.1.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.1.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.1.0 → django_ninja_aio_crud-2.3.0}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
- {django_ninja_aio_crud-2.1.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.1.0 → django_ninja_aio_crud-2.3.0}/docs/getting_started/installation.md +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/getting_started/quick_start.md +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/images/bar-swagger.png +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/images/favicon.ico +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/images/foo-swagger.png +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/images/logo.png +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/release_notes.md +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/requirements.txt +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/tutorial/authentication.md +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/tutorial/crud.md +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/tutorial/filtering.md +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/tutorial/model.md +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/examples/ex_1/models.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/examples/ex_1/urls.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/examples/ex_1/views.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/examples/ex_2/auth.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/examples/ex_2/models.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/examples/ex_2/urls.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/examples/ex_2/views.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/main.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/models.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/renders.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/schemas/__init__.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/schemas/api.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/schemas/generics.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/views/mixins.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/requirements.dev.txt +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/run-local-coverage.sh +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/__init__.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/core/__init__.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/core/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/core/test_exceptions_api.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/core/test_renderer_parser.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/generics/__init__.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/generics/literals.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/generics/models.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/generics/request.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/generics/views.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/models/__init__.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/models/test_model_util.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/models/test_models_extra.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/test_app/__init__.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/test_app/models.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/test_app/schema.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/test_app/views.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/test_auth.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/test_exceptions.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/test_query_util.py +0 -0
- {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/test_settings.py +0 -0
- {django_ninja_aio_crud-2.1.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
|
|
@@ -96,7 +98,7 @@ jobs:
|
|
|
96
98
|
if [ "$MAKE_LATEST" = "true" ]; then
|
|
97
99
|
echo "Deploying $VERSION as latest and default"
|
|
98
100
|
mike deploy --push --update-aliases "$VERSION" latest --ignore-remote-status
|
|
99
|
-
mike set-default
|
|
101
|
+
mike set-default --push latest
|
|
100
102
|
else
|
|
101
103
|
echo "Deploying $VERSION (non-latest)"
|
|
102
104
|
mike deploy --push "$VERSION" --ignore-remote-status
|
|
@@ -1,9 +1,9 @@
|
|
|
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
|
-
Requires-Python: >=3.10
|
|
6
|
+
Requires-Python: >=3.10, <=3.14
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Classifier: Operating System :: OS Independent
|
|
9
9
|
Classifier: Topic :: Internet
|
|
@@ -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
|
|
|
@@ -614,188 +614,11 @@ Support both JWT and API Key:
|
|
|
614
614
|
class ArticleViewSet(APIViewSet):
|
|
615
615
|
model = Article
|
|
616
616
|
api = api
|
|
617
|
-
auth = [JWTAuth()
|
|
617
|
+
auth = [JWTAuth(), APIKeyAuth()] # Either JWT or API Key
|
|
618
618
|
```
|
|
619
619
|
|
|
620
620
|
Django Ninja will try both methods; if either succeeds, the request is authenticated.
|
|
621
621
|
|
|
622
|
-
## Token Validation
|
|
623
|
-
|
|
624
|
-
### Expiration Validation
|
|
625
|
-
|
|
626
|
-
JWT tokens include `exp` claim (expiration timestamp):
|
|
627
|
-
|
|
628
|
-
```python
|
|
629
|
-
# Token payload
|
|
630
|
-
{
|
|
631
|
-
"sub": "123",
|
|
632
|
-
"exp": 1704067200, # Unix timestamp
|
|
633
|
-
"iat": 1704063600
|
|
634
|
-
}
|
|
635
|
-
```
|
|
636
|
-
|
|
637
|
-
AsyncJwtBearer automatically validates expiration:
|
|
638
|
-
|
|
639
|
-
```python
|
|
640
|
-
class JWTAuth(AsyncJwtBearer):
|
|
641
|
-
jwt_public = jwk.RSAKey.import_key(PUBLIC_KEY)
|
|
642
|
-
# Expiration checked automatically
|
|
643
|
-
```
|
|
644
|
-
|
|
645
|
-
**Error Response:**
|
|
646
|
-
|
|
647
|
-
```json
|
|
648
|
-
{
|
|
649
|
-
"detail": "Token has expired"
|
|
650
|
-
}
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
### Not Before Validation
|
|
654
|
-
|
|
655
|
-
Use `nbf` claim for tokens that become valid in the future:
|
|
656
|
-
|
|
657
|
-
```python
|
|
658
|
-
# Token payload
|
|
659
|
-
{
|
|
660
|
-
"sub": "123",
|
|
661
|
-
"nbf": 1704063600, # Not valid before this time
|
|
662
|
-
"exp": 1704067200
|
|
663
|
-
}
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
Automatically validated by AsyncJwtBearer.
|
|
667
|
-
|
|
668
|
-
### Custom Validation
|
|
669
|
-
|
|
670
|
-
```python
|
|
671
|
-
class StrictAuth(AsyncJwtBearer):
|
|
672
|
-
jwt_public = jwk.RSAKey.import_key(PUBLIC_KEY)
|
|
673
|
-
|
|
674
|
-
async def auth_handler(self, request):
|
|
675
|
-
# Check token type
|
|
676
|
-
token_type = self.dcd.claims.get("typ")
|
|
677
|
-
if token_type != "access":
|
|
678
|
-
return False
|
|
679
|
-
|
|
680
|
-
# Check IP whitelist
|
|
681
|
-
allowed_ips = self.dcd.claims.get("allowed_ips", [])
|
|
682
|
-
client_ip = request.META.get('REMOTE_ADDR')
|
|
683
|
-
if allowed_ips and client_ip not in allowed_ips:
|
|
684
|
-
return False
|
|
685
|
-
|
|
686
|
-
# Continue with normal auth
|
|
687
|
-
user_id = self.dcd.claims.get("sub")
|
|
688
|
-
return await User.objects.aget(id=user_id)
|
|
689
|
-
```
|
|
690
|
-
|
|
691
|
-
## Testing Authentication
|
|
692
|
-
|
|
693
|
-
### Unit Tests
|
|
694
|
-
|
|
695
|
-
```python
|
|
696
|
-
import pytest
|
|
697
|
-
from ninja.testing import TestAsyncClient
|
|
698
|
-
from myapp.views import api
|
|
699
|
-
from myapp.auth import JWTAuth
|
|
700
|
-
import jwt
|
|
701
|
-
from datetime import datetime, timedelta
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
def create_token(user_id: int, **claims) -> str:
|
|
705
|
-
payload = {
|
|
706
|
-
"sub": str(user_id),
|
|
707
|
-
"exp": datetime.utcnow() + timedelta(hours=1),
|
|
708
|
-
"iat": datetime.utcnow(),
|
|
709
|
-
**claims
|
|
710
|
-
}
|
|
711
|
-
return jwt.encode(payload, PRIVATE_KEY, algorithm="RS256")
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
@pytest.mark.asyncio
|
|
715
|
-
async def test_authenticated_request():
|
|
716
|
-
client = TestAsyncClient(api)
|
|
717
|
-
|
|
718
|
-
# Create user
|
|
719
|
-
user = await User.objects.acreate(username="testuser")
|
|
720
|
-
|
|
721
|
-
# Create token
|
|
722
|
-
token = create_token(user.id)
|
|
723
|
-
|
|
724
|
-
# Make authenticated request
|
|
725
|
-
response = await client.get(
|
|
726
|
-
"/article/",
|
|
727
|
-
headers={"Authorization": f"Bearer {token}"}
|
|
728
|
-
)
|
|
729
|
-
|
|
730
|
-
assert response.status_code == 200
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
@pytest.mark.asyncio
|
|
734
|
-
async def test_missing_token():
|
|
735
|
-
client = TestAsyncClient(api)
|
|
736
|
-
|
|
737
|
-
response = await client.get("/article/")
|
|
738
|
-
assert response.status_code == 401
|
|
739
|
-
assert "detail" in response.json()
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
@pytest.mark.asyncio
|
|
743
|
-
async def test_expired_token():
|
|
744
|
-
client = TestAsyncClient(api)
|
|
745
|
-
|
|
746
|
-
# Create expired token
|
|
747
|
-
payload = {
|
|
748
|
-
"sub": "123",
|
|
749
|
-
"exp": datetime.utcnow() - timedelta(hours=1), # Expired
|
|
750
|
-
"iat": datetime.utcnow() - timedelta(hours=2)
|
|
751
|
-
}
|
|
752
|
-
token = jwt.encode(payload, PRIVATE_KEY, algorithm="RS256")
|
|
753
|
-
|
|
754
|
-
response = await client.get(
|
|
755
|
-
"/article/",
|
|
756
|
-
headers={"Authorization": f"Bearer {token}"}
|
|
757
|
-
)
|
|
758
|
-
|
|
759
|
-
assert response.status_code == 401
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
@pytest.mark.asyncio
|
|
763
|
-
async def test_invalid_signature():
|
|
764
|
-
client = TestAsyncClient(api)
|
|
765
|
-
|
|
766
|
-
# Create token with wrong key
|
|
767
|
-
payload = {"sub": "123", "exp": datetime.utcnow() + timedelta(hours=1)}
|
|
768
|
-
token = jwt.encode(payload, "wrong-secret", algorithm="HS256")
|
|
769
|
-
|
|
770
|
-
response = await client.get(
|
|
771
|
-
"/article/",
|
|
772
|
-
headers={"Authorization": f"Bearer {token}"}
|
|
773
|
-
)
|
|
774
|
-
|
|
775
|
-
assert response.status_code == 401
|
|
776
|
-
```
|
|
777
|
-
|
|
778
|
-
### Mock Authentication
|
|
779
|
-
|
|
780
|
-
For testing without real tokens:
|
|
781
|
-
|
|
782
|
-
```python
|
|
783
|
-
from unittest.mock import AsyncMock, patch
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
@pytest.mark.asyncio
|
|
787
|
-
@patch('myapp.auth.JWTAuth.auth_handler')
|
|
788
|
-
async def test_with_mock_auth(mock_auth):
|
|
789
|
-
# Mock auth to return test user
|
|
790
|
-
user = await User.objects.acreate(username="testuser")
|
|
791
|
-
mock_auth.return_value = user
|
|
792
|
-
|
|
793
|
-
client = TestAsyncClient(api)
|
|
794
|
-
response = await client.get("/article/")
|
|
795
|
-
|
|
796
|
-
assert response.status_code == 200
|
|
797
|
-
```
|
|
798
|
-
|
|
799
622
|
## Best Practices
|
|
800
623
|
|
|
801
624
|
1. **Use RSA (asymmetric) keys for production:**
|
|
@@ -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,18 +126,16 @@ Registers all defined views to the API instance.
|
|
|
81
126
|
|
|
82
127
|
**Returns:** The router instance
|
|
83
128
|
|
|
84
|
-
**
|
|
85
|
-
|
|
86
|
-
```python
|
|
87
|
-
view = UserAPIView()
|
|
88
|
-
view.add_views_to_route()
|
|
89
|
-
```
|
|
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.
|
|
90
130
|
|
|
91
131
|
## Complete Example
|
|
92
132
|
|
|
133
|
+
**Recommended:**
|
|
134
|
+
|
|
93
135
|
```python
|
|
94
136
|
from ninja_aio import NinjaAIO
|
|
95
137
|
from ninja_aio.views import APIView
|
|
138
|
+
from ninja_aio.decorators import api_get, api_post
|
|
96
139
|
from ninja import Schema
|
|
97
140
|
|
|
98
141
|
api = NinjaAIO(title="My API")
|
|
@@ -101,6 +144,22 @@ class StatsSchema(Schema):
|
|
|
101
144
|
total: int
|
|
102
145
|
active: int
|
|
103
146
|
|
|
147
|
+
@api.view(prefix="/analytics", tags=["Analytics"])
|
|
148
|
+
class AnalyticsView(APIView):
|
|
149
|
+
@api_get("/dashboard", response=StatsSchema)
|
|
150
|
+
async def dashboard(self, request):
|
|
151
|
+
return {"total": 1000, "active": 750}
|
|
152
|
+
|
|
153
|
+
@api_post("/track")
|
|
154
|
+
async def track_event(self, request, event: str):
|
|
155
|
+
return {"tracked": event}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Alternative implementation:**
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
api = NinjaAIO(title="My API")
|
|
162
|
+
|
|
104
163
|
class AnalyticsView(APIView):
|
|
105
164
|
api = api
|
|
106
165
|
router_tag = "Analytics"
|
|
@@ -109,26 +168,22 @@ class AnalyticsView(APIView):
|
|
|
109
168
|
def views(self):
|
|
110
169
|
@self.router.get("/dashboard", response=StatsSchema)
|
|
111
170
|
async def dashboard(request):
|
|
112
|
-
return {
|
|
113
|
-
"total": 1000,
|
|
114
|
-
"active": 750
|
|
115
|
-
}
|
|
171
|
+
return {"total": 1000, "active": 750}
|
|
116
172
|
|
|
117
173
|
@self.router.post("/track")
|
|
118
174
|
async def track_event(request, event: str):
|
|
119
|
-
# tracking logic
|
|
120
175
|
return {"tracked": event}
|
|
121
176
|
|
|
122
|
-
# Register views
|
|
123
177
|
AnalyticsView().add_views_to_route()
|
|
124
178
|
```
|
|
125
179
|
|
|
126
180
|
## Notes
|
|
127
181
|
|
|
128
182
|
- Use `APIView` for simple, non-CRUD endpoints
|
|
129
|
-
- For CRUD operations, use [`APIViewSet`](api_view_set.md)
|
|
130
|
-
- All views are
|
|
131
|
-
-
|
|
183
|
+
- For CRUD operations, use [`APIViewSet`](api_view_set.md)
|
|
184
|
+
- All views are async-compatible
|
|
185
|
+
- Standard error codes are available via `self.error_codes`
|
|
186
|
+
- Decorator-based endpoints are preferred for clarity and better OpenAPI signatures.
|
|
132
187
|
|
|
133
188
|
Note:
|
|
134
189
|
|