-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #13 from alaouimehdi1995/integrating-deserializer-…
…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
Showing
13 changed files
with
933 additions
and
439 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Oops, something went wrong.