Source code for yarhp.view

#!/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 = []