Navegador

navegador / navegador / graph / store.py
Blame History Raw 126 lines
1
"""
2
GraphStore — thin wrapper over FalkorDB (SQLite or Redis backend).
3
4
Usage:
5
# SQLite (local, zero-infra)
6
store = GraphStore.sqlite(".navegador/graph.db")
7
8
# Redis-backed FalkorDB (production)
9
store = GraphStore.redis("redis://localhost:6379")
10
"""
11
12
import logging
13
from pathlib import Path
14
from typing import Any
15
16
logger = logging.getLogger(__name__)
17
18
19
class GraphStore:
20
"""
21
Wraps a FalkorDB graph, providing helpers for navegador node/edge operations.
22
23
The underlying graph is named "navegador" within the database.
24
"""
25
26
GRAPH_NAME = "navegador"
27
28
def __init__(self, client: Any) -> None:
29
self._client = client
30
self._graph = client.select_graph(self.GRAPH_NAME)
31
32
# ── Constructors ──────────────────────────────────────────────────────────
33
34
@classmethod
35
def sqlite(cls, db_path: str | Path = ".navegador/graph.db") -> "GraphStore":
36
"""
37
Open a SQLite-backed FalkorDB graph via falkordblite (zero-infra).
38
39
Requires: pip install FalkorDB falkordblite
40
"""
41
try:
42
from redislite import FalkorDB # type: ignore[import] # provided by falkordblite
43
except ImportError as e:
44
raise ImportError(
45
"Install graph dependencies: pip install FalkorDB falkordblite"
46
) from e
47
48
db_path = Path(db_path)
49
db_path.parent.mkdir(parents=True, exist_ok=True)
50
client = FalkorDB(str(db_path))
51
logger.info("GraphStore opened (SQLite/falkordblite): %s", db_path)
52
return cls(client)
53
54
@classmethod
55
def redis(cls, url: str = "redis://localhost:6379") -> "GraphStore":
56
"""
57
Open a Redis-backed FalkorDB graph (production use).
58
59
Requires: pip install FalkorDB redis
60
"""
61
try:
62
import falkordb # type: ignore[import]
63
except ImportError as e:
64
raise ImportError("Install falkordb: pip install FalkorDB redis") from e
65
66
client = falkordb.FalkorDB.from_url(url)
67
logger.info("GraphStore opened (Redis): %s", url)
68
return cls(client)
69
70
# ── Core operations ───────────────────────────────────────────────────────
71
72
def query(self, cypher: str, params: dict[str, Any] | None = None) -> Any:
73
"""Execute a raw Cypher query and return the result."""
74
return self._graph.query(cypher, params or {})
75
76
def create_node(self, label: str, props: dict[str, Any]) -> None:
77
"""Upsert a node by (label, name[, file_path])."""
78
# Ensure merge key fields exist
79
props.setdefault("name", "")
80
props.setdefault("file_path", "")
81
# Filter out None values — FalkorDB rejects them as params
82
props = {k: ("" if v is None else v) for k, v in props.items()}
83
prop_str = ", ".join(f"n.{k} = ${k}" for k in props)
84
cypher = f"MERGE (n:{label} {{name: $name, file_path: $file_path}}) SET {prop_str}"
85
self.query(cypher, props)
86
87
def create_edge(
88
self,
89
from_label: str,
90
from_key: dict[str, Any],
91
edge_type: str,
92
to_label: str,
93
to_key: dict[str, Any],
94
props: dict[str, Any] | None = None,
95
) -> None:
96
"""Create a directed edge between two nodes, merging if it already exists."""
97
from_match = ", ".join(f"{k}: $from_{k}" for k in from_key)
98
to_match = ", ".join(f"{k}: $to_{k}" for k in to_key)
99
prop_set = ""
100
if props:
101
prop_set = " SET " + ", ".join(f"r.{k} = $p_{k}" for k in props)
102
103
cypher = (
104
f"MATCH (a:{from_label} {{{from_match}}}), (b:{to_label} {{{to_match}}}) "
105
f"MERGE (a)-[r:{edge_type}]->(b){prop_set}"
106
)
107
params = {f"from_{k}": v for k, v in from_key.items()}
108
params.update({f"to_{k}": v for k, v in to_key.items()})
109
if props:
110
params.update({f"p_{k}": v for k, v in props.items()})
111
112
self.query(cypher, params)
113
114
def clear(self) -> None:
115
"""Delete all nodes and edges in the graph."""
116
self.query("MATCH (n) DETACH DELETE n")
117
logger.info("Graph cleared")
118
119
def node_count(self) -> int:
120
result = self.query("MATCH (n) RETURN count(n) AS c")
121
return result.result_set[0][0] if result.result_set else 0
122
123
def edge_count(self) -> int:
124
result = self.query("MATCH ()-[r]->() RETURN count(r) AS c")
125
return result.result_set[0][0] if result.result_set else 0
126

Keyboard Shortcuts

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