Source code for yarhp.resource

#!/usr/bin/python
# -*- coding: utf-8 -*-

import logging
from yarhp.renderers import JsonRendererFactory, YarhpJsonRendererFactory
from yarhp.utils import snake2camel, maybe_dotted


log = logging.getLogger(__name__)
            
from pyramid.httpexceptions import (
                HTTPOk,
                HTTPCreated,
                HTTPNotFound,
                HTTPConflict)

ACTIONS = {
    'index':['GET', 'JSON'],
    'show': ['GET', 'JSON', HTTPNotFound.code],
    'create': ['POST', HTTPOk.code, HTTPConflict.code],
    'update': ['PUT', HTTPNotFound.code, HTTPOk.code],
    'delete': ['DELETE', HTTPNotFound.code, HTTPOk.code],
    'edit': ['GET', 'JSON', HTTPNotFound.code],
    'new': ['GET', 'JSON', HTTPNotFound.code]
}

[docs]def adjust_actions(kwargs): "Returns the adjusted list of actions based on ``kwargs``." actions = set(ACTIONS) excluded_actions = set(kwargs.pop('exclude', [])) included_actions = set(kwargs.pop('include', [])) if 'all' in excluded_actions: return [] if included_actions: actions.intersection_update(included_actions) else: actions.difference_update(excluded_actions) return actions
[docs]def get_root_resource(config): "Returns the root resource." return config.registry._root_resources.setdefault(config.package_name, Resource(config))
def includeme(config): config.add_directive('get_root_resource', get_root_resource) config.add_renderer('json', JsonRendererFactory) config.add_renderer('yarhp_json', YarhpJsonRendererFactory) config.registry._root_resources = {} config.registry._root_resources_map = {}
[docs]def resource_factory(request): """Permits to associate a request with the concerned resource.""" route_name = request.matched_route.name resource_map = request.registry._root_resources_map return resource_map.get(route_name)
[docs]def add_resource(config, view, member_name, collection_name, **kwargs): """ ``view`` is a dotted name of (or direct reference to) a Python view class, e.g. ``'my.package.views.MyView'``. ``member_name`` should be the appropriate singular version of the resource given your locale and used with members of the collection. ``collection_name`` will be used to refer to the resource collection methods and should be a plural version of the member_name argument. All keyword arguments are optional. ``path_prefix`` Prepends the URL path for the Route with the path_prefix given. This is most useful for cases where you want to mix resources or relations between resources. ``name_prefix`` Perpends the route names that are generated with the name_prefix given. Combined with the path_prefix option, it's easy to generate route names and paths that represent resources that are in relations. Example:: config.add_resource('myproject.views:CategoryView', 'message', 'messages', path_prefix='/category/:category_id', name_prefix="category_") # GET /category/7/messages/1 # has named route "category_message" """ view = maybe_dotted(view) path_prefix = kwargs.pop('path_prefix', '') name_prefix = kwargs.pop('name_prefix', '') path = path_prefix.strip('/') + '/' + (collection_name or member_name) action_route = {} added_routes = {} def add_route_and_view(config, action, route_name, path, request_method): if route_name not in added_routes: config.add_route(route_name, path, factory=resource_factory) added_routes[route_name] = path action_route[action] = route_name config.add_view(view=view, attr=action, route_name=route_name, request_method=request_method, **kwargs) # collection_name is blank if resource is singular. _id = ('/:id' if collection_name else '') add_route_and_view(config, 'index', name_prefix + (collection_name or member_name), path, 'GET') add_route_and_view(config, 'new', name_prefix + 'new_' + member_name, path + '/new', 'GET') add_route_and_view(config, 'update', name_prefix + member_name, path + _id, 'PUT') # add a view for tunneling PUT via POST using _method=put param config.add_view(view=view, route_name=name_prefix + member_name, attr='update', request_param='_method=put', request_method='POST', **kwargs) add_route_and_view(config, 'create', name_prefix + (collection_name or member_name), path, 'POST') add_route_and_view(config, 'delete', name_prefix + member_name, path + _id, 'DELETE') # add a view for tunneling DELETE via POST using _method=delete param config.add_view(view=view, route_name=name_prefix + member_name, attr='delete', request_param='_method=delete', request_method='POST', **kwargs) add_route_and_view(config, 'edit', name_prefix + 'edit_' + member_name, path + _id + '/edit', 'GET') add_route_and_view(config, 'show', name_prefix + member_name, path + _id, 'GET') return action_route
[docs]def default_view(resource): "Returns the dotted path to the default view class." view_file = '%s' % '_'.join([a.member_name for a in resource.ancestors] + [resource.collection_name or resource.member_name]) view = '%s:%sView' % (view_file, snake2camel(view_file)) return '%s.views.%s' % (resource.config.package_name, view)
[docs]def default_model(resource): "Returns a dotted path to the default model class." return '%s.model.%s.%s' % (resource.config.package_name, resource.uid, snake2camel(resource.uid))
[docs]class Resource(object): """Class providing the core functionality. :: m = Resource(config) pa = m.add('parent', 'parents') pa.add('child', 'children') """ def __init__(self, config, member_name='', collection_name='', parent=None, uid='', children=None, model=None): self.__dict__.update(locals()) self.children = children or [] self._ancestors = [] def __repr__(self): return "%s(uid='%s')" % (self.__class__.__name__, self.uid)
[docs] def get_ancestors(self): "Returns the list of ancestor resources." if self._ancestors: return self._ancestors if not self.parent: return [] obj = self.resource_map.get(self.parent.uid) while obj and obj.member_name: self._ancestors.append(obj) obj = obj.parent self._ancestors.reverse() return self._ancestors
ancestors = property(get_ancestors) resource_map = property(lambda self: \ self.config.registry._root_resources_map) is_root = property(lambda self: not self.member_name) is_singular = property(lambda self: not self.is_root \ and not self.collection_name)
[docs] def add(self, member_name, collection_name='', parent=None, uid='', **kwargs): """ :param member_name: singular name of the resource. It should be the appropriate singular version of the resource given your locale and used with members of the collection. :param collection_name: plural name of the resource. It will be used to refer to the resource collection methods and should be a plural version of the ``member_name`` argument. Note: if collection_name is empty, it means resource is singular :param parent: parent resource name or object. :param uid: unique name for the resource :param kwargs: view: custom view to overwrite the default one. model: custom model class to override the default one used in default view. the rest of the keyward arguments are passed to add_resource call. :return: ResourceMap object """ # self is the parent resource on which this method is called. parent = (self.resource_map.get(parent) if type(parent) is str else parent or self) uid = (uid or parent.uid + '_' + member_name if parent.uid else member_name) if uid in self.resource_map: raise ValueError('%s already exists in resource map' % uid) new_resource = Resource(self.config, member_name=member_name, collection_name=collection_name, parent=parent, uid=uid) new_resource.actions = adjust_actions(kwargs) view = maybe_dotted(kwargs.pop('view', None) or default_view(new_resource)) view.root_resource = self.config.get_root_resource() model = kwargs.pop('model', None) new_resource.model = maybe_dotted(view.__model_class__ or model or default_model(new_resource)) # first check the view if it provides model class # then check if model comes as arg in this method # finally get the default new_resource.view = view path_prefix = kwargs.get('path_prefix', None) path_segs = ([path_prefix] if path_prefix else []) for res in new_resource.ancestors: if not res.is_singular: path_segs.append('%s/:%s_id' % (res.collection_name, res.member_name)) else: path_segs.append(res.member_name) if path_segs: kwargs['path_prefix'] = '/'.join(path_segs) name_prefix = kwargs.get('name_prefix', None) name_segs = ([name_prefix] if name_prefix else []) name_segs.extend([a.member_name for a in new_resource.ancestors]) if name_segs: kwargs['name_prefix'] = '_'.join(name_segs) + '_' new_resource.renderer = kwargs.setdefault('renderer', view._default_renderer) new_resource.action_route_map = add_resource(self.config, view, member_name, collection_name, **kwargs) self.resource_map[uid] = new_resource # add all route names for this resource as keys in the dict, so its easy to find it in the view. self.resource_map.update(dict.fromkeys(new_resource.action_route_map.values(), new_resource)) parent.children.append(new_resource) return new_resource
[docs] def add_from(self, resource): '''add a resource with its all children resources to the current resource''' new_resource = self.add(resource.member_name, resource.collection_name) for child in resource.children: new_resource.add_from(child)