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.
@@ -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,3 @@
1
+ from .operations import ApiMethodFactory
2
+
3
+ __all__ = ["ApiMethodFactory"]
@@ -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