diff --git a/nx_arangodb/classes/dict/adj.py b/nx_arangodb/classes/dict/adj.py index 7126897..b1b2ddc 100644 --- a/nx_arangodb/classes/dict/adj.py +++ b/nx_arangodb/classes/dict/adj.py @@ -1815,7 +1815,7 @@ def propagate_edge_directed_symmetric( set_adj_inner_dict_mirror(src_node_id) set_adj_inner_dict_mirror(dst_node_id) - edge_attr_or_key_dict = set_edge_func( # type: ignore[operator] + edge_attr_or_key_dict = set_edge_func( src_node_id, dst_node_id, edge_or_edges ) diff --git a/nx_arangodb/classes/digraph.py b/nx_arangodb/classes/digraph.py index 3bfb943..c247bcf 100644 --- a/nx_arangodb/classes/digraph.py +++ b/nx_arangodb/classes/digraph.py @@ -9,7 +9,7 @@ from .dict.adj import AdjListOuterDict from .enum import TraversalDirection -from .function import get_node_id +from .function import get_node_id, mirror_to_nxcg networkx_api = nxadb.utils.decorators.networkx_class(nx.DiGraph) # type: ignore @@ -131,6 +131,15 @@ class DiGraph(Graph, nx.DiGraph): this operation is irreversible and will result in the loss of all data in the graph. NOTE: If set to True, Collection Indexes will also be lost. + mirror_crud_to_nxcg : bool (optional, default: False) + Whether to mirror any CRUD operations performed on the NetworkX-ArangoDB Graph + to the cached NetworkX-cuGraph Graph (if available). This allows you to maintain + an up-to-date in-memory NetworkX-cuGraph graph while performing CRUD operations + on the NetworkX-ArangoDB Graph. NOTE: The first time you perform a CRUD + operation on the NetworkX-ArangoDB Graph with an existing NetworkX-cuGraph cache + will require downtime to copy the NetworkX-cuGraph Graph from GPU memory to CPU + memory. Subsequent CRUD operations will not require this downtime. + args: positional arguments for nx.Graph Additional arguments passed to nx.Graph. @@ -161,6 +170,7 @@ def __init__( symmetrize_edges: bool = False, use_arango_views: bool = False, overwrite_graph: bool = False, + mirror_crud_to_nxcg: bool = False, *args: Any, **kwargs: Any, ): @@ -179,16 +189,18 @@ def __init__( symmetrize_edges, use_arango_views, overwrite_graph, + mirror_crud_to_nxcg, *args, **kwargs, ) if self.graph_exists_in_db: self.clear_edges = self.clear_edges_override + self.reverse = self.reverse_override + self.add_node = self.add_node_override self.add_nodes_from = self.add_nodes_from_override self.remove_node = self.remove_node_override - self.reverse = self.reverse_override assert isinstance(self._succ, AdjListOuterDict) assert isinstance(self._pred, AdjListOuterDict) @@ -234,6 +246,7 @@ def clear_edges_override(self): super().clear_edges() + @mirror_to_nxcg def add_node_override(self, node_for_adding, **attr): if node_for_adding is None: raise ValueError("None cannot be a node") @@ -269,6 +282,7 @@ def add_node_override(self, node_for_adding, **attr): nx._clear_cache(self) + @mirror_to_nxcg def add_nodes_from_override(self, nodes_for_adding, **attr): for n in nodes_for_adding: try: @@ -312,6 +326,7 @@ def add_nodes_from_override(self, nodes_for_adding, **attr): nx._clear_cache(self) + @mirror_to_nxcg def remove_node_override(self, n): if isinstance(n, (str, int)): n = get_node_id(str(n), self.default_node_type) diff --git a/nx_arangodb/classes/function.py b/nx_arangodb/classes/function.py index 491c0cd..ef5ef1b 100644 --- a/nx_arangodb/classes/function.py +++ b/nx_arangodb/classes/function.py @@ -6,6 +6,7 @@ from __future__ import annotations +from functools import wraps from typing import Any, Callable, Generator, Tuple import networkx as nx @@ -932,3 +933,19 @@ def upsert_collection_edges( ) return results + + +def mirror_to_nxcg(func): + @wraps(func) + def wrapper(self, *args, **kwargs): + result = func(self, *args, **kwargs) + if self.mirror_crud_to_nxcg and self.nxcg_graph is not None: + if "_override" not in func.__name__: + m = f"Function '{func.__name__}' is not an override function." + raise ValueError(m) + + func_name = func.__name__.replace("_override", "") + getattr(self.nxcg_graph, func_name)(*args, **kwargs) + return result + + return wrapper diff --git a/nx_arangodb/classes/graph.py b/nx_arangodb/classes/graph.py index 336cc3b..a112dd8 100644 --- a/nx_arangodb/classes/graph.py +++ b/nx_arangodb/classes/graph.py @@ -29,7 +29,7 @@ node_attr_dict_factory, node_dict_factory, ) -from .function import get_node_id +from .function import get_node_id, mirror_to_nxcg from .reportviews import ArangoEdgeView, ArangoNodeView networkx_api = nxadb.utils.decorators.networkx_class(nx.Graph) # type: ignore @@ -165,6 +165,15 @@ class Graph(nx.Graph): this operation is irreversible and will result in the loss of all data in the graph. NOTE: If set to True, Collection Indexes will also be lost. + mirror_crud_to_nxcg : bool (optional, default: False) + Whether to mirror any CRUD operations performed on the NetworkX-ArangoDB Graph + to the cached NetworkX-cuGraph Graph (if available). This allows you to maintain + an up-to-date in-memory NetworkX-cuGraph graph while performing CRUD operations + on the NetworkX-ArangoDB Graph. NOTE: The first time you perform a CRUD + operation on the NetworkX-ArangoDB Graph with an existing NetworkX-cuGraph cache + will require downtime to copy the NetworkX-cuGraph Graph from GPU memory to CPU + memory. Subsequent CRUD operations will not require this downtime. + args: positional arguments for nx.Graph Additional arguments passed to nx.Graph. @@ -195,12 +204,14 @@ def __init__( symmetrize_edges: bool = False, use_arango_views: bool = False, overwrite_graph: bool = False, + mirror_crud_to_nxcg: bool = False, *args: Any, **kwargs: Any, ): self.__db = None self.__use_arango_views = use_arango_views self.__graph_exists_in_db = False + self.__mirror_crud_to_nxcg = mirror_crud_to_nxcg self.__set_db(db) if all([self.__db, name]): @@ -261,11 +272,19 @@ def __init__( self.subgraph = self.subgraph_override self.clear = self.clear_override self.clear_edges = self.clear_edges_override - self.add_node = self.add_node_override - self.add_nodes_from = self.add_nodes_from_override self.number_of_edges = self.number_of_edges_override self.nbunch_iter = self.nbunch_iter_override + self.add_node = self.add_node_override + self.add_nodes_from = self.add_nodes_from_override + self.remove_node = self.remove_node_override + self.remove_nodes_from = self.remove_nodes_from_override + self.add_edge = self.add_edge_override + self.add_edges_from = self.add_edges_from_override + self.remove_edge = self.remove_edge_override + self.remove_edges_from = self.remove_edges_from_override + self.update = self.update_override + # If incoming_graph_data wasn't loaded by the NetworkX Adapter, # then we can rely on the CRUD operations of the modified dictionaries # to load the data into the graph. However, if the graph is directed @@ -541,6 +560,10 @@ def is_smart(self) -> bool: def smart_field(self) -> str | None: return self.__smart_field + @property + def mirror_crud_to_nxcg(self) -> bool: + return self.__mirror_crud_to_nxcg + ########### # Setters # ########### @@ -691,81 +714,6 @@ def clear_edges_override(self): nbr_dict.clear() nx._clear_cache(self) - def add_node_override(self, node_for_adding, **attr): - if node_for_adding is None: - raise ValueError("None cannot be a node") - - if node_for_adding not in self._node: - self._adj[node_for_adding] = self.adjlist_inner_dict_factory() - - ###################### - # NOTE: monkey patch # - ###################### - - # Old: - # attr_dict = self._node[node_for_adding] = self.node_attr_dict_factory() - # attr_dict.update(attr) - - # New: - node_attr_dict = self.node_attr_dict_factory() - node_attr_dict.data = attr - self._node[node_for_adding] = node_attr_dict - - # Reason: - # We can optimize the process of adding a node by creating avoiding - # the creation of a new dictionary and updating it with the attributes. - # Instead, we can create a new node_attr_dict object and set the attributes - # directly. This only makes 1 network call to the database instead of 2. - - ########################### - - else: - self._node[node_for_adding].update(attr) - - nx._clear_cache(self) - - def add_nodes_from_override(self, nodes_for_adding, **attr): - for n in nodes_for_adding: - try: - newnode = n not in self._node - newdict = attr - except TypeError: - n, ndict = n - newnode = n not in self._node - newdict = attr.copy() - newdict.update(ndict) - if newnode: - if n is None: - raise ValueError("None cannot be a node") - self._adj[n] = self.adjlist_inner_dict_factory() - - ###################### - # NOTE: monkey patch # - ###################### - - # Old: - # self._node[n] = self.node_attr_dict_factory() - # - # self._node[n].update(newdict) - - # New: - node_attr_dict = self.node_attr_dict_factory() - node_attr_dict.data = newdict - self._node[n] = node_attr_dict - - else: - self._node[n].update(newdict) - - # Reason: - # We can optimize the process of adding a node by creating avoiding - # the creation of a new dictionary and updating it with the attributes. - # Instead, we create a new node_attr_dict object and set the attributes - # directly. This only makes 1 network call to the database instead of 2. - - ########################### - - nx._clear_cache(self) - def number_of_edges_override(self, u=None, v=None): if u is not None: return super().number_of_edges(u, v) @@ -847,3 +795,108 @@ def bunch_iter(nlist, adj): bunch = bunch_iter(nbunch, self._adj) return bunch + + @mirror_to_nxcg + def add_node_override(self, node_for_adding, **attr): + if node_for_adding is None: + raise ValueError("None cannot be a node") + + if node_for_adding not in self._node: + self._adj[node_for_adding] = self.adjlist_inner_dict_factory() + + ###################### + # NOTE: monkey patch # + ###################### + + # Old: + # attr_dict = self._node[node_for_adding] = self.node_attr_dict_factory() + # attr_dict.update(attr) + + # New: + node_attr_dict = self.node_attr_dict_factory() + node_attr_dict.data = attr + self._node[node_for_adding] = node_attr_dict + + # Reason: + # We can optimize the process of adding a node by creating avoiding + # the creation of a new dictionary and updating it with the attributes. + # Instead, we can create a new node_attr_dict object and set the attributes + # directly. This only makes 1 network call to the database instead of 2. + + ########################### + + else: + self._node[node_for_adding].update(attr) + + nx._clear_cache(self) + + @mirror_to_nxcg + def add_nodes_from_override(self, nodes_for_adding, **attr): + for n in nodes_for_adding: + try: + newnode = n not in self._node + newdict = attr + except TypeError: + n, ndict = n + newnode = n not in self._node + newdict = attr.copy() + newdict.update(ndict) + if newnode: + if n is None: + raise ValueError("None cannot be a node") + self._adj[n] = self.adjlist_inner_dict_factory() + + ###################### + # NOTE: monkey patch # + ###################### + + # Old: + # self._node[n] = self.node_attr_dict_factory() + # + # self._node[n].update(newdict) + + # New: + node_attr_dict = self.node_attr_dict_factory() + node_attr_dict.data = newdict + self._node[n] = node_attr_dict + + else: + self._node[n].update(newdict) + + # Reason: + # We can optimize the process of adding a node by creating avoiding + # the creation of a new dictionary and updating it with the attributes. + # Instead, we create a new node_attr_dict object and set the attributes + # directly. This only makes 1 network call to the database instead of 2. + + ########################### + + nx._clear_cache(self) + + @mirror_to_nxcg + def remove_node_override(self, n): + super().remove_node(n) + + @mirror_to_nxcg + def remove_nodes_from_override(self, nodes): + super().remove_nodes_from(nodes) + + @mirror_to_nxcg + def add_edge_override(self, u, v, **attr): + super().add_edge(u, v, **attr) + + @mirror_to_nxcg + def add_edges_from_override(self, ebunch_to_add, **attr): + super().add_edges_from(ebunch_to_add, **attr) + + @mirror_to_nxcg + def remove_edge_override(self, u, v): + super().remove_edge(u, v) + + @mirror_to_nxcg + def remove_edges_from_override(self, ebunch): + super().remove_edges_from(ebunch) + + @mirror_to_nxcg + def update_override(self, *args, **kwargs): + super().update(*args, **kwargs) diff --git a/nx_arangodb/classes/multidigraph.py b/nx_arangodb/classes/multidigraph.py index f115ab8..2f06942 100644 --- a/nx_arangodb/classes/multidigraph.py +++ b/nx_arangodb/classes/multidigraph.py @@ -7,6 +7,9 @@ import nx_arangodb as nxadb from nx_arangodb.classes.digraph import DiGraph from nx_arangodb.classes.multigraph import MultiGraph +from nx_arangodb.logger import logger + +from .function import mirror_to_nxcg networkx_api = nxadb.utils.decorators.networkx_class(nx.MultiDiGraph) # type: ignore @@ -141,6 +144,15 @@ class MultiDiGraph(MultiGraph, DiGraph, nx.MultiDiGraph): this operation is irreversible and will result in the loss of all data in the graph. NOTE: If set to True, Collection Indexes will also be lost. + mirror_crud_to_nxcg : bool (optional, default: False) + Whether to mirror any CRUD operations performed on the NetworkX-ArangoDB Graph + to the cached NetworkX-cuGraph Graph (if available). This allows you to maintain + an up-to-date in-memory NetworkX-cuGraph graph while performing CRUD operations + on the NetworkX-ArangoDB Graph. NOTE: The first time you perform a CRUD + operation on the NetworkX-ArangoDB Graph with an existing NetworkX-cuGraph cache + will require downtime to copy the NetworkX-cuGraph Graph from GPU memory to CPU + memory. Subsequent CRUD operations will not require this downtime. + args: positional arguments for nx.Graph Additional arguments passed to nx.Graph. @@ -172,6 +184,7 @@ def __init__( symmetrize_edges: bool = False, use_arango_views: bool = False, overwrite_graph: bool = False, + mirror_crud_to_nxcg: bool = False, *args: Any, **kwargs: Any, ): @@ -191,6 +204,7 @@ def __init__( symmetrize_edges, use_arango_views, overwrite_graph, + mirror_crud_to_nxcg, *args, **kwargs, ) diff --git a/nx_arangodb/classes/multigraph.py b/nx_arangodb/classes/multigraph.py index c494d34..0eebd43 100644 --- a/nx_arangodb/classes/multigraph.py +++ b/nx_arangodb/classes/multigraph.py @@ -8,6 +8,7 @@ from nx_arangodb.logger import logger from .dict import edge_key_dict_factory +from .function import mirror_to_nxcg networkx_api = nxadb.utils.decorators.networkx_class(nx.MultiGraph) # type: ignore @@ -142,6 +143,15 @@ class MultiGraph(Graph, nx.MultiGraph): this operation is irreversible and will result in the loss of all data in the graph. NOTE: If set to True, Collection Indexes will also be lost. + mirror_crud_to_nxcg : bool (optional, default: False) + Whether to mirror any CRUD operations performed on the NetworkX-ArangoDB Graph + to the cached NetworkX-cuGraph Graph (if available). This allows you to maintain + an up-to-date in-memory NetworkX-cuGraph graph while performing CRUD operations + on the NetworkX-ArangoDB Graph. NOTE: The first time you perform a CRUD + operation on the NetworkX-ArangoDB Graph with an existing NetworkX-cuGraph cache + will require downtime to copy the NetworkX-cuGraph Graph from GPU memory to CPU + memory. Subsequent CRUD operations will not require this downtime. + args: positional arguments for nx.Graph Additional arguments passed to nx.Graph. @@ -173,6 +183,7 @@ def __init__( symmetrize_edges: bool = False, use_arango_views: bool = False, overwrite_graph: bool = False, + mirror_crud_to_nxcg: bool = False, *args: Any, **kwargs: Any, ): @@ -191,15 +202,18 @@ def __init__( symmetrize_edges, use_arango_views, overwrite_graph, + mirror_crud_to_nxcg, *args, **kwargs, ) if self.graph_exists_in_db: - self.add_edge = self.add_edge_override self.has_edge = self.has_edge_override self.copy = self.copy_override + self.add_edge = self.add_edge_override + self.remove_edge = self.remove_edge_override + if incoming_graph_data is not None and not self._loaded_incoming_graph_data: # Taken from networkx.MultiGraph.__init__ if isinstance(incoming_graph_data, dict) and multigraph_input is not False: @@ -243,34 +257,6 @@ def _set_factory_methods(self) -> None: # nx.MultiGraph Overides # ########################## - def add_edge_override(self, u_for_edge, v_for_edge, key=None, **attr): - if key is not None: - m = "ArangoDB MultiGraph does not support custom edge keys yet." - logger.warning(m) - - _ = super().add_edge(u_for_edge, v_for_edge, key="-1", **attr) - - ###################### - # NOTE: monkey patch # - ###################### - - # Old: - # return key - - # New: - keys = list(self._adj[u_for_edge][v_for_edge].data.keys()) - last_key = keys[-1] - return last_key - - # Reason: - # nxadb.MultiGraph does not yet support the ability to work - # with custom edge keys. As a Database, we must rely on the official - # ArangoDB Edge _id to uniquely identify edges. The EdgeKeyDict.__setitem__ - # method will be responsible for setting the edge key to the _id of the edge - # document. This will allow us to use the edge key as a unique identifier - - ########################### - def has_edge_override(self, u, v, key=None): try: if key is None: @@ -302,3 +288,36 @@ def copy_override(self, *args, **kwargs): G = super().copy(*args, **kwargs) G.edge_key_dict_factory = nx.MultiGraph.edge_key_dict_factory return G + + @mirror_to_nxcg + def add_edge_override(self, u_for_edge, v_for_edge, key=None, **attr): + if key is not None: + m = "ArangoDB MultiGraph does not support custom edge keys yet." + logger.warning(m) + + _ = super().add_edge(u_for_edge, v_for_edge, key="-1", **attr) + + ###################### + # NOTE: monkey patch # + ###################### + + # Old: + # return key + + # New: + keys = list(self._adj[u_for_edge][v_for_edge].data.keys()) + last_key = keys[-1] + return last_key + + # Reason: + # nxadb.MultiGraph does not yet support the ability to work + # with custom edge keys. As a Database, we must rely on the official + # ArangoDB Edge _id to uniquely identify edges. The EdgeKeyDict.__setitem__ + # method will be responsible for setting the edge key to the _id of the edge + # document. This will allow us to use the edge key as a unique identifier + + ########################### + + @mirror_to_nxcg + def remove_edge_override(self, u, v, key=None): + super().remove_edge(u, v, key)