예제 #1
0
 def __init__(self, source_proxy, target_proxy):
     self._src_prx = source_proxy
     self._tgt_prx = target_proxy
     self.__trv_path = TraversalPath()
     self.__root_is_sequence = \
         (not source_proxy is None and
          source_proxy.proxy_for == RESOURCE_KINDS.COLLECTION) \
         or (not target_proxy is None and
             target_proxy.proxy_for == RESOURCE_KINDS.COLLECTION)
     if __debug__:
         self.__logger = get_logger('everest.traversal')
     else:
         self.__logger = None
예제 #2
0
 def test_basics(self):
     tp = TraversalPath()
     self.assert_equal(len(tp), 0)
     self.assert_is_none(tp.parent)
     self.assert_is_none(tp.relation_operation)
     parent = 'proxy'
     attr = 'attribute'
     key = ('source', 'target')
     rel_op = 'relation_operation'
     tp.push(parent, key, attr, rel_op)
     self.assert_equal(len(tp), 1)
     self.assert_true(key in tp)
     self.assert_equal(tp.parent, parent)
     self.assert_equal(tp.relation_operation, rel_op)
     tp.pop()
     self.assert_equal(len(tp), 0)
     self.assert_false(key in tp)
예제 #3
0
 def __init__(self, source_proxy, target_proxy):
     self._src_prx = source_proxy
     self._tgt_prx = target_proxy
     self.__trv_path = TraversalPath()
     self.__root_is_sequence = \
         (not source_proxy is None and
          source_proxy.proxy_for == RESOURCE_KINDS.COLLECTION) \
         or (not target_proxy is None and
             target_proxy.proxy_for == RESOURCE_KINDS.COLLECTION)
     if __debug__:
         self.__logger = get_logger('everest.traversal')
     else:
         self.__logger = None
예제 #4
0
 def test_basics(self):
     tp = TraversalPath()
     self.assert_equal(len(tp), 0)
     self.assert_is_none(tp.parent)
     self.assert_is_none(tp.relation_operation)
     parent = 'proxy'
     attr = 'attribute'
     key = ('source', 'target')
     rel_op = 'relation_operation'
     tp.push(parent, key, attr, rel_op)
     self.assert_equal(len(tp), 1)
     self.assert_true(key in tp)
     self.assert_equal(tp.parent, parent)
     self.assert_equal(tp.relation_operation, rel_op)
     tp.pop()
     self.assert_equal(len(tp), 0)
     self.assert_false(key in tp)
