django-ninja-aio-crud 0.10.2__py3-none-any.whl → 2.4.0__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.
- django_ninja_aio_crud-2.4.0.dist-info/METADATA +382 -0
- django_ninja_aio_crud-2.4.0.dist-info/RECORD +29 -0
- ninja_aio/__init__.py +1 -1
- ninja_aio/api.py +24 -2
- ninja_aio/auth.py +186 -4
- ninja_aio/decorators/__init__.py +23 -0
- ninja_aio/decorators/operations.py +9 -0
- ninja_aio/decorators/views.py +219 -0
- ninja_aio/exceptions.py +36 -1
- ninja_aio/factory/__init__.py +3 -0
- ninja_aio/factory/operations.py +296 -0
- ninja_aio/helpers/__init__.py +0 -0
- ninja_aio/helpers/api.py +506 -0
- ninja_aio/helpers/query.py +108 -0
- ninja_aio/models/__init__.py +4 -0
- ninja_aio/models/serializers.py +738 -0
- ninja_aio/models/utils.py +894 -0
- ninja_aio/renders.py +26 -26
- ninja_aio/schemas/__init__.py +23 -0
- ninja_aio/{schemas.py → schemas/api.py} +0 -5
- ninja_aio/schemas/generics.py +5 -0
- ninja_aio/schemas/helpers.py +170 -0
- ninja_aio/types.py +3 -1
- ninja_aio/views/__init__.py +3 -0
- ninja_aio/views/api.py +582 -0
- ninja_aio/views/mixins.py +275 -0
- django_ninja_aio_crud-0.10.2.dist-info/METADATA +0 -526
- django_ninja_aio_crud-0.10.2.dist-info/RECORD +0 -14
- ninja_aio/models.py +0 -549
- ninja_aio/views.py +0 -522
- {django_ninja_aio_crud-0.10.2.dist-info → django_ninja_aio_crud-2.4.0.dist-info}/WHEEL +0 -0
- {django_ninja_aio_crud-0.10.2.dist-info → django_ninja_aio_crud-2.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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")
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
|
|
3
|
+
from django.db.transaction import Atomic
|
|
4
|
+
from asgiref.sync import sync_to_async
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AsyncAtomicContextManager(Atomic):
|
|
8
|
+
def __init__(self, using=None, savepoint=True, durable=False):
|
|
9
|
+
super().__init__(using, savepoint, durable)
|
|
10
|
+
|
|
11
|
+
async def __aenter__(self):
|
|
12
|
+
await sync_to_async(super().__enter__)()
|
|
13
|
+
return self
|
|
14
|
+
|
|
15
|
+
async def __aexit__(self, exc_type, exc_value, traceback):
|
|
16
|
+
await sync_to_async(super().__exit__)(exc_type, exc_value, traceback)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def aatomic(func):
|
|
20
|
+
"""
|
|
21
|
+
Decorator that executes the wrapped async function inside an asynchronous atomic
|
|
22
|
+
database transaction context.
|
|
23
|
+
|
|
24
|
+
This is useful when you want all ORM write operations performed by the coroutine
|
|
25
|
+
to either fully succeed or fully roll back on error, preserving data integrity.
|
|
26
|
+
|
|
27
|
+
Parameters:
|
|
28
|
+
func (Callable): The asynchronous function to wrap.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Callable: A new async function that, when awaited, runs inside an
|
|
32
|
+
AsyncAtomicContextManager transaction.
|
|
33
|
+
|
|
34
|
+
Behavior:
|
|
35
|
+
- Opens an async atomic transaction before invoking the wrapped coroutine.
|
|
36
|
+
- Commits if the coroutine completes successfully.
|
|
37
|
+
- Rolls back if an exception is raised and propagates the original exception.
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
@aatomic
|
|
41
|
+
async def create_order(user_id: int, items: list[Item]):
|
|
42
|
+
# Perform multiple related DB writes atomically
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
Notes:
|
|
46
|
+
- Ensure AsyncAtomicContextManager is properly implemented to integrate with
|
|
47
|
+
your async ORM / database backend.
|
|
48
|
+
- Only use on async functions.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
@wraps(func)
|
|
52
|
+
async def wrapper(*args, **kwargs):
|
|
53
|
+
async with AsyncAtomicContextManager():
|
|
54
|
+
return await func(*args, **kwargs)
|
|
55
|
+
|
|
56
|
+
return wrapper
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def unique_view(self: object | str, plural: bool = False):
|
|
60
|
+
"""
|
|
61
|
+
Return a decorator that appends a model-specific suffix to a function's __name__ for uniqueness.
|
|
62
|
+
|
|
63
|
+
This is helpful when multiple view functions share a common base name but must be
|
|
64
|
+
distinct (e.g., for route registration, debugging, or introspection) per model context.
|
|
65
|
+
|
|
66
|
+
self : object | str
|
|
67
|
+
- If a string, it is used directly as the suffix.
|
|
68
|
+
- If an object, its `model_util` attribute is inspected for:
|
|
69
|
+
* model_util.model_name (when plural is False)
|
|
70
|
+
* model_util.verbose_name_view_resolver() (when plural is True)
|
|
71
|
+
Missing attributes or call failures result in no suffix being applied.
|
|
72
|
+
plural : bool, default False
|
|
73
|
+
If True and `self` is an object with `model_util.verbose_name_view_resolver`,
|
|
74
|
+
the resolved pluralized verbose name is used; otherwise the singular model name
|
|
75
|
+
(model_util.model_name) is used.
|
|
76
|
+
|
|
77
|
+
Callable[[Callable], Callable]
|
|
78
|
+
A decorator. When applied, it mutates the target function's __name__ in place to:
|
|
79
|
+
"<original_name>_<suffix>" if a suffix is resolved. If no suffix is found, the
|
|
80
|
+
function is returned unchanged.
|
|
81
|
+
|
|
82
|
+
- Does NOT wrap or alter the call signature or async/sync nature of the function.
|
|
83
|
+
- Performs a simple in-place mutation of func.__name__ before returning the original function.
|
|
84
|
+
- No metadata (e.g., __doc__, __qualname__) is altered besides __name__.
|
|
85
|
+
|
|
86
|
+
Suffix Resolution Logic
|
|
87
|
+
-----------------------
|
|
88
|
+
1. If `self` is a str: suffix = self
|
|
89
|
+
2. Else if `self` has `model_util`:
|
|
90
|
+
- plural == True: suffix = model_util.verbose_name_view_resolver()
|
|
91
|
+
- plural == False: suffix = model_util.model_name
|
|
92
|
+
3. If resolution fails or yields a falsy value, no mutation occurs.
|
|
93
|
+
|
|
94
|
+
Side Effects
|
|
95
|
+
------------
|
|
96
|
+
- Modifies function.__name__, which can affect:
|
|
97
|
+
- Debugging output
|
|
98
|
+
- Route registration relying on function names
|
|
99
|
+
- Tools expecting the original name
|
|
100
|
+
- Because mutation is in place, reusing the original function object elsewhere
|
|
101
|
+
may produce unexpected naming.
|
|
102
|
+
|
|
103
|
+
Examples
|
|
104
|
+
# Using a string suffix directly
|
|
105
|
+
@unique_view("book")
|
|
106
|
+
def list_items():
|
|
107
|
+
|
|
108
|
+
# Using an object with model_util.model_name
|
|
109
|
+
@unique_view(viewset_instance) # where viewset_instance.model_util.model_name == "author"
|
|
110
|
+
def retrieve():
|
|
111
|
+
# Resulting function name: "retrieve_author"
|
|
112
|
+
|
|
113
|
+
# Using plural form via verbose_name_view_resolver()
|
|
114
|
+
@unique_view(viewset_instance, plural=True) # e.g., returns "authors"
|
|
115
|
+
def list():
|
|
116
|
+
# Resulting function name: "list_authors"
|
|
117
|
+
|
|
118
|
+
Caveats
|
|
119
|
+
- If the underlying attributes or resolver callable raise exceptions, they are not caught.
|
|
120
|
+
- Ensure that the modified name does not conflict with other functions after decoration.
|
|
121
|
+
- Use cautiously when decorators relying on original __name__ appear earlier in the chain.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def decorator(func):
|
|
125
|
+
# Allow usage as unique_view(self_instance) or unique_view("model_name")
|
|
126
|
+
if isinstance(self, str):
|
|
127
|
+
suffix = self
|
|
128
|
+
else:
|
|
129
|
+
suffix = (
|
|
130
|
+
getattr(
|
|
131
|
+
getattr(self, "model_util", None),
|
|
132
|
+
"verbose_name_view_resolver",
|
|
133
|
+
None,
|
|
134
|
+
)()
|
|
135
|
+
if plural
|
|
136
|
+
else getattr(
|
|
137
|
+
getattr(self, "model_util", None),
|
|
138
|
+
"model_name",
|
|
139
|
+
None,
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
if suffix:
|
|
143
|
+
func.__name__ = f"{func.__name__}_{suffix}"
|
|
144
|
+
return func # Return original function (no wrapper)
|
|
145
|
+
|
|
146
|
+
return decorator
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def decorate_view(*decorators):
|
|
150
|
+
"""
|
|
151
|
+
Compose and apply multiple decorators to a view (sync or async) without adding an extra wrapper.
|
|
152
|
+
|
|
153
|
+
This utility was introduced to support class-based patterns where Django Ninja’s
|
|
154
|
+
built-in `decorate_view` does not fit well. For APIs implemented with vanilla
|
|
155
|
+
Django Ninja (function-based style), you should continue using Django Ninja’s
|
|
156
|
+
native `decorate_view`.
|
|
157
|
+
|
|
158
|
+
Behavior:
|
|
159
|
+
- Applies decorators in the same order as Python’s stacking syntax:
|
|
160
|
+
@d1
|
|
161
|
+
@d2
|
|
162
|
+
is equivalent to: view = d1(d2(view))
|
|
163
|
+
- Supports both synchronous and asynchronous views.
|
|
164
|
+
- Ignores None values, enabling conditional decoration.
|
|
165
|
+
- Does not introduce an additional wrapper; composition depends on each
|
|
166
|
+
decorator for signature/metadata preservation (e.g., using functools.wraps).
|
|
167
|
+
|
|
168
|
+
*decorators: Decorator callables to apply to the target view. Any None values
|
|
169
|
+
are skipped.
|
|
170
|
+
|
|
171
|
+
Callable: A decorator that applies the provided decorators in Python stacking order.
|
|
172
|
+
|
|
173
|
+
Method usage in class-based patterns:
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
*decorators: Decorator callables to apply to the target view. Any None
|
|
177
|
+
values are skipped.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
A decorator that applies the provided decorators in Python stacking order.
|
|
181
|
+
|
|
182
|
+
Examples:
|
|
183
|
+
Basic usage:
|
|
184
|
+
class MyAPIViewSet(APIViewSet):
|
|
185
|
+
api = api
|
|
186
|
+
model = MyModel
|
|
187
|
+
|
|
188
|
+
def views(self):
|
|
189
|
+
@self.router.get('some-endpoint/')
|
|
190
|
+
@decorate_view(authenticate, log_request)
|
|
191
|
+
async def some_view(request):
|
|
192
|
+
...
|
|
193
|
+
|
|
194
|
+
Conditional decoration (skips None):
|
|
195
|
+
class MyAPIViewSet(APIViewSet):
|
|
196
|
+
api = api
|
|
197
|
+
model = MyModel
|
|
198
|
+
cache_dec = cache_page(60) if settings.ENABLE_CACHE else None
|
|
199
|
+
def views(self):
|
|
200
|
+
@self.router.get('data/')
|
|
201
|
+
@decorate_view(self.cache_dec, authenticate)
|
|
202
|
+
async def data_view(request):
|
|
203
|
+
...
|
|
204
|
+
|
|
205
|
+
Notes:
|
|
206
|
+
- Each decorator is applied in the order provided, with the first decorator
|
|
207
|
+
wrapping the result of the second, and so on.
|
|
208
|
+
- Ensure that each decorator is compatible with the view’s sync/async nature.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def _decorator(view):
|
|
212
|
+
wrapped = view
|
|
213
|
+
for dec in reversed(decorators):
|
|
214
|
+
if dec is None:
|
|
215
|
+
continue
|
|
216
|
+
wrapped = dec(wrapped)
|
|
217
|
+
return wrapped
|
|
218
|
+
|
|
219
|
+
return _decorator
|
ninja_aio/exceptions.py
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
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
|
|
6
5
|
from pydantic import ValidationError
|
|
6
|
+
from django.db.models import Model
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class BaseException(Exception):
|
|
10
|
+
"""Base application exception carrying a serializable error payload and status code."""
|
|
11
|
+
|
|
10
12
|
error: str | dict = ""
|
|
11
13
|
status_code: int = 400
|
|
12
14
|
|
|
@@ -16,6 +18,11 @@ class BaseException(Exception):
|
|
|
16
18
|
status_code: int | None = None,
|
|
17
19
|
details: str | None = None,
|
|
18
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
|
+
"""
|
|
19
26
|
if isinstance(error, str):
|
|
20
27
|
self.error = {"error": error}
|
|
21
28
|
if isinstance(error, dict):
|
|
@@ -24,31 +31,56 @@ class BaseException(Exception):
|
|
|
24
31
|
self.status_code = status_code or self.status_code
|
|
25
32
|
|
|
26
33
|
def get_error(self):
|
|
34
|
+
"""Return the error body and HTTP status code tuple for response creation."""
|
|
27
35
|
return self.error, self.status_code
|
|
28
36
|
|
|
29
37
|
|
|
30
38
|
class SerializeError(BaseException):
|
|
39
|
+
"""Raised when serialization to or from request/response payloads fails."""
|
|
40
|
+
|
|
31
41
|
pass
|
|
32
42
|
|
|
33
43
|
|
|
34
44
|
class AuthError(BaseException):
|
|
45
|
+
"""Raised when authentication or authorization fails."""
|
|
46
|
+
|
|
35
47
|
pass
|
|
36
48
|
|
|
37
49
|
|
|
50
|
+
class NotFoundError(BaseException):
|
|
51
|
+
"""Raised when a requested model instance cannot be found."""
|
|
52
|
+
|
|
53
|
+
status_code = 404
|
|
54
|
+
error = "not found"
|
|
55
|
+
|
|
56
|
+
def __init__(self, model: Model, details=None):
|
|
57
|
+
"""Build a not-found error referencing the model's verbose name."""
|
|
58
|
+
super().__init__(
|
|
59
|
+
error={model._meta.verbose_name.replace(" ", "_"): self.error},
|
|
60
|
+
status_code=self.status_code,
|
|
61
|
+
details=details,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
38
65
|
class PydanticValidationError(BaseException):
|
|
66
|
+
"""Wrapper for pydantic ValidationError to normalize the API error response."""
|
|
67
|
+
|
|
39
68
|
def __init__(self, details=None):
|
|
69
|
+
"""Create a validation error with 400 status and provided details list."""
|
|
40
70
|
super().__init__("Validation Error", 400, details)
|
|
41
71
|
|
|
42
72
|
|
|
43
73
|
def _default_error(
|
|
44
74
|
request: HttpRequest, exc: BaseException, api: type[NinjaAPI]
|
|
45
75
|
) -> HttpResponse:
|
|
76
|
+
"""Default handler: convert BaseException to an API response."""
|
|
46
77
|
return api.create_response(request, exc.error, status=exc.status_code)
|
|
47
78
|
|
|
48
79
|
|
|
49
80
|
def _pydantic_validation_error(
|
|
50
81
|
request: HttpRequest, exc: ValidationError, api: type[NinjaAPI]
|
|
51
82
|
) -> HttpResponse:
|
|
83
|
+
"""Translate a pydantic ValidationError into a normalized API error response."""
|
|
52
84
|
error = PydanticValidationError(exc.errors(include_input=False))
|
|
53
85
|
return api.create_response(request, error.error, status=error.status_code)
|
|
54
86
|
|
|
@@ -56,11 +88,13 @@ def _pydantic_validation_error(
|
|
|
56
88
|
def _jose_error(
|
|
57
89
|
request: HttpRequest, exc: JoseError, api: type[NinjaAPI]
|
|
58
90
|
) -> HttpResponse:
|
|
91
|
+
"""Translate a JOSE library error into an unauthorized API response."""
|
|
59
92
|
error = BaseException(**parse_jose_error(exc), status_code=401)
|
|
60
93
|
return api.create_response(request, error.error, status=error.status_code)
|
|
61
94
|
|
|
62
95
|
|
|
63
96
|
def set_api_exception_handlers(api: type[NinjaAPI]) -> None:
|
|
97
|
+
"""Register exception handlers for common error types on the NinjaAPI instance."""
|
|
64
98
|
api.add_exception_handler(BaseException, partial(_default_error, api=api))
|
|
65
99
|
api.add_exception_handler(JoseError, partial(_jose_error, api=api))
|
|
66
100
|
api.add_exception_handler(
|
|
@@ -69,6 +103,7 @@ def set_api_exception_handlers(api: type[NinjaAPI]) -> None:
|
|
|
69
103
|
|
|
70
104
|
|
|
71
105
|
def parse_jose_error(jose_exc: JoseError) -> dict:
|
|
106
|
+
"""Extract error and optional description from a JoseError into a dict."""
|
|
72
107
|
error_msg = {"error": jose_exc.error}
|
|
73
108
|
return (
|
|
74
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
|
|
File without changes
|