text: #!/usr/bin/env python
  # -*- encoding: utf-8 -*-
  
  # Copyright 2011-2020, Nigel Small
  #
  # Licensed under the Apache License, Version 2.0 (the "License");
  # you may not use this file except in compliance with the License.
  # You may obtain a copy of the License at
  #
  #     http://www.apache.org/licenses/LICENSE-2.0
  #
  # Unless required by applicable law or agreed to in writing, software
  # distributed under the License is distributed on an "AS IS" BASIS,
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  # See the License for the specific language governing permissions and
  # limitations under the License.
  
  
  __all__ = [
      "Property",
      "Label",
      "Related",
      "RelatedTo",
      "RelatedFrom",
      "RelatedObjects",
      "GraphObjectType",
      "GraphObject",
      "GraphObjectMatch",
      "GraphObjectMatcher",
      "Repository",
  ]
  
  
  from py2neo.compat import metaclass, deprecated
  from py2neo.cypher import cypher_escape
  from py2neo.data import Node, PropertyDict
  from py2neo.database import Graph
  from py2neo.matching import NodeMatch, NodeMatcher
  from py2neo.text import Words
  
  
  OUTGOING = 1
  UNDIRECTED = 0
  INCOMING = -1
  
  
  class Property(object):
      """ Property definition for a :class:`.GraphObject`.
  
      Attributes:
          key: The name of the node property within the database.
          default: The default value for the property, if it would
                   otherwise be :const:`None`.
      """
  
      def __init__(self, key=None, default=None):
          """ Initialise a property definition.
  
          Args:
              key: The name of the node property within the database. If
                   omitted, the name of the class attribute is used.
              default: The default value for the property, if it would
                       otherwise be :const:`None`.
          """
          self.key = key
          self.default = default
  
      def __get__(self, instance, owner):
          value = instance.__node__[self.key]
          if value is None:
              value = self.default
          return value
  
      def __set__(self, instance, value):
          instance.__node__[self.key] = value
  
  
  class Label(object):
      """ Label definition for a :class:`.GraphObject`.
  
      Labels are toggleable tags applied to an object that can be used as type
      information or other forms of classification.
      """
  
      def __init__(self, name=None):
          self.name = name
  
      def __get__(self, instance, owner):
          return instance.__node__.has_label(self.name)
  
      def __set__(self, instance, value):
          if value:
              instance.__node__.add_label(self.name)
          else:
              instance.__node__.remove_label(self.name)
  
  
  class Related(object):
      """ Descriptor for a set of related objects in a :class:`.GraphObject`.
  
      Attributes:
          related_class: The class of object to which these relationships
                         connect. This class is used to coerce nodes to and
                         from :class:`GraphObject` instances.
          relationship_type: The underlying relationship type for these
                             relationships. Note that the relationship
                             type should be unique for each class of related
                             object as the `related_class` is only used for
                             object coercion and not as part of the underlying
                             database query.
      """
  
      direction = UNDIRECTED
  
      def __init__(self, related_class, relationship_type=None):
          """ Initialise a property definition.
  
          Args:
              related_class: The class of object to which these relationships
                             connect.
              relationship_type: The underlying relationship type for these
                                 relationships.
          """
          self.related_class = related_class
          self.relationship_type = relationship_type
  
      def __get__(self, instance, owner):
          return instance.__ogm__.related(self.direction, self.relationship_type,
                                          self._resolve_class(self.related_class, instance))
  
      @classmethod
      def _resolve_class(cls, ogm_class, instance):
          if isinstance(ogm_class, type):
              return ogm_class
          module_name, _, class_name = ogm_class.rpartition(".")
          if not module_name:
              module_name = instance.__class__.__module__
          module = __import__(module_name, fromlist=".")
          return getattr(module, class_name)
  
  
  class RelatedTo(Related):
      """ Descriptor for a set of related objects for a :class:`.GraphObject`
      that are connected by outgoing relationships.
      """
  
      direction = OUTGOING
  
  
  class RelatedFrom(Related):
      """ Descriptor for a set of related objects for a :class:`.GraphObject`
      that are connected by incoming relationships.
      """
  
      direction = INCOMING
  
  
  class RelatedObjects(object):
      """ A set of similarly-typed and similarly-related objects,
      relative to a central node.
      """
  
      def __init__(self, node, direction, relationship_type, related_class):
          assert isinstance(direction, int) and not isinstance(direction, bool)
          self.node = node
          self.related_class = related_class
          self.__related_objects = None
          if direction > 0:
              self.__match_args = {"nodes": (self.node, None), "r_type": relationship_type}
              self.__start_node = False
              self.__end_node = True
              self.__relationship_pattern = "(a)-[_:%s]->(b)" % cypher_escape(relationship_type)
          elif direction < 0:
              self.__match_args = {"nodes": (None, self.node), "r_type": relationship_type}
              self.__start_node = True
              self.__end_node = False
              self.__relationship_pattern = "(a)<-[_:%s]-(b)" % cypher_escape(relationship_type)
          else:
              self.__match_args = {"nodes": {self.node, None}, "r_type": relationship_type}
              self.__start_node = True
              self.__end_node = True
              self.__relationship_pattern = "(a)-[_:%s]-(b)" % cypher_escape(relationship_type)
  
      def __iter__(self):
          for obj, _ in self._related_objects:
              yield obj
  
      def __len__(self):
          return len(self._related_objects)
  
      def __contains__(self, obj):
          if not isinstance(obj, GraphObject):
              raise TypeError("Related objects must be GraphObject instances")
          for related_object, _ in self._related_objects:
              if related_object == obj:
                  return True
          return False
  
      @property
      def _related_objects(self):
          if self.__related_objects is None:
              self.__related_objects = []
              if self.node.graph:
                  with self.node.graph.begin() as tx:
                      self.__db_pull__(tx)
          return self.__related_objects
  
      def add(self, obj, properties=None, **kwproperties):
          """ Add or update a related object.
  
          :param obj: the :py:class:`.GraphObject` to relate
          :param properties: dictionary of properties to attach to the relationship (optional)
          :param kwproperties: additional keyword properties (optional)
          """
          if not isinstance(obj, GraphObject):
              raise TypeError("Related objects must be GraphObject instances")
          related_objects = self._related_objects
          properties = dict(properties or {}, **kwproperties)
          added = False
          for i, (related_object, p) in enumerate(related_objects):
              if related_object == obj:
                  related_objects[i] = (obj, PropertyDict(p, **properties))
                  added = True
          if not added:
              related_objects.append((obj, properties))
  
      def clear(self):
          """ Remove all related objects from this set.
          """
          self._related_objects[:] = []
  
      def get(self, obj, key, default=None):
          """ Return a relationship property associated with a specific related object.
  
          :param obj: related object
          :param key: relationship property key
          :param default: default value, in case the key is not found
          :return: property value
          """
          if not isinstance(obj, GraphObject):
              raise TypeError("Related objects must be GraphObject instances")
          for related_object, properties in self._related_objects:
              if related_object == obj:
                  return properties.get(key, default)
          return default
  
      def remove(self, obj):
          """ Remove a related object.
  
          :param obj: the :py:class:`.GraphObject` to separate
          """
          if not isinstance(obj, GraphObject):
              raise TypeError("Related objects must be GraphObject instances")
          related_objects = self._related_objects
          related_objects[:] = [(related_object, properties)
                                for related_object, properties in related_objects
                                if related_object != obj]
  
      @deprecated("RelatedObjects.update is deprecated, "
                  "please use RelatedObjects.add instead")
      def update(self, obj, properties=None, **kwproperties):
          """ Add or update a related object.
  
          :param obj: the :py:class:`.GraphObject` to relate
          :param properties: dictionary of properties to attach to the relationship (optional)
          :param kwproperties: additional keyword properties (optional)
          """
          self.add(obj, properties, **kwproperties)
  
      def __db_pull__(self, tx):
          related_objects = {}
          for r in tx.graph.match(**self.__match_args):
              nodes = []
              n = self.node
              a = r.start_node
              b = r.end_node
              if a == b:
                  nodes.append(a)
              else:
                  if self.__start_node and a != n:
                      nodes.append(r.start_node)
                  if self.__end_node and b != n:
                      nodes.append(r.end_node)
              for node in nodes:
                  related_object = self.related_class.wrap(node)
                  related_objects[node] = (related_object, PropertyDict(r))
          self._related_objects[:] = related_objects.values()
  
      def __db_push__(self, tx):
          related_objects = self._related_objects
          # 1. merge all nodes (create ones that don't)
          for related_object, _ in related_objects:
              tx.merge(related_object)
          # 2a. remove any relationships not in list of nodes
          subject_id = self.node.identity
          tx.run("MATCH %s WHERE id(a) = $x AND NOT id(b) IN $y DELETE _" % self.__relationship_pattern,
                 x=subject_id, y=[obj.__node__.identity for obj, _ in related_objects])
          # 2b. merge all relationships
          for related_object, properties in related_objects:
              tx.run("MATCH (a) WHERE id(a) = $x MATCH (b) WHERE id(b) = $y "
                     "MERGE %s SET _ = $z" % self.__relationship_pattern,
                     x=subject_id, y=related_object.__node__.identity, z=properties)
  
  
  class OGM(object):
  
      def __init__(self, node):
          self.node = node
          self._related = {}
  
      def all_related(self):
          """ Return an iterator through all :class:`.RelatedObjects`.
          """
          return iter(self._related.values())
  
      def related(self, direction, relationship_type, related_class):
          """ Return :class:`.RelatedObjects` for given criteria.
          """
          key = (direction, relationship_type)
          if key not in self._related:
              self._related[key] = RelatedObjects(self.node, direction, relationship_type, related_class)
          return self._related[key]
  
  
  class GraphObjectType(type):
  
      def __new__(mcs, name, bases, attributes):
          for attr_name, attr in list(attributes.items()):
              if isinstance(attr, Property):
                  if attr.key is None:
                      attr.key = attr_name
              elif isinstance(attr, Label):
                  if attr.name is None:
                      attr.name = Words(attr_name).camel(upper_first=True)
              elif isinstance(attr, Related):
                  if attr.relationship_type is None:
                      attr.relationship_type = Words(attr_name).upper("_")
  
          attributes.setdefault("__primarylabel__", name)
  
          primary_key = attributes.get("__primarykey__")
          if primary_key is None:
              for base in bases:
                  if primary_key is None and hasattr(base, "__primarykey__"):
                      primary_key = getattr(base, "__primarykey__")
                      break
              else:
                  primary_key = "__id__"
              attributes["__primarykey__"] = primary_key
  
          return super(GraphObjectType, mcs).__new__(mcs, name, bases, attributes)
  
  
  @metaclass(GraphObjectType)
  class GraphObject(object):
      """ The base class for all OGM classes.
      """
  
      __primarylabel__ = None
      __primarykey__ = None
  
      __ogm = None
  
      def __eq__(self, other):
          if self is other:
              return True
          try:
              self_node = self.__node__
              other_node = other.__node__
              if any(x is None for x in [self_node.graph, other_node.graph, self_node.identity, other_node.identity]):
                  return self.__primarylabel__ == other.__primarylabel__ and \
                         self.__primarykey__ == other.__primarykey__ and \
                         self.__primaryvalue__ == other.__primaryvalue__
              return (type(self) is type(other) and
                      self_node.graph == other_node.graph and
                      self_node.identity == other_node.identity)
          except (AttributeError, TypeError):
              return False
  
      def __ne__(self, other):
          return not self.__eq__(other)
  
      @property
      def __ogm__(self):
          if self.__ogm is None:
              self.__ogm = OGM(Node(self.__primarylabel__))
          node = self.__ogm.node
          if not hasattr(node, "__primarylabel__"):
              setattr(node, "__primarylabel__", self.__primarylabel__)
          if not hasattr(node, "__primarykey__"):
              setattr(node, "__primarykey__", self.__primarykey__)
          return self.__ogm
  
      @classmethod
      def wrap(cls, node):
          """ Convert a :class:`.Node` into a :class:`.GraphObject`.
  
          :param node:
          :return:
          """
          if node is None:
              return None
          inst = GraphObject()
          inst.__ogm = OGM(node)
          inst.__class__ = cls
          return inst
  
      @classmethod
      def match(cls, repository, primary_value=None):
          """ Select one or more nodes from the database, wrapped as instances of this class.
  
          :param repository: the :class:`.Repository` in which to match
          :param primary_value: value of the primary property (optional)
          :rtype: :class:`.GraphObjectMatch`
          """
          return GraphObjectMatcher(cls, repository).match(primary_value)
  
      def __repr__(self):
          return "<%s %s=%r>" % (self.__class__.__name__, self.__primarykey__, self.__primaryvalue__)
  
      @property
      def __primaryvalue__(self):
          node = self.__node__
          primary_key = self.__primarykey__
          if primary_key == "__id__":
              return node.identity
          else:
              return node[primary_key]
  
      @property
      def __node__(self):
          """ The :class:`.Node` wrapped by this :class:`.GraphObject`.
          """
          return self.__ogm__.node
  
      def __db_create__(self, tx):
          self.__db_merge__(tx)
  
      def __db_delete__(self, tx):
          ogm = self.__ogm__
          tx.delete(ogm.node)
          for related_objects in ogm.all_related():
              related_objects.clear()
  
      def __db_exists__(self, tx):
          return tx.exists(self.__node__)
  
      def __db_merge__(self, tx, primary_label=None, primary_key=None):
          ogm = self.__ogm__
          node = ogm.node
          if primary_label is None:
              primary_label = getattr(node, "__primarylabel__", None)
          if primary_key is None:
              primary_key = getattr(node, "__primarykey__", "__id__")
          if node.graph is None:
              if primary_key == "__id__":
                  node.add_label(primary_label)
                  tx.create(node)
              else:
                  tx.merge(node, primary_label, primary_key)
              for related_objects in ogm.all_related():
                  related_objects.__db_push__(tx)
  
      def __db_pull__(self, tx):
          ogm = self.__ogm__
          if ogm.node.graph is None:
              matcher = GraphObjectMatcher(self.__class__, tx.graph)
              matcher._match_class = NodeMatch
              ogm.node = matcher.match(self.__primaryvalue__).first()
          tx.pull(ogm.node)
          for related_objects in ogm.all_related():
              related_objects.__db_pull__(tx)
  
      def __db_push__(self, tx):
          ogm = self.__ogm__
          node = ogm.node
          if node.graph is not None:
              tx.push(node)
          else:
              primary_key = getattr(node, "__primarykey__", "__id__")
              if primary_key == "__id__":
                  tx.create(node)
              else:
                  tx.merge(node)
          for related_objects in ogm.all_related():
              related_objects.__db_push__(tx)
  
  
  class GraphObjectMatch(NodeMatch):
      """ A selection of :class:`.GraphObject` instances that match a
      given set of criteria.
      """
  
      _object_class = GraphObject
  
      def __iter__(self):
          """ Iterate through items drawn from the underlying repository
          that match the given criteria.
          """
          wrap = self._object_class.wrap
          for node in super(GraphObjectMatch, self).__iter__():
              yield wrap(node)
  
      def first(self):
          """ Return the first item that matches the given criteria.
          """
          return self._object_class.wrap(super(GraphObjectMatch, self).first())
  
  
  class GraphObjectMatcher(NodeMatcher):
  
      _match_class = GraphObjectMatch
  
      @classmethod
      def _coerce_to_graph(cls, obj):
          if isinstance(obj, Repository):
              return obj.graph
          elif isinstance(obj, Graph):
              return obj
          else:
              raise TypeError("Cannot coerce object %r to Graph" % obj)
  
      def __init__(self, object_class, repository):
          NodeMatcher.__init__(self, self._coerce_to_graph(repository))
          self._object_class = object_class
          self._match_class = type("%sMatch" % self._object_class.__name__,
                                   (GraphObjectMatch,), {"_object_class": object_class})
  
      def match(self, primary_value=None):
          cls = self._object_class
          if cls.__primarykey__ == "__id__":
              match = NodeMatcher.match(self, cls.__primarylabel__).where("id(_) = %d" % primary_value)
          elif primary_value is None:
              match = NodeMatcher.match(self, cls.__primarylabel__)
          else:
              match = NodeMatcher.match(self, cls.__primarylabel__).where(**{cls.__primarykey__: primary_value})
          return match
  
  
  class Repository(object):
      """ Storage container for :class:`.GraphObject` instances.
      """
  
      @classmethod
      def wrap(cls, graph):
          """ Wrap an existing :class:`.Graph` object as a
          :class:`.Repository`.
          """
          obj = object.__new__(Repository)
          obj.graph = graph
          return obj
  
      def __init__(self, profile=None, name=None, **settings):
          self.graph = Graph(profile, name=name, **settings)
  
      def reload(self, obj):
          """ Reload data from the remote graph into the local object.
          """
          self.graph.pull(obj)
  
      def save(self, *objects):
          """ Save data from the local object into the remote graph.
          """
  
          def push_all(tx):
              for obj in objects:
                  tx.push(obj)
  
          self.graph.play(push_all)
  
      def delete(self, obj):
          """ Delete the object in the remote graph.
          """
          self.graph.delete(obj)
  
      def exists(self, obj):
          """ Check whether the object exists in the remote graph.
          """
          return self.graph.exists(obj)
  
      def match(self, ogm_class, primary_value=None):
          """ Select one or more objects from the remote graph.
  
          :param ogm_class: the :class:`.GraphObject` subclass to match
          :param primary_value: value of the primary property (optional)
          :rtype: :class:`.GraphObjectMatch`
          """
          return GraphObjectMatcher(ogm_class, self).match(primary_value)
  
      def get(self, ogm_class, primary_value=None):
          """ Match and return a single object from the remote graph.
  
          :param ogm_class: the :class:`.GraphObject` subclass to match
          :param primary_value: value of the primary property (optional)
          :rtype: :class:`.GraphObject`
          """
          return self.match(ogm_class, primary_value).first()
  
      @deprecated("Repository.create is a compatibility alias, "
                  "please use Repository.save instead")
      def create(self, obj):
          self.graph.create(obj)
  
      @deprecated("Repository.merge is a compatibility alias, "
                  "please use Repository.save instead")
      def merge(self, obj):
          self.graph.merge(obj)
  
      @deprecated("Repository.pull is a compatibility alias, "
                  "please use Repository.load instead")
      def pull(self, obj):
          self.graph.pull(obj)
  
      @deprecated("Repository.push is a compatibility alias, "
                  "please use Repository.save instead")
      def push(self, obj):
          self.graph.push(obj)
  