예제 #5
0
class SourceTargetDataTreeTraverser(object):
    """
    Traverser for synchronous traversal of a source and a target data tree.

    For each data item pair, starting with the root of the source and target
    data tree, iterate over the non-terminal attributes of the associated type
    and obtain the attribute value (child data item) for both the source and
    the target. If the parent source or target data item is `None`, the
    corresponding child data item is also `None`.

    A child node is traversed along the ADD cascade when only the source
    was found, along the REMOVE cascade when only the target was found, and
    along the UPDATE cascade when both source and target were found. Traversal
    is suppressed when the parent attribute does not have the appropriate
    cascading flag set.

    When traversing along the ADD cascade, a child node of a node that is
    being added is only traversed (along the ADD cascade) when it has no ID
    (i.e., ID is None).

    When traversing along the REMOVE cascade, a child node of a node that is
    being removed is also removed if it has an ID (i.e., the "id" attribute
    is not None).
    """
    def __init__(self, source_proxy, target_proxy):
        self._src_prx = source_proxy
        self._tgt_prx = target_proxy
        self.__trv_path = TraversalPath()
        self.__root_is_sequence = \
            (not source_proxy is None and
             source_proxy.proxy_for == RESOURCE_KINDS.COLLECTION) \
            or (not target_proxy is None and
                target_proxy.proxy_for == RESOURCE_KINDS.COLLECTION)
        if __debug__:
            self.__logger = get_logger('everest.traversal')
        else:
            self.__logger = None

    @classmethod
    def make_traverser(cls, source_data, target_data, relation_operation,
                       accessor=None, manage_back_references=True,
                       source_proxy_options=None, target_proxy_options=None):
        """
        Factory method to create a tree traverser depending on the input
        source and target data combination.

        :param source_data: Source data.
        :param target_target: Target data.
        :param str relation_operation: Relation operation. On of the constants
          defined in :class:`everest.constants.RELATION_OPERATIONS`.
        :param accessor: Accessor for looking up target nodes for update
          operations.
        :param bool manage_back_references: Flag passed to the target proxy.
        """
        reg = get_current_registry()
        prx_fac = reg.getUtility(IDataTraversalProxyFactory)
        if relation_operation == RELATION_OPERATIONS.ADD \
           or relation_operation == RELATION_OPERATIONS.UPDATE:
            if relation_operation == RELATION_OPERATIONS.ADD \
               and not target_data is None:
                raise ValueError('Must not provide target data with '
                                 'relation operation ADD.')
            source_proxy = \
                    prx_fac.make_source_proxy(source_data,
                                              options=source_proxy_options)
            source_is_sequence = \
                source_proxy.proxy_for == RESOURCE_KINDS.COLLECTION
            if not source_is_sequence:
                source_id = source_proxy.get_id()
        else:
            source_proxy = None
            source_is_sequence = False
        if relation_operation == RELATION_OPERATIONS.REMOVE \
           or relation_operation == RELATION_OPERATIONS.UPDATE:
            if target_proxy_options is None:
                target_proxy_options = {}
            if relation_operation == RELATION_OPERATIONS.REMOVE:
                if not source_data is None:
                    raise ValueError('Must not provide source data with '
                                     'relation operation REMOVE.')
                target_proxy = prx_fac.make_target_proxy(
                                            target_data,
                                            accessor,
                                            manage_back_references=
                                                     manage_back_references,
                                            options=target_proxy_options)
            else: # UPDATE
                if accessor is None:
                    raise ValueError('Need to provide an accessor when '
                                     'performing UPDATE operations.')
                if not target_data is None:
                    target_root = target_data
                elif not source_is_sequence:
                    # Look up the (single) target to update.
                    target_root = accessor.get_by_id(source_id)
                    if target_root is None:
                        raise ValueError('Entity with ID %s to update not '
                                         'found.' % source_id)
                else:
                    # Look up collection of targets to update.
                    target_root = []
                    for src_prx in source_proxy:
                        tgt_ent_id = src_prx.get_id()
                        if tgt_ent_id is None:
                            continue
                        tgt_ent = accessor.get_by_id(tgt_ent_id)
                        if tgt_ent is None:
                            continue
                        target_root.append(tgt_ent)
                target_proxy = prx_fac.make_target_proxy(
                                            target_root,
                                            accessor,
                                            manage_back_references=
                                                 manage_back_references,
                                            options=target_proxy_options)
            target_is_sequence = \
                    target_proxy.proxy_for == RESOURCE_KINDS.COLLECTION
        else:
            target_proxy = None
            target_is_sequence = False
        if not source_proxy is None and not target_proxy is None:
            # Check for source/target consistency.
            if not ((source_is_sequence and target_is_sequence) or
                    (not source_is_sequence and not target_is_sequence)):
                raise ValueError('When both source and target root nodes are '
                                 'given, they can either both be sequences '
                                 'or both not be sequences.')
        return cls(source_proxy, target_proxy)

    def run(self, visitor):
        """
        :param visitor: visitor to call with every node in the domain tree.
        :type visitor: subclass of
            :class:`everest.entities.traversal.DomainVisitor`
        """
        if __debug__:
            self.__log_run(visitor)
        visitor.prepare()
        if self.__root_is_sequence:
            if not self._tgt_prx is None:
                tgts = iter(self._tgt_prx)
            else:
                tgts = None
            if not self._src_prx is None:
                srcs = iter(self._src_prx)
            else:
                srcs = None
            self.traverse_many(None, srcs, tgts, visitor)
        else:
            self.traverse_one(None, self._src_prx, self._tgt_prx, visitor)
        visitor.finalize()

    def traverse_one(self, attribute, source, target, visitor):
        """
        :param source: source data proxy
        :type source: instance of `DataTraversalProxy` or None
        :param target: target data proxy
        :type target: instance of `DataTraversalProxy` or None
        """
        if __debug__:
            self.__log_traverse_one(self.__trv_path, attribute, source, target)
        prx = source or target
        if prx.do_traverse():
            rel_op = RELATION_OPERATIONS.check(source, target)
            for attr in prx.get_relationship_attributes():
                # Check cascade settings.
                if not bool(attr.cascade & rel_op):
                    continue
                if not source is None:
                    try:
                        attr_source = source.get_attribute_proxy(attr)
                    except AttributeError:
                        # If the source does not have the attribute set, we
                        # do nothing (as opposed to when the value is None).
                        continue
                else:
                    attr_source = None
                if not target is None:
                    attr_target = target.get_attribute_proxy(attr)
                else:
                    attr_target = None
                attr_rel_op = RELATION_OPERATIONS.check(attr_source,
                                                        attr_target)
                if attr_rel_op == RELATION_OPERATIONS.ADD:
                    if rel_op == RELATION_OPERATIONS.ADD:
                        parent = source
                    else:
                        parent = target
                elif attr_rel_op == RELATION_OPERATIONS.REMOVE:
                    parent = target
                else: # UPDATE
                    parent = target
                card = get_attribute_cardinality(attr)
                if card == CARDINALITY_CONSTANTS.ONE:
                    if attr_source is None and attr_target is None:
                        # If both source and target have None values, there is
                        # nothing to do.
                        continue
                    if attr_rel_op == RELATION_OPERATIONS.ADD:
