Navegador

navegador / navegador / graph / export.py
Blame History Raw 175 lines
1
"""
2
Text-based graph export and import for navegador.
3
4
Exports the full graph to a deterministic JSON Lines format (.jsonl)
5
suitable for committing to version control. Each line is a self-contained
6
JSON object — either a node or an edge.
7
8
Format:
9
{"kind": "node", "label": "Function", "props": {"name": "foo", ...}}
10
{"kind": "edge", "type": "CALLS", "from": {...}, "to": {...}}
11
"""
12
13
import json
14
import logging
15
from pathlib import Path
16
17
from navegador.graph.store import GraphStore
18
19
logger = logging.getLogger(__name__)
20
21
22
def export_graph(store: GraphStore, output_path: str | Path) -> dict[str, int]:
23
"""
24
Export the full graph to a JSONL file.
25
26
Returns:
27
Dict with counts: nodes, edges.
28
"""
29
output_path = Path(output_path)
30
output_path.parent.mkdir(parents=True, exist_ok=True)
31
32
nodes = _export_nodes(store)
33
edges = _export_edges(store)
34
35
# Sort for deterministic output
36
nodes.sort(key=lambda n: (n["label"], json.dumps(n["props"], sort_keys=True)))
37
edges.sort(
38
key=lambda e: (
39
e["type"],
40
json.dumps(e["from"], sort_keys=True),
41
json.dumps(e["to"], sort_keys=True),
42
)
43
)
44
45
with output_path.open("w", encoding="utf-8") as f:
46
for node in nodes:
47
f.write(json.dumps(node, sort_keys=True) + "\n")
48
for edge in edges:
49
f.write(json.dumps(edge, sort_keys=True) + "\n")
50
51
logger.info("Exported %d nodes, %d edges to %s", len(nodes), len(edges), output_path)
52
return {"nodes": len(nodes), "edges": len(edges)}
53
54
55
def import_graph(store: GraphStore, input_path: str | Path, clear: bool = True) -> dict[str, int]:
56
"""
57
Import a graph from a JSONL file.
58
59
Args:
60
store: Target GraphStore.
61
input_path: Path to the JSONL file.
62
clear: If True (default), wipe the graph before importing.
63
64
Returns:
65
Dict with counts: nodes, edges.
66
"""
67
input_path = Path(input_path)
68
if not input_path.exists():
69
raise FileNotFoundError(f"Export file not found: {input_path}")
70
71
if clear:
72
store.clear()
73
74
node_count = 0
75
edge_count = 0
76
77
with input_path.open("r", encoding="utf-8") as f:
78
for line in f:
79
line = line.strip()
80
if not line:
81
continue
82
record = json.loads(line)
83
84
if record["kind"] == "node":
85
_import_node(store, record)
86
node_count += 1
87
elif record["kind"] == "edge":
88
_import_edge(store, record)
89
edge_count += 1
90
91
logger.info("Imported %d nodes, %d edges from %s", node_count, edge_count, input_path)
92
return {"nodes": node_count, "edges": edge_count}
93
94
95
def _export_nodes(store: GraphStore) -> list[dict]:
96
"""Export all nodes with their labels and properties."""
97
result = store.query("MATCH (n) RETURN labels(n)[0] AS label, properties(n) AS props")
98
nodes = []
99
for row in result.result_set or []:
100
label = row[0]
101
props = row[1] if isinstance(row[1], dict) else {}
102
nodes.append({"kind": "node", "label": label, "props": props})
103
return nodes
104
105
106
def _export_edges(store: GraphStore) -> list[dict]:
107
"""Export all edges with type and endpoint identifiers."""
108
result = store.query(
109
"MATCH (a)-[r]->(b) "
110
"RETURN type(r) AS type, labels(a)[0] AS from_label, "
111
"a.name AS from_name, coalesce(a.file_path, a.path, '') AS from_path, "
112
"labels(b)[0] AS to_label, b.name AS to_name, "
113
"coalesce(b.file_path, b.path, '') AS to_path"
114
)
115
edges = []
116
for row in result.result_set or []:
117
edges.append(
118
{
119
"kind": "edge",
120
"type": row[0],
121
"from": {"label": row[1], "name": row[2], "path": row[3]},
122
"to": {"label": row[4], "name": row[5], "path": row[6]},
123
}
124
)
125
return edges
126
127
128
def _import_node(store: GraphStore, record: dict) -> None:
129
"""Create a node from an export record."""
130
label = record["label"]
131
props = record["props"]
132
# Ensure required merge keys exist
133
if "name" not in props:
134
props["name"] = ""
135
if "file_path" not in props and "path" not in props:
136
props["file_path"] = ""
137
138
prop_str = ", ".join(f"n.{k} = ${k}" for k in props)
139
# Use name + file_path or path for merge key
140
if "file_path" in props:
141
cypher = f"MERGE (n:{label} {{name: $name, file_path: $file_path}}) SET {prop_str}"
142
else:
143
cypher = f"MERGE (n:{label} {{name: $name, path: $path}}) SET {prop_str}"
144
store.query(cypher, props)
145
146
147
def _import_edge(store: GraphStore, record: dict) -> None:
148
"""Create an edge from an export record."""
149
edge_type = record["type"]
150
from_info = record["from"]
151
to_info = record["to"]
152
153
from_key = "name: $from_name"
154
to_key = "name: $to_name"
155
156
params = {
157
"from_name": from_info["name"],
158
"to_name": to_info["name"],
159
}
160
161
if from_info.get("path"):
162
from_key += ", file_path: $from_path"
163
params["from_path"] = from_info["path"]
164
165
if to_info.get("path"):
166
to_key += ", path: $to_path"
167
params["to_path"] = to_info["path"]
168
169
cypher = (
170
f"MATCH (a:{from_info['label']} {{{from_key}}}), "
171
f"(b:{to_info['label']} {{{to_key}}}) "
172
f"MERGE (a)-[r:{edge_type}]->(b)"
173
)
174
store.query(cypher, params)
175

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button