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.
Files changed (37) hide show
  1. anywidget_graph-0.2.1/.github/workflows/ci.yml +35 -0
  2. anywidget_graph-0.2.1/.github/workflows/pypi.yml +40 -0
  3. {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/PKG-INFO +4 -3
  4. {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/README.md +1 -1
  5. {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/pyproject.toml +3 -2
  6. {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/src/anywidget_graph/__init__.py +1 -1
  7. anywidget_graph-0.2.1/src/anywidget_graph/backends/__init__.py +71 -0
  8. anywidget_graph-0.2.1/src/anywidget_graph/backends/arango.py +201 -0
  9. anywidget_graph-0.2.1/src/anywidget_graph/backends/grafeo.py +70 -0
  10. anywidget_graph-0.2.1/src/anywidget_graph/backends/ladybug.py +94 -0
  11. anywidget_graph-0.2.1/src/anywidget_graph/backends/neo4j.py +143 -0
  12. anywidget_graph-0.2.1/src/anywidget_graph/converters/__init__.py +35 -0
  13. anywidget_graph-0.2.1/src/anywidget_graph/converters/base.py +77 -0
  14. anywidget_graph-0.2.1/src/anywidget_graph/converters/common.py +92 -0
  15. anywidget_graph-0.2.1/src/anywidget_graph/converters/cypher.py +107 -0
  16. anywidget_graph-0.2.1/src/anywidget_graph/converters/gql.py +15 -0
  17. anywidget_graph-0.2.1/src/anywidget_graph/converters/graphql.py +208 -0
  18. anywidget_graph-0.2.1/src/anywidget_graph/converters/gremlin.py +159 -0
  19. anywidget_graph-0.2.1/src/anywidget_graph/converters/sparql.py +167 -0
  20. anywidget_graph-0.2.1/src/anywidget_graph/ui/__init__.py +19 -0
  21. anywidget_graph-0.2.1/src/anywidget_graph/ui/icons.js +15 -0
  22. anywidget_graph-0.2.1/src/anywidget_graph/ui/index.js +199 -0
  23. anywidget_graph-0.2.1/src/anywidget_graph/ui/neo4j.js +193 -0
  24. anywidget_graph-0.2.1/src/anywidget_graph/ui/properties.js +137 -0
  25. anywidget_graph-0.2.1/src/anywidget_graph/ui/schema.js +178 -0
  26. anywidget_graph-0.2.1/src/anywidget_graph/ui/settings.js +299 -0
  27. anywidget_graph-0.2.1/src/anywidget_graph/ui/styles.css +584 -0
  28. anywidget_graph-0.2.1/src/anywidget_graph/ui/toolbar.js +106 -0
  29. anywidget_graph-0.2.1/src/anywidget_graph/widget.py +484 -0
  30. {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/uv.lock +3 -1
  31. anywidget_graph-0.1.0/src/anywidget_graph/widget.py +0 -293
  32. {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/.gitignore +0 -0
  33. {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/.python-version +0 -0
  34. {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/examples/demo.py +0 -0
  35. {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/src/anywidget_graph/py.typed +0 -0
  36. {anywidget_graph-0.1.0 → anywidget_graph-0.2.1}/tests/__init__.py +0 -0
  37. {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.0
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.14.14; extra == 'dev'
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-wasm](https://github.com/GrafeoDB/grafeo-wasm) — Grafeo in the browser
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-wasm](https://github.com/GrafeoDB/grafeo-wasm) — Grafeo in the browser
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.0"
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.14.14",
32
+ "ruff>=0.15.0",
32
33
  "ty>=0.0.14",
33
34
  "marimo>=0.19.7",
34
35
  ]
@@ -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