#                        if not attr_source.get_id() is None:
#                            # We only ADD new items.
#                            continue
                        src_items = [attr_source]
                        tgt_items = None
                    elif attr_rel_op == RELATION_OPERATIONS.REMOVE:
                        src_items = None
                        tgt_items = [attr_target]
                    else: # UPDATE
                        src_items = [attr_source]
                        tgt_items = [attr_target]
                        src_id = attr_source.get_id()
                        tgt_id = attr_target.get_id()
                        if src_id != tgt_id:
                            if not src_id is None:
                                # If the source ID is None, this is a replace
                                # operation (ADD source, REMOVE target).
                                src_target = attr_target.get_matching(src_id)
                                if not src_target is None:
                                    tgt_items.append(src_target)
                else:
                    src_items = attr_source
                    tgt_items = attr_target
                self.__trv_path.push(parent, (source, target), attr, rel_op)
                self.traverse_many(attr, src_items, tgt_items, visitor)
                self.__trv_path.pop() # path.pop()
        visitor.visit(self.__trv_path, attribute, source, target)

    def traverse_many(self, attribute, source_sequence, target_sequence,
                      visitor):
        """
        Traverses the given source and target sequences and makes appropriate
        calls to :method:`traverse_one`.

        Algorithm:
        1) Build a map target item ID -> target data item from the target
           sequence;
        2) For each source data item in the source sequence check if it
           has a not-None ID; if yes, remove the corresponding target from the
           map generated in step 1) and use as target data item for the
           source data item; if no, use `None` as target data item;
        3) For the remaining items in the target map from 1), call
           :method:`traverse_one` passing `None` as source (REMOVE);
        4) For all source/target data item pairs generated in 2, call
           :method:`traverse_one` (ADD or UPDATE depending on whether target
           item is `None`).

        :param source_sequence: iterable of source data proxies
        :type source_sequence: iterator yielding instances of
                               `DataTraversalProxy` or None
        :param target_sequence: iterable of target data proxies
        :type target_sequence: iterator yielding instances of
                               `DataTraversalProxy` or None
        """
        target_map = {}
        if not target_sequence is None:
            for target in target_sequence:
                target_map[target.get_id()] = target
        src_tgt_pairs = []
        if not source_sequence is None:
            for source in source_sequence:
                source_id = source.get_id()
                if not source_id is None:
                    # Check if target exists for UPDATE.
                    target = target_map.pop(source_id, None)
                else:
                    # Source is new, there is no target, so ADD.
                    target = None
                src_tgt_pairs.append((source, target))
        # All targets that are now still in the map where not present in the
        # source and therefore need to be REMOVEd.
        for target in itervalues_(target_map):
            if not (None, target) in self.__trv_path:
                self.traverse_one(attribute, None, target, visitor)
        #
        for source, target in src_tgt_pairs:
            if not (source, target) in self.__trv_path:
                self.traverse_one(attribute, source, target, visitor)

    def __log_run(self, visitor):
        self.__logger.debug('Traversing %s->%s with %s'
                            % (self._src_prx, self._tgt_prx, visitor))

    def __log_traverse_one(self, path, attribute, source, target):
        if target is None:
            mode = 'ADD'
            data = '%s,None' % source
        elif source is None:
            mode = 'REMOVE'
            data = 'None,%s' % target
        else:
            mode = 'UPDATE'
            data = '%s,%s' % (source, target)
        if not attribute is None:
            parent = "(%s)" % path.parent
            self.__logger.debug('%s%s %s.%s (%s)' %
                                ("  "*len(path), mode, parent,
                                 attribute.resource_attr, data))
        else:
            self.__logger.debug('%s ROOT (%s)' % (mode, data))
