Skip to content

Commit

Permalink
Merge pull request #13 from alaouimehdi1995/integrating-deserializer-…
Browse files Browse the repository at this point in the history
…in-main-library-decorator

Improving library's core decorator `@api_view`, by:

* Supporting `deserializer_class` parameter.
* Supporting deserializer class mapping (custom deserializers for each http method).
* Adding `allow_forms` parameter to accept forms data or raise error.
* Supporting for `@api_view` syntax (only `@api_view()` was supported before)
* Checking view's type to make sure it's either a function or a `django.views.View` class
* Normalizing request's payload between content_types, so that the output's always in the same format.
* Extracting query params and passing custom parameters for the view (query params, payload, url params, etc.).
* Decorating only class view's relevant methods (http methods) instead of all methods.
* Moving helpers function into separate file.
* Type hinting to improve code's readability.
* Assuring support for all python's versions (2.7+, 3+), and django's versions (1.11+).
  • Loading branch information
alaouimehdi1995 authored Apr 22, 2020
2 parents 269c471 + c0ba940 commit b4bd36d
Show file tree
Hide file tree
Showing 13 changed files with 933 additions and 439 deletions.
74 changes: 0 additions & 74 deletions django_rest/decorators.py

This file was deleted.

72 changes: 72 additions & 0 deletions django_rest/decorators/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-

import inspect


from django.views import View

from django_rest.deserializers import (
Deserializer,
PerforatedDeserializer,
)
from django_rest.http.methods import ALL_HTTP_METHODS
from django_rest.decorators.utils import (
build_class_wrapper,
build_deserializer_map,
build_function_wrapper,
)
from django_rest.permissions import AllowAny, BasePermission


FORMS_CONTENT_TYPES = (
"application/x-www-form-urlencoded",
"multipart/form-data",
)


def api_view(
permission_class=AllowAny, # type: Union[AbstractPermission, Any]
allowed_methods=ALL_HTTP_METHODS, # type: Tuple[str]
deserializer_class=PerforatedDeserializer, # type: Union[ClassVar[Deserializer], Dict[str, ClassVar[Deserializer]]]
allow_forms=False, # type: bool
): # type:(...) -> Callable

if not (
inspect.isclass(permission_class)
and issubclass(permission_class, BasePermission)
):
# In case the decorator's called without parenthesis (`@api_view`),
# the `permission_class` variable holds the real view.
return api_view()(permission_class)

deserializers_http_methods_map = build_deserializer_map(deserializer_class)

def view_decorator(view):
# type:(Union[Callable, ClassVar]) -> Union[Callable, ClassVar]
if inspect.isfunction(view):
return build_function_wrapper(
permission_class,
allowed_methods,
deserializers_http_methods_map,
allow_forms,
view,
)
elif inspect.isclass(view) and issubclass(view, View):
return build_class_wrapper(
permission_class,
allowed_methods,
deserializers_http_methods_map,
allow_forms,
view,
)
given_str = (
"{} class".format(view.__name__)
if inspect.isclass(view)
else "{} object".format(view.__class__.__name__)
)
raise TypeError(
"The `@api_view` decorator is applied to either a function or a "
"class inheriting from django.views.View. Given: {}".format(given_str)
)

return view_decorator
174 changes: 174 additions & 0 deletions django_rest/decorators/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# -*- coding: utf-8 -*-

import inspect
import json
from functools import wraps


from django.http import JsonResponse
from django.views import View

from django_rest.deserializers import (
Deserializer,
PerforatedDeserializer,
)
from django_rest.http.exceptions import (
BadRequest,
BaseAPIException,
InternalServerError,
MethodNotAllowed,
PermissionDenied,
)
from django_rest.http.methods import ALL_HTTP_METHODS, HTTP_METHODS_SUPPORTING_PAYLOAD
from django_rest.permissions import BasePermission

FORMS_CONTENT_TYPES = (
"application/x-www-form-urlencoded",
"multipart/form-data",
)


