#!/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)