graph-ted-db 0.1.0__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.
- graph_ted_db/__init__.py +23 -0
- graph_ted_db/__main__.py +3 -0
- graph_ted_db/cli.py +367 -0
- graph_ted_db/driver/__init__.py +16 -0
- graph_ted_db/driver/graphiti.py +201 -0
- graph_ted_db/driver/http.py +158 -0
- graph_ted_db/engine/__init__.py +17 -0
- graph_ted_db/engine/ast.py +222 -0
- graph_ted_db/engine/errors.py +11 -0
- graph_ted_db/engine/eval.py +287 -0
- graph_ted_db/engine/executor.py +927 -0
- graph_ted_db/engine/fulltext.py +84 -0
- graph_ted_db/engine/functions.py +259 -0
- graph_ted_db/engine/lexer.py +206 -0
- graph_ted_db/engine/parser.py +593 -0
- graph_ted_db/engine/values.py +189 -0
- graph_ted_db/index/__init__.py +5 -0
- graph_ted_db/index/local.py +211 -0
- graph_ted_db/server/__init__.py +19 -0
- graph_ted_db/server/http.py +414 -0
- graph_ted_db/store/__init__.py +42 -0
- graph_ted_db/store/aliases.py +228 -0
- graph_ted_db/store/format.py +78 -0
- graph_ted_db/store/graph.py +1561 -0
- graph_ted_db/store/init.py +88 -0
- graph_ted_db/store/jsonl.py +265 -0
- graph_ted_db/store/lock.py +81 -0
- graph_ted_db/store/lww.py +81 -0
- graph_ted_db/store/paths.py +196 -0
- graph_ted_db/store/records.py +446 -0
- graph_ted_db-0.1.0.dist-info/METADATA +173 -0
- graph_ted_db-0.1.0.dist-info/RECORD +36 -0
- graph_ted_db-0.1.0.dist-info/WHEEL +5 -0
- graph_ted_db-0.1.0.dist-info/entry_points.txt +3 -0
- graph_ted_db-0.1.0.dist-info/licenses/LICENSE +21 -0
- graph_ted_db-0.1.0.dist-info/top_level.txt +1 -0
graph_ted_db/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Local property-graph storage for Python: open a folder and query it in-process (import graph_ted_db)."""
|
|
2
|
+
|
|
3
|
+
from graph_ted_db.engine import CypherError
|
|
4
|
+
from graph_ted_db.store import (
|
|
5
|
+
EdgeRecord,
|
|
6
|
+
GraphStore,
|
|
7
|
+
NodeRecord,
|
|
8
|
+
Tombstone,
|
|
9
|
+
VectorRecord,
|
|
10
|
+
init_graph,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
__all__ = [
|
|
15
|
+
"CypherError",
|
|
16
|
+
"EdgeRecord",
|
|
17
|
+
"GraphStore",
|
|
18
|
+
"NodeRecord",
|
|
19
|
+
"Tombstone",
|
|
20
|
+
"VectorRecord",
|
|
21
|
+
"__version__",
|
|
22
|
+
"init_graph",
|
|
23
|
+
]
|
graph_ted_db/__main__.py
ADDED
graph_ted_db/cli.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""Command-line entry: `graph-ted-db` (alias `graphted-db`)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import errno
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from graph_ted_db import __version__
|
|
14
|
+
from graph_ted_db.engine import CypherError
|
|
15
|
+
from graph_ted_db.store import GraphFormatError, GraphStore, init_graph, load_graph_meta
|
|
16
|
+
|
|
17
|
+
UPDATED_BY_ENV = "GRAPH_TED_DB_UPDATED_BY"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _who(explicit: str | None) -> str:
|
|
21
|
+
"""Author string for written records: --by, else $GRAPH_TED_DB_UPDATED_BY, else empty.
|
|
22
|
+
|
|
23
|
+
Never the OS login name: records travel with the folder when it is synced.
|
|
24
|
+
"""
|
|
25
|
+
if explicit:
|
|
26
|
+
return explicit
|
|
27
|
+
return os.environ.get(UPDATED_BY_ENV, "")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _parse_prop(raw: str) -> tuple[str, Any]:
|
|
31
|
+
key, sep, value = raw.partition("=")
|
|
32
|
+
if not key or sep != "=":
|
|
33
|
+
raise argparse.ArgumentTypeError(f"expected key=value, got {raw!r}")
|
|
34
|
+
try:
|
|
35
|
+
return key, json.loads(value)
|
|
36
|
+
except json.JSONDecodeError:
|
|
37
|
+
return key, value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _store(path: Path) -> GraphStore:
|
|
41
|
+
root = path.expanduser().resolve()
|
|
42
|
+
if not (root / "graph.json").is_file():
|
|
43
|
+
raise GraphFormatError(f"no graph at {path}; create one with `graph-ted-db init {path}`")
|
|
44
|
+
return GraphStore.open(root)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main(argv: list[str] | None = None) -> int:
|
|
48
|
+
parser = argparse.ArgumentParser(
|
|
49
|
+
prog="graph-ted-db",
|
|
50
|
+
description="Local property-graph storage. Open a folder and query it in-process; no database server for the default path.",
|
|
51
|
+
epilog="The graphted-db command is the same entry point.",
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
54
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
55
|
+
|
|
56
|
+
init_p = sub.add_parser("init", help="create an empty graph folder")
|
|
57
|
+
init_p.add_argument("path", type=Path)
|
|
58
|
+
init_p.add_argument("--name", default="graph")
|
|
59
|
+
init_p.add_argument("--exist-ok", action="store_true")
|
|
60
|
+
|
|
61
|
+
info_p = sub.add_parser("info", help="print graph.json for a folder")
|
|
62
|
+
info_p.add_argument(
|
|
63
|
+
"--check",
|
|
64
|
+
action="store_true",
|
|
65
|
+
help="also open the store and print problem counts (skipped lines, files still syncing)",
|
|
66
|
+
)
|
|
67
|
+
info_p.add_argument("path", type=Path)
|
|
68
|
+
|
|
69
|
+
put_n = sub.add_parser("put-node", help="create or overwrite a node (LWW)")
|
|
70
|
+
put_n.add_argument("path", type=Path)
|
|
71
|
+
put_n.add_argument("--id", dest="record_id", default=None)
|
|
72
|
+
put_n.add_argument("--label", action="append", default=[])
|
|
73
|
+
put_n.add_argument("--prop", action="append", default=[], type=_parse_prop)
|
|
74
|
+
put_n.add_argument(
|
|
75
|
+
"--by",
|
|
76
|
+
"--updated-by",
|
|
77
|
+
dest="updated_by",
|
|
78
|
+
default=None,
|
|
79
|
+
help="author stored on the record (default: $GRAPH_TED_DB_UPDATED_BY, else empty)",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
get_n = sub.add_parser("get-node", help="print one node as JSON")
|
|
83
|
+
get_n.add_argument("path", type=Path)
|
|
84
|
+
get_n.add_argument("record_id")
|
|
85
|
+
|
|
86
|
+
ls_n = sub.add_parser("ls-nodes", help="print live nodes as JSON lines")
|
|
87
|
+
ls_n.add_argument("path", type=Path)
|
|
88
|
+
|
|
89
|
+
del_n = sub.add_parser("delete-node", help="tombstone a node")
|
|
90
|
+
del_n.add_argument("path", type=Path)
|
|
91
|
+
del_n.add_argument("record_id")
|
|
92
|
+
del_n.add_argument(
|
|
93
|
+
"--by",
|
|
94
|
+
"--updated-by",
|
|
95
|
+
dest="updated_by",
|
|
96
|
+
default=None,
|
|
97
|
+
help="author stored on the record (default: $GRAPH_TED_DB_UPDATED_BY, else empty)",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
put_e = sub.add_parser("put-edge", help="create or overwrite an edge (LWW)")
|
|
101
|
+
put_e.add_argument("path", type=Path)
|
|
102
|
+
put_e.add_argument("--type", required=True, dest="edge_type")
|
|
103
|
+
put_e.add_argument("--from", required=True, dest="from_id")
|
|
104
|
+
put_e.add_argument("--to", required=True, dest="to_id")
|
|
105
|
+
put_e.add_argument("--id", dest="record_id", default=None)
|
|
106
|
+
put_e.add_argument("--prop", action="append", default=[], type=_parse_prop)
|
|
107
|
+
put_e.add_argument(
|
|
108
|
+
"--by",
|
|
109
|
+
"--updated-by",
|
|
110
|
+
dest="updated_by",
|
|
111
|
+
default=None,
|
|
112
|
+
help="author stored on the record (default: $GRAPH_TED_DB_UPDATED_BY, else empty)",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
get_e = sub.add_parser("get-edge", help="print one edge as JSON")
|
|
116
|
+
get_e.add_argument("path", type=Path)
|
|
117
|
+
get_e.add_argument("record_id")
|
|
118
|
+
|
|
119
|
+
ls_e = sub.add_parser("ls-edges", help="print live edges as JSON lines")
|
|
120
|
+
ls_e.add_argument("path", type=Path)
|
|
121
|
+
|
|
122
|
+
del_e = sub.add_parser("delete-edge", help="tombstone an edge")
|
|
123
|
+
del_e.add_argument("path", type=Path)
|
|
124
|
+
del_e.add_argument("record_id")
|
|
125
|
+
del_e.add_argument(
|
|
126
|
+
"--by",
|
|
127
|
+
"--updated-by",
|
|
128
|
+
dest="updated_by",
|
|
129
|
+
default=None,
|
|
130
|
+
help="author stored on the record (default: $GRAPH_TED_DB_UPDATED_BY, else empty)",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
compact_p = sub.add_parser(
|
|
134
|
+
"compact",
|
|
135
|
+
help="rewrite canonical shards to LWW winners (keeps conflict copies)",
|
|
136
|
+
)
|
|
137
|
+
compact_p.add_argument("path", type=Path)
|
|
138
|
+
|
|
139
|
+
export_p = sub.add_parser(
|
|
140
|
+
"export", help="write the current state (live records) to one JSONL file, for backup"
|
|
141
|
+
)
|
|
142
|
+
export_p.add_argument("path", type=Path)
|
|
143
|
+
export_p.add_argument("out", type=Path, help="export file (outside the graph folder)")
|
|
144
|
+
|
|
145
|
+
import_p = sub.add_parser("import", help="create a new graph folder from an export file")
|
|
146
|
+
import_p.add_argument("export_file", type=Path)
|
|
147
|
+
import_p.add_argument("dest", type=Path, help="new graph folder (must not exist yet)")
|
|
148
|
+
import_p.add_argument("--name", default=None)
|
|
149
|
+
|
|
150
|
+
doctor_p = sub.add_parser(
|
|
151
|
+
"doctor",
|
|
152
|
+
help="repair torn JSONL, rebuild labels, report dangling edges (--fix tombstones them)",
|
|
153
|
+
description=(
|
|
154
|
+
"Repair torn JSONL lines, remove leftover tmp files, rebuild labels.json, and report "
|
|
155
|
+
"dangling edges (an endpoint is not a live node). Dangling edges are only reported "
|
|
156
|
+
"unless --fix is given."
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
doctor_p.add_argument("path", type=Path)
|
|
160
|
+
doctor_p.add_argument(
|
|
161
|
+
"--fix",
|
|
162
|
+
action="store_true",
|
|
163
|
+
help=(
|
|
164
|
+
"tombstone dangling edges. Permanent, and it syncs to every device: in a synced "
|
|
165
|
+
"folder, wait until sync has finished, because an edge can arrive before its nodes"
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
cypher_p = sub.add_parser(
|
|
170
|
+
"cypher", help="run an openCypher query (supported subset; see docs/cypher.md)"
|
|
171
|
+
)
|
|
172
|
+
cypher_p.add_argument("path", type=Path)
|
|
173
|
+
cypher_p.add_argument("query", nargs="?", default=None, help="query; omit to read stdin")
|
|
174
|
+
cypher_p.add_argument(
|
|
175
|
+
"--params",
|
|
176
|
+
default="{}",
|
|
177
|
+
help="JSON object of query parameters",
|
|
178
|
+
)
|
|
179
|
+
cypher_p.add_argument(
|
|
180
|
+
"--by",
|
|
181
|
+
"--updated-by",
|
|
182
|
+
dest="updated_by",
|
|
183
|
+
default=None,
|
|
184
|
+
help='author stored on written records (default: $GRAPH_TED_DB_UPDATED_BY, else "cypher")',
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
serve_p = sub.add_parser(
|
|
188
|
+
"serve",
|
|
189
|
+
help="localhost HTTP daemon (POST /cypher, GET /health); see docs/http.md",
|
|
190
|
+
)
|
|
191
|
+
serve_p.add_argument("path", type=Path, help="graph folder to serve")
|
|
192
|
+
serve_p.add_argument("--host", default=None, help="bind address (default 127.0.0.1)")
|
|
193
|
+
serve_p.add_argument("--port", type=int, default=None, help="TCP port (default 8099)")
|
|
194
|
+
serve_p.add_argument(
|
|
195
|
+
"--token",
|
|
196
|
+
default=None,
|
|
197
|
+
help="HTTP token (or GRAPH_TED_DB_TOKEN); required when --host is not loopback",
|
|
198
|
+
)
|
|
199
|
+
serve_p.add_argument(
|
|
200
|
+
"--max-records",
|
|
201
|
+
type=int,
|
|
202
|
+
default=None,
|
|
203
|
+
help="truncate query results after this many rows (default 500)",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
args, extra = parser.parse_known_args(argv)
|
|
207
|
+
# Python < 3.12 argparse binds the optional `query` positional before it
|
|
208
|
+
# sees later options, so `cypher PATH --params P QUERY` leaves QUERY over.
|
|
209
|
+
if getattr(args, "command", None) == "cypher" and args.query is None and len(extra) == 1:
|
|
210
|
+
args.query, extra = extra[0], []
|
|
211
|
+
if extra:
|
|
212
|
+
parser.error(f"unrecognized arguments: {' '.join(extra)}")
|
|
213
|
+
try:
|
|
214
|
+
return _dispatch(args)
|
|
215
|
+
except (
|
|
216
|
+
OSError,
|
|
217
|
+
RuntimeError,
|
|
218
|
+
GraphFormatError,
|
|
219
|
+
FileExistsError,
|
|
220
|
+
ValueError,
|
|
221
|
+
CypherError,
|
|
222
|
+
json.JSONDecodeError,
|
|
223
|
+
) as exc:
|
|
224
|
+
print(f"graph-ted-db: {exc}", file=sys.stderr)
|
|
225
|
+
return 1
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _dispatch(args: argparse.Namespace) -> int:
|
|
229
|
+
if args.command == "init":
|
|
230
|
+
meta = init_graph(args.path, name=args.name, exist_ok=args.exist_ok)
|
|
231
|
+
print(f"initialized {args.path} id={meta.id} name={meta.name}")
|
|
232
|
+
return 0
|
|
233
|
+
if args.command == "info":
|
|
234
|
+
meta = load_graph_meta(args.path)
|
|
235
|
+
print(
|
|
236
|
+
f"{meta.name} id={meta.id} format={meta.format} "
|
|
237
|
+
f"v{meta.format_version} created_at={meta.created_at}"
|
|
238
|
+
)
|
|
239
|
+
if args.check:
|
|
240
|
+
store = _store(args.path)
|
|
241
|
+
problems = store.problems()
|
|
242
|
+
print(
|
|
243
|
+
f"writer={store.writer_id} writers={len(store.writers())} "
|
|
244
|
+
+ " ".join(f"{k}={v}" for k, v in problems.items())
|
|
245
|
+
)
|
|
246
|
+
return 1 if any(problems.values()) else 0
|
|
247
|
+
return 0
|
|
248
|
+
if args.command == "import":
|
|
249
|
+
from graph_ted_db.store import import_export
|
|
250
|
+
|
|
251
|
+
imported = import_export(args.export_file, args.dest, name=args.name)
|
|
252
|
+
print(f"imported into {args.dest} id={imported.meta.id}")
|
|
253
|
+
return 0
|
|
254
|
+
|
|
255
|
+
store = _store(args.path)
|
|
256
|
+
if args.command == "export":
|
|
257
|
+
counts = store.export(args.out)
|
|
258
|
+
print(" ".join(f"{k}={v}" for k, v in counts.items()))
|
|
259
|
+
return 0
|
|
260
|
+
|
|
261
|
+
if args.command == "put-node":
|
|
262
|
+
rec = store.make_node(
|
|
263
|
+
labels=args.label,
|
|
264
|
+
props=dict(args.prop),
|
|
265
|
+
record_id=args.record_id,
|
|
266
|
+
updated_by=_who(args.updated_by),
|
|
267
|
+
)
|
|
268
|
+
print(json.dumps(rec.to_dict(), ensure_ascii=False))
|
|
269
|
+
return 0
|
|
270
|
+
if args.command == "get-node":
|
|
271
|
+
found_node = store.get_node(args.record_id)
|
|
272
|
+
if found_node is None:
|
|
273
|
+
print(f"graph-ted-db: node {args.record_id} not found", file=sys.stderr)
|
|
274
|
+
return 1
|
|
275
|
+
print(json.dumps(found_node.to_dict(), ensure_ascii=False))
|
|
276
|
+
return 0
|
|
277
|
+
if args.command == "ls-nodes":
|
|
278
|
+
for rec in store.iter_nodes():
|
|
279
|
+
print(json.dumps(rec.to_dict(), ensure_ascii=False))
|
|
280
|
+
return 0
|
|
281
|
+
if args.command == "delete-node":
|
|
282
|
+
tomb = store.delete_node(args.record_id, updated_by=_who(args.updated_by))
|
|
283
|
+
print(json.dumps(tomb.to_dict(), ensure_ascii=False))
|
|
284
|
+
return 0
|
|
285
|
+
if args.command == "put-edge":
|
|
286
|
+
edge = store.make_edge(
|
|
287
|
+
type=args.edge_type,
|
|
288
|
+
from_id=args.from_id,
|
|
289
|
+
to_id=args.to_id,
|
|
290
|
+
props=dict(args.prop),
|
|
291
|
+
record_id=args.record_id,
|
|
292
|
+
updated_by=_who(args.updated_by),
|
|
293
|
+
)
|
|
294
|
+
print(json.dumps(edge.to_dict(), ensure_ascii=False))
|
|
295
|
+
return 0
|
|
296
|
+
if args.command == "get-edge":
|
|
297
|
+
found_edge = store.get_edge(args.record_id)
|
|
298
|
+
if found_edge is None:
|
|
299
|
+
print(f"graph-ted-db: edge {args.record_id} not found", file=sys.stderr)
|
|
300
|
+
return 1
|
|
301
|
+
print(json.dumps(found_edge.to_dict(), ensure_ascii=False))
|
|
302
|
+
return 0
|
|
303
|
+
if args.command == "ls-edges":
|
|
304
|
+
for edge in store.iter_edges():
|
|
305
|
+
print(json.dumps(edge.to_dict(), ensure_ascii=False))
|
|
306
|
+
return 0
|
|
307
|
+
if args.command == "delete-edge":
|
|
308
|
+
tomb = store.delete_edge(args.record_id, updated_by=_who(args.updated_by))
|
|
309
|
+
print(json.dumps(tomb.to_dict(), ensure_ascii=False))
|
|
310
|
+
return 0
|
|
311
|
+
if args.command == "compact":
|
|
312
|
+
store.compact()
|
|
313
|
+
print(f"compacted {args.path}")
|
|
314
|
+
return 0
|
|
315
|
+
if args.command == "doctor":
|
|
316
|
+
report = store.doctor(fix=args.fix)
|
|
317
|
+
print(report.summary(), end="")
|
|
318
|
+
return 0
|
|
319
|
+
if args.command == "cypher":
|
|
320
|
+
query = args.query if args.query is not None else sys.stdin.read()
|
|
321
|
+
params = json.loads(args.params) if args.params else {}
|
|
322
|
+
if not isinstance(params, dict):
|
|
323
|
+
raise ValueError("--params must be a JSON object")
|
|
324
|
+
rows = store.execute(query, params, updated_by=_who(args.updated_by) or "cypher")
|
|
325
|
+
for row in rows:
|
|
326
|
+
print(json.dumps(row, ensure_ascii=False))
|
|
327
|
+
return 0
|
|
328
|
+
if args.command == "serve":
|
|
329
|
+
from graph_ted_db.server import (
|
|
330
|
+
DEFAULT_HOST,
|
|
331
|
+
DEFAULT_PORT,
|
|
332
|
+
LOOPBACK_HOSTS,
|
|
333
|
+
MAX_QUERY_RECORDS,
|
|
334
|
+
serve,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
host = args.host or DEFAULT_HOST
|
|
338
|
+
port = DEFAULT_PORT if args.port is None else args.port
|
|
339
|
+
max_records = MAX_QUERY_RECORDS if args.max_records is None else args.max_records
|
|
340
|
+
if args.token is not None:
|
|
341
|
+
token = args.token.strip()
|
|
342
|
+
else:
|
|
343
|
+
token = os.environ.get("GRAPH_TED_DB_TOKEN", "").strip()
|
|
344
|
+
if host not in LOOPBACK_HOSTS and not token:
|
|
345
|
+
print(
|
|
346
|
+
"graph-ted-db: refusing to bind off loopback without a token "
|
|
347
|
+
"(pass --token or set GRAPH_TED_DB_TOKEN)",
|
|
348
|
+
file=sys.stderr,
|
|
349
|
+
)
|
|
350
|
+
return 1
|
|
351
|
+
try:
|
|
352
|
+
serve(store, host=host, port=port, max_records=max_records, token=token)
|
|
353
|
+
except OSError as exc:
|
|
354
|
+
if exc.errno in (errno.EADDRINUSE, 10048): # 10048: WSAEADDRINUSE
|
|
355
|
+
print(
|
|
356
|
+
f"graph-ted-db: port {port} in use; pass --port "
|
|
357
|
+
"(another graph-ted-db serve may still be running)",
|
|
358
|
+
file=sys.stderr,
|
|
359
|
+
)
|
|
360
|
+
return 1
|
|
361
|
+
raise
|
|
362
|
+
return 0
|
|
363
|
+
raise AssertionError(f"unhandled command {args.command}")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
if __name__ == "__main__":
|
|
367
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Optional Graphiti GraphDriver. Import GraphTedDbDriver from .graphiti."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def __getattr__(name: str) -> Any:
|
|
9
|
+
if name == "GraphTedDbDriver":
|
|
10
|
+
from graph_ted_db.driver.graphiti import GraphTedDbDriver
|
|
11
|
+
|
|
12
|
+
return GraphTedDbDriver
|
|
13
|
+
raise AttributeError(name)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = ["GraphTedDbDriver"]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""graphiti-core GraphDriver over a graph-ted-db folder or HTTP serve.
|
|
2
|
+
|
|
3
|
+
Reports GraphProvider.NEO4J so Graphiti emits the query dialect this engine's openCypher subset targets.
|
|
4
|
+
Pass an http(s) URL to use serve (one process owns the folder). Pass a
|
|
5
|
+
filesystem path to open GraphStore in-process (library / CLI use).
|
|
6
|
+
Install graphiti-core in the process that constructs this driver.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from collections.abc import Coroutine
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from graph_ted_db.driver.http import (
|
|
17
|
+
get_health,
|
|
18
|
+
is_http_url,
|
|
19
|
+
normalize_base,
|
|
20
|
+
post_cypher,
|
|
21
|
+
post_cypher_many,
|
|
22
|
+
)
|
|
23
|
+
from graph_ted_db.store import GraphStore, init_graph
|
|
24
|
+
from graph_ted_db.store.init import GraphFormatError
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from graphiti_core.driver.driver import (
|
|
28
|
+
GraphDriver,
|
|
29
|
+
GraphDriverSession,
|
|
30
|
+
GraphProvider,
|
|
31
|
+
)
|
|
32
|
+
except ImportError as exc: # pragma: no cover
|
|
33
|
+
raise ImportError(
|
|
34
|
+
"GraphTedDbDriver requires graphiti-core. Install it in the Graphiti MCP environment."
|
|
35
|
+
) from exc
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _params_from_kwargs(kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
39
|
+
kwargs = dict(kwargs)
|
|
40
|
+
kwargs.pop("routing_", None)
|
|
41
|
+
kwargs.pop("database_", None)
|
|
42
|
+
nested = kwargs.pop("params", None)
|
|
43
|
+
params: dict[str, Any] = dict(nested) if isinstance(nested, dict) else {}
|
|
44
|
+
params.update(kwargs)
|
|
45
|
+
return params
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class GraphTedDbSession(GraphDriverSession):
|
|
49
|
+
provider = GraphProvider.NEO4J
|
|
50
|
+
|
|
51
|
+
def __init__(self, driver: GraphTedDbDriver):
|
|
52
|
+
self.driver = driver
|
|
53
|
+
self.store = driver.store
|
|
54
|
+
self._batch: list[tuple[str, dict[str, Any]]] | None = None
|
|
55
|
+
|
|
56
|
+
async def __aenter__(self):
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
async def __aexit__(self, exc_type, exc, tb):
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
async def close(self):
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def _enqueue(self, query: str | list, kwargs: dict[str, Any]) -> None:
|
|
66
|
+
assert self._batch is not None
|
|
67
|
+
if isinstance(query, list):
|
|
68
|
+
for cypher, params in query:
|
|
69
|
+
self._batch.append((str(cypher), dict(params or {})))
|
|
70
|
+
return
|
|
71
|
+
self._batch.append((str(query), _params_from_kwargs(kwargs)))
|
|
72
|
+
|
|
73
|
+
async def run(self, query: str | list, **kwargs: Any) -> Any:
|
|
74
|
+
if self._batch is not None:
|
|
75
|
+
self._enqueue(query, kwargs)
|
|
76
|
+
return None
|
|
77
|
+
if self.driver._http:
|
|
78
|
+
if isinstance(query, list):
|
|
79
|
+
statements = [(str(cypher), dict(params or {})) for cypher, params in query]
|
|
80
|
+
await asyncio.to_thread(post_cypher_many, self.driver._http, statements)
|
|
81
|
+
else:
|
|
82
|
+
await asyncio.to_thread(
|
|
83
|
+
post_cypher,
|
|
84
|
+
self.driver._http,
|
|
85
|
+
str(query),
|
|
86
|
+
_params_from_kwargs(kwargs),
|
|
87
|
+
)
|
|
88
|
+
return None
|
|
89
|
+
assert self.store is not None
|
|
90
|
+
if isinstance(query, list):
|
|
91
|
+
for cypher, params in query:
|
|
92
|
+
await self._execute_store(cypher, dict(params or {}))
|
|
93
|
+
return None
|
|
94
|
+
await self._execute_store(query, _params_from_kwargs(kwargs))
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
async def _execute_store(self, query: str, params: dict[str, Any]) -> list:
|
|
98
|
+
"""Run queries off the event loop, unless this thread already holds the store lock.
|
|
99
|
+
|
|
100
|
+
``execute_write`` keeps the file lock on the loop thread and calls
|
|
101
|
+
``run`` from there. Moving that call to a worker would see the lock
|
|
102
|
+
as re-entrant and corrupt ``_lock_depth``.
|
|
103
|
+
"""
|
|
104
|
+
assert self.store is not None
|
|
105
|
+
if self.store._tx is not None or self.store._lock_depth:
|
|
106
|
+
return self.store.execute(str(query), params)
|
|
107
|
+
return await asyncio.to_thread(self.store.execute, str(query), params)
|
|
108
|
+
|
|
109
|
+
async def execute_write(self, func, *args, **kwargs):
|
|
110
|
+
if self.driver._http:
|
|
111
|
+
self._batch = []
|
|
112
|
+
try:
|
|
113
|
+
result = await func(self, *args, **kwargs)
|
|
114
|
+
statements = self._batch
|
|
115
|
+
self._batch = None
|
|
116
|
+
if statements:
|
|
117
|
+
await asyncio.to_thread(post_cypher_many, self.driver._http, statements)
|
|
118
|
+
return result
|
|
119
|
+
except Exception:
|
|
120
|
+
self._batch = None
|
|
121
|
+
raise
|
|
122
|
+
assert self.store is not None
|
|
123
|
+
with self.store._lock():
|
|
124
|
+
self.store._begin_unlocked()
|
|
125
|
+
try:
|
|
126
|
+
result = await func(self, *args, **kwargs)
|
|
127
|
+
self.store._commit_unlocked()
|
|
128
|
+
return result
|
|
129
|
+
except Exception:
|
|
130
|
+
self.store._rollback_unlocked()
|
|
131
|
+
raise
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class GraphTedDbDriver(GraphDriver):
|
|
135
|
+
provider = GraphProvider.NEO4J
|
|
136
|
+
aoss_client = None
|
|
137
|
+
fulltext_syntax = ""
|
|
138
|
+
|
|
139
|
+
def __init__(self, root: str | Path, *, data_dir: Path | None = None):
|
|
140
|
+
super().__init__()
|
|
141
|
+
raw = str(root).strip()
|
|
142
|
+
if is_http_url(raw):
|
|
143
|
+
self._http: str | None = normalize_base(raw)
|
|
144
|
+
self.store = None
|
|
145
|
+
else:
|
|
146
|
+
self._http = None
|
|
147
|
+
path = Path(raw).expanduser().resolve()
|
|
148
|
+
try:
|
|
149
|
+
self.store = GraphStore.open(path, data_dir=data_dir)
|
|
150
|
+
except GraphFormatError:
|
|
151
|
+
init_graph(path, name=path.name, exist_ok=True)
|
|
152
|
+
self.store = GraphStore.open(path, data_dir=data_dir)
|
|
153
|
+
self._database = "neo4j"
|
|
154
|
+
self.client = self
|
|
155
|
+
|
|
156
|
+
async def execute_query(self, cypher_query_, **kwargs: Any):
|
|
157
|
+
params = _params_from_kwargs(kwargs)
|
|
158
|
+
if self._http:
|
|
159
|
+
rows = await asyncio.to_thread(
|
|
160
|
+
post_cypher,
|
|
161
|
+
self._http,
|
|
162
|
+
str(cypher_query_),
|
|
163
|
+
params,
|
|
164
|
+
max_records=0,
|
|
165
|
+
)
|
|
166
|
+
return rows, None, None
|
|
167
|
+
assert self.store is not None
|
|
168
|
+
if self.store._tx is not None or self.store._lock_depth:
|
|
169
|
+
rows = self.store.execute(str(cypher_query_), params)
|
|
170
|
+
else:
|
|
171
|
+
rows = await asyncio.to_thread(self.store.execute, str(cypher_query_), params)
|
|
172
|
+
return rows, None, None
|
|
173
|
+
|
|
174
|
+
def session(self, database: str | None = None) -> GraphTedDbSession:
|
|
175
|
+
del database
|
|
176
|
+
return GraphTedDbSession(self)
|
|
177
|
+
|
|
178
|
+
async def close(self) -> None:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def close_sync(self) -> None:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
def delete_all_indexes(self) -> Coroutine:
|
|
185
|
+
async def _noop():
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
return _noop()
|
|
189
|
+
|
|
190
|
+
async def verify_connectivity(self) -> None:
|
|
191
|
+
if self._http:
|
|
192
|
+
try:
|
|
193
|
+
await asyncio.to_thread(get_health, self._http)
|
|
194
|
+
return
|
|
195
|
+
except ValueError:
|
|
196
|
+
rows = await asyncio.to_thread(post_cypher, self._http, "RETURN 1 AS ok")
|
|
197
|
+
else:
|
|
198
|
+
assert self.store is not None
|
|
199
|
+
rows = await asyncio.to_thread(self.store.execute, "RETURN 1 AS ok")
|
|
200
|
+
if not rows or rows[0].get("ok") not in (1, "1"):
|
|
201
|
+
raise RuntimeError("graph-ted-db connectivity check failed")
|