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.
Files changed (106) hide show
  1. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/.github/workflows/docs.yml +3 -1
  2. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/PKG-INFO +21 -16
  3. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/README.md +19 -14
  4. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/api/authentication.md +1 -178
  5. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/api/views/api_view.md +72 -17
  6. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/api/views/api_view_set.md +188 -58
  7. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/api/views/decorators.md +38 -0
  8. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/index.md +11 -15
  9. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/mkdocs.yml +1 -2
  10. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/__init__.py +1 -1
  11. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/api.py +24 -0
  12. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/auth.py +3 -3
  13. django_ninja_aio_crud-2.3.0/ninja_aio/decorators/__init__.py +23 -0
  14. django_ninja_aio_crud-2.3.0/ninja_aio/decorators/operations.py +9 -0
  15. 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
  16. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/exceptions.py +23 -1
  17. django_ninja_aio_crud-2.3.0/ninja_aio/factory/__init__.py +3 -0
  18. django_ninja_aio_crud-2.3.0/ninja_aio/factory/operations.py +296 -0
  19. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/helpers/api.py +16 -2
  20. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/helpers/query.py +6 -1
  21. django_ninja_aio_crud-2.3.0/ninja_aio/schemas/helpers.py +170 -0
  22. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/types.py +1 -1
  23. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/views/api.py +114 -34
  24. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/pyproject.toml +1 -1
  25. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/helpers/test_many_to_many_api.py +12 -12
  26. django_ninja_aio_crud-2.3.0/tests/views/test_views.py +144 -0
  27. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/views/test_viewset.py +104 -1
  28. django_ninja_aio_crud-2.1.0/ninja_aio/schemas/helpers.py +0 -90
  29. django_ninja_aio_crud-2.1.0/tests/views/test_views.py +0 -57
  30. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/.github/dependabot.yml +0 -0
  31. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/.github/workflows/coverage.yml +0 -0
  32. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/.github/workflows/publish.yml +0 -0
  33. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/.gitignore +0 -0
  34. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/.pre-commit-config.yaml +0 -0
  35. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/LICENSE +0 -0
  36. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/CNAME +0 -0
  37. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/api/models/model_serializer.md +0 -0
  38. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/api/models/model_util.md +0 -0
  39. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/api/pagination.md +0 -0
  40. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/api/renderers/orjson_renderer.md +0 -0
  41. {django_ninja_aio_crud-2.1.0/docs → django_ninja_aio_crud-2.3.0/docs/api/views}/mixins.md +0 -0
  42. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/auth.md +0 -0
  43. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/contributing.md +0 -0
  44. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/extra.css +0 -0
  45. {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
  46. {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
  47. {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
  48. {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
  49. {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
  50. {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
  51. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/getting_started/installation.md +0 -0
  52. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/getting_started/quick_start.md +0 -0
  53. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/images/bar-swagger.png +0 -0
  54. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/images/favicon.ico +0 -0
  55. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/images/foo-swagger.png +0 -0
  56. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/images/logo.png +0 -0
  57. {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
  58. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/release_notes.md +0 -0
  59. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/requirements.txt +0 -0
  60. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/tutorial/authentication.md +0 -0
  61. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/tutorial/crud.md +0 -0
  62. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/tutorial/filtering.md +0 -0
  63. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/docs/tutorial/model.md +0 -0
  64. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/examples/ex_1/models.py +0 -0
  65. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/examples/ex_1/urls.py +0 -0
  66. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/examples/ex_1/views.py +0 -0
  67. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/examples/ex_2/auth.py +0 -0
  68. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/examples/ex_2/models.py +0 -0
  69. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/examples/ex_2/urls.py +0 -0
  70. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/examples/ex_2/views.py +0 -0
  71. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/main.py +0 -0
  72. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/helpers/__init__.py +0 -0
  73. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/models.py +0 -0
  74. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/parsers.py +0 -0
  75. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/renders.py +0 -0
  76. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/schemas/__init__.py +0 -0
  77. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/schemas/api.py +0 -0
  78. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/schemas/generics.py +0 -0
  79. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/views/__init__.py +0 -0
  80. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/views/mixins.py +0 -0
  81. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/requirements.dev.txt +0 -0
  82. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/run-local-coverage.sh +0 -0
  83. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/__init__.py +0 -0
  84. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/core/__init__.py +0 -0
  85. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/core/test_decorators.py +0 -0
  86. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/core/test_exceptions_api.py +0 -0
  87. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/core/test_renderer_parser.py +0 -0
  88. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/generics/__init__.py +0 -0
  89. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/generics/literals.py +0 -0
  90. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/generics/models.py +0 -0
  91. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/generics/request.py +0 -0
  92. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/generics/views.py +0 -0
  93. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/helpers/__init__.py +0 -0
  94. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/models/__init__.py +0 -0
  95. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/models/test_model_util.py +0 -0
  96. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/models/test_models_extra.py +0 -0
  97. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/test_app/__init__.py +0 -0
  98. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/test_app/models.py +0 -0
  99. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/test_app/schema.py +0 -0
  100. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/test_app/views.py +0 -0
  101. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/test_auth.py +0 -0
  102. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/test_decorators.py +0 -0
  103. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/test_exceptions.py +0 -0
  104. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/test_query_util.py +0 -0
  105. {django_ninja_aio_crud-2.1.0 → django_ninja_aio_crud-2.3.0}/tests/test_settings.py +0 -0
  106. {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 "$VERSION"
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.1.0
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
- model = Book
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
- model = Book
225
- api = api
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
- model = Book
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
- model = Book
190
- api = api
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() | APIKeyAuth()] # Either JWT or API Key
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
- ### `views()`
34
+ ### Recommended: decorator-based endpoints
35
35
 
36
- Override this method to define your custom endpoints.
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
- **Usage:**
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) instead
130
- - All views are automatically async-compatible
131
- - Error codes `{400, 401, 404, 428}` are available via `self.error_codes`
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