Skip to content

Commit

Permalink
Merge pull request #229 from eadwinCode/route_context_refactor
Browse files Browse the repository at this point in the history
fix: RouteContext Refactor
  • Loading branch information
eadwinCode authored Jan 13, 2025
2 parents f0eb068 + c932020 commit 9651e8a
Show file tree
Hide file tree
Showing 7 changed files with 381 additions and 106 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Django Ninja Extra is a powerful extension for [Django Ninja](https://django-nin
- 📝 **Type Safety**: Comprehensive type hints for better development experience
- 🎯 **Django Integration**: Seamless integration with Django's ecosystem
- 📚 **OpenAPI Support**: Automatic API documentation with Swagger/ReDoc
- 🔒 **API Throttling**: Rate limiting for your API

### Extra Features
- 🏗️ **Class-Based Controllers**:
Expand Down
326 changes: 248 additions & 78 deletions docs/api_controller/api_controller_permission.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@

# Django Ninja Extra

## Overview

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.

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

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

## Requirements

- Python >= 3.6
- Django >= 2.1
- Pydantic >= 1.6
Expand Down Expand Up @@ -161,7 +161,7 @@ class UserController:

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

![Swagger UI](docs/images/ui_swagger_preview_readme.gif)
![Swagger UI](/images/ui_swagger_preview_readme.gif)

## Learning Resources

Expand Down
2 changes: 1 addition & 1 deletion ninja_extra/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Django Ninja Extra - Class Based Utility and more for Django Ninja(Fast Django REST framework)"""

__version__ = "0.22.0"
__version__ = "0.22.2"

import django

Expand Down
106 changes: 95 additions & 11 deletions ninja_extra/controllers/route/context.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,115 @@
from typing import Any, List, Optional, Union
from typing import TYPE_CHECKING, Any, List, Optional, Union

import pydantic
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponse
from django.http.request import HttpRequest
from ninja.errors import ValidationError
from ninja.types import DictStrAny
from pydantic import BaseModel as PydanticModel
from pydantic import Field

from ninja_extra.details import ViewSignature
from ninja_extra.types import PermissionType

if TYPE_CHECKING:
from ninja_extra.main import NinjaExtraAPI

class RouteContext(PydanticModel):

class RouteContext:
"""
APIController Context which will be available to the class instance when handling request
"""

class Config:
arbitrary_types_allowed = True
__slots__ = [
"permission_classes",
"request",
"response",
"args",
"kwargs",
"_api",
"_view_signature",
"_has_computed_route_parameters",
]

permission_classes: PermissionType
request: Union[Any, HttpRequest, None]
response: Union[Any, HttpResponse, None]
args: List[Any]
kwargs: DictStrAny

def __init__(
self,
request: HttpRequest,
args: Optional[List[Any]] = None,
permission_classes: Optional[PermissionType] = None,
kwargs: Optional[DictStrAny] = None,
response: Optional[HttpResponse] = None,
api: Optional["NinjaExtraAPI"] = None,
view_signature: Optional[ViewSignature] = None,
):
self.request = request
self.response = response
self.args: List[Any] = args or []
self.kwargs: DictStrAny = kwargs or {}
self.permission_classes: PermissionType = permission_classes or []
self._api = api
self._view_signature = view_signature
self._has_computed_route_parameters = False

@property
def has_computed_route_parameters(self) -> bool:
return self._has_computed_route_parameters

def compute_route_parameters(
self,
) -> None:
if self._view_signature is None or self._api is None:
raise ImproperlyConfigured(
"view_signature and api are required. "
"Or you are taking an approach that is not supported "
"RouteContext to compute route parameters."
)

if self._has_computed_route_parameters:
return

values, errors = {}, []
for model in self._view_signature.models:
try:
data = model.resolve(self.request, self._api, self.kwargs)
values.update(data)
except pydantic.ValidationError as e:
items = []
for i in e.errors(include_url=False):
i["loc"] = (
model.__ninja_param_source__,
) + model.__ninja_flatten_map_reverse__.get(i["loc"], i["loc"])
# removing pydantic hints
del i["input"] # type: ignore
if (
"ctx" in i
and "error" in i["ctx"]
and isinstance(i["ctx"]["error"], Exception)
):
i["ctx"]["error"] = str(i["ctx"]["error"])
items.append(dict(i))
errors.extend(items)

if errors:
raise ValidationError(errors)

if self._view_signature.response_arg:
values[self._view_signature.response_arg] = self.response

permission_classes: PermissionType = Field([])
request: Union[Any, HttpRequest, None] = None
response: Union[Any, HttpResponse, None] = None
args: List[Any] = Field([])
kwargs: DictStrAny = Field({})
self.kwargs.update(values)
self._has_computed_route_parameters = True


def get_route_execution_context(
request: HttpRequest,
temporal_response: Optional[HttpResponse] = None,
permission_classes: Optional[PermissionType] = None,
api: Optional["NinjaExtraAPI"] = None,
view_signature: Optional[ViewSignature] = None,
*args: Any,
**kwargs: Any,
) -> RouteContext:
Expand All @@ -39,6 +121,8 @@ def get_route_execution_context(
"kwargs": kwargs,
"response": temporal_response,
"args": args,
"api": api,
"view_signature": view_signature,
}
context = RouteContext(**init_kwargs) # type:ignore[arg-type]
return context
36 changes: 24 additions & 12 deletions ninja_extra/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,12 @@ def _prep_run(

def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase:
try:
temporal_response = self.api.create_temporal_response(request)
with self._prep_run(
request, temporal_response=temporal_response, **kw
request,
temporal_response=self.api.create_temporal_response(request),
api=self.api,
view_signature=self.signature,
**kw,
) as ctx:
error = self._run_checks(request)
if error:
Expand All @@ -205,12 +208,15 @@ def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase:
if route_function:
route_function.run_permission_check(ctx)

values = self._get_values(request, kw, temporal_response)
ctx.kwargs.update(values)
result = self.view_func(request, **values)
if not ctx.has_computed_route_parameters:
ctx.compute_route_parameters()

result = self.view_func(request, **ctx.kwargs)
assert ctx.response is not None
_processed_results = self._result_to_response(
request, result, temporal_response
request, result, ctx.response
)

return _processed_results
except Exception as e:
if isinstance(e, TypeError) and "required positional argument" in str(
Expand Down Expand Up @@ -321,9 +327,12 @@ async def _prep_run( # type:ignore

async def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase: # type: ignore
try:
temporal_response = self.api.create_temporal_response(request)
async with self._prep_run(
request, temporal_response=temporal_response, **kw
request,
temporal_response=self.api.create_temporal_response(request),
api=self.api,
view_signature=self.signature,
**kw,
) as ctx:
error = await self._run_checks(request)
if error:
Expand All @@ -333,12 +342,15 @@ async def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase: # typ
if route_function:
await route_function.async_run_check_permissions(ctx) # type: ignore[attr-defined]

values = await self._get_values(request, kw, temporal_response) # type: ignore
ctx.kwargs.update(values)
result = await self.view_func(request, **values)
if not ctx.has_computed_route_parameters:
ctx.compute_route_parameters()

result = await self.view_func(request, **ctx.kwargs)
assert ctx.response is not None
_processed_results = await self._result_to_response(
request, result, temporal_response
request, result, ctx.response
)

return cast(HttpResponseBase, _processed_results)
except Exception as e:
return self.api.on_exception(request, e)
Expand Down
10 changes: 9 additions & 1 deletion tests/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,15 @@ def test_controller_base_get_object_or_exception_works(self):
group_instance = Group.objects.create(name="_groupowner")

controller_object = SomeController()
context = RouteContext(request=Mock(), permission_classes=[AllowAny])
context = RouteContext(
request=Mock(),
permission_classes=[AllowAny],
response=None,
args=[],
kwargs={},
api=None,
view_signature=None,
)
controller_object.context = context
with patch.object(
AllowAny, "has_object_permission", return_value=True
Expand Down

0 comments on commit 9651e8a

Please sign in to comment.