Skip to content

Commit 9651e8a

Browse files
authored
Merge pull request #229 from eadwinCode/route_context_refactor
fix: RouteContext Refactor
2 parents f0eb068 + c932020 commit 9651e8a

File tree

7 files changed

+381
-106
lines changed

7 files changed

+381
-106
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Django Ninja Extra is a powerful extension for [Django Ninja](https://django-nin
1818
- 📝 **Type Safety**: Comprehensive type hints for better development experience
1919
- 🎯 **Django Integration**: Seamless integration with Django's ecosystem
2020
- 📚 **OpenAPI Support**: Automatic API documentation with Swagger/ReDoc
21+
- 🔒 **API Throttling**: Rate limiting for your API
2122

2223
### Extra Features
2324
- 🏗️ **Class-Based Controllers**:

docs/api_controller/api_controller_permission.md

Lines changed: 248 additions & 78 deletions
Large diffs are not rendered by default.

docs/index.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88

99
# Django Ninja Extra
1010

11-
## Overview
12-
1311
Django Ninja Extra is a powerful extension for [Django Ninja](https://django-ninja.rest-framework.com) that enhances your Django REST API development experience. It introduces class-based views and advanced features while maintaining the high performance and simplicity of Django Ninja. Whether you're building a small API or a large-scale application, Django Ninja Extra provides the tools you need for clean, maintainable, and efficient API development.
1412

1513
## Features
@@ -20,6 +18,7 @@ Django Ninja Extra is a powerful extension for [Django Ninja](https://django-nin
2018
- 📝 **Type Safety**: Comprehensive type hints for better development experience
2119
- 🎯 **Django Integration**: Seamless integration with Django's ecosystem
2220
- 📚 **OpenAPI Support**: Automatic API documentation with Swagger/ReDoc
21+
- 🔒 **API Throttling**: Rate limiting for your API
2322

2423
### Extra Features
2524
- 🏗️ **Class-Based Controllers**:
@@ -43,6 +42,7 @@ Django Ninja Extra is a powerful extension for [Django Ninja](https://django-nin
4342
- Reusable components
4443

4544
## Requirements
45+
4646
- Python >= 3.6
4747
- Django >= 2.1
4848
- Pydantic >= 1.6
@@ -161,7 +161,7 @@ class UserController:
161161

162162
Access your API's interactive documentation at `/api/docs`:
163163

164-
![Swagger UI](docs/images/ui_swagger_preview_readme.gif)
164+
![Swagger UI](/images/ui_swagger_preview_readme.gif)
165165

166166
## Learning Resources
167167

ninja_extra/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Django Ninja Extra - Class Based Utility and more for Django Ninja(Fast Django REST framework)"""
22

3-
__version__ = "0.22.0"
3+
__version__ = "0.22.2"
44

55
import django
66

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,115 @@
1-
from typing import Any, List, Optional, Union
1+
from typing import TYPE_CHECKING, Any, List, Optional, Union
22

3+
import pydantic
4+
from django.core.exceptions import ImproperlyConfigured
35
from django.http import HttpResponse
46
from django.http.request import HttpRequest
7+
from ninja.errors import ValidationError
58
from ninja.types import DictStrAny
6-
from pydantic import BaseModel as PydanticModel
7-
from pydantic import Field
89

10+
from ninja_extra.details import ViewSignature
911
from ninja_extra.types import PermissionType
1012

13+
if TYPE_CHECKING:
14+
from ninja_extra.main import NinjaExtraAPI
1115

12-
class RouteContext(PydanticModel):
16+
17+
class RouteContext:
1318
"""
1419
APIController Context which will be available to the class instance when handling request
1520
"""
1621

17-
class Config:
18-
arbitrary_types_allowed = True
22+
__slots__ = [
23+
"permission_classes",
24+
"request",
25+
"response",
26+
"args",
27+
"kwargs",
28+
"_api",
29+
"_view_signature",
30+
"_has_computed_route_parameters",
31+
]
32+
33+
permission_classes: PermissionType
34+
request: Union[Any, HttpRequest, None]
35+
response: Union[Any, HttpResponse, None]
36+
args: List[Any]
37+
kwargs: DictStrAny
38+
39+
def __init__(
40+
self,
41+
request: HttpRequest,
42+
args: Optional[List[Any]] = None,
43+
permission_classes: Optional[PermissionType] = None,
44+
kwargs: Optional[DictStrAny] = None,
45+
response: Optional[HttpResponse] = None,
46+
api: Optional["NinjaExtraAPI"] = None,
47+
view_signature: Optional[ViewSignature] = None,
48+
):
49+
self.request = request
50+
self.response = response
51+
self.args: List[Any] = args or []
52+
self.kwargs: DictStrAny = kwargs or {}
53+
self.permission_classes: PermissionType = permission_classes or []
54+
self._api = api
55+
self._view_signature = view_signature
56+
self._has_computed_route_parameters = False
57+
58+
@property
59+
def has_computed_route_parameters(self) -> bool:
60+
return self._has_computed_route_parameters
61+
62+
def compute_route_parameters(
63+
self,
64+
) -> None:
65+
if self._view_signature is None or self._api is None:
66+
raise ImproperlyConfigured(
67+
"view_signature and api are required. "
68+
"Or you are taking an approach that is not supported "
69+
"RouteContext to compute route parameters."
70+
)
71+
72+
if self._has_computed_route_parameters:
73+
return
74+
75+
values, errors = {}, []
76+
for model in self._view_signature.models:
77+
try:
78+
data = model.resolve(self.request, self._api, self.kwargs)
79+
values.update(data)
80+
except pydantic.ValidationError as e:
81+
items = []
82+
for i in e.errors(include_url=False):
83+
i["loc"] = (
84+
model.__ninja_param_source__,
85+
) + model.__ninja_flatten_map_reverse__.get(i["loc"], i["loc"])
86+
# removing pydantic hints
87+
del i["input"] # type: ignore
88+
if (
89+
"ctx" in i
90+
and "error" in i["ctx"]
91+
and isinstance(i["ctx"]["error"], Exception)
92+
):
93+
i["ctx"]["error"] = str(i["ctx"]["error"])
94+
items.append(dict(i))
95+
errors.extend(items)
96+
97+
if errors:
98+
raise ValidationError(errors)
99+
100+
if self._view_signature.response_arg:
101+
values[self._view_signature.response_arg] = self.response
19102

20-
permission_classes: PermissionType = Field([])
21-
request: Union[Any, HttpRequest, None] = None
22-
response: Union[Any, HttpResponse, None] = None
23-
args: List[Any] = Field([])
24-
kwargs: DictStrAny = Field({})
103+
self.kwargs.update(values)
104+
self._has_computed_route_parameters = True
25105

26106

27107
def get_route_execution_context(
28108
request: HttpRequest,
29109
temporal_response: Optional[HttpResponse] = None,
30110
permission_classes: Optional[PermissionType] = None,
111+
api: Optional["NinjaExtraAPI"] = None,
112+
view_signature: Optional[ViewSignature] = None,
31113
*args: Any,
32114
**kwargs: Any,
33115
) -> RouteContext:
@@ -39,6 +121,8 @@ def get_route_execution_context(
39121
"kwargs": kwargs,
40122
"response": temporal_response,
41123
"args": args,
124+
"api": api,
125+
"view_signature": view_signature,
42126
}
43127
context = RouteContext(**init_kwargs) # type:ignore[arg-type]
44128
return context

ninja_extra/operation.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,12 @@ def _prep_run(
193193

194194
def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase:
195195
try:
196-
temporal_response = self.api.create_temporal_response(request)
197196
with self._prep_run(
198-
request, temporal_response=temporal_response, **kw
197+
request,
198+
temporal_response=self.api.create_temporal_response(request),
199+
api=self.api,
200+
view_signature=self.signature,
201+
**kw,
199202
) as ctx:
200203
error = self._run_checks(request)
201204
if error:
@@ -205,12 +208,15 @@ def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase:
205208
if route_function:
206209
route_function.run_permission_check(ctx)
207210

208-
values = self._get_values(request, kw, temporal_response)
209-
ctx.kwargs.update(values)
210-
result = self.view_func(request, **values)
211+
if not ctx.has_computed_route_parameters:
212+
ctx.compute_route_parameters()
213+
214+
result = self.view_func(request, **ctx.kwargs)
215+
assert ctx.response is not None
211216
_processed_results = self._result_to_response(
212-
request, result, temporal_response
217+
request, result, ctx.response
213218
)
219+
214220
return _processed_results
215221
except Exception as e:
216222
if isinstance(e, TypeError) and "required positional argument" in str(
@@ -321,9 +327,12 @@ async def _prep_run( # type:ignore
321327

322328
async def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase: # type: ignore
323329
try:
324-
temporal_response = self.api.create_temporal_response(request)
325330
async with self._prep_run(
326-
request, temporal_response=temporal_response, **kw
331+
request,
332+
temporal_response=self.api.create_temporal_response(request),
333+
api=self.api,
334+
view_signature=self.signature,
335+
**kw,
327336
) as ctx:
328337
error = await self._run_checks(request)
329338
if error:
@@ -333,12 +342,15 @@ async def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase: # typ
333342
if route_function:
334343
await route_function.async_run_check_permissions(ctx) # type: ignore[attr-defined]
335344

336-
values = await self._get_values(request, kw, temporal_response) # type: ignore
337-
ctx.kwargs.update(values)
338-
result = await self.view_func(request, **values)
345+
if not ctx.has_computed_route_parameters:
346+
ctx.compute_route_parameters()
347+
348+
result = await self.view_func(request, **ctx.kwargs)
349+
assert ctx.response is not None
339350
_processed_results = await self._result_to_response(
340-
request, result, temporal_response
351+
request, result, ctx.response
341352
)
353+
342354
return cast(HttpResponseBase, _processed_results)
343355
except Exception as e:
344356
return self.api.on_exception(request, e)

tests/test_controller.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,15 @@ def test_controller_base_get_object_or_exception_works(self):
165165
group_instance = Group.objects.create(name="_groupowner")
166166

167167
controller_object = SomeController()
168-
context = RouteContext(request=Mock(), permission_classes=[AllowAny])
168+
context = RouteContext(
169+
request=Mock(),
170+
permission_classes=[AllowAny],
171+
response=None,
172+
args=[],
173+
kwargs={},
174+
api=None,
175+
view_signature=None,
176+
)
169177
controller_object.context = context
170178
with patch.object(
171179
AllowAny, "has_object_permission", return_value=True

0 commit comments

Comments
 (0)