예제 #6
0
class SourceTargetDataTreeTraverser(object):
    """
    Traverser for synchronous traversal of a source and a target data tree.

    For each data item pair, starting with the root of the source and target
    data tree, iterate over the non-terminal attributes of the associated type
    and obtain the attribute value (child data item) for both the source and
    the target. If the parent source or target data item is `None`, the
    corresponding child data item is also `None`.

    A child node is traversed along the ADD cascade when only the source
    was found, along the REMOVE cascade when only the target was found, and
    along the UPDATE cascade when both source and target were found. Traversal
    is suppressed when the parent attribute does not have the appropriate
    cascading flag set.

    When traversing along the ADD cascade, a child node of a node that is
    being added is only traversed (along the ADD cascade) when it has no ID
    (i.e., ID is None).

    When traversing along the REMOVE cascade, a child node of a node that is
    being removed is also removed if it has an ID (i.e., the "id" attribute
    is not None).
    """
    def __init__(self, source_proxy, target_proxy):
        self._src_prx = source_proxy
        self._tgt_prx = target_proxy
        self.__trv_path = TraversalPath()
        self.__root_is_sequence = \
            (not source_proxy is None and
             source_proxy.proxy_for == RESOURCE_KINDS.COLLECTION) \
            or (not target_proxy is None and
                target_proxy.proxy_for == RESOURCE_KINDS.COLLECTION)
        if __debug__:
            self.__logger = get_logger('everest.traversal')
        else:
            self.__logger = None

    @classmethod
    def make_traverser(cls,
                       source_data,
                       target_data,
                       relation_operation,
                       accessor=None,
                       manage_back_references=True):
        """
        Factory method to create a tree traverser depending on the input
        source and target data combination.

        :param source_data: Source data.
        :param target_target: Target data.
        :param str relation_operation: Relation operation. On of the constants
          defined in :class:`everest.constants.RELATION_OPERATIONS`.
        :param accessor: Accessor for looking up target nodes for update
          operations.
        :param bool manage_back_references: If set, backreferences will
          automatically be updated in the target data.
        """
        reg = get_current_registry()
        prx_fac = reg.getUtility(IDataTraversalProxyFactory)
        if relation_operation == RELATION_OPERATIONS.ADD \
           or relation_operation == RELATION_OPERATIONS.UPDATE:
            if relation_operation == RELATION_OPERATIONS.ADD \
               and not target_data is None:
                raise ValueError('Must not provide target data with '
                                 'relation operation ADD.')
            source_proxy = prx_fac.make_proxy(source_data, None,
                                              RELATIONSHIP_DIRECTIONS.NONE,
                                              relation_operation)
            source_is_sequence = \
                source_proxy.proxy_for == RESOURCE_KINDS.COLLECTION
            if not source_is_sequence:
                source_id = source_proxy.get_id()
        else:
            source_proxy = None
            source_is_sequence = False
        if relation_operation == RELATION_OPERATIONS.REMOVE \
           or relation_operation == RELATION_OPERATIONS.UPDATE:
            rel_dir = RELATIONSHIP_DIRECTIONS.BIDIRECTIONAL
            if not manage_back_references:
                rel_dir &= ~RELATIONSHIP_DIRECTIONS.REVERSE
            if relation_operation == RELATION_OPERATIONS.REMOVE:
                if not source_data is None:
                    raise ValueError('Must not provide source data with '
                                     'relation operation REMOVE.')
                target_proxy = prx_fac.make_proxy(target_data, accessor,
                                                  rel_dir, relation_operation)
            else:  # UPDATE
                if accessor is None:
                    raise ValueError('Need to provide an accessor when '
                                     'performing UPDATE operations.')
                if not target_data is None:
                    target_root = target_data
                elif not source_is_sequence:
                    # Look up the (single) target to update.
                    target_root = accessor.get_by_id(source_id)
                    if target_root is None:
                        raise ValueError('Entity with ID %s to update not '
                                         'found.' % source_id)
                else:
                    # Look up collection of targets to update.
                    target_root = []
                    for src_prx in source_proxy:
                        tgt_ent_id = src_prx.get_id()
                        if tgt_ent_id is None:
                            continue
                        tgt_ent = accessor.get_by_id(tgt_ent_id)
                        if tgt_ent is None:
                            continue
                        target_root.append(tgt_ent)
                target_proxy = prx_fac.make_proxy(target_root, accessor,
                                                  rel_dir, relation_operation)
            target_is_sequence = \
                    target_proxy.proxy_for == RESOURCE_KINDS.COLLECTION
        else:
            target_proxy = None
            target_is_sequence = False
        if not source_proxy is None and not target_proxy is None:
            # Check for source/target consistency.
            if not ((source_is_sequence and target_is_sequence) or
                    (not source_is_sequence and not target_is_sequence)):
                raise ValueError('When both source and target root nodes are '
                                 'given, they can either both be sequences '
                                 'or both not be sequences.')
        return cls(source_proxy, target_proxy)

    def run(self, visitor):
        """
        :param visitor: visitor to call with every node in the domain tree.
        :type visitor: subclass of
            :class:`everest.entities.traversal.DomainVisitor`
        """
        if __debug__:
            self.__log_run(visitor)
        visitor.prepare()
        if self.__root_is_sequence:
            if not self._tgt_prx is None:
                tgts = iter(self._tgt_prx)
            else:
                tgts = None
            if not self._src_prx is None:
                srcs = iter(self._src_prx)
            else:
                srcs = None
            self.traverse_many(None, srcs, tgts, visitor)
        else:
            self.traverse_one(None, self._src_prx, self._tgt_prx, visitor)
        visitor.finalize()

    def traverse_one(self, attribute, source, target, visitor):
        """
        :param source: source data proxy
        :type source: instance of `DataTraversalProxy` or None
        :param target: target data proxy
        :type target: instance of `DataTraversalProxy` or None
        """
        if __debug__:
            self.__log_traverse_one(self.__trv_path, attribute, source, target)
        prx = source or target
        rel_op = RELATION_OPERATIONS.check(source, target)
        if prx.do_traverse() \
           and (rel_op == prx.relation_operation or attribute is None):
            for attr in prx.get_relationship_attributes():
                # Check cascade settings.
                if not bool(attr.cascade & rel_op):
                    continue
                if not source is None:
                    try:
                        attr_source = source.get_attribute_proxy(attr)
                    except AttributeError:
                        # If the source does not have the attribute set, we
                        # do nothing (as opposed to when the value is None).
                        continue
                else:
                    attr_source = None
                if not target is None:
                    attr_target = target.get_attribute_proxy(attr)
                else:
                    attr_target = None
                attr_rel_op = RELATION_OPERATIONS.check(
                    attr_source, attr_target)
                if attr_rel_op == RELATION_OPERATIONS.ADD:
                    if rel_op == RELATION_OPERATIONS.ADD:
                        parent = source
                    else:
                        parent = target
                elif attr_rel_op == RELATION_OPERATIONS.REMOVE:
                    parent = target
                else:  # UPDATE
                    parent = target
                card = get_attribute_cardinality(attr)
                if card == CARDINALITY_CONSTANTS.ONE:
                    if attr_source is None and attr_target is None:
                        # If both source and target have None values, there is
                        # nothing to do.
                        continue
                    if attr_rel_op == RELATION_OPERATIONS.ADD:
                        #                        if not attr_source.get_id() is None:
                        #                            # We only ADD new items.
                        #                            continue
                        src_items = [attr_source]
                        tgt_items = None
                    elif attr_rel_op == RELATION_OPERATIONS.REMOVE:
                        src_items = None
                        tgt_items = [attr_target]
                    else:  # UPDATE
                        src_items = [attr_source]
                        tgt_items = [attr_target]
                        src_id = attr_source.get_id()
                        tgt_id = attr_target.get_id()
                        if src_id != tgt_id:
                            if not src_id is None:
                                # If the source ID is None, this is a replace
                                # operation (ADD source, REMOVE target).
                                src_target = attr_target.get_matching(src_id)
                                if not src_target is None:
                                    tgt_items.append(src_target)
                else:
                    src_items = attr_source
                    tgt_items = attr_target
                self.__trv_path.push(parent, (source, target), attr, rel_op)
                self.traverse_many(attr, src_items, tgt_items, visitor)
                self.__trv_path.pop()  # path.pop()
        visitor.visit(self.__trv_path, attribute, source, target)

    def traverse_many(self, attribute, source_sequence, target_sequence,
                      visitor):
        """
        Traverses the given source and target sequences and makes appropriate
        calls to :method:`traverse_one`.

        Algorithm:
        1) Build a map target item ID -> target data item from the target
           sequence;
        2) For each source data item in the source sequence check if it
           has a not-None ID; if yes, remove the corresponding target from the
           map generated in step 1) and use as target data item for the
           source data item; if no, use `None` as target data item;
        3) For the remaining items in the target map from 1), call
           :method:`traverse_one` passing `None` as source (REMOVE);
        4) For all source/target data item pairs generated in 2, call
           :method:`traverse_one` (ADD or UPDATE depending on whether target
           item is `None`).

        :param source_sequence: iterable of source data proxies
        :type source_sequence: iterator yielding instances of
                               `DataTraversalProxy` or None
        :param target_sequence: iterable of target data proxies
        :type target_sequence: iterator yielding instances of
                               `DataTraversalProxy` or None
        """
        target_map = {}
        if not target_sequence is None:
            for target in target_sequence:
                target_map[target.get_id()] = target
        src_tgt_pairs = []
        if not source_sequence is None:
            for source in source_sequence:
                source_id = source.get_id()
                if not source_id is None:
                    # Check if target exists for UPDATE.
                    target = target_map.pop(source_id, None)
                else:
                    # Source is new, there is no target, so ADD.
                    target = None
                src_tgt_pairs.append((source, target))
        # All targets that are now still in the map where not present in the
        # source and therefore need to be REMOVEd.
        for target in itervalues_(target_map):
            if not (None, target) in self.__trv_path:
                self.traverse_one(attribute, None, target, visitor)
        #
        for source, target in src_tgt_pairs:
            if not (source, target) in self.__trv_path:
                self.traverse_one(attribute, source, target, visitor)

    def __log_run(self, visitor):
        self.__logger.debug('Traversing %s->%s with %s' %
                            (self._src_prx, self._tgt_prx, visitor))

    def __log_traverse_one(self, path, attribute, source, target):
        if target is None:
            mode = 'ADD'
            data = '%s,None' % source
        elif source is None:
            mode = 'REMOVE'
            data = 'None,%s' % target
        else:
            mode = 'UPDATE'
            data = '%s,%s' % (source, target)
        if not attribute is None:
            parent = "(%s)" % path.parent
            self.__logger.debug('%s%s %s.%s (%s)' %
                                ("  " * len(path), mode, parent,
                                 attribute.resource_attr, data))
        else:
            self.__logger.debug('%s ROOT (%s)' % (mode, data))