diff --git a/_nx_arangodb/VERSION b/_nx_arangodb/VERSION index 8cfbc90..867e524 100644 --- a/_nx_arangodb/VERSION +++ b/_nx_arangodb/VERSION @@ -1 +1 @@ -1.1.1 \ No newline at end of file +1.2.0 \ No newline at end of file diff --git a/nx_arangodb/classes/dict/graph.py b/nx_arangodb/classes/dict/graph.py index 249cafc..5a031ae 100644 --- a/nx_arangodb/classes/dict/graph.py +++ b/nx_arangodb/classes/dict/graph.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os from collections import UserDict from typing import Any, Callable @@ -39,6 +40,8 @@ def graph_attr_dict_factory( # Graph # ######### +GRAPH_FIELD = "networkx" + def build_graph_attr_dict_data( parent: GraphAttrDict, data: dict[str, Any] @@ -104,7 +107,11 @@ class GraphDict(UserDict[str, Any]): Given that ArangoDB does not have a concept of graph attributes, this class stores the attributes in a collection with the graph name as the document key. - For now, the collection is called 'nxadb_graphs'. + The default collection is called `_graphs`. However, if the + `DATABASE_GRAPH_COLLECTION` environment variable is specified, + then that collection will be used. This variable is useful when the + database user does not have permission to access the `_graphs` + system collection. Parameters ---------- @@ -123,23 +130,31 @@ class GraphDict(UserDict[str, Any]): >>> del G.graph['foo'] """ - def __init__(self, db: StandardDatabase, graph: Graph, *args: Any, **kwargs: Any): + def __init__( + self, + db: StandardDatabase, + graph: Graph, + *args: Any, + **kwargs: Any, + ): super().__init__(*args, **kwargs) self.data: dict[str, Any] = {} self.db = db self.adb_graph = graph self.graph_name = graph.name - self.COLLECTION_NAME = "nxadb_graphs" - self.graph_id = f"{self.COLLECTION_NAME}/{self.graph_name}" + self.collection_name = os.environ.get("DATABASE_GRAPH_COLLECTION", "_graphs") - self.collection = create_collection(db, self.COLLECTION_NAME) + self.graph_id = f"{self.collection_name}/{self.graph_name}" + self.parent_keys = [GRAPH_FIELD] + + self.collection = create_collection(db, self.collection_name) self.graph_attr_dict_factory = graph_attr_dict_factory( self.db, self.adb_graph, self.graph_id ) - result = doc_get_or_insert(self.db, self.COLLECTION_NAME, self.graph_id) - for k, v in result.items(): + result = doc_get_or_insert(self.db, self.collection_name, self.graph_id) + for k, v in result.get(GRAPH_FIELD, {}).items(): self.data[k] = self.__process_graph_dict_value(k, v) def __process_graph_dict_value(self, key: str, value: Any) -> Any: @@ -147,7 +162,7 @@ def __process_graph_dict_value(self, key: str, value: Any) -> Any: return value graph_attr_dict = self.graph_attr_dict_factory() - graph_attr_dict.parent_keys = [key] + graph_attr_dict.parent_keys += [key] graph_attr_dict.data = build_graph_attr_dict_data(graph_attr_dict, value) return graph_attr_dict @@ -158,7 +173,7 @@ def __contains__(self, key: str) -> bool: if key in self.data: return True - return aql_doc_has_key(self.db, self.graph_id, key) + return aql_doc_has_key(self.db, self.graph_id, key, self.parent_keys) @key_is_string def __getitem__(self, key: str) -> Any: @@ -167,7 +182,7 @@ def __getitem__(self, key: str) -> Any: if value := self.data.get(key): return value - result = aql_doc_get_key(self.db, self.graph_id, key) + result = aql_doc_get_key(self.db, self.graph_id, key, self.parent_keys) if result is None: raise KeyError(key) @@ -187,14 +202,17 @@ def __setitem__(self, key: str, value: Any) -> None: graph_dict_value = self.__process_graph_dict_value(key, value) self.data[key] = graph_dict_value - doc_update(self.db, self.graph_id, {key: value}) + + update_dict = get_update_dict(self.parent_keys, {key: value}) + doc_update(self.db, self.graph_id, update_dict) @key_is_string @key_is_not_reserved def __delitem__(self, key: str) -> None: """del G.graph['foo']""" self.data.pop(key, None) - doc_update(self.db, self.graph_id, {key: None}) + update_dict = get_update_dict(self.parent_keys, {key: None}) + doc_update(self.db, self.graph_id, update_dict) # @values_are_json_serializable # TODO? def update(self, attrs: Any) -> None: # type: ignore @@ -208,7 +226,8 @@ def update(self, attrs: Any) -> None: # type: ignore graph_attr_dict.data = graph_attr_dict_data self.data.update(graph_attr_dict_data) - doc_update(self.db, self.graph_id, attrs) + update_dict = get_update_dict(self.parent_keys, attrs) + doc_update(self.db, self.graph_id, update_dict) def clear(self) -> None: """G.graph.clear()""" @@ -256,7 +275,7 @@ def __init__( self.graph = graph self.graph_id: str = graph_id - self.parent_keys: list[str] = [] + self.parent_keys: list[str] = [GRAPH_FIELD] self.graph_attr_dict_factory = graph_attr_dict_factory( self.db, self.graph, self.graph_id ) diff --git a/tests/test.py b/tests/test.py index c454554..db1e7e8 100644 --- a/tests/test.py +++ b/tests/test.py @@ -13,6 +13,7 @@ import nx_arangodb as nxadb from nx_arangodb.classes.dict.adj import AdjListOuterDict, EdgeAttrDict, EdgeKeyDict +from nx_arangodb.classes.dict.graph import GRAPH_FIELD from nx_arangodb.classes.dict.node import NodeAttrDict, NodeDict from .conftest import create_grid_graph, create_line_graph, db, run_gpu_tests @@ -1638,7 +1639,7 @@ def test_multidigraph_edges_crud(load_karate_graph: Any) -> None: def test_graph_dict_init(load_karate_graph: Any) -> None: G = nxadb.Graph(name="KarateGraph", default_node_type="person") assert db.collection("_graphs").has("KarateGraph") - graph_document = db.collection("_graphs").get("KarateGraph") + graph_document = db.document(f"_graphs/{G.name}") assert graph_document["_key"] == "KarateGraph" assert graph_document["edgeDefinitions"] == [ {"collection": "knows", "from": ["person"], "to": ["person"]}, @@ -1655,33 +1656,31 @@ def test_graph_dict_init_extended(load_karate_graph: Any) -> None: G = nxadb.Graph(name="KarateGraph", foo="bar", bar={"baz": True}) G.graph["foo"] = "!!!" G.graph["bar"]["baz"] = False - assert db.document(G.graph.graph_id)["foo"] == "!!!" - assert db.document(G.graph.graph_id)["bar"]["baz"] is False - assert "baz" not in db.document(G.graph.graph_id) + assert db.document(G.graph.graph_id)[GRAPH_FIELD]["foo"] == "!!!" + assert db.document(G.graph.graph_id)[GRAPH_FIELD]["bar"]["baz"] is False + assert "baz" not in db.document(G.graph.graph_id)[GRAPH_FIELD] def test_graph_dict_clear_will_not_remove_remote_data(load_karate_graph: Any) -> None: - G_adb = nxadb.Graph( + G = nxadb.Graph( name="KarateGraph", foo="bar", bar={"a": 4}, ) - G_adb.graph["ant"] = {"b": 5} - G_adb.graph["ant"]["b"] = 6 - G_adb.clear() + G.graph["ant"] = {"b": 5} + G.graph["ant"]["b"] = 6 + G.clear() try: - G_adb.graph["ant"] + G.graph["ant"] except KeyError: raise AssertionError("Not allowed to fail.") - assert db.document(G_adb.graph.graph_id)["ant"] == {"b": 6} + assert db.document(G.graph.graph_id)[GRAPH_FIELD]["ant"] == {"b": 6} def test_graph_dict_set_item(load_karate_graph: Any) -> None: - name = "KarateGraph" - db.collection("nxadb_graphs").delete(name, ignore_missing=True) - G = nxadb.Graph(name=name, default_node_type="person") + G = nxadb.Graph(name="KarateGraph", default_node_type="person") json_values = [ "aString", @@ -1699,122 +1698,124 @@ def test_graph_dict_set_item(load_karate_graph: Any) -> None: G.graph["json"] = value if value is None: - assert "json" not in db.document(G.graph.graph_id) + assert "json" not in db.document(G.graph.graph_id)[GRAPH_FIELD] else: assert G.graph["json"] == value - assert db.document(G.graph.graph_id)["json"] == value + assert db.document(G.graph.graph_id)[GRAPH_FIELD]["json"] == value def test_graph_dict_update(load_karate_graph: Any) -> None: G = nxadb.Graph(name="KarateGraph", default_node_type="person") - G.clear() G.graph["a"] = "b" to_update = {"c": "d"} G.graph.update(to_update) # local - assert G.graph["a"] == "b" - assert G.graph["c"] == "d" + assert G.graph.data["a"] == G.graph["a"] == "b" + assert G.graph.data["c"] == G.graph["c"] == "d" # remote - adb_doc = db.collection("nxadb_graphs").get(G.name) + adb_doc = db.document(f"_graphs/{G.name}")[GRAPH_FIELD] assert adb_doc["a"] == "b" assert adb_doc["c"] == "d" def test_graph_attr_dict_nested_update(load_karate_graph: Any) -> None: G = nxadb.Graph(name="KarateGraph", default_node_type="person") - G.clear() G.graph["a"] = {"b": "c"} G.graph["a"].update({"d": "e"}) assert G.graph["a"]["b"] == "c" assert G.graph["a"]["d"] == "e" - assert db.document(G.graph.graph_id)["a"]["b"] == "c" - assert db.document(G.graph.graph_id)["a"]["d"] == "e" + assert db.document(G.graph.graph_id)[GRAPH_FIELD]["a"]["b"] == "c" + assert db.document(G.graph.graph_id)[GRAPH_FIELD]["a"]["d"] == "e" def test_graph_dict_nested_1(load_karate_graph: Any) -> None: G = nxadb.Graph(name="KarateGraph", default_node_type="person") - G.clear() icon = {"football_icon": "MJ7"} G.graph["a"] = {"b": icon} assert G.graph["a"]["b"] == icon - assert db.document(G.graph.graph_id)["a"]["b"] == icon + assert db.document(G.graph.graph_id)[GRAPH_FIELD]["a"]["b"] == icon def test_graph_dict_nested_2(load_karate_graph: Any) -> None: G = nxadb.Graph(name="KarateGraph", default_node_type="person") - G.clear() icon = {"football_icon": "MJ7"} G.graph["x"] = {"y": icon} G.graph["x"]["y"]["amount_of_goals"] = 1337 assert G.graph["x"]["y"]["amount_of_goals"] == 1337 - assert db.document(G.graph.graph_id)["x"]["y"]["amount_of_goals"] == 1337 + assert ( + db.document(G.graph.graph_id)[GRAPH_FIELD]["x"]["y"]["amount_of_goals"] == 1337 + ) def test_graph_dict_empty_values(load_karate_graph: Any) -> None: G = nxadb.Graph(name="KarateGraph", default_node_type="person") - G.clear() G.graph["empty"] = {} assert G.graph["empty"] == {} - assert db.document(G.graph.graph_id)["empty"] == {} + assert db.document(G.graph.graph_id)[GRAPH_FIELD]["empty"] == {} G.graph["none"] = None - assert "none" not in db.document(G.graph.graph_id) + assert "none" not in db.document(G.graph.graph_id)[GRAPH_FIELD] assert "none" not in G.graph def test_graph_dict_nested_overwrite(load_karate_graph: Any) -> None: G = nxadb.Graph(name="KarateGraph", default_node_type="person") - G.clear() icon1 = {"football_icon": "MJ7"} icon2 = {"basketball_icon": "MJ23"} G.graph["a"] = {"b": icon1} G.graph["a"]["b"]["football_icon"] = "ChangedIcon" assert G.graph["a"]["b"]["football_icon"] == "ChangedIcon" - assert db.document(G.graph.graph_id)["a"]["b"]["football_icon"] == "ChangedIcon" + assert ( + db.document(G.graph.graph_id)[GRAPH_FIELD]["a"]["b"]["football_icon"] + == "ChangedIcon" + ) # Overwrite entire nested dictionary G.graph["a"] = {"b": icon2} assert G.graph["a"]["b"]["basketball_icon"] == "MJ23" - assert db.document(G.graph.graph_id)["a"]["b"]["basketball_icon"] == "MJ23" + assert ( + db.document(G.graph.graph_id)[GRAPH_FIELD]["a"]["b"]["basketball_icon"] + == "MJ23" + ) def test_graph_dict_complex_nested(load_karate_graph: Any) -> None: G = nxadb.Graph(name="KarateGraph", default_node_type="person") - G.clear() complex_structure = {"level1": {"level2": {"level3": {"key": "value"}}}} G.graph["complex"] = complex_structure assert G.graph["complex"]["level1"]["level2"]["level3"]["key"] == "value" assert ( - db.document(G.graph.graph_id)["complex"]["level1"]["level2"]["level3"]["key"] + db.document(G.graph.graph_id)[GRAPH_FIELD]["complex"]["level1"]["level2"][ + "level3" + ]["key"] == "value" ) def test_graph_dict_nested_deletion(load_karate_graph: Any) -> None: G = nxadb.Graph(name="KarateGraph", default_node_type="person") - G.clear() icon = {"football_icon": "MJ7", "amount_of_goals": 1337} G.graph["x"] = {"y": icon} del G.graph["x"]["y"]["amount_of_goals"] assert "amount_of_goals" not in G.graph["x"]["y"] - assert "amount_of_goals" not in db.document(G.graph.graph_id)["x"]["y"] + assert "amount_of_goals" not in db.document(G.graph.graph_id)[GRAPH_FIELD]["x"]["y"] # Delete top-level key del G.graph["x"] assert "x" not in G.graph - assert "x" not in db.document(G.graph.graph_id) + assert "x" not in db.document(G.graph.graph_id)[GRAPH_FIELD] def test_readme(load_karate_graph: Any) -> None: diff --git a/tests/test_graph.py b/tests/test_graph.py index 867399c..2d48d97 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -17,7 +17,7 @@ AdjListOuterDict, EdgeAttrDict, ) -from nx_arangodb.classes.dict.graph import GraphDict +from nx_arangodb.classes.dict.graph import GRAPH_FIELD, GraphDict from nx_arangodb.classes.dict.node import NodeAttrDict, NodeDict from .conftest import db @@ -463,11 +463,11 @@ def test_graph_attr(self): assert isinstance(G.graph, GraphDict) assert G.graph["foo"] == "bar" del G.graph["foo"] - graph_doc = get_doc(f"nxadb_graphs/{GRAPH_NAME}") + graph_doc = get_doc(f"_graphs/{GRAPH_NAME}")[GRAPH_FIELD] assert G.graph == graph_doc H = self.K3Graph(foo="bar") assert H.graph["foo"] == "bar" - graph_doc = get_doc(f"nxadb_graphs/{GRAPH_NAME}") + graph_doc = get_doc(f"_graphs/{GRAPH_NAME}")[GRAPH_FIELD] assert H.graph == graph_doc def test_node_attr(self): @@ -1105,7 +1105,7 @@ def test_update(self): else: for src, dst in G.edges(): assert G.adj[dst][src] == G.adj[src][dst] - assert G.graph == get_doc(G.graph.graph_id) + assert G.graph == get_doc(G.graph.graph_id)[GRAPH_FIELD] # no keywords -- order is edges, nodes G = self.K3Graph() @@ -1126,7 +1126,7 @@ def test_update(self): else: for src, dst in G.edges(): assert G.adj[dst][src] == G.adj[src][dst] - assert G.graph == get_doc(G.graph.graph_id) + assert G.graph == get_doc(G.graph.graph_id)[GRAPH_FIELD] # update using only a graph G = self.K3Graph()