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.
@@ -3,4 +3,4 @@
3
3
  from anywidget_graph.widget import Graph
4
4
 
5
5
  __all__ = ["Graph"]
6
- __version__ = "0.1.0"
6
+ __version__ = "0.2.1"
@@ -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
+ ...