anywidget-graph 0.2.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.
@@ -0,0 +1,92 @@
1
+ """Common node and edge conversion utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def is_node(obj: Any) -> bool:
9
+ """Check if an object represents a graph node."""
10
+ if hasattr(obj, "labels") and hasattr(obj, "element_id"):
11
+ return True
12
+ if hasattr(obj, "labels") and hasattr(obj, "properties"):
13
+ return True
14
+ if isinstance(obj, dict) and "id" in obj:
15
+ return True
16
+ return False
17
+
18
+
19
+ def is_relationship(obj: Any) -> bool:
20
+ """Check if an object represents a graph relationship."""
21
+ if hasattr(obj, "type") and hasattr(obj, "start_node"):
22
+ return True
23
+ if hasattr(obj, "type") and hasattr(obj, "source") and hasattr(obj, "target"):
24
+ return True
25
+ if isinstance(obj, dict) and "source" in obj and "target" in obj:
26
+ return True
27
+ return False
28
+
29
+
30
+ def get_node_id(node: Any) -> str:
31
+ """Extract the ID from a node object."""
32
+ if hasattr(node, "element_id"):
33
+ return str(node.element_id)
34
+ if hasattr(node, "id"):
35
+ return str(node.id)
36
+ if isinstance(node, dict):
37
+ return str(node.get("id", id(node)))
38
+ return str(id(node))
39
+
40
+
41
+ def node_to_dict(node: Any) -> dict[str, Any]:
42
+ """Convert a node object to a dictionary."""
43
+ result: dict[str, Any] = {"id": get_node_id(node)}
44
+
45
+ if hasattr(node, "labels"):
46
+ labels = list(node.labels) if hasattr(node.labels, "__iter__") else [node.labels]
47
+ if labels:
48
+ result["label"] = labels[0]
49
+ result["labels"] = labels
50
+
51
+ if hasattr(node, "properties"):
52
+ props = node.properties if isinstance(node.properties, dict) else dict(node.properties)
53
+ result.update(props)
54
+ elif hasattr(node, "items"):
55
+ result.update(dict(node))
56
+ elif isinstance(node, dict):
57
+ result.update(node)
58
+
59
+ if "label" not in result and "name" in result:
60
+ result["label"] = result["name"]
61
+
62
+ return result
63
+
64
+
65
+ def relationship_to_dict(rel: Any) -> dict[str, Any]:
66
+ """Convert a relationship object to a dictionary."""
67
+ result: dict[str, Any] = {}
68
+
69
+ if hasattr(rel, "start_node") and hasattr(rel, "end_node"):
70
+ result["source"] = get_node_id(rel.start_node)
71
+ result["target"] = get_node_id(rel.end_node)
72
+ elif hasattr(rel, "source") and hasattr(rel, "target"):
73
+ result["source"] = get_node_id(rel.source)
74
+ result["target"] = get_node_id(rel.target)
75
+ elif isinstance(rel, dict):
76
+ result["source"] = str(rel.get("source", ""))
77
+ result["target"] = str(rel.get("target", ""))
78
+
79
+ if hasattr(rel, "type"):
80
+ result["label"] = str(rel.type)
81
+ elif isinstance(rel, dict) and "type" in rel:
82
+ result["label"] = str(rel["type"])
83
+ elif isinstance(rel, dict) and "label" in rel:
84
+ result["label"] = str(rel["label"])
85
+
86
+ if hasattr(rel, "properties"):
87
+ props = rel.properties if isinstance(rel.properties, dict) else dict(rel.properties)
88
+ result.update(props)
89
+ elif hasattr(rel, "items") and not isinstance(rel, dict):
90
+ result.update(dict(rel))
91
+
92
+ return result
@@ -0,0 +1,107 @@
1
+ """Converter for Cypher/Neo4j query results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from anywidget_graph.converters.base import GraphData
8
+ from anywidget_graph.converters.common import (
9
+ get_node_id,
10
+ is_node,
11
+ is_relationship,
12
+ node_to_dict,
13
+ relationship_to_dict,
14
+ )
15
+
16
+
17
+ class CypherConverter:
18
+ """Convert Cypher query results to graph data.
19
+
20
+ Handles various result formats from Neo4j and compatible databases:
21
+ - Neo4j Python driver Result objects
22
+ - Record objects with .items() or .data() methods
23
+ - Path objects containing nodes and relationships
24
+ - Lists of nodes/relationships
25
+
26
+ Example
27
+ -------
28
+ >>> from neo4j import GraphDatabase
29
+ >>> driver = GraphDatabase.driver(uri, auth=auth)
30
+ >>> with driver.session() as session:
31
+ ... result = session.run("MATCH (n)-[r]->(m) RETURN n, r, m LIMIT 10")
32
+ ... converter = CypherConverter()
33
+ ... data = converter.convert(result)
34
+ ... print(f"Found {len(data['nodes'])} nodes")
35
+ """
36
+
37
+ def convert(self, result: Any) -> GraphData:
38
+ """Convert Cypher result to nodes and edges.
39
+
40
+ Parameters
41
+ ----------
42
+ result : Any
43
+ Query result from Neo4j driver or compatible database.
44
+
45
+ Returns
46
+ -------
47
+ GraphData
48
+ Dictionary with 'nodes' and 'edges' lists.
49
+ """
50
+ nodes: dict[str, dict[str, Any]] = {}
51
+ edges: list[dict[str, Any]] = []
52
+
53
+ records = list(result) if hasattr(result, "__iter__") else [result]
54
+
55
+ for record in records:
56
+ for _key, value in self._extract_items(record):
57
+ self._process_value(value, nodes, edges)
58
+
59
+ return {"nodes": list(nodes.values()), "edges": edges}
60
+
61
+ def _extract_items(self, record: Any) -> list[tuple[str, Any]]:
62
+ """Extract key-value items from a record.
63
+
64
+ Handles multiple record formats:
65
+ - Records with .items() method (Neo4j Record)
66
+ - Records with .data() method
67
+ - Plain dictionaries
68
+ """
69
+ if hasattr(record, "items"):
70
+ items = record.items() if callable(record.items) else record.items
71
+ return list(items)
72
+ elif hasattr(record, "data"):
73
+ return list(record.data().items())
74
+ elif isinstance(record, dict):
75
+ return list(record.items())
76
+ return []
77
+
78
+ def _process_value(
79
+ self,
80
+ value: Any,
81
+ nodes: dict[str, dict[str, Any]],
82
+ edges: list[dict[str, Any]],
83
+ ) -> None:
84
+ """Process a single value, handling paths and collections recursively."""
85
+ if value is None:
86
+ return
87
+
88
+ # Handle Path objects (have .nodes and .relationships attributes)
89
+ if hasattr(value, "nodes") and hasattr(value, "relationships"):
90
+ for node in value.nodes:
91
+ node_id = get_node_id(node)
92
+ if node_id not in nodes:
93
+ nodes[node_id] = node_to_dict(node)
94
+ for rel in value.relationships:
95
+ edges.append(relationship_to_dict(rel))
96
+ # Handle individual nodes
97
+ elif is_node(value):
98
+ node_id = get_node_id(value)
99
+ if node_id not in nodes:
100
+ nodes[node_id] = node_to_dict(value)
101
+ # Handle individual relationships
102
+ elif is_relationship(value):
103
+ edges.append(relationship_to_dict(value))
104
+ # Handle lists/collections
105
+ elif isinstance(value, (list, tuple)):
106
+ for item in value:
107
+ self._process_value(item, nodes, edges)
@@ -0,0 +1,15 @@
1
+ """Converter for ISO GQL query results.
2
+
3
+ GQL (Graph Query Language) is the ISO standard graph query language.
4
+ Its result format is similar to Cypher, so this module provides an
5
+ alias to the CypherConverter.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from anywidget_graph.converters.cypher import CypherConverter
11
+
12
+ # GQL uses the same result format as Cypher
13
+ GQLConverter = CypherConverter
14
+
15
+ __all__ = ["GQLConverter"]
@@ -0,0 +1,208 @@
1
+ """Converter for GraphQL query results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from anywidget_graph.converters.base import GraphData
8
+
9
+
10
+ class GraphQLConverter:
11
+ """Convert GraphQL JSON responses to graph data.
12
+
13
+ Handles various GraphQL schema patterns:
14
+ - Relay-style connections with nodes/edges
15
+ - Simple lists of objects with references
16
+ - Nested object structures
17
+
18
+ Parameters
19
+ ----------
20
+ id_field : str
21
+ Field name used for node IDs (default: "id").
22
+ label_field : str
23
+ Field name used for node labels (default: "name").
24
+ nodes_path : str | None
25
+ Dot-separated path to nodes list (e.g., "data.allPeople.nodes").
26
+ If None, auto-detection is used.
27
+ edges_path : str | None
28
+ Dot-separated path to edges list (e.g., "data.connections.edges").
29
+ If None, auto-detection is used.
30
+ source_field : str
31
+ Field name for edge source (default: "source").
32
+ target_field : str
33
+ Field name for edge target (default: "target").
34
+
35
+ Example
36
+ -------
37
+ >>> import requests
38
+ >>> response = requests.post(url, json={"query": query})
39
+ >>> result = response.json()
40
+ >>> converter = GraphQLConverter(
41
+ ... nodes_path="data.characters.results",
42
+ ... id_field="id",
43
+ ... label_field="name",
44
+ ... )
45
+ >>> data = converter.convert(result)
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ id_field: str = "id",
51
+ label_field: str = "name",
52
+ nodes_path: str | None = None,
53
+ edges_path: str | None = None,
54
+ source_field: str = "source",
55
+ target_field: str = "target",
56
+ ) -> None:
57
+ self.id_field = id_field
58
+ self.label_field = label_field
59
+ self.nodes_path = nodes_path
60
+ self.edges_path = edges_path
61
+ self.source_field = source_field
62
+ self.target_field = target_field
63
+
64
+ def convert(self, result: Any) -> GraphData:
65
+ """Convert GraphQL result to nodes and edges.
66
+
67
+ Parameters
68
+ ----------
69
+ result : Any
70
+ GraphQL JSON response.
71
+
72
+ Returns
73
+ -------
74
+ GraphData
75
+ Dictionary with 'nodes' and 'edges' lists.
76
+ """
77
+ # Unwrap data envelope if present
78
+ data = result.get("data", result) if isinstance(result, dict) else result
79
+
80
+ nodes: dict[str, dict[str, Any]] = {}
81
+ edges: list[dict[str, Any]] = []
82
+
83
+ # If explicit paths provided, use them
84
+ if self.nodes_path:
85
+ node_list = self._get_path(data, self.nodes_path) or []
86
+ for item in node_list:
87
+ if isinstance(item, dict):
88
+ node = self._item_to_node(item)
89
+ nodes[node["id"]] = node
90
+
91
+ if self.edges_path:
92
+ edge_list = self._get_path(data, self.edges_path) or []
93
+ for item in edge_list:
94
+ if isinstance(item, dict):
95
+ edge = self._item_to_edge(item)
96
+ if edge.get("source") and edge.get("target"):
97
+ edges.append(edge)
98
+
99
+ # Otherwise, auto-detect structure
100
+ if not self.nodes_path and not self.edges_path:
101
+ self._auto_extract(data, nodes, edges, None)
102
+
103
+ return {"nodes": list(nodes.values()), "edges": edges}
104
+
105
+ def _get_path(self, data: Any, path: str) -> Any:
106
+ """Navigate nested dict by dot-separated path."""
107
+ for key in path.split("."):
108
+ if isinstance(data, dict):
109
+ data = data.get(key)
110
+ elif isinstance(data, list) and key.isdigit():
111
+ idx = int(key)
112
+ data = data[idx] if idx < len(data) else None
113
+ else:
114
+ return None
115
+ return data
116
+
117
+ def _item_to_node(self, item: dict[str, Any]) -> dict[str, Any]:
118
+ """Convert a dict item to node format."""
119
+ node_id = str(item.get(self.id_field, id(item)))
120
+ label = item.get(self.label_field, "")
121
+
122
+ result: dict[str, Any] = {
123
+ "id": node_id,
124
+ "label": str(label) if label else node_id,
125
+ }
126
+
127
+ # Add scalar properties (skip nested objects and lists)
128
+ for key, value in item.items():
129
+ if key not in (self.id_field,) and not isinstance(value, (dict, list)):
130
+ result[key] = value
131
+
132
+ return result
133
+
134
+ def _item_to_edge(self, item: dict[str, Any]) -> dict[str, Any]:
135
+ """Convert a dict item to edge format."""
136
+ source = item.get(self.source_field, item.get("from", ""))
137
+ target = item.get(self.target_field, item.get("to", ""))
138
+
139
+ # Handle nested node references
140
+ if isinstance(source, dict):
141
+ source = source.get(self.id_field, "")
142
+ if isinstance(target, dict):
143
+ target = target.get(self.id_field, "")
144
+
145
+ return {
146
+ "source": str(source),
147
+ "target": str(target),
148
+ "label": str(item.get("label", item.get("type", item.get("__typename", "")))),
149
+ }
150
+
151
+ def _auto_extract(
152
+ self,
153
+ data: Any,
154
+ nodes: dict[str, dict[str, Any]],
155
+ edges: list[dict[str, Any]],
156
+ parent_id: str | None,
157
+ ) -> None:
158
+ """Auto-extract graph structure from nested data."""
159
+ if isinstance(data, dict):
160
+ # Check if this looks like a node (has ID field)
161
+ if self.id_field in data:
162
+ node = self._item_to_node(data)
163
+ nodes[node["id"]] = node
164
+
165
+ # Create edge from parent
166
+ if parent_id and parent_id != node["id"]:
167
+ edges.append(
168
+ {
169
+ "source": parent_id,
170
+ "target": node["id"],
171
+ "label": "contains",
172
+ }
173
+ )
174
+
175
+ # Process nested references
176
+ for key, value in data.items():
177
+ if isinstance(value, dict) and self.id_field in value:
178
+ # Direct reference to another node
179
+ ref_node = self._item_to_node(value)
180
+ nodes[ref_node["id"]] = ref_node
181
+ edges.append(
182
+ {
183
+ "source": node["id"],
184
+ "target": ref_node["id"],
185
+ "label": key,
186
+ }
187
+ )
188
+ elif isinstance(value, list):
189
+ # List of possible references
190
+ for item in value:
191
+ if isinstance(item, dict) and self.id_field in item:
192
+ ref_node = self._item_to_node(item)
193
+ nodes[ref_node["id"]] = ref_node
194
+ edges.append(
195
+ {
196
+ "source": node["id"],
197
+ "target": ref_node["id"],
198
+ "label": key,
199
+ }
200
+ )
201
+ else:
202
+ # Not a node, recurse into values
203
+ for value in data.values():
204
+ self._auto_extract(value, nodes, edges, parent_id)
205
+
206
+ elif isinstance(data, list):
207
+ for item in data:
208
+ self._auto_extract(item, nodes, edges, parent_id)
@@ -0,0 +1,159 @@
1
+ """Converter for Gremlin/TinkerPop query results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from anywidget_graph.converters.base import GraphData
8
+
9
+
10
+ class GremlinConverter:
11
+ """Convert Gremlin traversal results to graph data.
12
+
13
+ Handles various TinkerPop result types:
14
+ - Vertex objects
15
+ - Edge objects
16
+ - Path objects from path() traversals
17
+ - ValueMap results from valueMap() traversals
18
+ - Dictionaries from elementMap() traversals
19
+
20
+ Example
21
+ -------
22
+ >>> from gremlin_python.driver import client
23
+ >>> gremlin_client = client.Client('ws://localhost:8182/gremlin', 'g')
24
+ >>> result = gremlin_client.submit("g.V().limit(10)").all().result()
25
+ >>> converter = GremlinConverter()
26
+ >>> data = converter.convert(result)
27
+ """
28
+
29
+ def convert(self, result: Any) -> GraphData:
30
+ """Convert Gremlin result to nodes and edges.
31
+
32
+ Parameters
33
+ ----------
34
+ result : Any
35
+ Query result from Gremlin server.
36
+
37
+ Returns
38
+ -------
39
+ GraphData
40
+ Dictionary with 'nodes' and 'edges' lists.
41
+ """
42
+ nodes: dict[str, dict[str, Any]] = {}
43
+ edges: list[dict[str, Any]] = []
44
+
45
+ items = list(result) if hasattr(result, "__iter__") else [result]
46
+
47
+ for item in items:
48
+ self._process_item(item, nodes, edges)
49
+
50
+ return {"nodes": list(nodes.values()), "edges": edges}
51
+
52
+ def _process_item(
53
+ self,
54
+ item: Any,
55
+ nodes: dict[str, dict[str, Any]],
56
+ edges: list[dict[str, Any]],
57
+ ) -> None:
58
+ """Process a single result item."""
59
+ if item is None:
60
+ return
61
+
62
+ # Path object (has .objects attribute)
63
+ if hasattr(item, "objects"):
64
+ for obj in item.objects:
65
+ self._process_item(obj, nodes, edges)
66
+ # Vertex
67
+ elif self._is_vertex(item):
68
+ node_id = str(item.id)
69
+ if node_id not in nodes:
70
+ nodes[node_id] = self._vertex_to_dict(item)
71
+ # Edge
72
+ elif self._is_edge(item):
73
+ edges.append(self._edge_to_dict(item))
74
+ # Dict (from valueMap, elementMap, etc.)
75
+ elif isinstance(item, dict):
76
+ self._process_dict(item, nodes, edges)
77
+ # List/collection
78
+ elif isinstance(item, (list, tuple)):
79
+ for sub_item in item:
80
+ self._process_item(sub_item, nodes, edges)
81
+
82
+ def _is_vertex(self, obj: Any) -> bool:
83
+ """Check if object is a Gremlin Vertex."""
84
+ # Vertex has id and label but not outV/inV
85
+ return hasattr(obj, "id") and hasattr(obj, "label") and not hasattr(obj, "outV") and not hasattr(obj, "inV")
86
+
87
+ def _is_edge(self, obj: Any) -> bool:
88
+ """Check if object is a Gremlin Edge."""
89
+ # Edge has outV and inV
90
+ return hasattr(obj, "outV") and hasattr(obj, "inV")
91
+
92
+ def _vertex_to_dict(self, vertex: Any) -> dict[str, Any]:
93
+ """Convert Vertex to dict."""
94
+ result: dict[str, Any] = {
95
+ "id": str(vertex.id),
96
+ "label": str(vertex.label),
97
+ }
98
+ # Extract properties if available
99
+ if hasattr(vertex, "properties"):
100
+ props = vertex.properties
101
+ if callable(props):
102
+ props = props()
103
+ if hasattr(props, "__iter__"):
104
+ for prop in props:
105
+ if hasattr(prop, "key") and hasattr(prop, "value"):
106
+ result[prop.key] = prop.value
107
+ return result
108
+
109
+ def _edge_to_dict(self, edge: Any) -> dict[str, Any]:
110
+ """Convert Edge to dict."""
111
+ result: dict[str, Any] = {
112
+ "source": str(edge.outV.id if hasattr(edge.outV, "id") else edge.outV),
113
+ "target": str(edge.inV.id if hasattr(edge.inV, "id") else edge.inV),
114
+ "label": str(edge.label),
115
+ }
116
+ # Extract properties if available
117
+ if hasattr(edge, "properties"):
118
+ props = edge.properties
119
+ if callable(props):
120
+ props = props()
121
+ if isinstance(props, dict):
122
+ result.update(props)
123
+ return result
124
+
125
+ def _process_dict(
126
+ self,
127
+ item: dict[str, Any],
128
+ nodes: dict[str, dict[str, Any]],
129
+ edges: list[dict[str, Any]],
130
+ ) -> None:
131
+ """Process a dictionary result (from elementMap/valueMap)."""
132
+ # Check for edge indicators
133
+ if "IN" in item and "OUT" in item:
134
+ # This is an edge from elementMap()
135
+ edge = {
136
+ "source": str(item.get("OUT", {}).get("id", item.get("OUT", ""))),
137
+ "target": str(item.get("IN", {}).get("id", item.get("IN", ""))),
138
+ "label": str(item.get("label", "")),
139
+ }
140
+ # Add other properties
141
+ for key, value in item.items():
142
+ if key not in ("id", "label", "IN", "OUT"):
143
+ edge[key] = self._flatten_value(value)
144
+ edges.append(edge)
145
+ elif "id" in item:
146
+ # This is a vertex
147
+ node_id = str(item["id"])
148
+ if node_id not in nodes:
149
+ node = {"id": node_id}
150
+ for key, value in item.items():
151
+ if key != "id":
152
+ node[key] = self._flatten_value(value)
153
+ nodes[node_id] = node
154
+
155
+ def _flatten_value(self, value: Any) -> Any:
156
+ """Flatten single-element lists (common in valueMap results)."""
157
+ if isinstance(value, list) and len(value) == 1:
158
+ return value[0]
159
+ return value