"""Construction kit for Pyramid resource classes.
Example usage::
from bag.web.pyramid.resources import (
BaseRootResource, BaseResource, IntResource,
ancestor, ancestor_model, model_property)
from pyramid.decorator import reify
from pyramid.security import (Allow, Deny, Everyone, Authenticated,
ALL_PERMISSIONS)
from .models import User, Address
class RootResource(BaseRootResource):
__acl__ = [
(Allow, "group:admin", ALL_PERMISSIONS),
(Allow, Authenticated, ("view_dashboard", "edit_users")),
(Deny, Everyone, ALL_PERMISSIONS),
]
factories = {} # a static registry of Resource classes
class UserResource(IntResource): # /users/1/
factories = {} # a static registry of Resource classes
@reify
def model(self):
return sas.query(User).get(self.__name__)
@reify
def __acl__(self):
user_id = self._request.authenticated_userid
return [(
Allow, f"u:{user_id}", self.model.get_permissions(user_id)
)]
class UsersResource(BaseResource): # /users/
contains_cls = UserResource
@reify
def models(self):
return sas.query(User)
RootResource.factories["users"] = UsersResource
class AddressResource(IntResource): # /users/1/addresses/1
factories = {} # a static registry of Resource classes
model = model_property(sas, Address, user=User)
class AddressesResource(BaseResource): # /users/1/addresses
contains_cls = AddressResource
@reify
def models(self):
return ancestor_model(self, User).addresses
UserResource.factories["addresses"] = AddressesResource
"""
from bag import first
from pyramid.decorator import reify
from pyramid.httpexceptions import HTTPNotFound
[docs]def ancestor_finder(resource, predicate, include_self=False):
"""Generate ancestors that satisfy ``predicate``.
Generator that climbs the tree yielding resources for which
``predicate(current_resource)`` returns True.
"""
resource = resource if include_self else getattr(resource, "__parent__", None)
while resource is not None:
if predicate(resource):
yield resource
resource = getattr(resource, "__parent__", None)
[docs]def ancestor(resource, cls, include_self=False): # noqa
"""Return the first ancestor of ``resource`` that is of type ``cls``."""
def predicate(resource):
return isinstance(resource, cls)
return first(ancestor_finder(resource, predicate, include_self))
[docs]def ancestor_model(resource, cls, include_self=False): # noqa
"""Find in ancestors a model instance of type ``cls``.
The search is done in the ``model`` attribute of the ancestors of
``resource``. Returns None if not found.
"""
def predicate(resource):
return hasattr(resource, "model") and isinstance(resource.model, cls)
o = first(ancestor_finder(resource, predicate, include_self))
return o.model if o else None
[docs]def find_root(resource):
"""Find and return the root resource."""
return ancestor(resource, type(None))
[docs]def model_property(sas, model_cls, **ancestors): # noqa
"""Return a property that checks ancestor IDs.
If you are using SQLAlchemy, this function returns a model property
that checks some ancestor ID(s) against its foreign key(s). Example usage::
class AddressResource(BaseResource):
model = model_property(sas, Address, user=User)
"""
def wrapped(self):
o = sas.query(model_cls).get(self.__name__)
if o is None:
raise HTTPNotFound()
for key, cls in ancestors.items():
if not getattr(o, key) is ancestor_model(self, cls):
raise HTTPNotFound()
return o
return reify(wrapped)
[docs]class BaseRootResource:
"""Base class for your Root resource."""
__name__ = ""
__parent__ = None
def __init__(self, request): # noqa
self._request = request
def __repr__(self):
return "<{}>".format(type(self).__name__)
def _make_descendant(self, factory, name):
o = factory()
o._request = self._request
o.__parent__ = self
if hasattr(o, "validate_name"):
o.__name__ = o.validate_name(name)
else:
o.__name__ = name
return o
def __getitem__(self, name):
if hasattr(self, "contains_cls"):
return self._make_descendant(self.contains_cls, name)
elif hasattr(self, "factories"):
return self._make_descendant(self.factories[name], name)
raise KeyError(name)
[docs]class BaseResource(BaseRootResource):
"""Base class for Pyramid traversal resources.
Subclasses may define a static ``factories`` dict, used to
map URL elements to other resource classes or factories.
This is useful for any resource whose children fork into
separate trees.
Subclasses may also represent collections, such as /books/.
These subclasses must define a ``contains_cls`` attribute,
whose value is to be the contained resource class.
For a subclass representing a single model instance ―
e. g. /books/1/ ―, you can implement a ``model`` property;
then a descendant view such as /books/1/authors/ may call
``ancestor_model(Book)``, which will find the ancestor and
return the ``model`` property value. Example::
@reify
def model(self):
return sas.query(Book).get(self.__name__)
"""
def __init__(self):
"""Construct without arguments."""
def __str__(self):
return '<{} "{}">'.format(type(self).__name__, self.__name__)
def __repr__(self):
alist = []
for element in reversed(
list(ancestor_finder(self, lambda resource: True, include_self=True))
):
alist.append(str(element))
return " / ".join(alist)
[docs]class IntResource(BaseResource):
"""Base class for resources whose name must be an int, e.g. /books/1."""
[docs] def validate_name(self, name): # noqa
try:
return int(name)
except ValueError:
raise KeyError(name)