#!/usr/bin/python
# -*- coding: utf-8 -*-
import logging
from pyramid.httpexceptions import HTTPBadRequest, HTTPMethodNotAllowed, \
HTTPUnsupportedMediaType, HTTPNotFound
from yarhp import utils, wrappers
from yarhp.resource import ACTIONS
log = logging.getLogger(__name__)
[docs]class ViewMapper(object):
"Custom mapper class for BaseView"
def __init__(self, **kwargs):
self.kwargs = kwargs
def __call__(self, view):
#i.e index, create etc.
action_name = self.kwargs['attr']
def view_mapper_wrapper(context, request):
matchdict = request.matchdict.copy()
matchdict.pop('action', None)
#instance of BaseView (or child of)
view_obj = view(context, request)
action = getattr(view_obj, action_name)
#save the action and context (its a resource object) in the request.
request.action = action_name
request.context = context
# we should not run "after_calls" here, so lets save them in request as filters
# they will be ran in YarhpJsonRendererFactory
request.filters = view_obj._after_calls
try:
# run before_calls (validators) before running the action
for call in view_obj._before_calls.get(action_name, []):
call(request=request)
except wrappers.ValidationError, e:
log.error('validation error: %s', e)
raise HTTPBadRequest(e.args)
except wrappers.ResourceNotFound, e:
log.error('resource not found: %s', e)
raise HTTPNotFound()
return action(**matchdict)
return view_mapper_wrapper
[docs]class NoModel(object):
"No-op model class to use when defining a resource who doesnt have a model"
pass
[docs]class BaseView(object):
"""Base class for yarhp views.
This class holds some common objects used by the derived SQLAView and DynamoDBView classes.
Those are:
``_default_actions``
``__model_class__``
``__validation_schema__``
``_default_actions`` are those for which the derived view can omit the implementation for and it will be
handled by the parent class.
.. code-block:: python
class MyView(SQLAView):
def index(self):
...
In this example we have MyView derived from SQLAView and it defines only the index action.
The rest of the actions will by default handled by SQLAView.
In case the particular view does not need a certain action,
it should declare it so when adding the resource:
.. code-block:: python
root.add('user', 'users', include=['index'])
Here the resource ``user`` defines only the ``index`` action.
``__model_class__`` is a class object or dotted path to it which is associated with this view.
By default ``yarhp`` expects the model to be defined in
``your_project.model.<resource_member_name>.<Resource>View.py``
There are two ways to override this default:
.. code-block:: python
root.add('user', 'users', model='path.to.my.model.ClassView')
root.add('whatever',model='yarhp.NoModel') #redefine it to NoModel
or
.. code-block:: python
class MyView(BaseView):
__model_class__ = ResourceView
``__validation_schema__`` is a dict of dicts in the form of:
.. code-block:: python
{field_name1:
{'type':<python type>,
'required':<bool>},
field_name2:{...}
...}
This is bare minimum that is checked by validators to make sure that request.params correspond
to the model's requirements.
If you want to supply custom validation schema, you should override the ``validate_schema``
method of your view, which must return the dict in the format above.
"""
__view_mapper__ = ViewMapper
_default_renderer = 'yarhp_json'
_default_actions = frozenset([])
__model_class__ = None
__validation_schema__ = {}
def __init__(self, context, request):
self.resource = context
self.request = request
self.__model_class__ = utils.maybe_dotted(self.__model_class__
or self.resource.model)
#dict of the callables {'action':[callable1, callable2..]}
# as name implies, before calls are executed before the action is called
# after_calls are called after the action returns.
self._before_calls = {}
self._after_calls = {}
#holding the object which is being created/updated/deleted by default actions
self.active_obj = None
# no accept headers, use default
if '' in request.accept:
request.override_renderer = self.resource.renderer
elif 'application/json' in request.accept:
request.override_renderer = 'yarhp_json'
elif 'text/plain' in request.accept:
request.override_renderer = 'string'
ctype = request.content_type
if not (ctype.startswith(('application/json', 'text/plain')) or ctype
in ['application/x-www-form-urlencoded', 'multipart/form-data'
, '']):
raise HTTPUnsupportedMediaType()
self.setup_default_wrappers()
self.scan_action_decorators()
[docs] def setup_default_wrappers(self):
"""Add all the default before and after calls.
``before calls`` are validate_types and validate_required which are called
with the validation schema returned by ``validation_schema`` method.
This allows any subclass to change validation schema.
``after calls`` are those that modify the result returned by the actions:
pager : paginates the results, if the result is a sequence.
obj2dict: if result is an python object, it calls ``to_dict`` on it.
if its a list of objects, it enumerates and calls ``to_dict`` on each element.
wrap_in_dict: if action result is a list, it wraps it into a dict.
add_pagination_links: if the result was paginated, it appends pagination links
"""
self._before_calls['create'] = \
[wrappers.validate_types(**self.validation_schema()),
wrappers.validate_required(**self.validation_schema())]
self._before_calls['update'] = \
[wrappers.validate_types(**self.validation_schema())]
self._after_calls['index'] = [
wrappers.pager(),
wrappers.obj2dict,
wrappers.add_parent_links,
wrappers.wrap_in_dict,
wrappers.add_pagination_links,
]
self._after_calls['show'] = [wrappers.obj2dict,
wrappers.add_self_links,
wrappers.add_parent_links]
[docs] def scan_action_decorators(self):
"""Iterates over actions and appends decorated
wrappers for before and after calls to the corresponding before or after dict"""
for action_name in self.resource.actions:
action = getattr(self, action_name, None)
if action:
for call_kind in ['_before_calls', '_after_calls']:
#get all callables that were injected into the action method object by the decorators
action_calls = getattr(action, call_kind, [])
#get all callables from the view
view_calls = getattr(self,
call_kind).setdefault(action_name, [])
for acall in action_calls:
for vcall in view_calls:
# if its the same wrapper, just update the arguments,
# which will allow to avoid running the same validator more than once
# but will also be able to update the arguments
if acall == vcall and hasattr(acall, 'kwargs'):
vcall.kwargs.update(acall.kwargs)
# add the decorated wrappers which are not in the view already
for call in [c for c in action_calls if c
not in view_calls]:
view_calls.append(call)
if action_calls:
getattr(self, call_kind)[action_name] = view_calls
[docs] def validation_schema(self):
"Override this method in your own view to provide custom validation schema"
return self.__validation_schema__
def __getattr__(self, attr):
if attr not in ACTIONS:
raise AttributeError(attr)
#at this point the attr is in the ACTIONS list
# lets see if the actions were limited during the resource definition.
if attr not in self.resource.actions:
#even though the action is valid crud, its been disallowed from the resouce.
return self.not_allowed_action
#valid action is called, but not implemented, so call the default.
return self.default_action(attr)
def not_allowed_action(self, *a, **k):
raise HTTPMethodNotAllowed()
[docs] def default_action(self, name):
"""``default_index``, ``default_create``, etc must be defined in the child class.
See SQLAView or DynamoDBView for examples.
also ``_default_actions`` is by default empty. It must be defined by the derived view class.
"""
if name in self._default_actions:
return getattr(self, 'default_%s' % name)
raise AttributeError(name)
[docs] def new(self, **kw):
"by default ``new`` is not allowed, unless its redefined in the child view"
raise HTTPMethodNotAllowed()
[docs] def edit(self, **kw):
"by default ``edit`` is not allowed, unless its redefined in the child view"
raise HTTPMethodNotAllowed()
[docs]class ViewNotImplemented(BaseView):
'''Use this view for the routes that raise HTTPMethodNotAllowed for any action'''
__model_class__ = NoModel
def __init__(self, *args, **kwargs):
BaseView.__init__(self, *args, **kwargs)
# no actions defined
self.resource.actions = []