django-ninja-aio-crud 2.2.0__py3-none-any.whl → 2.3.1__py3-none-any.whl
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.dist-info → django_ninja_aio_crud-2.3.1.dist-info}/METADATA +38 -24
- django_ninja_aio_crud-2.3.1.dist-info/RECORD +27 -0
- ninja_aio/__init__.py +1 -1
- ninja_aio/decorators/__init__.py +23 -0
- ninja_aio/decorators/operations.py +9 -0
- ninja_aio/{decorators.py → decorators/views.py} +4 -3
- ninja_aio/exceptions.py +23 -1
- ninja_aio/factory/__init__.py +3 -0
- ninja_aio/factory/operations.py +296 -0
- ninja_aio/helpers/api.py +16 -2
- ninja_aio/helpers/query.py +6 -1
- ninja_aio/schemas/helpers.py +98 -18
- ninja_aio/views/api.py +20 -7
- django_ninja_aio_crud-2.2.0.dist-info/RECORD +0 -23
- {django_ninja_aio_crud-2.2.0.dist-info → django_ninja_aio_crud-2.3.1.dist-info}/WHEEL +0 -0
- {django_ninja_aio_crud-2.2.0.dist-info → django_ninja_aio_crud-2.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-ninja-aio-crud
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.1
|
|
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.15
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Classifier: Operating System :: OS Independent
|
|
9
9
|
Classifier: Topic :: Internet
|
|
@@ -74,6 +74,7 @@ Add to your project’s dependencies and ensure Django Ninja is installed.
|
|
|
74
74
|
## 🚀 Quick Start
|
|
75
75
|
|
|
76
76
|
models.py
|
|
77
|
+
|
|
77
78
|
```python
|
|
78
79
|
from django.db import models
|
|
79
80
|
from ninja_aio.models import ModelSerializer
|
|
@@ -93,6 +94,7 @@ class Book(ModelSerializer):
|
|
|
93
94
|
```
|
|
94
95
|
|
|
95
96
|
views.py
|
|
97
|
+
|
|
96
98
|
```python
|
|
97
99
|
from ninja_aio import NinjaAIO
|
|
98
100
|
from ninja_aio.views import APIViewSet
|
|
@@ -100,11 +102,10 @@ from .models import Book
|
|
|
100
102
|
|
|
101
103
|
api = NinjaAIO()
|
|
102
104
|
|
|
105
|
+
@api.viewset(Book)
|
|
103
106
|
class BookViewSet(APIViewSet):
|
|
104
|
-
|
|
105
|
-
api = api
|
|
107
|
+
pass
|
|
106
108
|
|
|
107
|
-
BookViewSet().add_views_to_route()
|
|
108
109
|
```
|
|
109
110
|
|
|
110
111
|
Visit `/docs` → CRUD endpoints ready.
|
|
@@ -114,9 +115,8 @@ Visit `/docs` → CRUD endpoints ready.
|
|
|
114
115
|
## 🔄 Query Filtering
|
|
115
116
|
|
|
116
117
|
```python
|
|
118
|
+
@api.viewset(Book)
|
|
117
119
|
class BookViewSet(APIViewSet):
|
|
118
|
-
model = Book
|
|
119
|
-
api = api
|
|
120
120
|
query_params = {"published": (bool, None), "title": (str, None)}
|
|
121
121
|
|
|
122
122
|
async def query_params_handler(self, queryset, filters):
|
|
@@ -128,6 +128,7 @@ class BookViewSet(APIViewSet):
|
|
|
128
128
|
```
|
|
129
129
|
|
|
130
130
|
Request:
|
|
131
|
+
|
|
131
132
|
```
|
|
132
133
|
GET /book/?published=true&title=python
|
|
133
134
|
```
|
|
@@ -151,9 +152,8 @@ class Article(ModelSerializer):
|
|
|
151
152
|
class ReadSerializer:
|
|
152
153
|
fields = ["id", "title", "tags"]
|
|
153
154
|
|
|
155
|
+
@api.viewset(Article)
|
|
154
156
|
class ArticleViewSet(APIViewSet):
|
|
155
|
-
model = Article
|
|
156
|
-
api = api
|
|
157
157
|
m2m_relations = [
|
|
158
158
|
M2MRelationSchema(
|
|
159
159
|
model=Tag,
|
|
@@ -168,10 +168,10 @@ class ArticleViewSet(APIViewSet):
|
|
|
168
168
|
queryset = queryset.filter(name__icontains=n)
|
|
169
169
|
return queryset
|
|
170
170
|
|
|
171
|
-
ArticleViewSet().add_views_to_route()
|
|
172
171
|
```
|
|
173
172
|
|
|
174
173
|
Endpoints:
|
|
174
|
+
|
|
175
175
|
- `GET /article/{pk}/tag?name=dev`
|
|
176
176
|
- `POST /article/{pk}/tag/` body: `{"add":[1,2],"remove":[3]}`
|
|
177
177
|
|
|
@@ -195,9 +195,8 @@ class JWTAuth(AsyncJwtBearer):
|
|
|
195
195
|
book_id = self.dcd.claims.get("sub")
|
|
196
196
|
return await Book.objects.aget(id=book_id)
|
|
197
197
|
|
|
198
|
+
@api.viewset(Book)
|
|
198
199
|
class SecureBookViewSet(APIViewSet):
|
|
199
|
-
model = Book
|
|
200
|
-
api = api
|
|
201
200
|
auth = [JWTAuth()]
|
|
202
201
|
get_auth = None # list/retrieve public
|
|
203
202
|
```
|
|
@@ -207,6 +206,7 @@ class SecureBookViewSet(APIViewSet):
|
|
|
207
206
|
## 📑 Lifecycle Hooks (ModelSerializer)
|
|
208
207
|
|
|
209
208
|
Available on every save/delete:
|
|
209
|
+
|
|
210
210
|
- `on_create_before_save`
|
|
211
211
|
- `on_create_after_save`
|
|
212
212
|
- `before_save`
|
|
@@ -220,10 +220,21 @@ Available on every save/delete:
|
|
|
220
220
|
## 🧩 Adding Custom Endpoints
|
|
221
221
|
|
|
222
222
|
```python
|
|
223
|
+
from ninja_aio.decorators import api_get
|
|
224
|
+
|
|
225
|
+
@api.viewset(Book)
|
|
223
226
|
class BookViewSet(APIViewSet):
|
|
224
|
-
|
|
225
|
-
|
|
227
|
+
@api_get("/stats/")
|
|
228
|
+
async def stats(self, request):
|
|
229
|
+
total = await Book.objects.acount()
|
|
230
|
+
return {"total": total}
|
|
231
|
+
```
|
|
226
232
|
|
|
233
|
+
Or
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
@api.viewset(Book)
|
|
237
|
+
class BookViewSet(APIViewSet):
|
|
227
238
|
def views(self):
|
|
228
239
|
@self.router.get("/stats/")
|
|
229
240
|
async def stats(request):
|
|
@@ -244,9 +255,8 @@ class LargePagination(PageNumberPagination):
|
|
|
244
255
|
page_size = 50
|
|
245
256
|
max_page_size = 200
|
|
246
257
|
|
|
258
|
+
@api.viewset(Book)
|
|
247
259
|
class BookViewSet(APIViewSet):
|
|
248
|
-
model = Book
|
|
249
|
-
api = api
|
|
250
260
|
pagination_class = LargePagination
|
|
251
261
|
```
|
|
252
262
|
|
|
@@ -255,6 +265,7 @@ class BookViewSet(APIViewSet):
|
|
|
255
265
|
## 🛠 Project Structure & Docs
|
|
256
266
|
|
|
257
267
|
Documentation (MkDocs + Material):
|
|
268
|
+
|
|
258
269
|
```
|
|
259
270
|
docs/
|
|
260
271
|
getting_started/
|
|
@@ -267,17 +278,19 @@ docs/
|
|
|
267
278
|
```
|
|
268
279
|
|
|
269
280
|
Browse full reference:
|
|
281
|
+
|
|
270
282
|
- APIViewSet: `docs/api/views/api_view_set.md`
|
|
271
283
|
- APIView: `docs/api/views/api_view.md`
|
|
272
284
|
- ModelSerializer: `docs/api/models/model_serializer.md`
|
|
273
285
|
- Authentication: `docs/api/authentication.md`
|
|
274
|
-
-
|
|
286
|
+
- Example repository: https://github.com/caspel26/ninja-aio-blog-example
|
|
275
287
|
|
|
276
288
|
---
|
|
277
289
|
|
|
278
290
|
## 🧪 Tests
|
|
279
291
|
|
|
280
292
|
Use Django test runner + async ORM patterns. Example async pattern:
|
|
293
|
+
|
|
281
294
|
```python
|
|
282
295
|
obj = await Book.objects.acreate(title="T1", published=True)
|
|
283
296
|
count = await Book.objects.acount()
|
|
@@ -288,9 +301,8 @@ count = await Book.objects.acount()
|
|
|
288
301
|
## 🚫 Disable Operations
|
|
289
302
|
|
|
290
303
|
```python
|
|
304
|
+
@api.viewset(Book)
|
|
291
305
|
class ReadOnlyBookViewSet(APIViewSet):
|
|
292
|
-
model = Book
|
|
293
|
-
api = api
|
|
294
306
|
disable = ["update", "delete"]
|
|
295
307
|
```
|
|
296
308
|
|
|
@@ -318,6 +330,7 @@ class ReadOnlyBookViewSet(APIViewSet):
|
|
|
318
330
|
## ⭐ Support
|
|
319
331
|
|
|
320
332
|
Star the repo or donate:
|
|
333
|
+
|
|
321
334
|
- [Buy me a coffee](https://buymeacoffee.com/caspel26)
|
|
322
335
|
|
|
323
336
|
---
|
|
@@ -330,11 +343,12 @@ MIT License. See [LICENSE](LICENSE).
|
|
|
330
343
|
|
|
331
344
|
## 🔗 Quick Links
|
|
332
345
|
|
|
333
|
-
| Item
|
|
334
|
-
|
|
335
|
-
| PyPI
|
|
336
|
-
| Docs
|
|
337
|
-
| Issues
|
|
346
|
+
| Item | Link |
|
|
347
|
+
| ------- | -------------------------------------------------------- |
|
|
348
|
+
| PyPI | https://pypi.org/project/django-ninja-aio-crud/ |
|
|
349
|
+
| Docs | https://django-ninja-aio.com |
|
|
350
|
+
| Issues | https://github.com/caspel26/django-ninja-aio-crud/issues |
|
|
351
|
+
| Example | https://github.com/caspel26/ninja-aio-blog-example |
|
|
338
352
|
|
|
339
353
|
---
|
|
340
354
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
ninja_aio/__init__.py,sha256=gBYKYp-SDpKV643SKBBLVlAZrsk3j6ndnV-oOP-Z56U,119
|
|
2
|
+
ninja_aio/api.py,sha256=tuC7vdvn7s1GkCnSFy9Kn1zv0glZfYptRQVvo8ZRtGQ,2429
|
|
3
|
+
ninja_aio/auth.py,sha256=4sWdFPjKiQgUL1d_CSGDblVjnY5ptP6LQha6XXdluJA,9157
|
|
4
|
+
ninja_aio/exceptions.py,sha256=_3xFqfFCOfrrMhSA0xbMqgXy8R0UQjhXaExrFvaDAjY,3891
|
|
5
|
+
ninja_aio/models.py,sha256=aJlo5a64O4o-fB8QESLMUJpoA5kcjRJxPBiAIMxg46k,47652
|
|
6
|
+
ninja_aio/parsers.py,sha256=e_4lGCPV7zs-HTqtdJTc8yQD2KPAn9njbL8nF_Mmgkc,153
|
|
7
|
+
ninja_aio/renders.py,sha256=VtmSliRJyZ6gjyoib8AXMVUBYF1jPNsiceCHujI_mAs,1699
|
|
8
|
+
ninja_aio/types.py,sha256=nFqWEopm7eoEaHRzbi6EyA9WZ5Cneyd602ilFKypeQI,577
|
|
9
|
+
ninja_aio/decorators/__init__.py,sha256=cDDHD_9EI4CP7c5eL1m2mGNl9bR24i8FAkQsT3_RNGM,371
|
|
10
|
+
ninja_aio/decorators/operations.py,sha256=L9yt2ku5oo4CpOLixCADmkcFjLGsWAn-cg-sDcjFhMA,343
|
|
11
|
+
ninja_aio/decorators/views.py,sha256=0RVU4XaM1HvTQ-BOt_NwUtbhwfHau06lh-O8El1LqQk,8139
|
|
12
|
+
ninja_aio/factory/__init__.py,sha256=IdH2z1ZZpv_IqonaDfVo7IsMzkgop6lHqz42RphUYBU,72
|
|
13
|
+
ninja_aio/factory/operations.py,sha256=OgWGqq4WJ4arSQrH9FGAby9kx-HTdS7MOITxHdYMk18,12051
|
|
14
|
+
ninja_aio/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
ninja_aio/helpers/api.py,sha256=YMzuZ4-ZpUrJBQIabE26gb_GYwsH2rVosWRE95YfdPQ,20775
|
|
16
|
+
ninja_aio/helpers/query.py,sha256=YJMdEonCuqx1XjmszCK74mg5hcUPh84ynXrsuoSQdNA,4519
|
|
17
|
+
ninja_aio/schemas/__init__.py,sha256=iLBwHg0pmL9k_UkIui5Q8QIl_gO4fgxSv2JHxDzqnSI,549
|
|
18
|
+
ninja_aio/schemas/api.py,sha256=-VwXhBRhmMsZLIAmWJ-P7tB5klxXS75eukjabeKKYsc,360
|
|
19
|
+
ninja_aio/schemas/generics.py,sha256=frjJsKJMAdM_NdNKv-9ddZNGxYy5PNzjIRGtuycgr-w,112
|
|
20
|
+
ninja_aio/schemas/helpers.py,sha256=W6IeHi5Tmbjh3FXwDYqjqlLBTVj5uTYq3_JVkNUWayo,7355
|
|
21
|
+
ninja_aio/views/__init__.py,sha256=DEzjWA6y3WF0V10nNF8eEurLNEodgxKzyFd09AqVp3s,148
|
|
22
|
+
ninja_aio/views/api.py,sha256=jJ0Awl9ynuvM6rWetgp9KHTKlnwREyjl2cxCobY4I_4,20158
|
|
23
|
+
ninja_aio/views/mixins.py,sha256=Jh6BG8Cs823nurVlODlzCquTxKrLH7Pmo5udPqUGZek,11378
|
|
24
|
+
django_ninja_aio_crud-2.3.1.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
|
|
25
|
+
django_ninja_aio_crud-2.3.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
26
|
+
django_ninja_aio_crud-2.3.1.dist-info/METADATA,sha256=74hdMdbaOSFTfpFXJVgZ_0bydVBGNsO8rnZHOBDTvZg,9047
|
|
27
|
+
django_ninja_aio_crud-2.3.1.dist-info/RECORD,,
|
ninja_aio/__init__.py
CHANGED
|
@@ -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
|
ninja_aio/exceptions.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from functools import partial
|
|
2
|
-
|
|
3
2
|
from joserfc.errors import JoseError
|
|
4
3
|
from ninja import NinjaAPI
|
|
5
4
|
from django.http import HttpRequest, HttpResponse
|
|
@@ -8,6 +7,8 @@ from django.db.models import Model
|
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
class BaseException(Exception):
|
|
10
|
+
"""Base application exception carrying a serializable error payload and status code."""
|
|
11
|
+
|
|
11
12
|
error: str | dict = ""
|
|
12
13
|
status_code: int = 400
|
|
13
14
|
|
|
@@ -17,6 +18,11 @@ class BaseException(Exception):
|
|
|
17
18
|
status_code: int | None = None,
|
|
18
19
|
details: str | None = None,
|
|
19
20
|
) -> None:
|
|
21
|
+
"""Initialize the exception with error content, optional HTTP status, and details.
|
|
22
|
+
|
|
23
|
+
If `error` is a string, it is wrapped into a dict under the `error` key.
|
|
24
|
+
If `error` is a dict, it is used directly. Optional `details` are merged.
|
|
25
|
+
"""
|
|
20
26
|
if isinstance(error, str):
|
|
21
27
|
self.error = {"error": error}
|
|
22
28
|
if isinstance(error, dict):
|
|
@@ -25,22 +31,30 @@ class BaseException(Exception):
|
|
|
25
31
|
self.status_code = status_code or self.status_code
|
|
26
32
|
|
|
27
33
|
def get_error(self):
|
|
34
|
+
"""Return the error body and HTTP status code tuple for response creation."""
|
|
28
35
|
return self.error, self.status_code
|
|
29
36
|
|
|
30
37
|
|
|
31
38
|
class SerializeError(BaseException):
|
|
39
|
+
"""Raised when serialization to or from request/response payloads fails."""
|
|
40
|
+
|
|
32
41
|
pass
|
|
33
42
|
|
|
34
43
|
|
|
35
44
|
class AuthError(BaseException):
|
|
45
|
+
"""Raised when authentication or authorization fails."""
|
|
46
|
+
|
|
36
47
|
pass
|
|
37
48
|
|
|
38
49
|
|
|
39
50
|
class NotFoundError(BaseException):
|
|
51
|
+
"""Raised when a requested model instance cannot be found."""
|
|
52
|
+
|
|
40
53
|
status_code = 404
|
|
41
54
|
error = "not found"
|
|
42
55
|
|
|
43
56
|
def __init__(self, model: Model, details=None):
|
|
57
|
+
"""Build a not-found error referencing the model's verbose name."""
|
|
44
58
|
super().__init__(
|
|
45
59
|
error={model._meta.verbose_name.replace(" ", "_"): self.error},
|
|
46
60
|
status_code=self.status_code,
|
|
@@ -49,19 +63,24 @@ class NotFoundError(BaseException):
|
|
|
49
63
|
|
|
50
64
|
|
|
51
65
|
class PydanticValidationError(BaseException):
|
|
66
|
+
"""Wrapper for pydantic ValidationError to normalize the API error response."""
|
|
67
|
+
|
|
52
68
|
def __init__(self, details=None):
|
|
69
|
+
"""Create a validation error with 400 status and provided details list."""
|
|
53
70
|
super().__init__("Validation Error", 400, details)
|
|
54
71
|
|
|
55
72
|
|
|
56
73
|
def _default_error(
|
|
57
74
|
request: HttpRequest, exc: BaseException, api: type[NinjaAPI]
|
|
58
75
|
) -> HttpResponse:
|
|
76
|
+
"""Default handler: convert BaseException to an API response."""
|
|
59
77
|
return api.create_response(request, exc.error, status=exc.status_code)
|
|
60
78
|
|
|
61
79
|
|
|
62
80
|
def _pydantic_validation_error(
|
|
63
81
|
request: HttpRequest, exc: ValidationError, api: type[NinjaAPI]
|
|
64
82
|
) -> HttpResponse:
|
|
83
|
+
"""Translate a pydantic ValidationError into a normalized API error response."""
|
|
65
84
|
error = PydanticValidationError(exc.errors(include_input=False))
|
|
66
85
|
return api.create_response(request, error.error, status=error.status_code)
|
|
67
86
|
|
|
@@ -69,11 +88,13 @@ def _pydantic_validation_error(
|
|
|
69
88
|
def _jose_error(
|
|
70
89
|
request: HttpRequest, exc: JoseError, api: type[NinjaAPI]
|
|
71
90
|
) -> HttpResponse:
|
|
91
|
+
"""Translate a JOSE library error into an unauthorized API response."""
|
|
72
92
|
error = BaseException(**parse_jose_error(exc), status_code=401)
|
|
73
93
|
return api.create_response(request, error.error, status=error.status_code)
|
|
74
94
|
|
|
75
95
|
|
|
76
96
|
def set_api_exception_handlers(api: type[NinjaAPI]) -> None:
|
|
97
|
+
"""Register exception handlers for common error types on the NinjaAPI instance."""
|
|
77
98
|
api.add_exception_handler(BaseException, partial(_default_error, api=api))
|
|
78
99
|
api.add_exception_handler(JoseError, partial(_jose_error, api=api))
|
|
79
100
|
api.add_exception_handler(
|
|
@@ -82,6 +103,7 @@ def set_api_exception_handlers(api: type[NinjaAPI]) -> None:
|
|
|
82
103
|
|
|
83
104
|
|
|
84
105
|
def parse_jose_error(jose_exc: JoseError) -> dict:
|
|
106
|
+
"""Extract error and optional description from a JoseError into a dict."""
|
|
85
107
|
error_msg = {"error": jose_exc.error}
|
|
86
108
|
return (
|
|
87
109
|
error_msg | {"details": jose_exc.description}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import (
|
|
3
|
+
Callable,
|
|
4
|
+
Dict,
|
|
5
|
+
List,
|
|
6
|
+
Optional,
|
|
7
|
+
Union,
|
|
8
|
+
Any,
|
|
9
|
+
)
|
|
10
|
+
import inspect
|
|
11
|
+
|
|
12
|
+
from ninja.constants import NOT_SET, NOT_SET_TYPE
|
|
13
|
+
from ninja.throttling import BaseThrottle
|
|
14
|
+
from ninja import Router
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ApiMethodFactory:
|
|
18
|
+
"""
|
|
19
|
+
Factory for creating class-bound API method decorators that register endpoints
|
|
20
|
+
on a Ninja Router from instance methods.
|
|
21
|
+
|
|
22
|
+
This class enables defining API handlers as instance methods while ensuring
|
|
23
|
+
the resulting callables exposed to Ninja are free of `self`/`cls` in their
|
|
24
|
+
OpenAPI signatures, preventing them from being interpreted as query params.
|
|
25
|
+
|
|
26
|
+
Typical usage:
|
|
27
|
+
- Use ApiMethodFactory.make("get" | "post" | "put" | "delete" | ...) to produce
|
|
28
|
+
a decorator that can be applied to an instance method on a view class.
|
|
29
|
+
- When the owning instance (e.g., a subclass of ninja_aio.views.api.API) is
|
|
30
|
+
created, the method is lazily registered on its `router` with the provided
|
|
31
|
+
configuration (path, auth, response, tags, etc.).
|
|
32
|
+
|
|
33
|
+
The factory supports both sync and async methods. It wraps the original method
|
|
34
|
+
with a handler whose first argument is `request` (as expected by Ninja),
|
|
35
|
+
internally binding `self` from the instance so you can still write methods
|
|
36
|
+
naturally.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
- method_name: The HTTP method name used to select the corresponding Router
|
|
40
|
+
adder (e.g., "get", "post", etc.).
|
|
41
|
+
|
|
42
|
+
__init__(method_name: str)
|
|
43
|
+
Initialize the factory for a specific HTTP method.
|
|
44
|
+
|
|
45
|
+
Parameters:
|
|
46
|
+
- method_name: The name of the Router method to call (e.g., "get", "post").
|
|
47
|
+
This determines which endpoint registration function is used on the router.
|
|
48
|
+
|
|
49
|
+
_build_handler(view_instance, original)
|
|
50
|
+
Build a callable that Ninja can use as the endpoint handler, correctly
|
|
51
|
+
binding `self` and presenting a `request`-first signature.
|
|
52
|
+
|
|
53
|
+
Behavior:
|
|
54
|
+
- If the original method is async, return an async wrapper that awaits it.
|
|
55
|
+
- If the original method is sync, return a sync wrapper that calls it.
|
|
56
|
+
- The wrapper passes (view_instance, request, *args, **kwargs) to the
|
|
57
|
+
original method, ensuring instance binding while exposing a clean handler
|
|
58
|
+
to Ninja.
|
|
59
|
+
|
|
60
|
+
Parameters:
|
|
61
|
+
- view_instance: The object instance that owns the router and the method.
|
|
62
|
+
- original: The original instance method to be wrapped.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
- A callable suitable for Ninja route registration (sync or async).
|
|
66
|
+
|
|
67
|
+
_apply_metadata(clean_handler, original)
|
|
68
|
+
Copy relevant metadata from the original method to the wrapped handler to
|
|
69
|
+
improve OpenAPI generation and introspection.
|
|
70
|
+
|
|
71
|
+
Behavior:
|
|
72
|
+
- Preserve the function name where possible.
|
|
73
|
+
- Replace the __signature__ to exclude the first parameter if it is
|
|
74
|
+
`self` or `cls`, ensuring Ninja does not treat them as parameters.
|
|
75
|
+
- Copy annotations while removing `self` to avoid unwanted schema entries.
|
|
76
|
+
|
|
77
|
+
Parameters:
|
|
78
|
+
- clean_handler: The wrapped function produced by _build_handler.
|
|
79
|
+
- original: The original method from which metadata will be copied.
|
|
80
|
+
|
|
81
|
+
build_decorator(
|
|
82
|
+
auth=NOT_SET,
|
|
83
|
+
throttle=NOT_SET,
|
|
84
|
+
response=NOT_SET,
|
|
85
|
+
Create and return a decorator that can be applied to an instance method to
|
|
86
|
+
lazily register it as an endpoint when the instance is initialized.
|
|
87
|
+
|
|
88
|
+
How it works:
|
|
89
|
+
- The decorator attaches an `_api_register` callable to the method.
|
|
90
|
+
- When invoked with an API view instance, `_api_register` resolves the
|
|
91
|
+
instance’s `router`, wraps the method via _build_handler, applies metadata
|
|
92
|
+
via _apply_metadata, and registers the handler using the router’s method
|
|
93
|
+
corresponding to `method_name` (e.g., router.get).
|
|
94
|
+
|
|
95
|
+
Parameters mirror Ninja Router endpoint registration and control OpenAPI
|
|
96
|
+
generation and request handling:
|
|
97
|
+
- path: Route path for the endpoint.
|
|
98
|
+
- auth: Authentication configuration or NOT_SET.
|
|
99
|
+
- throttle: Throttle configuration(s) or NOT_SET.
|
|
100
|
+
- response: Response schema/model or NOT_SET.
|
|
101
|
+
- operation_id: Optional OpenAPI operation identifier.
|
|
102
|
+
- summary: Short summary for OpenAPI.
|
|
103
|
+
- description: Detailed description for OpenAPI.
|
|
104
|
+
- tags: Grouping tags for OpenAPI.
|
|
105
|
+
- deprecated: Mark endpoint as deprecated in OpenAPI.
|
|
106
|
+
- by_alias, exclude_unset, exclude_defaults, exclude_none: Pydantic-related
|
|
107
|
+
serialization options for response models.
|
|
108
|
+
- url_name: Optional Django URL name.
|
|
109
|
+
- include_in_schema: Whether to include this endpoint in OpenAPI schema.
|
|
110
|
+
- openapi_extra: Additional raw OpenAPI metadata.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
- A decorator to apply to sync/async instance methods.
|
|
114
|
+
|
|
115
|
+
make(method_name: str)
|
|
116
|
+
Class method that returns a ready-to-use decorator function for the given
|
|
117
|
+
HTTP method, suitable for direct use on instance methods.
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
api_get = ApiMethodFactory.make("get")
|
|
121
|
+
|
|
122
|
+
class MyView(API):
|
|
123
|
+
router = Router()
|
|
124
|
+
|
|
125
|
+
@api_get("/items")
|
|
126
|
+
async def list_items(self, request):
|
|
127
|
+
...
|
|
128
|
+
|
|
129
|
+
Parameters:
|
|
130
|
+
- method_name: The HTTP method name to bind (e.g., "get", "post", "put").
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
- A function that mirrors build_decorator’s signature, named
|
|
134
|
+
"api_{method_name}", with a docstring indicating it registers the
|
|
135
|
+
corresponding HTTP endpoint on the instance router.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def __init__(self, method_name: str):
|
|
139
|
+
self.method_name = method_name
|
|
140
|
+
|
|
141
|
+
def _build_handler(self, view_instance, original):
|
|
142
|
+
is_async = asyncio.iscoroutinefunction(original)
|
|
143
|
+
|
|
144
|
+
if is_async:
|
|
145
|
+
|
|
146
|
+
async def clean_handler(request, *args, **kwargs):
|
|
147
|
+
return await original(view_instance, request, *args, **kwargs)
|
|
148
|
+
else:
|
|
149
|
+
|
|
150
|
+
def clean_handler(request, *args, **kwargs):
|
|
151
|
+
return original(view_instance, request, *args, **kwargs)
|
|
152
|
+
|
|
153
|
+
return clean_handler
|
|
154
|
+
|
|
155
|
+
def _apply_metadata(self, clean_handler, original):
|
|
156
|
+
# name
|
|
157
|
+
try:
|
|
158
|
+
clean_handler.__name__ = getattr(
|
|
159
|
+
original, "__name__", clean_handler.__name__
|
|
160
|
+
)
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
# signature and annotations without self/cls
|
|
165
|
+
try:
|
|
166
|
+
sig = inspect.signature(original)
|
|
167
|
+
params = sig.parameters
|
|
168
|
+
params_list = list(params.values())
|
|
169
|
+
if params_list and params_list[0].name in {"self", "cls"}:
|
|
170
|
+
params_list = params_list[1:]
|
|
171
|
+
clean_handler.__signature__ = sig.replace(parameters=params_list) # type: ignore[attr-defined]
|
|
172
|
+
|
|
173
|
+
anns = dict(getattr(original, "__annotations__", {}))
|
|
174
|
+
anns.pop("self", None)
|
|
175
|
+
clean_handler.__annotations__ = anns
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
def build_decorator(
|
|
180
|
+
self,
|
|
181
|
+
path: str,
|
|
182
|
+
*,
|
|
183
|
+
auth: Any = NOT_SET,
|
|
184
|
+
throttle: Union[BaseThrottle, List[BaseThrottle], NOT_SET_TYPE] = NOT_SET,
|
|
185
|
+
response: Any = NOT_SET,
|
|
186
|
+
operation_id: Optional[str] = None,
|
|
187
|
+
summary: Optional[str] = None,
|
|
188
|
+
description: Optional[str] = None,
|
|
189
|
+
tags: Optional[List[str]] = None,
|
|
190
|
+
deprecated: Optional[bool] = None,
|
|
191
|
+
by_alias: Optional[bool] = None,
|
|
192
|
+
exclude_unset: Optional[bool] = None,
|
|
193
|
+
exclude_defaults: Optional[bool] = None,
|
|
194
|
+
exclude_none: Optional[bool] = None,
|
|
195
|
+
url_name: Optional[str] = None,
|
|
196
|
+
include_in_schema: bool = True,
|
|
197
|
+
openapi_extra: Optional[Dict[str, Any]] = None,
|
|
198
|
+
decorators: Optional[List[Callable]] = None, # es. [paginate(...)]
|
|
199
|
+
):
|
|
200
|
+
"""
|
|
201
|
+
Returns a decorator that can be applied to an async or sync instance method.
|
|
202
|
+
When the instance is created and owns a `router`, the wrapped method is
|
|
203
|
+
registered on that router using the provided configuration.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
def decorator(func):
|
|
207
|
+
from ninja_aio.views.api import API
|
|
208
|
+
|
|
209
|
+
def register_on_instance(view_instance: API):
|
|
210
|
+
router: Router = getattr(view_instance, "router", None)
|
|
211
|
+
if router is None:
|
|
212
|
+
raise RuntimeError("The view instance does not have a router")
|
|
213
|
+
|
|
214
|
+
clean_handler = self._build_handler(view_instance, func)
|
|
215
|
+
self._apply_metadata(clean_handler, func)
|
|
216
|
+
|
|
217
|
+
# Apply additional decorators if any
|
|
218
|
+
if decorators:
|
|
219
|
+
for dec in reversed(decorators):
|
|
220
|
+
clean_handler = dec(clean_handler)
|
|
221
|
+
|
|
222
|
+
route_adder = getattr(router, self.method_name)
|
|
223
|
+
route_adder(
|
|
224
|
+
path=path,
|
|
225
|
+
auth=auth,
|
|
226
|
+
throttle=throttle,
|
|
227
|
+
response=response,
|
|
228
|
+
operation_id=operation_id,
|
|
229
|
+
summary=summary,
|
|
230
|
+
description=description,
|
|
231
|
+
tags=tags,
|
|
232
|
+
deprecated=deprecated,
|
|
233
|
+
by_alias=by_alias,
|
|
234
|
+
exclude_unset=exclude_unset,
|
|
235
|
+
exclude_defaults=exclude_defaults,
|
|
236
|
+
exclude_none=exclude_none,
|
|
237
|
+
url_name=url_name,
|
|
238
|
+
include_in_schema=include_in_schema,
|
|
239
|
+
openapi_extra=openapi_extra,
|
|
240
|
+
)(clean_handler)
|
|
241
|
+
|
|
242
|
+
setattr(func, "_api_register", register_on_instance)
|
|
243
|
+
return func
|
|
244
|
+
|
|
245
|
+
return decorator
|
|
246
|
+
|
|
247
|
+
@classmethod
|
|
248
|
+
def make(cls, method_name: str):
|
|
249
|
+
"""Factory returning a decorator function for the given HTTP method."""
|
|
250
|
+
|
|
251
|
+
def wrapper(
|
|
252
|
+
path: str,
|
|
253
|
+
*,
|
|
254
|
+
auth: Any = NOT_SET,
|
|
255
|
+
throttle: Union[BaseThrottle, List[BaseThrottle], NOT_SET_TYPE] = NOT_SET,
|
|
256
|
+
response: Any = NOT_SET,
|
|
257
|
+
operation_id: Optional[str] = None,
|
|
258
|
+
summary: Optional[str] = None,
|
|
259
|
+
description: Optional[str] = None,
|
|
260
|
+
tags: Optional[List[str]] = None,
|
|
261
|
+
deprecated: Optional[bool] = None,
|
|
262
|
+
by_alias: Optional[bool] = None,
|
|
263
|
+
exclude_unset: Optional[bool] = None,
|
|
264
|
+
exclude_defaults: Optional[bool] = None,
|
|
265
|
+
exclude_none: Optional[bool] = None,
|
|
266
|
+
url_name: Optional[str] = None,
|
|
267
|
+
include_in_schema: bool = True,
|
|
268
|
+
openapi_extra: Optional[Dict[str, Any]] = None,
|
|
269
|
+
decorators: Optional[List[Callable]] = None, # es. [paginate(...)]
|
|
270
|
+
):
|
|
271
|
+
return cls(method_name).build_decorator(
|
|
272
|
+
path,
|
|
273
|
+
auth=auth,
|
|
274
|
+
throttle=throttle,
|
|
275
|
+
response=response,
|
|
276
|
+
operation_id=operation_id,
|
|
277
|
+
summary=summary,
|
|
278
|
+
description=description,
|
|
279
|
+
tags=tags,
|
|
280
|
+
deprecated=deprecated,
|
|
281
|
+
by_alias=by_alias,
|
|
282
|
+
exclude_unset=exclude_unset,
|
|
283
|
+
exclude_defaults=exclude_defaults,
|
|
284
|
+
exclude_none=exclude_none,
|
|
285
|
+
url_name=url_name,
|
|
286
|
+
include_in_schema=include_in_schema,
|
|
287
|
+
openapi_extra=openapi_extra,
|
|
288
|
+
decorators=decorators,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
wrapper.__name__ = f"api_{method_name}"
|
|
292
|
+
wrapper.__doc__ = (
|
|
293
|
+
f"Class method decorator that lazily registers a {method_name.upper()} endpoint on the instance router.\n\n"
|
|
294
|
+
f"Parameters mirror api_get."
|
|
295
|
+
)
|
|
296
|
+
return wrapper
|
ninja_aio/helpers/api.py
CHANGED
|
@@ -337,6 +337,17 @@ class ManyToManyAPI:
|
|
|
337
337
|
remove=remove,
|
|
338
338
|
)
|
|
339
339
|
|
|
340
|
+
def _get_api_path(self, rel_path: str, append_slash: bool = None) -> str:
|
|
341
|
+
append_slash = append_slash if append_slash is not None else True
|
|
342
|
+
path = (
|
|
343
|
+
f"{self.view_set.path_retrieve}{rel_path}/"
|
|
344
|
+
if rel_path.startswith("/")
|
|
345
|
+
else f"{self.view_set.path_retrieve}/{rel_path}/"
|
|
346
|
+
)
|
|
347
|
+
if not append_slash:
|
|
348
|
+
path = path.rstrip("/")
|
|
349
|
+
return path
|
|
350
|
+
|
|
340
351
|
def _register_get_relation_view(
|
|
341
352
|
self,
|
|
342
353
|
*,
|
|
@@ -346,9 +357,10 @@ class ManyToManyAPI:
|
|
|
346
357
|
rel_path: str,
|
|
347
358
|
related_schema,
|
|
348
359
|
filters_schema,
|
|
360
|
+
append_slash: bool,
|
|
349
361
|
):
|
|
350
362
|
@self.router.get(
|
|
351
|
-
|
|
363
|
+
self._get_api_path(rel_path, append_slash=append_slash),
|
|
352
364
|
response={
|
|
353
365
|
200: list[related_schema],
|
|
354
366
|
self.view_set.error_codes: GenericMessageSchema,
|
|
@@ -400,7 +412,7 @@ class ManyToManyAPI:
|
|
|
400
412
|
summary = f"{action} {plural}"
|
|
401
413
|
|
|
402
414
|
@self.router.post(
|
|
403
|
-
|
|
415
|
+
self._get_api_path(rel_path),
|
|
404
416
|
response={
|
|
405
417
|
200: M2MSchemaOut,
|
|
406
418
|
self.view_set.error_codes: GenericMessageSchema,
|
|
@@ -465,6 +477,7 @@ class ManyToManyAPI:
|
|
|
465
477
|
related_schema = relation.related_schema
|
|
466
478
|
m2m_add, m2m_remove, m2m_get = relation.add, relation.remove, relation.get
|
|
467
479
|
filters_schema = self.relations_filters_schemas.get(related_name)
|
|
480
|
+
append_slash = relation.append_slash
|
|
468
481
|
|
|
469
482
|
if m2m_get:
|
|
470
483
|
self._register_get_relation_view(
|
|
@@ -474,6 +487,7 @@ class ManyToManyAPI:
|
|
|
474
487
|
rel_path=rel_path,
|
|
475
488
|
related_schema=related_schema,
|
|
476
489
|
filters_schema=filters_schema,
|
|
490
|
+
append_slash=append_slash,
|
|
477
491
|
)
|
|
478
492
|
|
|
479
493
|
if m2m_add or m2m_remove:
|
ninja_aio/helpers/query.py
CHANGED
|
@@ -8,10 +8,12 @@ from ninja_aio.schemas.helpers import (
|
|
|
8
8
|
|
|
9
9
|
class ScopeNamespace:
|
|
10
10
|
def __init__(self, **scopes):
|
|
11
|
+
"""Create a simple namespace where each provided scope becomes an attribute."""
|
|
11
12
|
for key, value in scopes.items():
|
|
12
13
|
setattr(self, key, value)
|
|
13
14
|
|
|
14
15
|
def __iter__(self):
|
|
16
|
+
"""Iterate over the stored scope values."""
|
|
15
17
|
return iter(self.__dict__.values())
|
|
16
18
|
|
|
17
19
|
|
|
@@ -51,6 +53,7 @@ class QueryUtil:
|
|
|
51
53
|
SCOPES: QueryUtilBaseScopesSchema
|
|
52
54
|
|
|
53
55
|
def __init__(self, model: ModelSerializerMeta):
|
|
56
|
+
"""Initialize QueryUtil, resolving base and extra scope configurations for a model."""
|
|
54
57
|
self.model = model
|
|
55
58
|
self._configuration = getattr(self.model, "QuerySet", None)
|
|
56
59
|
self._extra_configuration: list[ModelQuerySetExtraSchema] = getattr(
|
|
@@ -66,7 +69,9 @@ class QueryUtil:
|
|
|
66
69
|
**{scope: self._get_config(scope) for scope in self._BASE_SCOPES.values()},
|
|
67
70
|
**self.extra_configs,
|
|
68
71
|
}
|
|
69
|
-
self.read_config: ModelQuerySetSchema = self._configs.get(
|
|
72
|
+
self.read_config: ModelQuerySetSchema = self._configs.get(
|
|
73
|
+
self.SCOPES.READ, ModelQuerySetSchema()
|
|
74
|
+
)
|
|
70
75
|
self.queryset_request_config: ModelQuerySetSchema = self._configs.get(
|
|
71
76
|
self.SCOPES.QUERYSET_REQUEST, ModelQuerySetSchema()
|
|
72
77
|
)
|
ninja_aio/schemas/helpers.py
CHANGED
|
@@ -8,24 +8,50 @@ from pydantic import BaseModel, ConfigDict, model_validator
|
|
|
8
8
|
|
|
9
9
|
class M2MRelationSchema(BaseModel):
|
|
10
10
|
"""
|
|
11
|
-
Configuration schema for declaring a Many-to-Many relation in the API.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
11
|
+
Configuration schema for declaring and controlling a Many-to-Many (M2M) relation in the API.
|
|
12
|
+
|
|
13
|
+
This schema is used to describe an M2M relationship between a primary resource and its related
|
|
14
|
+
objects, and to automatically provision CRUD-like endpoints for managing that relation
|
|
15
|
+
(add, remove, and get). It supports both direct Django model classes and model serializers,
|
|
16
|
+
and can optionally expose a custom schema for the related output.
|
|
17
|
+
|
|
18
|
+
model (ModelSerializerMeta | Type[Model]):
|
|
19
|
+
The target related entity, provided either as a ModelSerializer (preferred) or a Django model.
|
|
20
|
+
If a plain model is supplied, you must also provide `related_schema`.
|
|
21
|
+
related_name (str):
|
|
22
|
+
The name of the M2M field on the Django model that links to the related objects.
|
|
23
|
+
add (bool):
|
|
24
|
+
Whether to enable an endpoint for adding related objects. Defaults to True.
|
|
25
|
+
remove (bool):
|
|
26
|
+
Whether to enable an endpoint for removing related objects. Defaults to True.
|
|
27
|
+
get (bool):
|
|
28
|
+
Whether to enable an endpoint for listing/retrieving related objects. Defaults to True.
|
|
29
|
+
path (str | None):
|
|
30
|
+
Optional custom URL path segment for the relation endpoints. If empty or None, a path
|
|
31
|
+
is auto-generated based on `related_name`.
|
|
32
|
+
auth (list | None):
|
|
33
|
+
Optional list of authentication backends to protect the relation endpoints.
|
|
34
|
+
filters (dict[str, tuple] | None):
|
|
35
|
+
Optional mapping of queryable filter fields for the GET endpoint, defined as:
|
|
36
|
+
field_name -> (type, default). Example: {"country": ("str", "")}.
|
|
37
|
+
related_schema (Type[Schema] | None):
|
|
38
|
+
Optional explicit schema to represent related objects in responses.
|
|
39
|
+
If `model` is a ModelSerializerMeta, this is auto-derived via `model.generate_related_s()`.
|
|
40
|
+
If `model` is a plain Django model, this must be provided.
|
|
41
|
+
append_slash (bool):
|
|
42
|
+
Whether to append a trailing slash to the generated GET endpoint path. Defaults to False for backward compatibility.
|
|
43
|
+
|
|
44
|
+
Validation:
|
|
45
|
+
- If `model` is not a ModelSerializerMeta, `related_schema` is required.
|
|
46
|
+
- When `model` is a ModelSerializerMeta and `related_schema` is not provided, it will be
|
|
47
|
+
automatically generated.
|
|
48
|
+
|
|
49
|
+
Usage example:
|
|
50
|
+
filters={"country": ("str", "")},
|
|
51
|
+
auth=[AuthBackend],
|
|
52
|
+
add=True,
|
|
53
|
+
remove=True,
|
|
54
|
+
get=True,
|
|
29
55
|
"""
|
|
30
56
|
|
|
31
57
|
model: ModelSerializerMeta | Type[Model]
|
|
@@ -37,6 +63,7 @@ class M2MRelationSchema(BaseModel):
|
|
|
37
63
|
auth: Optional[list] = None
|
|
38
64
|
filters: Optional[dict[str, tuple]] = None
|
|
39
65
|
related_schema: Optional[Type[Schema]] = None
|
|
66
|
+
append_slash: bool = False
|
|
40
67
|
|
|
41
68
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
42
69
|
|
|
@@ -61,28 +88,81 @@ class ModelQuerySetSchema(BaseModel):
|
|
|
61
88
|
|
|
62
89
|
|
|
63
90
|
class ModelQuerySetExtraSchema(ModelQuerySetSchema):
|
|
91
|
+
"""
|
|
92
|
+
Schema defining extra query parameters for model queryset operations in API endpoints.
|
|
93
|
+
Attributes:
|
|
94
|
+
scope (str): The scope defining the level of access for the queryset operation.
|
|
95
|
+
select_related (Optional[list[str]]): List of related fields for select_related optimization.
|
|
96
|
+
prefetch_related (Optional[list[str]]): List of related fields for prefetch_related optimization
|
|
97
|
+
"""
|
|
64
98
|
scope: str
|
|
65
99
|
|
|
66
100
|
|
|
67
101
|
class ObjectQuerySchema(ModelQuerySetSchema):
|
|
102
|
+
"""
|
|
103
|
+
Schema defining query parameters for single object retrieval in API endpoints.
|
|
104
|
+
Attributes:
|
|
105
|
+
getters (Optional[dict]): A dictionary of getters to apply to the query.
|
|
106
|
+
select_related (Optional[list[str]]): List of related fields for select_related optimization.
|
|
107
|
+
prefetch_related (Optional[list[str]]): List of related fields for prefetch_related optimization
|
|
108
|
+
"""
|
|
68
109
|
getters: Optional[dict] = {}
|
|
69
110
|
|
|
70
111
|
|
|
71
112
|
class ObjectsQuerySchema(ModelQuerySetSchema):
|
|
113
|
+
"""
|
|
114
|
+
Schema defining query parameters for multiple object retrieval in API endpoints.
|
|
115
|
+
Attributes:
|
|
116
|
+
filters (Optional[dict]): A dictionary of filters to apply to the query.
|
|
117
|
+
select_related (Optional[list[str]]): List of related fields for select_related optimization.
|
|
118
|
+
prefetch_related (Optional[list[str]]): List of related fields for prefetch_related optimization
|
|
119
|
+
"""
|
|
72
120
|
filters: Optional[dict] = {}
|
|
73
121
|
|
|
74
122
|
|
|
75
123
|
class QuerySchema(ModelQuerySetSchema):
|
|
124
|
+
"""
|
|
125
|
+
Schema defining query parameters for API endpoints.
|
|
126
|
+
Attributes:
|
|
127
|
+
filters (Optional[dict]): A dictionary of filters to apply to the query.
|
|
128
|
+
getters (Optional[dict]): A dictionary of getters to apply to the query.
|
|
129
|
+
select_related (Optional[list[str]]): List of related fields for select_related optimization.
|
|
130
|
+
prefetch_related (Optional[list[str]]): List of related fields for prefetch_related optimization
|
|
131
|
+
"""
|
|
76
132
|
filters: Optional[dict] = {}
|
|
77
133
|
getters: Optional[dict] = {}
|
|
78
134
|
|
|
79
135
|
|
|
80
136
|
class QueryUtilBaseScopesSchema(BaseModel):
|
|
137
|
+
"""
|
|
138
|
+
Schema defining base scopes for query utilities.
|
|
139
|
+
Attributes:
|
|
140
|
+
READ (str): Scope for read operations.
|
|
141
|
+
QUERYSET_REQUEST (str): Scope for queryset request operations.
|
|
142
|
+
"""
|
|
81
143
|
READ: str = "read"
|
|
82
144
|
QUERYSET_REQUEST: str = "queryset_request"
|
|
83
145
|
|
|
84
146
|
|
|
85
147
|
class DecoratorsSchema(Schema):
|
|
148
|
+
"""
|
|
149
|
+
Schema defining optional decorator lists for CRUD operations.
|
|
150
|
+
|
|
151
|
+
Attributes:
|
|
152
|
+
list (Optional[List]): Decorators applied to the list endpoint.
|
|
153
|
+
retrieve (Optional[List]): Decorators applied to the retrieve endpoint.
|
|
154
|
+
create (Optional[List]): Decorators applied to the create endpoint.
|
|
155
|
+
update (Optional[List]): Decorators applied to the update endpoint.
|
|
156
|
+
delete (Optional[List]): Decorators applied to the delete endpoint.
|
|
157
|
+
|
|
158
|
+
Notes:
|
|
159
|
+
- Each attribute holds an ordered collection of decorators (callables or decorator references)
|
|
160
|
+
to be applied to the corresponding endpoint.
|
|
161
|
+
- Defaults are empty lists, meaning no decorators are applied unless explicitly provided.
|
|
162
|
+
- Using mutable defaults (empty lists) at the class level may lead to shared state between instances.
|
|
163
|
+
Consider initializing these in __init__ or using default_factory (if using pydantic/dataclasses)
|
|
164
|
+
to avoid unintended side effects.
|
|
165
|
+
"""
|
|
86
166
|
list: Optional[List] = []
|
|
87
167
|
retrieve: Optional[List] = []
|
|
88
168
|
create: Optional[List] = []
|
ninja_aio/views/api.py
CHANGED
|
@@ -5,6 +5,7 @@ from ninja.constants import NOT_SET
|
|
|
5
5
|
from ninja.pagination import paginate, AsyncPaginationBase, PageNumberPagination
|
|
6
6
|
from django.http import HttpRequest
|
|
7
7
|
from django.db.models import Model, QuerySet
|
|
8
|
+
from django.conf import settings
|
|
8
9
|
from pydantic import create_model
|
|
9
10
|
|
|
10
11
|
from ninja_aio.schemas.helpers import ModelQuerySetSchema, QuerySchema, DecoratorsSchema
|
|
@@ -27,6 +28,7 @@ class API:
|
|
|
27
28
|
router_tags: list[str] = []
|
|
28
29
|
api_route_path: str = ""
|
|
29
30
|
auth: list | None = NOT_SET
|
|
31
|
+
router: Router = None
|
|
30
32
|
|
|
31
33
|
def views(self):
|
|
32
34
|
"""
|
|
@@ -63,7 +65,10 @@ class API:
|
|
|
63
65
|
pass
|
|
64
66
|
|
|
65
67
|
def _add_views(self):
|
|
66
|
-
|
|
68
|
+
for name in dir(self.__class__):
|
|
69
|
+
method = getattr(self.__class__, name)
|
|
70
|
+
if hasattr(method, "_api_register"):
|
|
71
|
+
method._api_register(self)
|
|
67
72
|
|
|
68
73
|
def add_views_to_route(self):
|
|
69
74
|
return self.api.add_router(f"{self.api_route_path}", self._add_views())
|
|
@@ -74,12 +79,13 @@ class APIView(API):
|
|
|
74
79
|
Base class to register custom, non-CRUD endpoints on a Ninja Router.
|
|
75
80
|
|
|
76
81
|
Usage:
|
|
82
|
+
from ninja_aio.decorations import api_get
|
|
83
|
+
|
|
77
84
|
@api.view(prefix="/custom", tags=["Custom"])
|
|
78
85
|
class CustomAPIView(APIView):
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return SomeSchema(...)
|
|
86
|
+
@api_get("/hello", response=SomeSchema)
|
|
87
|
+
async def hello(request):
|
|
88
|
+
return SomeSchema(...)
|
|
83
89
|
|
|
84
90
|
or
|
|
85
91
|
|
|
@@ -119,6 +125,7 @@ class APIView(API):
|
|
|
119
125
|
self.error_codes = ERROR_CODES
|
|
120
126
|
|
|
121
127
|
def _add_views(self):
|
|
128
|
+
super()._add_views()
|
|
122
129
|
self.views()
|
|
123
130
|
return self.router
|
|
124
131
|
|
|
@@ -255,10 +262,15 @@ class APIViewSet(API):
|
|
|
255
262
|
self.router_tag = self.router_tag or self.model_verbose_name
|
|
256
263
|
self.router_tags = self.router_tags or tags or [self.router_tag]
|
|
257
264
|
self.router = Router(tags=self.router_tags)
|
|
258
|
-
self.
|
|
265
|
+
self.append_slash = getattr(settings, "NINJA_AIO_APPEND_SLASH", True)
|
|
266
|
+
self.path = "/" if self.append_slash else ""
|
|
259
267
|
self.get_path = ""
|
|
260
|
-
self.path_retrieve = f"{{{self.model_util.model_pk_name}}}/"
|
|
261
268
|
self.get_path_retrieve = f"{{{self.model_util.model_pk_name}}}"
|
|
269
|
+
self.path_retrieve = (
|
|
270
|
+
f"{self.get_path_retrieve}/"
|
|
271
|
+
if self.append_slash
|
|
272
|
+
else self.get_path_retrieve
|
|
273
|
+
)
|
|
262
274
|
self.api_route_path = (
|
|
263
275
|
self.api_route_path
|
|
264
276
|
or prefix
|
|
@@ -501,6 +513,7 @@ class APIViewSet(API):
|
|
|
501
513
|
Register CRUD (unless disabled), custom views, and M2M endpoints.
|
|
502
514
|
If 'all' in disable only CRUD is skipped; M2M + custom still added.
|
|
503
515
|
"""
|
|
516
|
+
super()._add_views()
|
|
504
517
|
if "all" in self.disable:
|
|
505
518
|
return self._set_additional_views()
|
|
506
519
|
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
ninja_aio/__init__.py,sha256=Jgj89rpJ3n4pkGfoSYm9NYrwBjGzwhiOeU5c6mr-J8Q,119
|
|
2
|
-
ninja_aio/api.py,sha256=tuC7vdvn7s1GkCnSFy9Kn1zv0glZfYptRQVvo8ZRtGQ,2429
|
|
3
|
-
ninja_aio/auth.py,sha256=4sWdFPjKiQgUL1d_CSGDblVjnY5ptP6LQha6XXdluJA,9157
|
|
4
|
-
ninja_aio/decorators.py,sha256=BHoFIiqdIVMFqSxGh-F6WeZFo1xZK4ieDw3dzKfxZIM,8147
|
|
5
|
-
ninja_aio/exceptions.py,sha256=1-iRbrloIyi0CR6Tcrn5YR4_LloA7PPohKIBaxXJ0-8,2596
|
|
6
|
-
ninja_aio/models.py,sha256=aJlo5a64O4o-fB8QESLMUJpoA5kcjRJxPBiAIMxg46k,47652
|
|
7
|
-
ninja_aio/parsers.py,sha256=e_4lGCPV7zs-HTqtdJTc8yQD2KPAn9njbL8nF_Mmgkc,153
|
|
8
|
-
ninja_aio/renders.py,sha256=VtmSliRJyZ6gjyoib8AXMVUBYF1jPNsiceCHujI_mAs,1699
|
|
9
|
-
ninja_aio/types.py,sha256=nFqWEopm7eoEaHRzbi6EyA9WZ5Cneyd602ilFKypeQI,577
|
|
10
|
-
ninja_aio/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
ninja_aio/helpers/api.py,sha256=BTe7OL-X7YgWYeXmka8TmN4-gA43FVZhtH7q0dRjYX0,20238
|
|
12
|
-
ninja_aio/helpers/query.py,sha256=tE8RjXvSig-WB_0LRQ0LqoE4G_HMHsu0Na5QzTNIm6U,4262
|
|
13
|
-
ninja_aio/schemas/__init__.py,sha256=iLBwHg0pmL9k_UkIui5Q8QIl_gO4fgxSv2JHxDzqnSI,549
|
|
14
|
-
ninja_aio/schemas/api.py,sha256=-VwXhBRhmMsZLIAmWJ-P7tB5klxXS75eukjabeKKYsc,360
|
|
15
|
-
ninja_aio/schemas/generics.py,sha256=frjJsKJMAdM_NdNKv-9ddZNGxYy5PNzjIRGtuycgr-w,112
|
|
16
|
-
ninja_aio/schemas/helpers.py,sha256=rmE0D15lJg95Unv8PU44Hbf0VDTcErMCZZFG3D_znTo,2823
|
|
17
|
-
ninja_aio/views/__init__.py,sha256=DEzjWA6y3WF0V10nNF8eEurLNEodgxKzyFd09AqVp3s,148
|
|
18
|
-
ninja_aio/views/api.py,sha256=O_QQBxk-MltU-bPjxOSu5fFpHaDNP5J3Wk6E5rSBgi4,19744
|
|
19
|
-
ninja_aio/views/mixins.py,sha256=Jh6BG8Cs823nurVlODlzCquTxKrLH7Pmo5udPqUGZek,11378
|
|
20
|
-
django_ninja_aio_crud-2.2.0.dist-info/licenses/LICENSE,sha256=yrDAYcm0gRp_Qyzo3GQa4BjYjWRkAhGC8QRva__RYq0,1073
|
|
21
|
-
django_ninja_aio_crud-2.2.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
22
|
-
django_ninja_aio_crud-2.2.0.dist-info/METADATA,sha256=lJiJIHnNMaKXAM-aOwuKlcAmaJt8oV_wtiDGSFvjjJE,8680
|
|
23
|
-
django_ninja_aio_crud-2.2.0.dist-info/RECORD,,
|
|
File without changes
|
{django_ninja_aio_crud-2.2.0.dist-info → django_ninja_aio_crud-2.3.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|