Source code for everest.representers.traversal

"""
Resource data tree traversal.

This file is part of the everest project. 
See LICENSE.txt for licensing, CONTRIBUTORS.txt for contributor information.

Created on Apr 25, 2012.
"""
from collections import OrderedDict
from everest.entities.utils import get_entity_class
from everest.representers.config import IGNORE_ON_READ_OPTION
from everest.representers.config import IGNORE_ON_WRITE_OPTION
from everest.representers.config import WRITE_AS_LINK_OPTION
from everest.representers.interfaces import ICollectionDataElement
from everest.representers.interfaces import ILinkedDataElement
from everest.representers.interfaces import IMemberDataElement
from everest.representers.urlloader import LazyAttributeLoaderProxy
from everest.representers.urlloader import LazyUrlLoader
from everest.resources.attributes import ResourceAttributeKinds
from everest.resources.descriptors import CARDINALITY
from everest.resources.interfaces import ICollectionResource
from everest.resources.interfaces import IMemberResource
from everest.resources.kinds import ResourceKinds
from everest.resources.utils import new_stage_collection
from everest.url import url_to_resource
from zope.interface import providedBy as provided_by # pylint: disable=E0611,F0401

__docformat__ = 'reStructuredText en'
__all__ = ['AttributeKey',
           'DataElementBuilderResourceTreeVisitor',
           'DataElementTreeTraverserMixin',
           'DataTreeTraverser',
           'MappingDataElementTreeTraverser',
           'MappingResourceDataTreeTraverser',
           'ResourceBuilderDataElementTreeVisitor',
           'ResourceDataVisitor',
           'ResourceTreeTraverser',
           ]


