"""
Handlers modules
"""
import json
import re
import traceback
import webapp2
import logging
from webob import Response
from exceptions import NotFound, NotAuthorized, Forbidden, MissingParameter, BadRequest, MissingBody
from response import CorsResponse, JsonResponse
from helpers import get_object_from_urlsafe, get_middlewares, cached_property, get_key_from_urlsafe, \
get_model_class_from_query, load_class
from restae.conf import DEFAULT_PAGINATION_CLASS
from restae.exceptions import DispatchError
[docs]class BaseHandler(webapp2.RequestHandler):
"""
BaseHandler with some intelligence & automation
"""
model = None
lookup_field = None
query_param = None
def __init__(self, *args, **kwargs):
super(BaseHandler, self).__init__(*args, **kwargs)
self.route_args = {}
self.user = None
[docs] @cached_property
def middlewares(self):
"""
Cached property that load all middlewares
:return: :py:class:`list` of :py:class:`Middleware` instances
"""
return get_middlewares()
[docs] def dispatch(self):
"""
Function called to make the dispatch job.
Calls internally :py:meth:`restae.handlers.BaseHandler.apply_dispatch`
:return: :py:class:`webob.Response`
"""
return self.apply_dispatch()
[docs] def apply_dispatch(self):
"""
Does the hole mechanism of dispatch.
Loops over all middlewares before and after the dispatch method call
.. code-block:: python
for middleware in self.middlewares:
middleware.process_request(self.request)
self.route_args = self.get_route_args()
response = self.do_dispatch()
for middleware in self.middlewares:
middleware.process_response(self.request, response)
:return: :py:class:`webob.Response`
"""
try:
for middleware in self.middlewares:
middleware.process_request(self.request)
self.route_args = self.get_route_args()
response = self.do_dispatch()
for middleware in self.middlewares:
middleware.process_response(self.request, response)
return response
except NotFound as nf:
return JsonResponse(status=404, data=str(nf) or 'Not found')
except NotAuthorized as na:
return JsonResponse(status=403, data=str(na) or 'Not authorized')
except Forbidden as fo:
return JsonResponse(status=401, data=str(fo) or 'Forbidden')
except MissingParameter as mp:
return JsonResponse(status=400, data=str(mp) or 'Missing parameter')
except BadRequest as br:
return JsonResponse(status=400, data=str(br) or 'Bad request')
except MissingBody as mb:
return JsonResponse(status=400, data=str(mb) or 'Request is missing a body')
except ValueError as ve:
return JsonResponse(status=400, data=str(ve) or 'Value error')
except DispatchError:
return Response(status=404)
except Exception as err:
logging.error('%s: %s',
err.__class__.__name__,
str(err))
logging.error(traceback.format_exc())
return self.handle_exception(err, self.app.debug)
[docs] def do_dispatch(self):
"""
Function that does the dispatch job
:return: :py:class:`webob.Response`
"""
return super(BaseHandler, self).dispatch()
[docs] def get_body(self):
"""
Tries to parse the request body as JSON
:return: JSON serialized data as :py:class:`dict` instance.
"""
if self.request.body:
try:
json_body = json.loads(self.request.body)
return json_body
except Exception as err:
logging.warning('Request is missing a body: %s -> %s',
err.__class__.__name__, str(err))
raise MissingBody('Request is missing a body')
[docs] def get_route_args(self):
"""
Parses the route arguments based on the router generated url
:return: route arguments in a :py:class:`dict` instance.
For instance if the view url is ``r'/resource/(?P<slug>[^/]+)/'`` and the calling url is ``GET /resource/my-slug/``
The returned dict will be formatted like this
.. code-block:: python
{'slug': 'my-slug'}
.. note::
Those route arguments are then available in each of the Handler methods in the self.route_args attribute
"""
return self.request.route.regex.search(self.request.upath_info).groupdict()
@classmethod
def get_extra_actions(cls):
actions = []
for _method in cls.__dict__.values():
if getattr(_method, 'is_dynamic_action', False):
actions.append(_method)
return actions
class APIModelBaseHandler(BaseHandler):
"""
"""
lookup_field = 'urlsafe'
queryset = None
serializer_class = None
pagination_class = load_class(DEFAULT_PAGINATION_CLASS)
def get_object(self, urlsafe=None, query_param=None, lookup_field=None, raise_exception=True):
_object = None
_lookup_field = lookup_field or self.lookup_field
_query_param = query_param or self.query_param
if _lookup_field is None and urlsafe is None and _query_param is None:
if raise_exception is True:
raise BadRequest
else:
return None
try:
if urlsafe is None:
if _lookup_field is not None and _lookup_field in self.route_args:
urlsafe_from_request = self.route_args[_lookup_field]
elif _query_param is not None:
urlsafe_from_request = self.request.GET.get(_query_param, None)
else:
urlsafe_from_request = None
if urlsafe_from_request is None and raise_exception is False:
return None
if urlsafe_from_request is None:
raise MissingParameter('Parameter {} is missing from request'.format(
_lookup_field or _query_param
))
else:
urlsafe = urlsafe_from_request
_object = get_object_from_urlsafe(urlsafe)
# if not _object.check_object_permissions(self.user):
# raise NotAuthorized
except Exception as err:
logging.error('Error in get_object: %s -> %s\n%s',
err.__class__.__name__,
str(err),
traceback.format_exc())
if raise_exception:
raise NotFound
finally:
return _object
@property
def paginator(self):
"""
The paginator instance associated with the view, or `None`.
"""
if not hasattr(self, '_paginator'):
if self.pagination_class is None:
self._paginator = None
else:
self._paginator = self.pagination_class()
return self._paginator
def paginate_queryset(self, queryset):
"""
Return a single page of results, or `None` if pagination is disabled.
"""
if self.paginator is None:
return None
return self.paginator.paginate_queryset(queryset, self.request, view=self)
def get_paginated_response(self, data):
"""
Return a paginated style `Response` object for the given output data.
"""
assert self.paginator is not None
return self.paginator.get_paginated_response(data)
[docs]class APIHandler(BaseHandler):
def options(self, *args, **kwargs):
return CorsResponse()
def get(self, *args, **kwargs):
raise NotImplemented
def post(self, *args, **kwargs):
raise NotImplemented
def patch(self, *args, **kwargs):
raise NotImplemented
def delete(self, *args, **kwargs):
raise NotImplemented
def head(self, *args, **kwargs):
raise NotImplemented
def trace(self, *args, **kwargs):
raise NotImplemented
[docs]class APIModelListHandler(APIModelBaseHandler):
"""
ModelHandler that perform a generic list operation
"""
[docs] def list(self, request, **kwargs):
"""
List operation
:return: :py:class:`restae.response.JsonResponse`
"""
page = self.paginate_queryset(self.queryset)
if page is not None:
serializer = self.serializer_class(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.serializer_class(self.queryset, many=True)
return Response(serializer.data)
[docs]class APIModelCreateHandler(APIModelBaseHandler):
"""
ModelHandler that perform a generic create operation
"""
[docs] def create(self, request, **kwargs):
"""
:return: :py:class:`restae.response.JsonResponse`
"""
_model = get_model_class_from_query(self.queryset)
obj = _model(**self.serializer_class(data=self.get_body()).data)
obj.put()
return JsonResponse(data=self.serializer_class(obj).data)
[docs]class APIModelRetrieveHandler(APIModelBaseHandler):
"""
ModelHandler that perform a generic get operation
"""
[docs] def retrieve(self, request, key=None):
"""
:return: :py:class:`restae.response.JsonResponse`
"""
try:
obj = key.get()
if obj is None:
raise NotFound
except Exception:
raise NotFound
return JsonResponse(data=self.serializer_class(obj).data)
[docs]class APIModelUpdateHandler(APIModelBaseHandler):
"""
ModelHandler that perform a generic update operation
"""
def update(self, request, key=None):
raise NotImplementedError
[docs]class APIModelPatchHandler(APIModelBaseHandler):
"""
ModelHandler that perform a generic patch operation
"""
def partial_update(self, request, key=None):
raise NotImplementedError
[docs]class APIModelDestroyHandler(APIModelBaseHandler):
"""
ModelHandler that perform a generic delete operation
"""
[docs] def destroy(self, request, key=None):
"""
:return: :py:class:`restae.response.JsonResponse`
"""
key.delete()
return JsonResponse()
[docs]class APIModelMixinHandler(APIModelListHandler,
APIModelCreateHandler,
APIModelDestroyHandler,
APIModelRetrieveHandler,
APIModelUpdateHandler,
APIModelPatchHandler):
pass
[docs]class APIModelHandler(APIModelMixinHandler):
[docs] def do_dispatch(self):
"""
Dispatches the request.
This will first check if there's a handler_method defined in the
matched route, and if not it'll use the method correspondent to the
request method (``get()``, ``post()`` etc).
"""
route_args = self.get_route_args()
route_kwargs = {}
matched_url = None
for url in getattr(self, 'urls', []):
if re.match(url.regex, self.request.path_info):
matched_url = url
break
if matched_url is None:
raise NotFound('URL not found on this server')
if matched_url.route.detail is True:
try:
route_kwargs['key'] = get_key_from_urlsafe(route_args['urlsafe'])
except Exception:
return JsonResponse(status=400, data='Given urlsafe is invalid')
if self.request.method.lower() not in matched_url.route.mapping.keys():
raise DispatchError('Invalid method {}'.format(self.request.method))
method = getattr(self, matched_url.route.mapping[self.request.method.lower()], None)
# The handler only receives *args if no named variables are set.
args, kwargs = route_args, route_kwargs
if kwargs:
args = ()
return method(self, *args, **kwargs)