def build_deserializer_map(deserializer_class):
if isinstance(deserializer_class, dict):
if not all(
issubclass(elem, Deserializer) for elem in deserializer_class.values()
):
raise TypeError(
"The values of the `deserializer_class` mapping "
"should be subclass of `Deserializer`."
)
return {
http_method: deserializer_class.get(http_method, PerforatedDeserializer)
for http_method in HTTP_METHODS_SUPPORTING_PAYLOAD
}
if issubclass(deserializer_class, Deserializer):
return {
http_method: deserializer_class
for http_method in HTTP_METHODS_SUPPORTING_PAYLOAD
}

raise TypeError(
"`deserializer_class` parameter should be either a subclass of `Deserializer` "
"or a dict of `Deserializer` classes, not {}".format(
deserializer_class.__name__
)
)


def transform_query_dict_into_regular_dict(query_dict):
return {
key: list_val if len(list_val) > 1 else list_val[0]
for key, list_val in dict(query_dict).items()
}


def extract_request_payload(request, allow_form_data=False):
"""
Return the request's content (form or data).
For `POST`, `PUT` and `PATCH` requests:
- If the form data is allowed and the request is a form, returns a `dict`
- If the form data isn't allowed and the request is a form, raises
PermissionDenied exception.
- For application/json requests, returns a `dict` containing the request's
data
For other HTTP methods:
- Returns `None`
"""
method = request.method
if not allow_form_data and request.content_type in FORMS_CONTENT_TYPES:
raise PermissionDenied # OR bad request ?
elif request.content_type in FORMS_CONTENT_TYPES and request.method == "POST":
return transform_query_dict_into_regular_dict(request.POST)

if method not in HTTP_METHODS_SUPPORTING_PAYLOAD:
return None

return json.loads(request.body.decode())


def build_function_wrapper(
permission_class, # type: BasePermission
allowed_methods, # type: Iterable[str]
deserializers_http_methods_map, # type: Dict[str, ClassVar[Deserializer]]
allow_forms, # type: bool
view_function, # Callable
): # type:(...) -> Callable
@wraps(view_function)
def function_wrapper(request, *args, **kwargs):
# type:(HttpRequest, List[Any]) -> JsonResponse
try:
if request.method not in allowed_methods:
raise MethodNotAllowed
if not permission_class().has_permission(request, view_function):
raise PermissionDenied
query_params = transform_query_dict_into_regular_dict(request.GET)
payload = extract_request_payload(request, allow_forms)
if request.method in HTTP_METHODS_SUPPORTING_PAYLOAD:
deserializer = deserializers_http_methods_map[request.method](
data=payload
)
if not deserializer.is_valid():
raise BadRequest
deserialized_data = deserializer.data
else:
deserialized_data = None

return view_function(
request,
url_params=kwargs,
query_params=query_params,
deserialized_data=deserialized_data,
)
except BaseAPIException as e:
return JsonResponse({"error_msg": e.RESPONSE_MESSAGE}, status=e.STATUS_CODE)
except Exception:
return JsonResponse(
{"error_msg": InternalServerError.RESPONSE_MESSAGE},
status=InternalServerError.STATUS_CODE,
)

return function_wrapper


def build_class_wrapper(
permission_class, # type: BasePermission
allowed_methods, # type: Iterable[str]
deserializers_http_methods_map, # type: Dict[str, ClassVar[Deserializer]]
allow_forms, # type: bool
view_class, # type: ClassVar
): # type:(...) -> ClassVar
class ViewWrapper(View):
__doc__ = view_class.__doc__
__module__ = view_class.__module__
__slots__ = "_wrapped_view"

def __init__(self, *args, **kwargs):
self._wrapped_view = view_class(*args, **kwargs)

def dispatch(self, request, *args, **kwargs):
# type:(HttpRequest, List[Any]) -> JsonResponse
# No need for additional check on request.method, since it's been
# already checked
handler = getattr(self, request.method.lower())
return handler(request, *args, **kwargs)

def __getattribute__(self, name):
try:
return super(ViewWrapper, self).__getattribute__(name)
except AttributeError:
attribute = self._wrapped_view.__getattribute__(name)
if (
not inspect.ismethod(attribute)
or name.upper() not in ALL_HTTP_METHODS
):
return attribute
return build_function_wrapper(
permission_class,
allowed_methods,
deserializers_http_methods_map,
allow_forms,
attribute,
)

ViewWrapper.__name__ = view_class.__name__
return ViewWrapper
Empty file.
Loading

0 comments on commit b4bd36d

Please sign in to comment.