anywidget-graph 0.1.0__tar.gz → 0.2.1__tar.gz
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-0.2.1/.github/workflows/ci.yml +35 -0
- anywidget_graph-0.2.1/.github/workflows/pypi.yml +40 -0
- {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/PKG-INFO +4 -3
- {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/README.md +1 -1
- {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/pyproject.toml +3 -2
- {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/src/anywidget_graph/__init__.py +1 -1
- anywidget_graph-0.2.1/src/anywidget_graph/backends/__init__.py +71 -0
- anywidget_graph-0.2.1/src/anywidget_graph/backends/arango.py +201 -0
- anywidget_graph-0.2.1/src/anywidget_graph/backends/grafeo.py +70 -0
- anywidget_graph-0.2.1/src/anywidget_graph/backends/ladybug.py +94 -0
- anywidget_graph-0.2.1/src/anywidget_graph/backends/neo4j.py +143 -0
- anywidget_graph-0.2.1/src/anywidget_graph/converters/__init__.py +35 -0
- anywidget_graph-0.2.1/src/anywidget_graph/converters/base.py +77 -0
- anywidget_graph-0.2.1/src/anywidget_graph/converters/common.py +92 -0
- anywidget_graph-0.2.1/src/anywidget_graph/converters/cypher.py +107 -0
- anywidget_graph-0.2.1/src/anywidget_graph/converters/gql.py +15 -0
- anywidget_graph-0.2.1/src/anywidget_graph/converters/graphql.py +208 -0
- anywidget_graph-0.2.1/src/anywidget_graph/converters/gremlin.py +159 -0
- anywidget_graph-0.2.1/src/anywidget_graph/converters/sparql.py +167 -0
- anywidget_graph-0.2.1/src/anywidget_graph/ui/__init__.py +19 -0
- anywidget_graph-0.2.1/src/anywidget_graph/ui/icons.js +15 -0
- anywidget_graph-0.2.1/src/anywidget_graph/ui/index.js +199 -0
- anywidget_graph-0.2.1/src/anywidget_graph/ui/neo4j.js +193 -0
- anywidget_graph-0.2.1/src/anywidget_graph/ui/properties.js +137 -0
- anywidget_graph-0.2.1/src/anywidget_graph/ui/schema.js +178 -0
- anywidget_graph-0.2.1/src/anywidget_graph/ui/settings.js +299 -0
- anywidget_graph-0.2.1/src/anywidget_graph/ui/styles.css +584 -0
- anywidget_graph-0.2.1/src/anywidget_graph/ui/toolbar.js +106 -0
- anywidget_graph-0.2.1/src/anywidget_graph/widget.py +484 -0
- {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/uv.lock +3 -1
- anywidget_graph-0.1.0/src/anywidget_graph/widget.py +0 -293
- {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/.gitignore +0 -0
- {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/.python-version +0 -0
- {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/examples/demo.py +0 -0
- {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/src/anywidget_graph/py.typed +0 -0
- {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/tests/__init__.py +0 -0
- {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/tests/test_graph.py +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.12", "3.13"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v4
|
|
21
|
+
|
|
22
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
23
|
+
run: uv python install ${{ matrix.python-version }}
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: uv sync --dev
|
|
27
|
+
|
|
28
|
+
- name: Run ruff check
|
|
29
|
+
run: uv run ruff check .
|
|
30
|
+
|
|
31
|
+
- name: Run ruff format check
|
|
32
|
+
run: uv run ruff format --check .
|
|
33
|
+
|
|
34
|
+
- name: Run tests
|
|
35
|
+
run: uv run pytest -v
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- name: Install uv
|
|
15
|
+
uses: astral-sh/setup-uv@v4
|
|
16
|
+
|
|
17
|
+
- name: Build package
|
|
18
|
+
run: uv build
|
|
19
|
+
|
|
20
|
+
- name: Upload artifacts
|
|
21
|
+
uses: actions/upload-artifact@v4
|
|
22
|
+
with:
|
|
23
|
+
name: dist
|
|
24
|
+
path: dist/
|
|
25
|
+
|
|
26
|
+
publish:
|
|
27
|
+
needs: build
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
environment: pypi
|
|
30
|
+
permissions:
|
|
31
|
+
id-token: write
|
|
32
|
+
steps:
|
|
33
|
+
- name: Download artifacts
|
|
34
|
+
uses: actions/download-artifact@v4
|
|
35
|
+
with:
|
|
36
|
+
name: dist
|
|
37
|
+
path: dist/
|
|
38
|
+
|
|
39
|
+
- name: Publish to PyPI
|
|
40
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anywidget-graph
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Interactive graph visualization for Python notebooks using anywidget
|
|
5
5
|
Project-URL: Homepage, https://grafeo.dev/
|
|
6
6
|
Project-URL: Repository, https://github.com/GrafeoDB/anywidget-graph
|
|
@@ -19,11 +19,12 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
19
19
|
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
20
20
|
Requires-Python: >=3.12
|
|
21
21
|
Requires-Dist: anywidget>=0.9.21
|
|
22
|
+
Requires-Dist: marimo>=0.19.7
|
|
22
23
|
Provides-Extra: dev
|
|
23
24
|
Requires-Dist: marimo>=0.19.7; extra == 'dev'
|
|
24
25
|
Requires-Dist: prek>=0.3.1; extra == 'dev'
|
|
25
26
|
Requires-Dist: pytest>=9.0.2; extra == 'dev'
|
|
26
|
-
Requires-Dist: ruff>=0.
|
|
27
|
+
Requires-Dist: ruff>=0.15.0; extra == 'dev'
|
|
27
28
|
Requires-Dist: ty>=0.0.14; extra == 'dev'
|
|
28
29
|
Provides-Extra: networkx
|
|
29
30
|
Requires-Dist: networkx>=3.0; extra == 'networkx'
|
|
@@ -251,7 +252,7 @@ graph.to_json("graph.json")
|
|
|
251
252
|
|
|
252
253
|
- [anywidget](https://anywidget.dev/) — Custom Jupyter widgets made easy
|
|
253
254
|
- [Grafeo](https://github.com/GrafeoDB/grafeo) — Embeddable graph database
|
|
254
|
-
- [grafeo-
|
|
255
|
+
- [grafeo-web](https://github.com/GrafeoDB/grafeo-web) — Grafeo in the browser
|
|
255
256
|
|
|
256
257
|
## License
|
|
257
258
|
|
|
@@ -218,7 +218,7 @@ graph.to_json("graph.json")
|
|
|
218
218
|
|
|
219
219
|
- [anywidget](https://anywidget.dev/) — Custom Jupyter widgets made easy
|
|
220
220
|
- [Grafeo](https://github.com/GrafeoDB/grafeo) — Embeddable graph database
|
|
221
|
-
- [grafeo-
|
|
221
|
+
- [grafeo-web](https://github.com/GrafeoDB/grafeo-web) — Grafeo in the browser
|
|
222
222
|
|
|
223
223
|
## License
|
|
224
224
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "anywidget-graph"
|
|
3
|
-
version = "0.1
|
|
3
|
+
version = "0.2.1"
|
|
4
4
|
description = "Interactive graph visualization for Python notebooks using anywidget"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "Apache-2.0" }
|
|
@@ -22,13 +22,14 @@ classifiers = [
|
|
|
22
22
|
|
|
23
23
|
dependencies = [
|
|
24
24
|
"anywidget>=0.9.21",
|
|
25
|
+
"marimo>=0.19.7",
|
|
25
26
|
]
|
|
26
27
|
|
|
27
28
|
[project.optional-dependencies]
|
|
28
29
|
dev = [
|
|
29
30
|
"prek>=0.3.1",
|
|
30
31
|
"pytest>=9.0.2",
|
|
31
|
-
"ruff>=0.
|
|
32
|
+
"ruff>=0.15.0",
|
|
32
33
|
"ty>=0.0.14",
|
|
33
34
|
"marimo>=0.19.7",
|
|
34
35
|
]
|
|
@@ -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
|