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.

Files changed (105) hide show
  1. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/.github/workflows/docs.yml +2 -0
  2. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/PKG-INFO +20 -15
  3. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/README.md +19 -14
  4. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/api/views/api_view.md +56 -10
  5. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/api/views/api_view_set.md +128 -29
  6. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/api/views/decorators.md +38 -0
  7. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/__init__.py +1 -1
  8. django_ninja_aio_crud-2.3.0/ninja_aio/decorators/__init__.py +23 -0
  9. django_ninja_aio_crud-2.3.0/ninja_aio/decorators/operations.py +9 -0
  10. 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
  11. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/exceptions.py +23 -1
  12. django_ninja_aio_crud-2.3.0/ninja_aio/factory/__init__.py +3 -0
  13. django_ninja_aio_crud-2.3.0/ninja_aio/factory/operations.py +296 -0
  14. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/helpers/api.py +16 -2
  15. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/helpers/query.py +6 -1
  16. django_ninja_aio_crud-2.3.0/ninja_aio/schemas/helpers.py +170 -0
  17. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/views/api.py +20 -7
  18. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/helpers/test_many_to_many_api.py +11 -10
  19. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/views/test_views.py +45 -0
  20. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/views/test_viewset.py +50 -0
  21. django_ninja_aio_crud-2.2.0/ninja_aio/schemas/helpers.py +0 -90
  22. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/.github/dependabot.yml +0 -0
  23. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/.github/workflows/coverage.yml +0 -0
  24. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/.github/workflows/publish.yml +0 -0
  25. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/.gitignore +0 -0
  26. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/.pre-commit-config.yaml +0 -0
  27. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/LICENSE +0 -0
  28. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/CNAME +0 -0
  29. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/api/authentication.md +0 -0
  30. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/api/models/model_serializer.md +0 -0
  31. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/api/models/model_util.md +0 -0
  32. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/api/pagination.md +0 -0
  33. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/api/renderers/orjson_renderer.md +0 -0
  34. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/api/views/mixins.md +0 -0
  35. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/auth.md +0 -0
  36. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/contributing.md +0 -0
  37. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/extra.css +0 -0
  38. {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
  39. {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
  40. {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
  41. {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
  42. {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
  43. {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
  44. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/getting_started/installation.md +0 -0
  45. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/getting_started/quick_start.md +0 -0
  46. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/images/bar-swagger.png +0 -0
  47. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/images/favicon.ico +0 -0
  48. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/images/foo-swagger.png +0 -0
  49. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/images/logo.png +0 -0
  50. {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
  51. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/index.md +0 -0
  52. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/release_notes.md +0 -0
  53. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/requirements.txt +0 -0
  54. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/tutorial/authentication.md +0 -0
  55. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/tutorial/crud.md +0 -0
  56. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/tutorial/filtering.md +0 -0
  57. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/docs/tutorial/model.md +0 -0
  58. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/examples/ex_1/models.py +0 -0
  59. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/examples/ex_1/urls.py +0 -0
  60. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/examples/ex_1/views.py +0 -0
  61. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/examples/ex_2/auth.py +0 -0
  62. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/examples/ex_2/models.py +0 -0
  63. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/examples/ex_2/urls.py +0 -0
  64. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/examples/ex_2/views.py +0 -0
  65. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/main.py +0 -0
  66. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/mkdocs.yml +0 -0
  67. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/api.py +0 -0
  68. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/auth.py +0 -0
  69. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/helpers/__init__.py +0 -0
  70. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/models.py +0 -0
  71. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/parsers.py +0 -0
  72. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/renders.py +0 -0
  73. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/schemas/__init__.py +0 -0
  74. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/schemas/api.py +0 -0
  75. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/schemas/generics.py +0 -0
  76. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/types.py +0 -0
  77. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/views/__init__.py +0 -0
  78. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/ninja_aio/views/mixins.py +0 -0
  79. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/pyproject.toml +0 -0
  80. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/requirements.dev.txt +0 -0
  81. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/run-local-coverage.sh +0 -0
  82. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/__init__.py +0 -0
  83. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/core/__init__.py +0 -0
  84. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/core/test_decorators.py +0 -0
  85. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/core/test_exceptions_api.py +0 -0
  86. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/core/test_renderer_parser.py +0 -0
  87. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/generics/__init__.py +0 -0
  88. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/generics/literals.py +0 -0
  89. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/generics/models.py +0 -0
  90. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/generics/request.py +0 -0
  91. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/generics/views.py +0 -0
  92. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/helpers/__init__.py +0 -0
  93. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/models/__init__.py +0 -0
  94. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/models/test_model_util.py +0 -0
  95. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/models/test_models_extra.py +0 -0
  96. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/test_app/__init__.py +0 -0
  97. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/test_app/models.py +0 -0
  98. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/test_app/schema.py +0 -0
  99. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/test_app/views.py +0 -0
  100. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/test_auth.py +0 -0
  101. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/test_decorators.py +0 -0
  102. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/test_exceptions.py +0 -0
  103. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/test_query_util.py +0 -0
  104. {django_ninja_aio_crud-2.2.0 → django_ninja_aio_crud-2.3.0}/tests/test_settings.py +0 -0
  105. {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.2.0
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
- 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
 
@@ -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,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; the router is mounted automatically.
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
- def views(self):
104
- @self.router.get("/dashboard", response=StatsSchema)
105
- async def dashboard(request):
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
- @self.router.post("/track")
109
- async def track_event(request, event: str):
110
- return {"tracked": event}
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 has no trailing slash; update/delete include a trailing slash.
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
- - OperationId: `get_{base_model_name}_{rel_path}`
323
+ Path normalization rules:
248
324
 
249
- - POST relation: `/{base}/{pk}/{rel_path}/` (trailing slash)
250
- - OperationId: `manage_{base_model_name}_{rel_path}`
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
- Override `views()` to register extra endpoints:
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
- ```python
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 = NinjaAIO(title="My API")
340
-
341
- @api.viewset(model=User)
342
- class UserViewSet(APIViewSet):
343
- pass
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
- 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.
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
- Alternative implementation:
445
+ api = NinjaAIO(title="My API")
349
446
 
350
- ```python
351
- from ninja_aio import NinjaAIO
352
- from ninja_aio.views import APIViewSet
353
- from .models import User
447
+ class UserViewSet(APIViewSet):
448
+ model = User
449
+ api = api
354
450
 
355
- api = NinjaAIO(title="My API")
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
- class UserViewSet(APIViewSet):
358
- model = User
359
- api = api
457
+ UserViewSet().add_views_to_route()
458
+ ```
360
459
 
361
- UserViewSet().add_views_to_route()
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.
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "2.2.0"
3
+ __version__ = "2.3.0"
4
4
 
5
5
  from .api import NinjaAIO
6
6
 
@@ -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