__init__.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. # Copyright (c) "Neo4j"
  2. # Neo4j Sweden AB [https://neo4j.com]
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # https://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. """Graph data types as returned by the DBMS."""
  16. from __future__ import annotations
  17. import typing as t
  18. from collections.abc import Mapping
  19. from .._meta import (
  20. deprecated,
  21. deprecation_warn,
  22. )
  23. __all__ = [
  24. "Graph",
  25. "Node",
  26. "Path",
  27. "Relationship",
  28. ]
  29. _T = t.TypeVar("_T")
  30. class Graph:
  31. """
  32. A graph of nodes and relationships.
  33. Local, self-contained graph object that acts as a container for
  34. :class:`.Node` and :class:`.Relationship` instances.
  35. This is typically obtained via :meth:`.Result.graph` or
  36. :meth:`.AsyncResult.graph`.
  37. """
  38. def __init__(self) -> None:
  39. self._nodes: dict[str, Node] = {}
  40. self._legacy_nodes: dict[int, Node] = {} # TODO: 6.0 - remove
  41. self._relationships: dict[str, Relationship] = {}
  42. # TODO: 6.0 - remove
  43. self._legacy_relationships: dict[int, Relationship] = {}
  44. self._relationship_types: dict[str, type[Relationship]] = {}
  45. self._node_set_view = EntitySetView(self._nodes, self._legacy_nodes)
  46. self._relationship_set_view = EntitySetView(
  47. self._relationships, self._legacy_relationships
  48. )
  49. @property
  50. def nodes(self) -> EntitySetView[Node]:
  51. """Access a set view of the nodes in this graph."""
  52. return self._node_set_view
  53. @property
  54. def relationships(self) -> EntitySetView[Relationship]:
  55. """Access a set view of the relationships in this graph."""
  56. return self._relationship_set_view
  57. def relationship_type(self, name: str) -> type[Relationship]:
  58. """Obtain the relationship class for a given relationship type name."""
  59. try:
  60. cls = self._relationship_types[name]
  61. except KeyError:
  62. cls = self._relationship_types[name] = t.cast(
  63. t.Type[Relationship], type(str(name), (Relationship,), {})
  64. )
  65. return cls
  66. def __reduce__(self):
  67. state = self.__dict__.copy()
  68. relationship_types = tuple(state.pop("_relationship_types", {}).keys())
  69. restore_args = (relationship_types,)
  70. return Graph._restore, restore_args, state
  71. @staticmethod
  72. def _restore(relationship_types: tuple[str, ...]) -> Graph:
  73. graph = Graph().__new__(Graph)
  74. graph.__dict__["_relationship_types"] = {
  75. name: type(str(name), (Relationship,), {})
  76. for name in relationship_types
  77. }
  78. return graph
  79. class Entity(t.Mapping[str, t.Any]):
  80. """
  81. Graph entity base.
  82. Base class for :class:`.Node` and :class:`.Relationship` that
  83. provides :class:`.Graph` membership and property containment
  84. functionality.
  85. """
  86. def __init__(
  87. self,
  88. graph: Graph,
  89. element_id: str,
  90. id_: int,
  91. properties: dict[str, t.Any] | None,
  92. ) -> None:
  93. self._graph = graph
  94. self._element_id = element_id
  95. self._id = id_
  96. self._properties = {
  97. k: v for k, v in (properties or {}).items() if v is not None
  98. }
  99. def __eq__(self, other: t.Any) -> bool:
  100. # TODO: 6.0 - return NotImplemented on type mismatch instead of False
  101. try:
  102. return (
  103. type(self) is type(other)
  104. and self.graph == other.graph
  105. and self.element_id == other.element_id
  106. )
  107. except AttributeError:
  108. return False
  109. def __ne__(self, other: object) -> bool:
  110. return not self.__eq__(other)
  111. def __hash__(self):
  112. return hash(self._element_id)
  113. def __len__(self) -> int:
  114. return len(self._properties)
  115. def __getitem__(self, name: str) -> t.Any:
  116. return self._properties.get(name)
  117. def __contains__(self, name: object) -> bool:
  118. return name in self._properties
  119. def __iter__(self) -> t.Iterator[str]:
  120. return iter(self._properties)
  121. @property
  122. def graph(self) -> Graph:
  123. """The :class:`.Graph` to which this entity belongs."""
  124. return self._graph
  125. @property # type: ignore
  126. @deprecated("`id` is deprecated, use `element_id` instead")
  127. def id(self) -> int:
  128. """
  129. The legacy identity of this entity in its container :class:`.Graph`.
  130. Depending on the version of the server this entity was retrieved from,
  131. this may be empty (None).
  132. .. warning::
  133. This value can change for the same entity across multiple
  134. transactions. Don't rely on it for cross-transactional
  135. computations.
  136. .. deprecated:: 5.0
  137. Use :attr:`.element_id` instead.
  138. """
  139. return self._id
  140. @property
  141. def element_id(self) -> str:
  142. """
  143. The identity of this entity in its container :class:`.Graph`.
  144. .. warning::
  145. This value can change for the same entity across multiple
  146. transactions. Don't rely on it for cross-transactional
  147. computations.
  148. .. versionadded:: 5.0
  149. """
  150. return self._element_id
  151. def get(self, name: str, default: object = None) -> t.Any:
  152. """Get a property value by name, optionally with a default."""
  153. return self._properties.get(name, default)
  154. def keys(self) -> t.KeysView[str]:
  155. """Return an iterable of all property names."""
  156. return self._properties.keys()
  157. def values(self) -> t.ValuesView[t.Any]:
  158. """Return an iterable of all property values."""
  159. return self._properties.values()
  160. def items(self) -> t.ItemsView[str, t.Any]:
  161. """Return an iterable of all property name-value pairs."""
  162. return self._properties.items()
  163. class EntitySetView(Mapping, t.Generic[_T]):
  164. """View of a set of :class:`.Entity` instances within a :class:`.Graph`."""
  165. def __init__(
  166. self,
  167. entity_dict: dict[str, _T],
  168. legacy_entity_dict: dict[int, _T],
  169. ) -> None:
  170. self._entity_dict = entity_dict
  171. self._legacy_entity_dict = legacy_entity_dict # TODO: 6.0 - remove
  172. def __getitem__(self, e_id: int | str) -> _T:
  173. # TODO: 6.0 - remove this compatibility shim
  174. if isinstance(e_id, (int, float, complex)):
  175. deprecation_warn(
  176. "Accessing entities by an integer id is deprecated, "
  177. "use the new style element_id (str) instead"
  178. )
  179. return self._legacy_entity_dict[e_id]
  180. return self._entity_dict[e_id]
  181. def __len__(self) -> int:
  182. return len(self._entity_dict)
  183. def __iter__(self) -> t.Iterator[_T]:
  184. return iter(self._entity_dict.values())
  185. class Node(Entity):
  186. """Self-contained graph node."""
  187. def __init__(
  188. self,
  189. graph: Graph,
  190. element_id: str,
  191. id_: int,
  192. n_labels: t.Iterable[str] | None = None,
  193. properties: dict[str, t.Any] | None = None,
  194. ) -> None:
  195. Entity.__init__(self, graph, element_id, id_, properties)
  196. self._labels = frozenset(n_labels or ())
  197. def __repr__(self) -> str:
  198. return (
  199. f"<Node element_id={self._element_id!r} "
  200. f"labels={self._labels!r} properties={self._properties!r}>"
  201. )
  202. @property
  203. def labels(self) -> frozenset[str]:
  204. """The set of labels attached to this node."""
  205. return self._labels
  206. class Relationship(Entity):
  207. """Self-contained graph relationship."""
  208. def __init__(
  209. self,
  210. graph: Graph,
  211. element_id: str,
  212. id_: int,
  213. properties: dict[str, t.Any],
  214. ) -> None:
  215. Entity.__init__(self, graph, element_id, id_, properties)
  216. self._start_node: Node | None = None
  217. self._end_node: Node | None = None
  218. def __repr__(self) -> str:
  219. return (
  220. f"<Relationship element_id={self._element_id!r} "
  221. f"nodes={self.nodes!r} type={self.type!r} "
  222. f"properties={self._properties!r}>"
  223. )
  224. @property
  225. def nodes(self) -> tuple[Node | None, Node | None]:
  226. """Get the pair of nodes which this relationship connects."""
  227. return self._start_node, self._end_node
  228. @property
  229. def start_node(self) -> Node | None:
  230. """Get the start node of this relationship."""
  231. return self._start_node
  232. @property
  233. def end_node(self) -> Node | None:
  234. """Get the end node of this relationship."""
  235. return self._end_node
  236. @property
  237. def type(self) -> str:
  238. """
  239. Get the type name of this relationship.
  240. This is functionally equivalent to ``type(relationship).__name__``.
  241. """
  242. return type(self).__name__
  243. def __reduce__(self):
  244. return Relationship._restore, (self.graph, self.type), self.__dict__
  245. @staticmethod
  246. def _restore(graph: Graph, name: str) -> Relationship:
  247. type_ = graph.relationship_type(name)
  248. return type_.__new__(type_)
  249. class Path:
  250. """Self-contained graph path."""
  251. def __init__(self, start_node: Node, *relationships: Relationship) -> None:
  252. assert isinstance(start_node, Node)
  253. nodes = [start_node]
  254. for i, relationship in enumerate(relationships, start=1):
  255. assert isinstance(relationship, Relationship)
  256. if relationship.start_node == nodes[-1]:
  257. nodes.append(t.cast(Node, relationship.end_node))
  258. elif relationship.end_node == nodes[-1]:
  259. nodes.append(t.cast(Node, relationship.start_node))
  260. else:
  261. raise ValueError(
  262. f"Relationship {i} does not connect to the last node"
  263. )
  264. self._nodes = tuple(nodes)
  265. self._relationships = relationships
  266. def __repr__(self) -> str:
  267. return (
  268. f"<Path start={self.start_node!r} end={self.end_node!r} "
  269. f"size={len(self)}>"
  270. )
  271. def __eq__(self, other: t.Any) -> bool:
  272. # TODO: 6.0 - return NotImplemented on type mismatch instead of False
  273. try:
  274. return (
  275. self.start_node == other.start_node
  276. and self.relationships == other.relationships
  277. )
  278. except AttributeError:
  279. return False
  280. def __ne__(self, other: object) -> bool:
  281. return not self.__eq__(other)
  282. def __hash__(self):
  283. value = hash(self._nodes[0])
  284. for relationship in self._relationships:
  285. value ^= hash(relationship)
  286. return value
  287. def __len__(self) -> int:
  288. return len(self._relationships)
  289. def __iter__(self) -> t.Iterator[Relationship]:
  290. return iter(self._relationships)
  291. @property
  292. def graph(self) -> Graph:
  293. """The :class:`.Graph` to which this path belongs."""
  294. return self._nodes[0].graph
  295. @property
  296. def nodes(self) -> tuple[Node, ...]:
  297. """The sequence of :class:`.Node` objects in this path."""
  298. return self._nodes
  299. @property
  300. def start_node(self) -> Node:
  301. """The first :class:`.Node` in this path."""
  302. return self._nodes[0]
  303. @property
  304. def end_node(self) -> Node:
  305. """The last :class:`.Node` in this path."""
  306. return self._nodes[-1]
  307. @property
  308. def relationships(self) -> tuple[Relationship, ...]:
  309. """The sequence of :class:`.Relationship` objects in this path."""
  310. return self._relationships