topologicpy 0.8.54__py3-none-any.whl → 0.8.57__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.
- topologicpy/Graph.py +220 -3
- topologicpy/Honeybee.py +527 -2
- topologicpy/Kuzu.py +589 -0
- topologicpy/Vertex.py +78 -48
- topologicpy/version.py +1 -1
- {topologicpy-0.8.54.dist-info → topologicpy-0.8.57.dist-info}/METADATA +1 -1
- {topologicpy-0.8.54.dist-info → topologicpy-0.8.57.dist-info}/RECORD +10 -9
- {topologicpy-0.8.54.dist-info → topologicpy-0.8.57.dist-info}/WHEEL +0 -0
- {topologicpy-0.8.54.dist-info → topologicpy-0.8.57.dist-info}/licenses/LICENSE +0 -0
- {topologicpy-0.8.54.dist-info → topologicpy-0.8.57.dist-info}/top_level.txt +0 -0
topologicpy/Kuzu.py
ADDED
@@ -0,0 +1,589 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
import threading, contextlib, time, json
|
3
|
+
from typing import Dict, Any, List, Optional
|
4
|
+
|
5
|
+
|
6
|
+
# Optional TopologicPy imports (make this file safe to import without TopologicPy)
|
7
|
+
from topologicpy.Graph import Graph
|
8
|
+
from topologicpy.Vertex import Vertex
|
9
|
+
from topologicpy.Edge import Edge
|
10
|
+
from topologicpy.Dictionary import Dictionary
|
11
|
+
from topologicpy.Topology import Topology
|
12
|
+
|
13
|
+
import os
|
14
|
+
import warnings
|
15
|
+
|
16
|
+
try:
|
17
|
+
import kuzu
|
18
|
+
except:
|
19
|
+
print("Kuzu - Installing required kuzu library.")
|
20
|
+
try:
|
21
|
+
os.system("pip install kuzu")
|
22
|
+
except:
|
23
|
+
os.system("pip install kuzu --user")
|
24
|
+
try:
|
25
|
+
import kuzu
|
26
|
+
except:
|
27
|
+
warnings.warn("Kuzu - Error: Could not import Kuzu.")
|
28
|
+
kuzu = None
|
29
|
+
|
30
|
+
|
31
|
+
class _DBCache:
|
32
|
+
"""
|
33
|
+
One kuzu.Database per path. Thread-safe and process-local.
|
34
|
+
"""
|
35
|
+
def __init__(self):
|
36
|
+
self._lock = threading.RLock()
|
37
|
+
self._cache: Dict[str, "kuzu.Database"] = {}
|
38
|
+
|
39
|
+
def get(self, path: str) -> "kuzu.Database":
|
40
|
+
if kuzu is None:
|
41
|
+
raise "Kuzu - Error: Kuzu is not available"
|
42
|
+
with self._lock:
|
43
|
+
db = self._cache.get(path)
|
44
|
+
if db is None:
|
45
|
+
db = kuzu.Database(path)
|
46
|
+
self._cache[path] = db
|
47
|
+
return db
|
48
|
+
|
49
|
+
class _WriteGate:
|
50
|
+
"""
|
51
|
+
Serialize writes to avoid IO lock contention.
|
52
|
+
"""
|
53
|
+
def __init__(self):
|
54
|
+
self._lock = threading.RLock()
|
55
|
+
|
56
|
+
@contextlib.contextmanager
|
57
|
+
def hold(self):
|
58
|
+
with self._lock:
|
59
|
+
yield
|
60
|
+
|
61
|
+
_db_cache = _DBCache()
|
62
|
+
_write_gate = _WriteGate()
|
63
|
+
|
64
|
+
class _ConnectionPool:
|
65
|
+
"""
|
66
|
+
Per-thread kuzu.Connection pool bound to a Database instance.
|
67
|
+
"""
|
68
|
+
def __init__(self, db: "kuzu.Database"):
|
69
|
+
self.db = db
|
70
|
+
self._local = threading.local()
|
71
|
+
|
72
|
+
def _ensure(self) -> "kuzu.Connection":
|
73
|
+
if not hasattr(self._local, "conn"):
|
74
|
+
self._local.conn = kuzu.Connection(self.db)
|
75
|
+
return self._local.conn
|
76
|
+
|
77
|
+
@contextlib.contextmanager
|
78
|
+
def connection(self, write: bool = False, retries: int = 5, backoff: float = 0.15):
|
79
|
+
conn = self._ensure()
|
80
|
+
if not write:
|
81
|
+
yield conn
|
82
|
+
return
|
83
|
+
# Serialize writes and retry transient failures
|
84
|
+
with _write_gate.hold():
|
85
|
+
attempt = 0
|
86
|
+
while True:
|
87
|
+
try:
|
88
|
+
yield conn
|
89
|
+
break
|
90
|
+
except Exception as e:
|
91
|
+
attempt += 1
|
92
|
+
if attempt > retries:
|
93
|
+
raise f"Kuzu write failed after {retries} retries: {e}"
|
94
|
+
time.sleep(backoff * attempt)
|
95
|
+
|
96
|
+
class _Mgr:
|
97
|
+
"""
|
98
|
+
Lightweight facade (per-db-path) providing read/write execution and schema bootstrap.
|
99
|
+
"""
|
100
|
+
def __init__(self, db_path: str):
|
101
|
+
self.db_path = db_path
|
102
|
+
self._db = _db_cache.get(db_path)
|
103
|
+
self._pool = _ConnectionPool(self._db)
|
104
|
+
|
105
|
+
@contextlib.contextmanager
|
106
|
+
def read(self):
|
107
|
+
with self._pool.connection(write=False) as c:
|
108
|
+
yield c
|
109
|
+
|
110
|
+
@contextlib.contextmanager
|
111
|
+
def write(self):
|
112
|
+
with self._pool.connection(write=True) as c:
|
113
|
+
yield c
|
114
|
+
|
115
|
+
def exec(self, query: str, params: Optional[dict] = None, write: bool = False):
|
116
|
+
with (self.write() if write else self.read()) as c:
|
117
|
+
with c.execute(query, parameters=params or {}) as res:
|
118
|
+
try:
|
119
|
+
return res.rows_as_dict().get_all()
|
120
|
+
except Exception:
|
121
|
+
return None
|
122
|
+
|
123
|
+
def ensure_schema(self):
|
124
|
+
# Node tables
|
125
|
+
self.exec("""
|
126
|
+
CREATE NODE TABLE IF NOT EXISTS GraphCard(
|
127
|
+
id STRING,
|
128
|
+
label STRING,
|
129
|
+
num_nodes INT64,
|
130
|
+
num_edges INT64,
|
131
|
+
props STRING,
|
132
|
+
PRIMARY KEY(id)
|
133
|
+
);
|
134
|
+
""", write=True)
|
135
|
+
self.exec("""
|
136
|
+
CREATE NODE TABLE IF NOT EXISTS Vertex(
|
137
|
+
id STRING,
|
138
|
+
graph_id STRING,
|
139
|
+
label STRING,
|
140
|
+
x DOUBLE,
|
141
|
+
y DOUBLE,
|
142
|
+
z DOUBLE,
|
143
|
+
props STRING,
|
144
|
+
PRIMARY KEY(id)
|
145
|
+
);
|
146
|
+
""", write=True)
|
147
|
+
|
148
|
+
# Relationship tables
|
149
|
+
self.exec("""
|
150
|
+
CREATE REL TABLE IF NOT EXISTS CONNECT(FROM Vertex TO Vertex, label STRING, props STRING);
|
151
|
+
""", write=True)
|
152
|
+
|
153
|
+
# Figure out later if we need sessions and steps
|
154
|
+
# self.exec("""
|
155
|
+
# CREATE NODE TABLE IF NOT EXISTS Session(
|
156
|
+
# id STRING,
|
157
|
+
# title STRING,
|
158
|
+
# created_at STRING,
|
159
|
+
# PRIMARY KEY(id)
|
160
|
+
# );
|
161
|
+
# """, write=True)
|
162
|
+
# self.exec("""
|
163
|
+
# CREATE NODE TABLE IF NOT EXISTS Step(
|
164
|
+
# id STRING,
|
165
|
+
# session_id STRING,
|
166
|
+
# idx INT64,
|
167
|
+
# action STRING,
|
168
|
+
# ok BOOL,
|
169
|
+
# message STRING,
|
170
|
+
# snapshot_before STRING,
|
171
|
+
# snapshot_after STRING,
|
172
|
+
# evidence STRING,
|
173
|
+
# created_at STRING,
|
174
|
+
# PRIMARY KEY(id)
|
175
|
+
# );
|
176
|
+
# """, write=True)
|
177
|
+
# self.exec("CREATE REL TABLE IF NOT EXISTS SessionHasStep(FROM Session TO Step);", write=True)
|
178
|
+
|
179
|
+
|
180
|
+
class Kuzu:
|
181
|
+
"""
|
182
|
+
TopologicPy-style class of static methods for Kùzu integration.
|
183
|
+
|
184
|
+
Notes
|
185
|
+
-----
|
186
|
+
- All methods are *static* to match TopologicPy's style.
|
187
|
+
- Graph persistence:
|
188
|
+
* Vertices: stored in `Vertex` with (id, graph_id, label, props JSON)
|
189
|
+
* Edges: stored as `CONNECT` relations a->b with label + props JSON
|
190
|
+
* We assume undirected design intent; only one CONNECT is stored (a->b),
|
191
|
+
but TopologicPy Graph treats edges as undirected by default.
|
192
|
+
"""
|
193
|
+
|
194
|
+
# ---------- Core (DB + Connection + Schema) ----------
|
195
|
+
@staticmethod
|
196
|
+
def EnsureSchema(db_path: str, silent: bool = False) -> bool:
|
197
|
+
"""
|
198
|
+
Ensures the required Kùzu schema exists in the database at `db_path`.
|
199
|
+
|
200
|
+
Parameters
|
201
|
+
----------
|
202
|
+
db_path : str
|
203
|
+
Path to the Kùzu database directory. It will be created if it does not exist.
|
204
|
+
silent : bool , optional
|
205
|
+
If True, suppresses error messages. Default is False.
|
206
|
+
|
207
|
+
Returns
|
208
|
+
-------
|
209
|
+
bool
|
210
|
+
True if successful, False otherwise.
|
211
|
+
"""
|
212
|
+
try:
|
213
|
+
mgr = _Mgr(db_path)
|
214
|
+
mgr.ensure_schema()
|
215
|
+
return True
|
216
|
+
except Exception as e:
|
217
|
+
if not silent:
|
218
|
+
print(f"Kuzu.EnsureSchema - Error: {e}")
|
219
|
+
return False
|
220
|
+
|
221
|
+
@staticmethod
|
222
|
+
def Database(db_path: str):
|
223
|
+
"""
|
224
|
+
Returns the underlying `kuzu.Database` instance for `db_path`.
|
225
|
+
"""
|
226
|
+
return _db_cache.get(db_path)
|
227
|
+
|
228
|
+
@staticmethod
|
229
|
+
def Connection(db_path: str):
|
230
|
+
"""
|
231
|
+
Returns a `kuzu.Connection` bound to the database at `db_path`.
|
232
|
+
"""
|
233
|
+
mgr = _Mgr(db_path)
|
234
|
+
with mgr.read() as c:
|
235
|
+
return c # Note: returns a live connection (do not use across threads)
|
236
|
+
|
237
|
+
# ---------- Graph <-> DB Conversion ----------
|
238
|
+
|
239
|
+
@staticmethod
|
240
|
+
def UpsertGraph(db_path: str,
|
241
|
+
graph,
|
242
|
+
graphIDKey: Optional[str] = None,
|
243
|
+
vertexIDKey: Optional[str] = None,
|
244
|
+
vertexLabelKey: Optional[str] = None,
|
245
|
+
mantissa: int = 6,
|
246
|
+
silent: bool = False) -> str:
|
247
|
+
"""
|
248
|
+
Upserts (deletes prior + inserts new) a TopologicPy graph and its GraphCard.
|
249
|
+
|
250
|
+
Parameters
|
251
|
+
----------
|
252
|
+
db_path : str
|
253
|
+
Kùzu database path.
|
254
|
+
graph : topologicpy.Graph
|
255
|
+
The input TopologicPy graph.
|
256
|
+
graphIDKey : str , optional
|
257
|
+
The graph dictionary key under which the graph ID is stored. If None, a UUID is generated.
|
258
|
+
title, domain, geo, time_start, time_end, summary : str , optional
|
259
|
+
Optional metadata for GraphCard.
|
260
|
+
silent : bool , optional
|
261
|
+
If True, suppresses error messages. Default is False.
|
262
|
+
|
263
|
+
Returns
|
264
|
+
-------
|
265
|
+
str
|
266
|
+
The graph_id used.
|
267
|
+
"""
|
268
|
+
from topologicpy.Topology import Topology
|
269
|
+
from topologicpy.Dictionary import Dictionary
|
270
|
+
d = Topology.Dictionary(graph)
|
271
|
+
if graphIDKey is None:
|
272
|
+
gid = Topology.UUID(graph)
|
273
|
+
else:
|
274
|
+
gid = Dictionary.ValueAtKey(d, graphIDKey, Topology.UUID(graph))
|
275
|
+
g_props = Dictionary.PythonDictionary(d)
|
276
|
+
mesh_data = Graph.MeshData(graph, mantissa=mantissa)
|
277
|
+
verts = mesh_data['vertices']
|
278
|
+
v_props = mesh_data['vertexDictionaries']
|
279
|
+
edges = mesh_data['edges']
|
280
|
+
e_props = mesh_data['edgeDictionaries']
|
281
|
+
num_nodes = len(verts)
|
282
|
+
num_edges = len(edges)
|
283
|
+
mgr = _Mgr(db_path)
|
284
|
+
try:
|
285
|
+
mgr.ensure_schema()
|
286
|
+
# Upsert GraphCard
|
287
|
+
mgr.exec("MATCH (g:GraphCard) WHERE g.id = $id DELETE g;", {"id": gid}, write=True)
|
288
|
+
mgr.exec("""
|
289
|
+
CREATE (g:GraphCard {id:$id, num_nodes:$num_nodes, num_edges: $num_edges, props:$props});
|
290
|
+
""", {"id": gid, "num_nodes": num_nodes, "num_edges": num_edges, "props": json.dumps(g_props)}, write=True)
|
291
|
+
|
292
|
+
# Remove existing vertices/edges for this graph_id
|
293
|
+
mgr.exec("""
|
294
|
+
MATCH (a:Vertex)-[r:CONNECT]->(b:Vertex)
|
295
|
+
WHERE a.graph_id = $gid AND b.graph_id = $gid
|
296
|
+
DELETE r;
|
297
|
+
""", {"gid": gid}, write=True)
|
298
|
+
mgr.exec("MATCH (v:Vertex) WHERE v.graph_id = $gid DELETE v;", {"gid": gid}, write=True)
|
299
|
+
|
300
|
+
# Insert vertices
|
301
|
+
for i, v in enumerate(verts):
|
302
|
+
x,y,z = v
|
303
|
+
if vertexIDKey is None:
|
304
|
+
vid = f"{gid}:{i}"
|
305
|
+
else:
|
306
|
+
vid = v_props[i].get(vertexIDKey, f"{gid}:{i}")
|
307
|
+
if vertexLabelKey is None:
|
308
|
+
label = str(i)
|
309
|
+
else:
|
310
|
+
label = v_props[i].get(vertexIDKey, str(i))
|
311
|
+
mgr.exec("""
|
312
|
+
CREATE (v:Vertex {id:$id, graph_id:$gid, label:$label, props:$props, x:$x, y:$y, z:$z});
|
313
|
+
""", {"id": vid, "gid": gid, "label": label, "x": x, "y": y, "z": z,
|
314
|
+
"props": json.dumps(v_props[i])}, write=True)
|
315
|
+
|
316
|
+
# Insert edges
|
317
|
+
for i, e in enumerate(edges):
|
318
|
+
a_id = v_props[e[0]].get(vertexIDKey, f"{gid}:{e[0]}")
|
319
|
+
b_id = v_props[e[1]].get(vertexIDKey, f"{gid}:{e[1]}")
|
320
|
+
mgr.exec("""
|
321
|
+
MATCH (a:Vertex {id:$a}), (b:Vertex {id:$b})
|
322
|
+
CREATE (a)-[:CONNECT {label:$label, props:$props}]->(b);
|
323
|
+
""", {"a": a_id, "b": b_id,
|
324
|
+
"label": e_props[i].get("label", str(i)),
|
325
|
+
"props": json.dumps(e_props[i])}, write=True)
|
326
|
+
|
327
|
+
return gid
|
328
|
+
except Exception as e:
|
329
|
+
if not silent:
|
330
|
+
print(f"Kuzu.UpsertGraph - Error: {e}")
|
331
|
+
raise
|
332
|
+
|
333
|
+
@staticmethod
|
334
|
+
def GraphByID(db_path: str, graphID: str, silent: bool = False):
|
335
|
+
"""
|
336
|
+
Reads a graph with id `graph_id` from Kùzu and constructs a TopologicPy graph.
|
337
|
+
|
338
|
+
Returns
|
339
|
+
-------
|
340
|
+
topologicpy.Graph
|
341
|
+
A new TopologicPy Graph, or None on error.
|
342
|
+
"""
|
343
|
+
# if TGraph is None:
|
344
|
+
# raise _KuzuError("TopologicPy is required to use Kuzu.ReadTopologicGraph.")
|
345
|
+
import random
|
346
|
+
mgr = _Mgr(db_path)
|
347
|
+
|
348
|
+
try:
|
349
|
+
mgr.ensure_schema()
|
350
|
+
# Read the GraphCard
|
351
|
+
g = mgr.exec("""
|
352
|
+
MATCH (g:GraphCard) WHERE g.id = $id
|
353
|
+
RETURN g.id AS id, g.num_nodes AS num_nodes, g.num_edges AS num_edges, g.props AS props
|
354
|
+
;
|
355
|
+
""", {"id": graphID}, write=False) or None
|
356
|
+
if g is None:
|
357
|
+
return None
|
358
|
+
g = g[0]
|
359
|
+
g_dict = dict(json.loads(g.get("props") or "{}") or {})
|
360
|
+
g_dict = Dictionary.ByPythonDictionary(g_dict)
|
361
|
+
# Read vertices
|
362
|
+
rows_v = mgr.exec("""
|
363
|
+
MATCH (v:Vertex) WHERE v.graph_id = $gid
|
364
|
+
RETURN v.id AS id, v.label AS label, v.x AS x, v.y AS y, v.z AS z, v.props AS props
|
365
|
+
ORDER BY id;
|
366
|
+
""", {"gid": graphID}, write=False) or []
|
367
|
+
|
368
|
+
id_to_vertex = {}
|
369
|
+
vertices = []
|
370
|
+
for row in rows_v:
|
371
|
+
try:
|
372
|
+
x = row.get("x") or random.uniform(0,1000)
|
373
|
+
y = row.get("y") or random.uniform(0,1000)
|
374
|
+
z = row.get("z") or random.uniform(0,1000)
|
375
|
+
except:
|
376
|
+
x = random.uniform(0,1000)
|
377
|
+
y = random.uniform(0,1000)
|
378
|
+
z = random.uniform(0,1000)
|
379
|
+
v = Vertex.ByCoordinates(x,y,z)
|
380
|
+
props = {}
|
381
|
+
try:
|
382
|
+
props = json.loads(row.get("props") or "{}")
|
383
|
+
except Exception:
|
384
|
+
props = {}
|
385
|
+
# Ensure 'label' key present
|
386
|
+
props = dict(props or {})
|
387
|
+
if "label" not in props:
|
388
|
+
props["label"] = row.get("label") or ""
|
389
|
+
d = Dictionary.ByKeysValues(list(props.keys()), list(props.values()))
|
390
|
+
v = Topology.SetDictionary(v, d)
|
391
|
+
id_to_vertex[row["id"]] = v
|
392
|
+
vertices.append(v)
|
393
|
+
|
394
|
+
# Read edges
|
395
|
+
rows_e = mgr.exec("""
|
396
|
+
MATCH (a:Vertex)-[r:CONNECT]->(b:Vertex)
|
397
|
+
WHERE a.graph_id = $gid AND b.graph_id = $gid
|
398
|
+
RETURN a.id AS a_id, b.id AS b_id, r.label AS label, r.props AS props;
|
399
|
+
""", {"gid": graphID}, write=False) or []
|
400
|
+
edges = []
|
401
|
+
for row in rows_e:
|
402
|
+
va = id_to_vertex.get(row["a_id"])
|
403
|
+
vb = id_to_vertex.get(row["b_id"])
|
404
|
+
if not va or not vb:
|
405
|
+
continue
|
406
|
+
e = Edge.ByStartVertexEndVertex(va, vb)
|
407
|
+
props = {}
|
408
|
+
try:
|
409
|
+
props = json.loads(row.get("props") or "{}")
|
410
|
+
except Exception:
|
411
|
+
props = {}
|
412
|
+
props = dict(props or {})
|
413
|
+
if "label" not in props:
|
414
|
+
props["label"] = row.get("label") or "connect"
|
415
|
+
d = Dictionary.ByKeysValues(list(props.keys()), list(props.values()))
|
416
|
+
e = Topology.SetDictionary(e, d)
|
417
|
+
edges.append(e)
|
418
|
+
if len(vertices) > 0:
|
419
|
+
g = Graph.ByVerticesEdges(vertices, edges)
|
420
|
+
g = Topology.SetDictionary(g, g_dict)
|
421
|
+
else:
|
422
|
+
g = None
|
423
|
+
return g
|
424
|
+
except Exception as e:
|
425
|
+
if not silent:
|
426
|
+
print(f"Kuzu.GraphByID - Error: {e}")
|
427
|
+
return None
|
428
|
+
|
429
|
+
@staticmethod
|
430
|
+
def GraphsByQuery(
|
431
|
+
db_path: str,
|
432
|
+
query: str,
|
433
|
+
params: dict | None = None,
|
434
|
+
graphIDKey: str = "graph_id",
|
435
|
+
silent: bool = False,
|
436
|
+
):
|
437
|
+
"""
|
438
|
+
Executes a Kùzu Cypher query and returns a list of TopologicPy Graphs.
|
439
|
+
The query should return at least one column identifying each graph.
|
440
|
+
By default this column is expected to be named 'graph_id', but you can
|
441
|
+
override that via `graph_id_field`.
|
442
|
+
|
443
|
+
The method will:
|
444
|
+
1) run the query,
|
445
|
+
2) extract distinct graph IDs from the result set (using `graph_id_field`
|
446
|
+
if present; otherwise it attempts to infer IDs from common fields like
|
447
|
+
'a_id', 'b_id', or 'id' that look like '<graph_id>:<vertex_index>'),
|
448
|
+
3) reconstruct each graph via Kuzu.ReadTopologicGraph(...).
|
449
|
+
|
450
|
+
Parameters
|
451
|
+
----------
|
452
|
+
db_path : str
|
453
|
+
Path to the Kùzu database directory.
|
454
|
+
query : str
|
455
|
+
A valid Kùzu Cypher query.
|
456
|
+
params : dict , optional
|
457
|
+
Parameters to pass with the query.
|
458
|
+
graph_id_field : str , optional
|
459
|
+
The field name in the query result that contains the graph ID(s).
|
460
|
+
Default is "graph_id".
|
461
|
+
silent : bool , optional
|
462
|
+
If True, suppresses errors and returns an empty list on failure.
|
463
|
+
|
464
|
+
Returns
|
465
|
+
-------
|
466
|
+
list[topologicpy.Graph]
|
467
|
+
A list of reconstructed TopologicPy graphs.
|
468
|
+
"""
|
469
|
+
# if TGraph is None:
|
470
|
+
# raise _KuzuError("TopologicPy is required to use Kuzu.GraphsFromQuery.")
|
471
|
+
|
472
|
+
try:
|
473
|
+
mgr = _Mgr(db_path)
|
474
|
+
mgr.ensure_schema()
|
475
|
+
rows = mgr.exec(query, params or {}, write=False) or []
|
476
|
+
|
477
|
+
# Collect distinct graph IDs
|
478
|
+
gids = []
|
479
|
+
for r in rows:
|
480
|
+
gid = r.get(graphIDKey)
|
481
|
+
|
482
|
+
# Fallback: try to infer from common id fields like "<graph_id>:<i>"
|
483
|
+
if gid is None:
|
484
|
+
for k in ("a_id", "b_id", "id"):
|
485
|
+
v = r.get(k)
|
486
|
+
if isinstance(v, str) and ":" in v:
|
487
|
+
gid = v.split(":", 1)[0]
|
488
|
+
break
|
489
|
+
|
490
|
+
if gid and gid not in gids:
|
491
|
+
gids.append(gid)
|
492
|
+
|
493
|
+
# Reconstruct each graph
|
494
|
+
graphs = []
|
495
|
+
for gid in gids:
|
496
|
+
g = Kuzu.GraphByID(db_path, gid, silent=True)
|
497
|
+
if g is not None:
|
498
|
+
graphs.append(g)
|
499
|
+
return graphs
|
500
|
+
|
501
|
+
except Exception as e:
|
502
|
+
if not silent:
|
503
|
+
print(f"Kuzu.GraphsByQuery - Error: {e}")
|
504
|
+
return []
|
505
|
+
|
506
|
+
@staticmethod
|
507
|
+
def DeleteGraph(db_path: str, graph_id: str, silent: bool = False) -> bool:
|
508
|
+
"""
|
509
|
+
Deletes a graph (vertices/edges) and its GraphCard by id.
|
510
|
+
"""
|
511
|
+
try:
|
512
|
+
mgr = _Mgr(db_path)
|
513
|
+
mgr.ensure_schema()
|
514
|
+
# Delete edges
|
515
|
+
mgr.exec("""
|
516
|
+
MATCH (a:Vertex)-[r:CONNECT]->(b:Vertex)
|
517
|
+
WHERE a.graph_id = $gid AND b.graph_id = $gid
|
518
|
+
DELETE r;
|
519
|
+
""", {"gid": graph_id}, write=True)
|
520
|
+
# Delete vertices
|
521
|
+
mgr.exec("MATCH (v:Vertex) WHERE v.graph_id = $gid DELETE v;", {"gid": graph_id}, write=True)
|
522
|
+
# Delete card
|
523
|
+
mgr.exec("MATCH (g:GraphCard) WHERE g.id = $gid DELETE g;", {"gid": graph_id}, write=True)
|
524
|
+
return True
|
525
|
+
except Exception as e:
|
526
|
+
if not silent:
|
527
|
+
print(f"Kuzu.DeleteGraph - Error: {e}")
|
528
|
+
return False
|
529
|
+
|
530
|
+
@staticmethod
|
531
|
+
def EmptyDatabase(db_path: str, drop_schema: bool = False, recreate_schema: bool = True, silent: bool = False) -> bool:
|
532
|
+
"""
|
533
|
+
Empties the Kùzu database at `db_path`.
|
534
|
+
|
535
|
+
Two modes:
|
536
|
+
- Soft clear (default): delete ALL relationships, then ALL nodes across all tables.
|
537
|
+
- Hard reset (drop_schema=True): drop known node/rel tables, optionally recreate schema.
|
538
|
+
|
539
|
+
Parameters
|
540
|
+
----------
|
541
|
+
db_path : str
|
542
|
+
Path to the Kùzu database directory.
|
543
|
+
drop_schema : bool , optional
|
544
|
+
If True, DROP the known tables instead of deleting rows. Default False.
|
545
|
+
recreate_schema : bool , optional
|
546
|
+
If True and drop_schema=True, re-create the minimal schema after dropping. Default True.
|
547
|
+
silent : bool , optional
|
548
|
+
Suppress errors if True. Default False.
|
549
|
+
|
550
|
+
Returns
|
551
|
+
-------
|
552
|
+
bool
|
553
|
+
True on success, False otherwise.
|
554
|
+
"""
|
555
|
+
try:
|
556
|
+
mgr = _Mgr(db_path)
|
557
|
+
# Ensure DB exists (does not create tables unless needed)
|
558
|
+
mgr.ensure_schema()
|
559
|
+
|
560
|
+
if drop_schema:
|
561
|
+
# Drop relationship tables FIRST (to release dependencies), then node tables.
|
562
|
+
# IF EXISTS is convenient; if your Kùzu version doesn't support it, remove and ignore exceptions.
|
563
|
+
for stmt in [
|
564
|
+
"DROP TABLE IF EXISTS CONNECT;",
|
565
|
+
"DROP TABLE IF EXISTS Vertex;",
|
566
|
+
"DROP TABLE IF EXISTS GraphCard;",
|
567
|
+
]:
|
568
|
+
try:
|
569
|
+
mgr.exec(stmt, write=True)
|
570
|
+
except Exception as _e:
|
571
|
+
if not silent:
|
572
|
+
print(f"Kuzu.EmptyDatabase - Warning dropping table: {_e}")
|
573
|
+
|
574
|
+
if recreate_schema:
|
575
|
+
mgr.ensure_schema()
|
576
|
+
return True
|
577
|
+
|
578
|
+
# Soft clear: remove all relationships, then all nodes (covers all labels/tables).
|
579
|
+
# Delete all edges (any direction)
|
580
|
+
mgr.exec("MATCH (a)-[r]->(b) DELETE r;", write=True)
|
581
|
+
# Delete all nodes (from all node tables)
|
582
|
+
mgr.exec("MATCH (n) DELETE n;", write=True)
|
583
|
+
return True
|
584
|
+
|
585
|
+
except Exception as e:
|
586
|
+
if not silent:
|
587
|
+
print(f"Kuzu.EmptyDatabase - Error: {e}")
|
588
|
+
return False
|
589
|
+
|