Skip to content

Commit

Permalink
new: use _graphs instead of nxadb_graphs (#66)
Browse files Browse the repository at this point in the history
* new: use `_graphs` instead of `nxadb_graphs`

* fix: lint

* fix: typo

* cleanup

* cleanup: use `GRAPH_FIELD`

* fix: env var

* fix: lint

* bump version
  • Loading branch information
aMahanna authored Oct 21, 2024
1 parent 1e7df83 commit a9b92fd
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 57 deletions.
2 changes: 1 addition & 1 deletion _nx_arangodb/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.1.1
1.2.0
47 changes: 33 additions & 14 deletions nx_arangodb/classes/dict/graph.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import os
from collections import UserDict
from typing import Any, Callable

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
----------
Expand All @@ -123,31 +130,39 @@ 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:
if not isinstance(value, dict):
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
Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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()"""
Expand Down Expand Up @@ -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
)
Expand Down
75 changes: 38 additions & 37 deletions tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"]},
Expand All @@ -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",
Expand All @@ -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:
Expand Down
10 changes: 5 additions & 5 deletions tests/test_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down

0 comments on commit a9b92fd

Please sign in to comment.