[docs]class AttributeKey(object): """ Value object used as a key during resource data tree traversal. Each key consists of a tuple of attribute strings that uniquely determine a node's position in the resource data tree. """ def __init__(self, data): self.__data = tuple(data) self.offset = 0 def __hash__(self): return hash(self.__data) def __eq__(self, other): return self.__data == tuple(other) def __getitem__(self, index): return self.__data.__getitem__(index) def __len__(self): return len(self.__data) def __add__(self, other): return AttributeKey(self.__data + tuple(other)) def __str__(self): return 'AttributeKey(%s, %d)' % (self.__data, self.offset)
class ResourceDataVisitor(object): def visit_member(self, attribute_key, attribute, member_node, member_data, is_link_node, parent_data, index=None): """ Visits a member node in a resource data tree. :param tuple attribute_key: tuple containing the attribute tokens identifying the member node's position in the resource data tree. :param attribute: mapped attribute holding information about the member node's name (in the parent) and type etc. :type attribute: :class:`everest.representers.attributes.MappedAttribute` :param member_node: the node holding resource data. This is either a resource instance (when using a :class:`ResourceTreeTraverser` on a tree of resources) or a data element instance (when using a :class:`MappingDataElementTreeTraverser` on a data element tree. :param dict member_data: dictionary holding all member data extracted during traversal (with mapped attributes as keys). This will be empty during pre-order visits. :param bool is_link_node: indicates if the given member node is a link. :param dict parent_data: dictionary holding all parent data extracted during traversal (with mapped attributes as keys). :param int index: this indicates a member node's index in a collection parent node. If the parent node is a member node, it will be `None`. """ raise NotImplementedError('Abstract method.') def visit_collection(self, attribute_key, attribute, collection_node, collection_data, is_link_node, parent_data): """ Visits a collection node in a resource data tree. :param tuple attribute_key: tuple containing the attribute tokens identifying the collection node's position in the resource data tree. :param attribute: mapped attribute holding information about the collection node's name (in the parent) and type etc. :type attribute: :class:`everest.representers.attributes.MappedAttribute` :param collection_node: the node holding resource data. This is either a resource instance (when using a :class:`ResourceTreeTraverser` on a tree of resources) or a data element instance (when using a :class:`MappingDataElementTreeTraverser` on a data element tree. :param dict collection_data: dictionary mapping member index to member data for each member in the visited collection. :param bool is_link_node: indicates if the given member node is a link. :param dict parent_data: dictionary holding all parent data extracted during traversal (with mapped attributes as keys). """ raise NotImplementedError('Abstract method.') class DataElementBuilderResourceTreeVisitor(ResourceDataVisitor): def __init__(self, mapping): ResourceDataVisitor.__init__(self) self.__mapping = mapping self.__data_el = None def visit_member(self, attribute_key, attribute, member_node, member_data, is_link_node, parent_data, index=None): if is_link_node: mb_data_el = self.__write_link(member_node) else: mb_data_el = \ self.__mapping.create_data_element_from_resource(member_node) # Process attributes. for attr, value in member_data.iteritems(): if attr.kind == ResourceAttributeKinds.TERMINAL: mb_data_el.set_mapped_terminal(attr, value) else: mb_data_el.set_mapped_nested(attr, value) if not index is None: # Collection member. Store in parent data with index as key. parent_data[index] = mb_data_el elif len(attribute_key) == 0: # Top level - store root data element. self.__data_el = mb_data_el else: # Nested member. Store in parent data with attribute as key. parent_data[attribute] = mb_data_el def visit_collection(self, attribute_key, attribute, collection_node, collection_data, is_link_node, parent_data): if is_link_node: coll_data_el = self.__write_link(collection_node) else: coll_data_el = \ self.__mapping.create_data_element_from_resource(collection_node) for item in sorted(collection_data.items()): coll_data_el.add_member(item[1]) if len(attribute_key) == 0: # Top level. self.__data_el = coll_data_el else: parent_data[attribute] = coll_data_el @property def data_element(self): return self.__data_el def __write_link(self, resource): return \ self.__mapping.create_linked_data_element_from_resource(resource) class ResourceBuilderDataElementTreeVisitor(ResourceDataVisitor): def __init__(self, resolve_urls=True): ResourceDataVisitor.__init__(self) self.__resolve_urls = resolve_urls self.__resource = None def visit_member(self, attribute_key, attribute, member_node, member_data, is_link_node, parent_data, index=None): if is_link_node: url = member_node.get_url() if self.__resolve_urls: rc = url_to_resource(url) entity = rc.get_entity() else: entity = LazyUrlLoader(url, url_to_resource) else: entity_cls = get_entity_class(member_node.mapping.mapped_class) entity_data = {} nested_entity_data = {} for attr, value in member_data.iteritems(): if '.' in attr.entity_name: nested_entity_data[attr.entity_name] = value else: entity_data[attr.entity_name] = value if self.__resolve_urls: entity = entity_cls.create_from_data(entity_data) else: entity = LazyAttributeLoaderProxy.create(entity_cls, entity_data) # Set nested attribute values. # FIXME: lazy loading of nested attributes is not supported. for nested_attr, value in nested_entity_data.iteritems(): tokens = nested_attr.split('.') parent = reduce(getattr, tokens[:-1], entity) if not parent is None: setattr(parent, tokens[-1], value) if not index is None: # Collection member. Store in parent data with index as key. parent_data[index] = entity elif len(attribute_key) == 0: # Top level. Store root entity and create resource. mapped_cls = member_node.mapping.mapped_class self.__resource = mapped_cls.create_from_entity(entity) else: # Nested member. Store in parent data with attribute as key. parent_data[attribute] = entity def visit_collection(self, attribute_key, attribute, collection_node, collection_data, is_link_node, parent_data): if is_link_node: url = collection_node.get_url() if self.__resolve_urls: coll = url_to_resource(url) entities = [mb.get_entity() for mb in coll] else: raise NotImplementedError('Lazy-loading of collection links is ' 'not supported.') else: entities = [] for item in sorted(collection_data.items()): entities.append(item[1]) if len(attribute_key) == 0: # Top level. mapped_cls = collection_node.mapping.mapped_class self.__resource = new_stage_collection(mapped_cls) for ent in entities: self.__resource.create_member(ent) else: parent_data[attribute] = entities @property def resource(self): return self.__resource class DataTreeTraverser(object): def __init__(self, root): self.__root = root def run(self, visitor): """ Runs this traverser. """ self._dispatch(AttributeKey(()), None, self.__root, None, visitor) def _traverse_collection(self, attr_key, attr, collection_node, parent_data, visitor): is_link_node = self._is_link_node(collection_node, attr) collection_data = {} if not is_link_node: all_mb_nodes = self._get_node_members(collection_node) for idx, mb_node in enumerate(all_mb_nodes): self._traverse_member(attr_key, attr, mb_node, collection_data, visitor, index=idx) visitor.visit_collection(attr_key, attr, collection_node, collection_data, is_link_node, parent_data) def _traverse_member(self, attr_key, attr, member_node, parent_data, visitor, index=None): raise NotImplementedError('Abstract method.') def _is_link_node(self, node, attr): raise NotImplementedError('Abstract method.') def _get_node_members(self, node): raise NotImplementedError('Abstract method.') def _dispatch(self, attr_key, attr, node, parent_data, visitor): raise NotImplementedError('Abstract method.') class DataElementTreeTraverserMixin(object): def _dispatch(self, attr_key, attr, node, parent_data, visitor): ifcs = provided_by(node) if IMemberDataElement in ifcs: traverse_fn = self._traverse_member elif ICollectionDataElement in ifcs: traverse_fn = self._traverse_collection elif ILinkedDataElement in ifcs: kind = node.get_kind() if kind == ResourceKinds.MEMBER: traverse_fn = self._traverse_member else: # kind == ResourceKinds.COLLECTION traverse_fn = self._traverse_collection else: raise ValueError('Need MEMBER or COLLECTION data element; found ' '"%s".' % node) traverse_fn(attr_key, attr, node, parent_data, visitor) def _get_node_members(self, node): return node.get_members() def _is_link_node(self, node, attr): # pylint: disable=W0613 return ILinkedDataElement in provided_by(node)
[docs]class MappingResourceDataTreeTraverser(DataElementTreeTraverserMixin, DataTreeTraverser): """ Abstract base class for resource data tree traversers. """ def __init__(self, root, mapping): DataElementTreeTraverserMixin.__init__(self) DataTreeTraverser.__init__(self, root) self._mapping = mapping def _traverse_member(self, attr_key, attr, member_node, parent_data, visitor, index=None): member_data = OrderedDict() is_link_node = self._is_link_node(member_node, attr) # Ignore links for traversal. if not is_link_node: node_type = self._get_node_type(member_node) for mb_attr in self._mapping.attribute_iterator(node_type, attr_key): ignore_opt = self._get_ignore_option(mb_attr) if self.__ignore_attribute(ignore_opt, mb_attr, attr_key): continue if mb_attr.kind == ResourceAttributeKinds.TERMINAL: # Terminal attribute - extract. value = self._get_node_terminal(member_node, mb_attr) if value is None: # None values are ignored. continue member_data[mb_attr] = value else: # Nested attribute - traverse. nested_node = self._get_node_nested(member_node, mb_attr) if nested_node is None: # Stop condition - the given data element does not # contain a nested attribute of the given mapped # name. continue nested_attr_key = attr_key + (mb_attr.name,) if ignore_opt is False: # The offset in the attribute key ensures that # the defaults for ignoring attributes of the # nested attribute can be retrieved correctly. nested_attr_key.offset = len(nested_attr_key) self._dispatch(nested_attr_key, mb_attr, nested_node, member_data, visitor) visitor.visit_member(attr_key, attr, member_node, member_data, is_link_node, parent_data, index=index) def _get_node_type(self, node): raise NotImplementedError('Abstract method.') def _get_node_terminal(self, node, attr): raise NotImplementedError('Abstract method.') def _get_node_nested(self, node, attr): raise NotImplementedError('Abstract method.') def _get_ignore_option(self, attr): raise NotImplementedError('Abstract method.') def __ignore_attribute(self, ignore_opt, attr, attr_key): # Rules for ignoring attributes: # * always ignore when IGNORE_ON_XXX_OPTION is set to True # * always include when IGNORE_ON_XXX_OPTION is set to False # * also ignore member attributes when the length of the attribute # key is > 0 or the cardinality is not MANYTOONE # * also ignore collection attributes when the cardinality is # not MANYTOMANY do_ignore = ignore_opt if ignore_opt is None: if attr.kind == ResourceAttributeKinds.MEMBER: depth = len(attr_key) + 1 - attr_key.offset do_ignore = depth > 1 \ or attr.cardinality != CARDINALITY.MANYTOONE elif attr.kind == ResourceAttributeKinds.COLLECTION: do_ignore = attr.cardinality != CARDINALITY.MANYTOMANY return do_ignore
[docs]class MappingDataElementTreeTraverser(MappingResourceDataTreeTraverser): """ Mapping traverser for data element trees. """ def _get_node_type(self, node): return node.mapping.mapped_class def _get_node_terminal(self, node, attr): return node.get_mapped_terminal(attr) def _get_node_nested(self, node, attr): return node.get_mapped_nested(attr) def _get_ignore_option(self, attr): return attr.options.get(IGNORE_ON_READ_OPTION) #class NonMappingDataElementTreeTraverser(DataElementTreeTraverserMixin, # DataTreeTraverser): # """ # Non-mapping traverser for data element trees. # """ # def _traverse_member(self, attr_key, attr, member_node, parent_data, # visitor, index=None): # is_link_node = self._is_link_node(member_node, attr) # if is_link_node: # member_data = OrderedDict() # else: # member_data = member_node.data # for attr_name, value in member_data.iteritems(): # if not isinstance(value, DataElement): # continue # else: # self._dispatch(attr_key + (attr_name,), attr_name, value, # member_data, visitor) # visitor.visit_member(attr_key, attr, member_node, member_data, # is_link_node, parent_data, index=index)
[docs]class ResourceTreeTraverser(MappingResourceDataTreeTraverser): """ Mapping traverser for resource trees. """ def _dispatch(self, attr_key, attr, node, parent_data, visitor): ifcs = provided_by(node) if IMemberResource in ifcs: self._traverse_member(attr_key, attr, node, parent_data, visitor) elif ICollectionResource in ifcs: self._traverse_collection(attr_key, attr, node, parent_data, visitor) else: raise ValueError('Data must be a resource.') def _get_node_type(self, node): return type(node) def _get_node_terminal(self, node, attr): return getattr(node, attr.name) def _get_node_nested(self, node, attr): return getattr(node, attr.name) def _get_node_members(self, node): return iter(node) def _is_link_node(self, node, attr): return not attr is None and \ not attr.options.get(WRITE_AS_LINK_OPTION) is False def _get_ignore_option(self, attr): return attr.options.get(IGNORE_ON_WRITE_OPTION)

Project Versions