anywidget-graph 0.1.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- anywidget_graph/__init__.py +1 -1
- anywidget_graph/backends/__init__.py +71 -0
- anywidget_graph/backends/arango.py +201 -0
- anywidget_graph/backends/grafeo.py +70 -0
- anywidget_graph/backends/ladybug.py +94 -0
- anywidget_graph/backends/neo4j.py +143 -0
- anywidget_graph/converters/__init__.py +35 -0
- anywidget_graph/converters/base.py +77 -0
- anywidget_graph/converters/common.py +92 -0
- anywidget_graph/converters/cypher.py +107 -0
- anywidget_graph/converters/gql.py +15 -0
- anywidget_graph/converters/graphql.py +208 -0
- anywidget_graph/converters/gremlin.py +159 -0
- anywidget_graph/converters/sparql.py +167 -0
- anywidget_graph/ui/__init__.py +19 -0
- anywidget_graph/ui/icons.js +15 -0
- anywidget_graph/ui/index.js +199 -0
- anywidget_graph/ui/neo4j.js +193 -0
- anywidget_graph/ui/properties.js +137 -0
- anywidget_graph/ui/schema.js +178 -0
- anywidget_graph/ui/settings.js +299 -0
- anywidget_graph/ui/styles.css +584 -0
- anywidget_graph/ui/toolbar.js +106 -0
- anywidget_graph/widget.py +417 -226
- {anywidget_graph-0.1.0.dist-info → anywidget_graph-0.2.1.dist-info}/METADATA +4 -3
- anywidget_graph-0.2.1.dist-info/RECORD +28 -0
- anywidget_graph-0.1.0.dist-info/RECORD +0 -6
- {anywidget_graph-0.1.0.dist-info → anywidget_graph-0.2.1.dist-info}/WHEEL +0 -0
anywidget_graph/__init__.py
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Database backend abstractions for anywidget-graph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from anywidget_graph.backends.arango import ArangoBackend
|
|
8
|
+
from anywidget_graph.backends.grafeo import GrafeoBackend
|
|
9
|
+
from anywidget_graph.backends.ladybug import LadybugBackend
|
|
10
|
+
from anywidget_graph.backends.neo4j import Neo4jBackend
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
# Protocol
|
|
14
|
+
"DatabaseBackend",
|
|
15
|
+
# Backends
|
|
16
|
+
"ArangoBackend",
|
|
17
|
+
"GrafeoBackend",
|
|
18
|
+
"LadybugBackend",
|
|
19
|
+
"Neo4jBackend",
|
|
20
|
+
# Registries
|
|
21
|
+
"BACKENDS",
|
|
22
|
+
"QUERY_LANGUAGES",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@runtime_checkable
|
|
27
|
+
class DatabaseBackend(Protocol):
|
|
28
|
+
"""Protocol defining the interface for database backends.
|
|
29
|
+
|
|
30
|
+
Implementations must provide:
|
|
31
|
+
- execute(query) -> (nodes, edges)
|
|
32
|
+
- fetch_schema() -> (node_types, edge_types)
|
|
33
|
+
|
|
34
|
+
Example
|
|
35
|
+
-------
|
|
36
|
+
>>> class MyBackend:
|
|
37
|
+
... def execute(self, query: str) -> tuple[list[dict], list[dict]]:
|
|
38
|
+
... # Execute query and return nodes/edges
|
|
39
|
+
... return [], []
|
|
40
|
+
...
|
|
41
|
+
... def fetch_schema(self) -> tuple[list[dict], list[dict]]:
|
|
42
|
+
... return [], []
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def execute(self, query: str) -> tuple[list[dict], list[dict]]:
|
|
46
|
+
"""Execute a query and return (nodes, edges)."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
def fetch_schema(self) -> tuple[list[dict], list[dict]]:
|
|
50
|
+
"""Fetch schema and return (node_types, edge_types)."""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Backend registry for UI
|
|
55
|
+
BACKENDS = [
|
|
56
|
+
{"id": "neo4j", "name": "Neo4j", "side": "browser", "language": "cypher"},
|
|
57
|
+
{"id": "neo4j-python", "name": "Neo4j (Python)", "side": "python", "language": "cypher"},
|
|
58
|
+
{"id": "grafeo", "name": "Grafeo", "side": "python", "language": "cypher"},
|
|
59
|
+
{"id": "ladybug", "name": "LadybugDB", "side": "python", "language": "cypher"},
|
|
60
|
+
{"id": "arango", "name": "ArangoDB", "side": "python", "language": "aql"},
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
# Query language registry
|
|
64
|
+
QUERY_LANGUAGES = [
|
|
65
|
+
{"id": "cypher", "name": "Cypher", "enabled": True},
|
|
66
|
+
{"id": "gql", "name": "GQL", "enabled": True},
|
|
67
|
+
{"id": "sparql", "name": "SPARQL", "enabled": True},
|
|
68
|
+
{"id": "gremlin", "name": "Gremlin", "enabled": True},
|
|
69
|
+
{"id": "graphql", "name": "GraphQL", "enabled": True},
|
|
70
|
+
{"id": "aql", "name": "AQL", "enabled": True},
|
|
71
|
+
]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""ArangoDB database backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from anywidget_graph.converters.base import GraphData
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from arango.database import StandardDatabase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ArangoConverter:
|
|
14
|
+
"""Convert ArangoDB AQL results to graph data."""
|
|
15
|
+
|
|
16
|
+
def convert(self, result: Any) -> GraphData:
|
|
17
|
+
"""Convert AQL cursor result to nodes and edges.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
result : Any
|
|
22
|
+
AQL query cursor/result.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
GraphData
|
|
27
|
+
Dictionary with 'nodes' and 'edges' lists.
|
|
28
|
+
"""
|
|
29
|
+
nodes: dict[str, dict[str, Any]] = {}
|
|
30
|
+
edges: list[dict[str, Any]] = []
|
|
31
|
+
|
|
32
|
+
for doc in result:
|
|
33
|
+
if not isinstance(doc, dict):
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
# Check if it's an edge document (_from and _to fields)
|
|
37
|
+
if "_from" in doc and "_to" in doc:
|
|
38
|
+
edge = self._edge_to_dict(doc)
|
|
39
|
+
edges.append(edge)
|
|
40
|
+
# Otherwise treat as vertex document
|
|
41
|
+
elif "_key" in doc or "_id" in doc:
|
|
42
|
+
node = self._vertex_to_dict(doc)
|
|
43
|
+
nodes[node["id"]] = node
|
|
44
|
+
|
|
45
|
+
return {"nodes": list(nodes.values()), "edges": edges}
|
|
46
|
+
|
|
47
|
+
def _vertex_to_dict(self, doc: dict[str, Any]) -> dict[str, Any]:
|
|
48
|
+
"""Convert ArangoDB vertex document to node dict."""
|
|
49
|
+
# Extract ID from _key or _id
|
|
50
|
+
if "_key" in doc:
|
|
51
|
+
node_id = doc["_key"]
|
|
52
|
+
elif "_id" in doc:
|
|
53
|
+
node_id = doc["_id"].split("/")[-1]
|
|
54
|
+
else:
|
|
55
|
+
node_id = str(id(doc))
|
|
56
|
+
|
|
57
|
+
result: dict[str, Any] = {
|
|
58
|
+
"id": node_id,
|
|
59
|
+
"label": doc.get("name", doc.get("label", node_id)),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Add non-system properties
|
|
63
|
+
for key, value in doc.items():
|
|
64
|
+
if not key.startswith("_") and key not in ("name", "label"):
|
|
65
|
+
result[key] = value
|
|
66
|
+
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
def _edge_to_dict(self, doc: dict[str, Any]) -> dict[str, Any]:
|
|
70
|
+
"""Convert ArangoDB edge document to edge dict."""
|
|
71
|
+
result: dict[str, Any] = {
|
|
72
|
+
"source": doc["_from"].split("/")[-1],
|
|
73
|
+
"target": doc["_to"].split("/")[-1],
|
|
74
|
+
"label": doc.get("label", doc.get("_key", "")),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Add non-system properties
|
|
78
|
+
for key, value in doc.items():
|
|
79
|
+
if not key.startswith("_") and key not in ("label",):
|
|
80
|
+
result[key] = value
|
|
81
|
+
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ArangoBackend:
|
|
86
|
+
"""Backend for ArangoDB multi-model database.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
db : StandardDatabase | None
|
|
91
|
+
Existing ArangoDB database connection.
|
|
92
|
+
host : str
|
|
93
|
+
ArangoDB host URL (default: "http://localhost:8529").
|
|
94
|
+
username : str
|
|
95
|
+
Username for authentication (default: "root").
|
|
96
|
+
password : str
|
|
97
|
+
Password for authentication (default: "").
|
|
98
|
+
database : str
|
|
99
|
+
Database name (default: "_system").
|
|
100
|
+
|
|
101
|
+
Example
|
|
102
|
+
-------
|
|
103
|
+
>>> from anywidget_graph.backends import ArangoBackend
|
|
104
|
+
>>> backend = ArangoBackend(
|
|
105
|
+
... host="http://localhost:8529",
|
|
106
|
+
... username="root",
|
|
107
|
+
... password="password",
|
|
108
|
+
... database="mydb",
|
|
109
|
+
... )
|
|
110
|
+
>>> nodes, edges = backend.execute('''
|
|
111
|
+
... FOR v, e IN 1..2 OUTBOUND 'users/alice' GRAPH 'social'
|
|
112
|
+
... RETURN {vertex: v, edge: e}
|
|
113
|
+
... ''')
|
|
114
|
+
|
|
115
|
+
Or with an existing connection:
|
|
116
|
+
|
|
117
|
+
>>> from arango import ArangoClient
|
|
118
|
+
>>> client = ArangoClient(hosts="http://localhost:8529")
|
|
119
|
+
>>> db = client.db("mydb", username="root", password="password")
|
|
120
|
+
>>> backend = ArangoBackend(db=db)
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
db: StandardDatabase | None = None,
|
|
126
|
+
host: str = "http://localhost:8529",
|
|
127
|
+
username: str = "root",
|
|
128
|
+
password: str = "",
|
|
129
|
+
database: str = "_system",
|
|
130
|
+
) -> None:
|
|
131
|
+
self._db = db
|
|
132
|
+
self._host = host
|
|
133
|
+
self._username = username
|
|
134
|
+
self._password = password
|
|
135
|
+
self._database = database
|
|
136
|
+
self._converter = ArangoConverter()
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def query_language(self) -> str:
|
|
140
|
+
"""Return the default query language."""
|
|
141
|
+
return "aql"
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def db(self) -> StandardDatabase:
|
|
145
|
+
"""Get or create the database connection."""
|
|
146
|
+
if self._db is None:
|
|
147
|
+
from arango import ArangoClient
|
|
148
|
+
|
|
149
|
+
client = ArangoClient(hosts=self._host)
|
|
150
|
+
self._db = client.db(
|
|
151
|
+
self._database,
|
|
152
|
+
username=self._username,
|
|
153
|
+
password=self._password,
|
|
154
|
+
)
|
|
155
|
+
return self._db
|
|
156
|
+
|
|
157
|
+
def execute(self, query: str) -> tuple[list[dict], list[dict]]:
|
|
158
|
+
"""Execute AQL query and return (nodes, edges).
|
|
159
|
+
|
|
160
|
+
Parameters
|
|
161
|
+
----------
|
|
162
|
+
query : str
|
|
163
|
+
AQL query to execute.
|
|
164
|
+
|
|
165
|
+
Returns
|
|
166
|
+
-------
|
|
167
|
+
tuple[list[dict], list[dict]]
|
|
168
|
+
Tuple of (nodes, edges) lists.
|
|
169
|
+
"""
|
|
170
|
+
cursor = self.db.aql.execute(query)
|
|
171
|
+
data = self._converter.convert(cursor)
|
|
172
|
+
return data["nodes"], data["edges"]
|
|
173
|
+
|
|
174
|
+
def fetch_schema(self) -> tuple[list[dict], list[dict]]:
|
|
175
|
+
"""Fetch ArangoDB schema (collections).
|
|
176
|
+
|
|
177
|
+
Returns
|
|
178
|
+
-------
|
|
179
|
+
tuple[list[dict], list[dict]]
|
|
180
|
+
Tuple of (node_collections, edge_collections).
|
|
181
|
+
"""
|
|
182
|
+
node_types: list[dict[str, Any]] = []
|
|
183
|
+
edge_types: list[dict[str, Any]] = []
|
|
184
|
+
|
|
185
|
+
for coll in self.db.collections():
|
|
186
|
+
if coll["system"]:
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
coll_obj = self.db.collection(coll["name"])
|
|
190
|
+
info: dict[str, Any] = {
|
|
191
|
+
"name": coll["name"],
|
|
192
|
+
"count": coll_obj.count(),
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# Edge collections have type 3
|
|
196
|
+
if coll.get("type") == 3 or coll.get("type") == "edge":
|
|
197
|
+
edge_types.append(info)
|
|
198
|
+
else:
|
|
199
|
+
node_types.append(info)
|
|
200
|
+
|
|
201
|
+
return node_types, edge_types
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Grafeo database backend (Python-side execution)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from anywidget_graph.converters import (
|
|
8
|
+
get_node_id,
|
|
9
|
+
is_node,
|
|
10
|
+
is_relationship,
|
|
11
|
+
node_to_dict,
|
|
12
|
+
relationship_to_dict,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GrafeoBackend:
|
|
20
|
+
"""Backend for Grafeo database connections."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, db: Any) -> None:
|
|
23
|
+
"""Initialize with a GrafeoDB instance."""
|
|
24
|
+
self._db = db
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def db(self) -> Any:
|
|
28
|
+
"""Get the underlying database instance."""
|
|
29
|
+
return self._db
|
|
30
|
+
|
|
31
|
+
def execute(self, query: str) -> tuple[list[dict], list[dict]]:
|
|
32
|
+
"""Execute a Cypher query and return (nodes, edges)."""
|
|
33
|
+
result = self._db.execute(query)
|
|
34
|
+
return self._process_result(result)
|
|
35
|
+
|
|
36
|
+
def fetch_schema(self) -> tuple[list[dict], list[dict]]:
|
|
37
|
+
"""Fetch schema from Grafeo database."""
|
|
38
|
+
# Grafeo schema fetching would be implemented here
|
|
39
|
+
# For now, return empty schema
|
|
40
|
+
return [], []
|
|
41
|
+
|
|
42
|
+
def _process_result(self, result: Any) -> tuple[list[dict], list[dict]]:
|
|
43
|
+
"""Process query results into nodes and edges."""
|
|
44
|
+
nodes: dict[str, dict] = {}
|
|
45
|
+
edges: list[dict] = []
|
|
46
|
+
|
|
47
|
+
records = list(result) if hasattr(result, "__iter__") else [result]
|
|
48
|
+
|
|
49
|
+
for record in records:
|
|
50
|
+
items = self._extract_items(record)
|
|
51
|
+
for key, value in items:
|
|
52
|
+
if is_node(value):
|
|
53
|
+
node_id = get_node_id(value)
|
|
54
|
+
if node_id not in nodes:
|
|
55
|
+
nodes[node_id] = node_to_dict(value)
|
|
56
|
+
elif is_relationship(value):
|
|
57
|
+
edges.append(relationship_to_dict(value))
|
|
58
|
+
|
|
59
|
+
return list(nodes.values()), edges
|
|
60
|
+
|
|
61
|
+
def _extract_items(self, record: Any) -> list[tuple[str, Any]]:
|
|
62
|
+
"""Extract key-value items from a record."""
|
|
63
|
+
if hasattr(record, "items"):
|
|
64
|
+
items = record.items() if callable(record.items) else record.items
|
|
65
|
+
return list(items)
|
|
66
|
+
elif hasattr(record, "data"):
|
|
67
|
+
return list(record.data().items())
|
|
68
|
+
elif isinstance(record, dict):
|
|
69
|
+
return list(record.items())
|
|
70
|
+
return []
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""LadybugDB database backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from anywidget_graph.converters.cypher import CypherConverter
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LadybugBackend:
|
|
14
|
+
"""Backend for LadybugDB embedded graph database.
|
|
15
|
+
|
|
16
|
+
LadybugDB is an embedded graph database that supports Cypher queries.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
db : Any
|
|
21
|
+
LadybugDB database instance.
|
|
22
|
+
|
|
23
|
+
Example
|
|
24
|
+
-------
|
|
25
|
+
>>> from ladybug import LadybugDB
|
|
26
|
+
>>> db = LadybugDB()
|
|
27
|
+
>>> db.execute("CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'})")
|
|
28
|
+
>>>
|
|
29
|
+
>>> from anywidget_graph.backends import LadybugBackend
|
|
30
|
+
>>> backend = LadybugBackend(db)
|
|
31
|
+
>>> nodes, edges = backend.execute("MATCH (n)-[r]->(m) RETURN n, r, m")
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, db: Any) -> None:
|
|
35
|
+
self._db = db
|
|
36
|
+
self._converter = CypherConverter()
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def query_language(self) -> str:
|
|
40
|
+
"""Return the default query language."""
|
|
41
|
+
return "cypher"
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def db(self) -> Any:
|
|
45
|
+
"""Get the underlying database instance."""
|
|
46
|
+
return self._db
|
|
47
|
+
|
|
48
|
+
def execute(self, query: str) -> tuple[list[dict], list[dict]]:
|
|
49
|
+
"""Execute Cypher query and return (nodes, edges).
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
query : str
|
|
54
|
+
Cypher query to execute.
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
tuple[list[dict], list[dict]]
|
|
59
|
+
Tuple of (nodes, edges) lists.
|
|
60
|
+
"""
|
|
61
|
+
result = self._db.execute(query)
|
|
62
|
+
data = self._converter.convert(result)
|
|
63
|
+
return data["nodes"], data["edges"]
|
|
64
|
+
|
|
65
|
+
def fetch_schema(self) -> tuple[list[dict], list[dict]]:
|
|
66
|
+
"""Fetch LadybugDB schema.
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
tuple[list[dict], list[dict]]
|
|
71
|
+
Tuple of (node_types, edge_types).
|
|
72
|
+
"""
|
|
73
|
+
node_types: list[dict[str, Any]] = []
|
|
74
|
+
edge_types: list[dict[str, Any]] = []
|
|
75
|
+
|
|
76
|
+
# Try to get labels if supported
|
|
77
|
+
try:
|
|
78
|
+
labels_result = self._db.execute("CALL db.labels()")
|
|
79
|
+
for record in labels_result:
|
|
80
|
+
label = record[0] if hasattr(record, "__getitem__") else str(record)
|
|
81
|
+
node_types.append({"label": label, "properties": []})
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
# Try to get relationship types if supported
|
|
86
|
+
try:
|
|
87
|
+
types_result = self._db.execute("CALL db.relationshipTypes()")
|
|
88
|
+
for record in types_result:
|
|
89
|
+
rel_type = record[0] if hasattr(record, "__getitem__") else str(record)
|
|
90
|
+
edge_types.append({"type": rel_type, "properties": []})
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
return node_types, edge_types
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Neo4j database backend using the Python driver."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from anywidget_graph.converters.cypher import CypherConverter
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from neo4j import Driver
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Neo4jBackend:
|
|
14
|
+
"""Backend for Neo4j database using the Python driver.
|
|
15
|
+
|
|
16
|
+
This executes queries Python-side (not in browser).
|
|
17
|
+
Use for server-side notebooks or when browser connectivity is limited.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
driver : Driver | None
|
|
22
|
+
Existing Neo4j driver instance.
|
|
23
|
+
uri : str | None
|
|
24
|
+
Neo4j connection URI (e.g., "neo4j://localhost:7687").
|
|
25
|
+
auth : tuple[str, str] | None
|
|
26
|
+
Authentication tuple (username, password).
|
|
27
|
+
database : str
|
|
28
|
+
Database name to use (default: "neo4j").
|
|
29
|
+
|
|
30
|
+
Example
|
|
31
|
+
-------
|
|
32
|
+
>>> from anywidget_graph.backends import Neo4jBackend
|
|
33
|
+
>>> backend = Neo4jBackend(
|
|
34
|
+
... uri="neo4j://localhost:7687",
|
|
35
|
+
... auth=("neo4j", "password"),
|
|
36
|
+
... )
|
|
37
|
+
>>> nodes, edges = backend.execute("MATCH (n)-[r]->(m) RETURN n, r, m LIMIT 10")
|
|
38
|
+
|
|
39
|
+
Or with an existing driver:
|
|
40
|
+
|
|
41
|
+
>>> from neo4j import GraphDatabase
|
|
42
|
+
>>> driver = GraphDatabase.driver(uri, auth=auth)
|
|
43
|
+
>>> backend = Neo4jBackend(driver=driver)
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
driver: Driver | None = None,
|
|
49
|
+
uri: str | None = None,
|
|
50
|
+
auth: tuple[str, str] | None = None,
|
|
51
|
+
database: str = "neo4j",
|
|
52
|
+
) -> None:
|
|
53
|
+
self._driver = driver
|
|
54
|
+
self._uri = uri
|
|
55
|
+
self._auth = auth
|
|
56
|
+
self._database = database
|
|
57
|
+
self._converter = CypherConverter()
|
|
58
|
+
self._owns_driver = driver is None # Track if we created the driver
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def query_language(self) -> str:
|
|
62
|
+
"""Return the default query language."""
|
|
63
|
+
return "cypher"
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def driver(self) -> Driver:
|
|
67
|
+
"""Get or create the Neo4j driver."""
|
|
68
|
+
if self._driver is None:
|
|
69
|
+
if self._uri is None:
|
|
70
|
+
msg = "Either driver or uri must be provided"
|
|
71
|
+
raise ValueError(msg)
|
|
72
|
+
from neo4j import GraphDatabase
|
|
73
|
+
|
|
74
|
+
self._driver = GraphDatabase.driver(self._uri, auth=self._auth)
|
|
75
|
+
return self._driver
|
|
76
|
+
|
|
77
|
+
def execute(self, query: str) -> tuple[list[dict], list[dict]]:
|
|
78
|
+
"""Execute Cypher query and return (nodes, edges).
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
query : str
|
|
83
|
+
Cypher query to execute.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
tuple[list[dict], list[dict]]
|
|
88
|
+
Tuple of (nodes, edges) lists.
|
|
89
|
+
"""
|
|
90
|
+
with self.driver.session(database=self._database) as session:
|
|
91
|
+
result = session.run(query)
|
|
92
|
+
records = list(result) # Consume within session
|
|
93
|
+
|
|
94
|
+
data = self._converter.convert(records)
|
|
95
|
+
return data["nodes"], data["edges"]
|
|
96
|
+
|
|
97
|
+
def fetch_schema(self) -> tuple[list[dict], list[dict]]:
|
|
98
|
+
"""Fetch Neo4j schema.
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
tuple[list[dict], list[dict]]
|
|
103
|
+
Tuple of (node_types, edge_types) with label/type info and properties.
|
|
104
|
+
"""
|
|
105
|
+
node_types: list[dict[str, Any]] = []
|
|
106
|
+
edge_types: list[dict[str, Any]] = []
|
|
107
|
+
|
|
108
|
+
with self.driver.session(database=self._database) as session:
|
|
109
|
+
# Get node labels
|
|
110
|
+
labels_result = session.run("CALL db.labels()")
|
|
111
|
+
for record in labels_result:
|
|
112
|
+
label = record[0]
|
|
113
|
+
# Get properties for this label
|
|
114
|
+
props_result = session.run(f"MATCH (n:`{label}`) UNWIND keys(n) AS key RETURN DISTINCT key LIMIT 20")
|
|
115
|
+
properties = [r[0] for r in props_result]
|
|
116
|
+
node_types.append({"label": label, "properties": properties})
|
|
117
|
+
|
|
118
|
+
# Get relationship types
|
|
119
|
+
rel_types_result = session.run("CALL db.relationshipTypes()")
|
|
120
|
+
for record in rel_types_result:
|
|
121
|
+
rel_type = record[0]
|
|
122
|
+
# Get properties for this type
|
|
123
|
+
props_result = session.run(
|
|
124
|
+
f"MATCH ()-[r:`{rel_type}`]->() UNWIND keys(r) AS key RETURN DISTINCT key LIMIT 20"
|
|
125
|
+
)
|
|
126
|
+
properties = [r[0] for r in props_result]
|
|
127
|
+
edge_types.append({"type": rel_type, "properties": properties})
|
|
128
|
+
|
|
129
|
+
return node_types, edge_types
|
|
130
|
+
|
|
131
|
+
def close(self) -> None:
|
|
132
|
+
"""Close the driver connection if we own it."""
|
|
133
|
+
if self._driver and self._owns_driver:
|
|
134
|
+
self._driver.close()
|
|
135
|
+
self._driver = None
|
|
136
|
+
|
|
137
|
+
def __enter__(self) -> Neo4jBackend:
|
|
138
|
+
"""Context manager entry."""
|
|
139
|
+
return self
|
|
140
|
+
|
|
141
|
+
def __exit__(self, *args: Any) -> None:
|
|
142
|
+
"""Context manager exit."""
|
|
143
|
+
self.close()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Data conversion utilities for graph data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from anywidget_graph.converters.base import GraphData, ResultConverter
|
|
6
|
+
from anywidget_graph.converters.common import (
|
|
7
|
+
get_node_id,
|
|
8
|
+
is_node,
|
|
9
|
+
is_relationship,
|
|
10
|
+
node_to_dict,
|
|
11
|
+
relationship_to_dict,
|
|
12
|
+
)
|
|
13
|
+
from anywidget_graph.converters.cypher import CypherConverter
|
|
14
|
+
from anywidget_graph.converters.gql import GQLConverter
|
|
15
|
+
from anywidget_graph.converters.graphql import GraphQLConverter
|
|
16
|
+
from anywidget_graph.converters.gremlin import GremlinConverter
|
|
17
|
+
from anywidget_graph.converters.sparql import SPARQLConverter
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
# Base types
|
|
21
|
+
"GraphData",
|
|
22
|
+
"ResultConverter",
|
|
23
|
+
# Common utilities
|
|
24
|
+
"get_node_id",
|
|
25
|
+
"is_node",
|
|
26
|
+
"is_relationship",
|
|
27
|
+
"node_to_dict",
|
|
28
|
+
"relationship_to_dict",
|
|
29
|
+
# Converters
|
|
30
|
+
"CypherConverter",
|
|
31
|
+
"GQLConverter",
|
|
32
|
+
"GraphQLConverter",
|
|
33
|
+
"GremlinConverter",
|
|
34
|
+
"SPARQLConverter",
|
|
35
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Base types and protocols for result converters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Protocol, TypedDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NodeDict(TypedDict, total=False):
|
|
9
|
+
"""Standard node representation.
|
|
10
|
+
|
|
11
|
+
Required:
|
|
12
|
+
id: Unique identifier for the node.
|
|
13
|
+
|
|
14
|
+
Optional:
|
|
15
|
+
label: Display label for the node.
|
|
16
|
+
labels: List of type labels (e.g., Neo4j labels).
|
|
17
|
+
Additional properties are allowed.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
id: str
|
|
21
|
+
label: str
|
|
22
|
+
labels: list[str]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EdgeDict(TypedDict, total=False):
|
|
26
|
+
"""Standard edge representation.
|
|
27
|
+
|
|
28
|
+
Required:
|
|
29
|
+
source: ID of the source node.
|
|
30
|
+
target: ID of the target node.
|
|
31
|
+
|
|
32
|
+
Optional:
|
|
33
|
+
label: Display label/type for the edge.
|
|
34
|
+
Additional properties are allowed.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
source: str
|
|
38
|
+
target: str
|
|
39
|
+
label: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class GraphData(TypedDict):
|
|
43
|
+
"""Standard graph data format returned by converters."""
|
|
44
|
+
|
|
45
|
+
nodes: list[dict[str, Any]]
|
|
46
|
+
edges: list[dict[str, Any]]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ResultConverter(Protocol):
|
|
50
|
+
"""Protocol for converting query results to graph data.
|
|
51
|
+
|
|
52
|
+
Implementations should handle the specific result format of their
|
|
53
|
+
query language and return a standardized GraphData structure.
|
|
54
|
+
|
|
55
|
+
Example
|
|
56
|
+
-------
|
|
57
|
+
>>> class MyConverter:
|
|
58
|
+
... def convert(self, result: Any) -> GraphData:
|
|
59
|
+
... nodes = [{"id": "1", "label": "Node"}]
|
|
60
|
+
... edges = [{"source": "1", "target": "2"}]
|
|
61
|
+
... return {"nodes": nodes, "edges": edges}
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def convert(self, result: Any) -> GraphData:
|
|
65
|
+
"""Convert raw query result to nodes and edges.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
result : Any
|
|
70
|
+
Raw query result from the database driver.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
GraphData
|
|
75
|
+
Dictionary with 'nodes' and 'edges' lists.
|
|
76
|
+
"""
|
|
77
|
+
...
|