superb-memory 0.2.1__tar.gz → 0.2.2__tar.gz
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.
- {superb_memory-0.2.1 → superb_memory-0.2.2}/PKG-INFO +1 -1
- {superb_memory-0.2.1 → superb_memory-0.2.2}/cli.py +60 -6
- {superb_memory-0.2.1 → superb_memory-0.2.2}/memorydb.py +30 -0
- {superb_memory-0.2.1 → superb_memory-0.2.2}/memorydb_mcp.py +23 -1
- {superb_memory-0.2.1 → superb_memory-0.2.2}/pyproject.toml +1 -1
- {superb_memory-0.2.1 → superb_memory-0.2.2}/superb_memory.egg-info/PKG-INFO +1 -1
- {superb_memory-0.2.1 → superb_memory-0.2.2}/README.md +0 -0
- {superb_memory-0.2.1 → superb_memory-0.2.2}/setup.cfg +0 -0
- {superb_memory-0.2.1 → superb_memory-0.2.2}/superb_memory.egg-info/SOURCES.txt +0 -0
- {superb_memory-0.2.1 → superb_memory-0.2.2}/superb_memory.egg-info/dependency_links.txt +0 -0
- {superb_memory-0.2.1 → superb_memory-0.2.2}/superb_memory.egg-info/entry_points.txt +0 -0
- {superb_memory-0.2.1 → superb_memory-0.2.2}/superb_memory.egg-info/top_level.txt +0 -0
|
@@ -11,6 +11,7 @@ import hashlib
|
|
|
11
11
|
import json
|
|
12
12
|
import os
|
|
13
13
|
import platform
|
|
14
|
+
import re
|
|
14
15
|
import secrets
|
|
15
16
|
import shutil
|
|
16
17
|
import subprocess
|
|
@@ -23,7 +24,7 @@ from urllib.parse import urlparse
|
|
|
23
24
|
sys.path.insert(0, str(os.path.dirname(os.path.abspath(__file__))))
|
|
24
25
|
from memorydb import MemoryDB, MemoryDBError # noqa: E402
|
|
25
26
|
|
|
26
|
-
SERVER_VERSION = "0.2.
|
|
27
|
+
SERVER_VERSION = "0.2.2" # the release tag mdb serve downloads by default
|
|
27
28
|
REPO = "humaniods/MemoryDB"
|
|
28
29
|
|
|
29
30
|
|
|
@@ -50,6 +51,37 @@ def emit(obj):
|
|
|
50
51
|
print(json.dumps(obj, indent=2, default=str))
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
def parse_where(value):
|
|
55
|
+
"""Accept the API's JSON for --where, and the field=value shorthand `mdb demo` prints.
|
|
56
|
+
|
|
57
|
+
Shell quoting is fiddly enough that a hint has to work exactly as typed, so
|
|
58
|
+
region="Rajasthan" and region=Rajasthan both mean one equality term.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
parsed = json.loads(value)
|
|
62
|
+
except json.JSONDecodeError:
|
|
63
|
+
parsed = None
|
|
64
|
+
if isinstance(parsed, dict):
|
|
65
|
+
return [parsed]
|
|
66
|
+
if isinstance(parsed, list):
|
|
67
|
+
return parsed
|
|
68
|
+
terms = []
|
|
69
|
+
for chunk in re.split(r",|\s+and\s+", value.strip()):
|
|
70
|
+
field, sep, raw = chunk.strip().partition("=")
|
|
71
|
+
if not sep or not field.strip():
|
|
72
|
+
sys.exit('mdb: --where wants JSON like [{"field":"region","op":"eq","value":"Rajasthan"}] '
|
|
73
|
+
"or field=value terms, got: " + value)
|
|
74
|
+
raw = raw.strip().strip("\"'")
|
|
75
|
+
for cast in (int, float):
|
|
76
|
+
try:
|
|
77
|
+
raw = cast(raw)
|
|
78
|
+
break
|
|
79
|
+
except ValueError:
|
|
80
|
+
continue
|
|
81
|
+
terms.append({"field": field.strip(), "op": "eq", "value": raw})
|
|
82
|
+
return terms
|
|
83
|
+
|
|
84
|
+
|
|
53
85
|
# -- mdb serve: the database itself, installed over pip, with no Go or git -----------------
|
|
54
86
|
def home():
|
|
55
87
|
path = os.environ.get("MEMORYDB_HOME") or os.path.join(os.path.expanduser("~"), ".memorydb")
|
|
@@ -226,7 +258,7 @@ def main(argv=None):
|
|
|
226
258
|
p.add_argument("--json"); p.add_argument("--file"); p.add_argument("--version", type=int)
|
|
227
259
|
|
|
228
260
|
p = sub.add_parser("query"); p.add_argument("collection")
|
|
229
|
-
p.add_argument("--where", default="[]"); p.add_argument("--order"); p.add_argument("--desc", action="store_true")
|
|
261
|
+
p.add_argument("--where", default="[]", help='[{"field":…,"op":…,"value":…}] or region=Rajasthan shorthand'); p.add_argument("--order"); p.add_argument("--desc", action="store_true")
|
|
230
262
|
p.add_argument("--limit", type=int, default=50); p.add_argument("--fields", help="comma-separated projection")
|
|
231
263
|
|
|
232
264
|
p = sub.add_parser("memory"); p.add_argument("action", choices=["add", "ls", "history"])
|
|
@@ -236,8 +268,19 @@ def main(argv=None):
|
|
|
236
268
|
p = sub.add_parser("search"); p.add_argument("text")
|
|
237
269
|
p.add_argument("--kind", default=""); p.add_argument("--limit", type=int, default=10)
|
|
238
270
|
|
|
239
|
-
p = sub.add_parser("ingest"); p.add_argument("--file", required=True)
|
|
240
|
-
p = sub.add_parser("tx"); p.add_argument("--file", required=True); p.add_argument("--key")
|
|
271
|
+
p = sub.add_parser("ingest"); p.add_argument("--file", required=True, help='{"messages":[…],"facts":[…],"source":…}')
|
|
272
|
+
p = sub.add_parser("tx"); p.add_argument("--file", required=True, help='{"changes":[{"record":…,"expected":N}]}'); p.add_argument("--key")
|
|
273
|
+
|
|
274
|
+
p = sub.add_parser("sync", help="mirror an external table; writes only the rows that changed")
|
|
275
|
+
p.add_argument("source"); p.add_argument("--collection", required=True)
|
|
276
|
+
p.add_argument("--file", help="JSON array of rows (or objects with data/content)")
|
|
277
|
+
p.add_argument("--json", help="the same array inline")
|
|
278
|
+
p.add_argument("--key-field", default="id"); p.add_argument("--delete-missing", action="store_true")
|
|
279
|
+
p.add_argument("--tag", dest="source_tag", default="", help="value stored as the row's source")
|
|
280
|
+
p.add_argument("--idempotency-key")
|
|
281
|
+
|
|
282
|
+
p = sub.add_parser("sources"); p.add_argument("action", choices=["ls"], nargs="?", default="ls")
|
|
283
|
+
p = sub.add_parser("demo", help="load the built-in sample workspace into the server")
|
|
241
284
|
|
|
242
285
|
p = sub.add_parser("serve", help="install (if needed) and run the MemoryDB server")
|
|
243
286
|
p.add_argument("--addr", default="127.0.0.1:8080")
|
|
@@ -277,7 +320,7 @@ def main(argv=None):
|
|
|
277
320
|
else:
|
|
278
321
|
emit(db.collection(args.collection).objects.delete(pk=args.id, version=args.version))
|
|
279
322
|
elif args.cmd == "query":
|
|
280
|
-
q = {"where":
|
|
323
|
+
q = {"where": parse_where(args.where), "limit": args.limit}
|
|
281
324
|
if args.order:
|
|
282
325
|
q["order_by"] = args.order
|
|
283
326
|
q["descending"] = args.desc
|
|
@@ -290,7 +333,7 @@ def main(argv=None):
|
|
|
290
333
|
elif args.action == "ls":
|
|
291
334
|
emit(db.objects.filter(kind=args.kind if args.kind != "fact" else "", source=args.source, q=args.q, limit=args.limit))
|
|
292
335
|
else:
|
|
293
|
-
emit(db.objects.history(pk=args.id)
|
|
336
|
+
emit(db.objects.history(pk=args.id))
|
|
294
337
|
elif args.cmd == "search":
|
|
295
338
|
emit(db.objects.search(args.text, kind=args.kind, limit=args.limit))
|
|
296
339
|
elif args.cmd == "ingest":
|
|
@@ -301,6 +344,17 @@ def main(argv=None):
|
|
|
301
344
|
with open(args.file) as fh:
|
|
302
345
|
payload = json.load(fh)
|
|
303
346
|
emit(db.transaction(payload["changes"], idempotency_key=args.key))
|
|
347
|
+
elif args.cmd == "sync":
|
|
348
|
+
rows = read_json_arg(args.json, args.file)
|
|
349
|
+
if not isinstance(rows, list):
|
|
350
|
+
sys.exit("sync input must be a JSON array of rows")
|
|
351
|
+
emit(db.sources.sync(args.source, collection=args.collection, items=rows,
|
|
352
|
+
key_field=args.key_field, delete_missing=args.delete_missing,
|
|
353
|
+
source=args.source_tag, idempotency_key=args.idempotency_key))
|
|
354
|
+
elif args.cmd == "sources":
|
|
355
|
+
emit(db.sources.all())
|
|
356
|
+
elif args.cmd == "demo":
|
|
357
|
+
emit(db.demo_seed())
|
|
304
358
|
except MemoryDBError as exc:
|
|
305
359
|
sys.exit(f"mdb error [{exc.status}]: {exc}")
|
|
306
360
|
return 0
|
|
@@ -59,6 +59,28 @@ def parse_connection_string(connection_string):
|
|
|
59
59
|
return scheme + "://" + host + ":" + str(port), unquote(parts.username)
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
class SourceManager:
|
|
63
|
+
"""Mirror an external table into a collection.
|
|
64
|
+
|
|
65
|
+
A sync writes only the rows whose data changed since the last sync, so a cron job
|
|
66
|
+
that re-pulls a whole CSV or REST list costs one read, not a rewrite of every row.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self, client):
|
|
70
|
+
self.client = client
|
|
71
|
+
|
|
72
|
+
def sync(self, name, *, collection, items, key_field="id", delete_missing=False, source="", idempotency_key=None):
|
|
73
|
+
"""items is a list of dicts (one row each) or of {"data": {...}, "content": "..."} pairs."""
|
|
74
|
+
rows = [{"data": i, "content": ""} if isinstance(i, dict) and "data" not in i else i for i in items]
|
|
75
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
76
|
+
return self.client.request("POST", "sources/" + quote(name, safe="") + "/sync/",
|
|
77
|
+
dict(collection=collection, key_field=key_field, items=rows,
|
|
78
|
+
delete_missing=delete_missing, source=source), headers=headers)
|
|
79
|
+
|
|
80
|
+
def all(self):
|
|
81
|
+
return self.client.request("GET", "sources/")["results"]
|
|
82
|
+
|
|
83
|
+
|
|
62
84
|
class MemoryDB:
|
|
63
85
|
def __init__(self, url="http://127.0.0.1:8080", *, token=None, timeout=35):
|
|
64
86
|
if token is None:
|
|
@@ -74,6 +96,7 @@ class MemoryDB:
|
|
|
74
96
|
self.timeout = timeout
|
|
75
97
|
self.objects = MemoryManager(self)
|
|
76
98
|
self.collections = CollectionManager(self)
|
|
99
|
+
self.sources = SourceManager(self)
|
|
77
100
|
|
|
78
101
|
@classmethod
|
|
79
102
|
def from_url(cls, connection_string, **fields):
|
|
@@ -95,6 +118,13 @@ class MemoryDB:
|
|
|
95
118
|
def queue_ingest(self, **payload):
|
|
96
119
|
return self.request("POST", "jobs/", payload)
|
|
97
120
|
|
|
121
|
+
def demo_seed(self):
|
|
122
|
+
"""Load the built-in sample workspace (four mirrored tables, notes and facts).
|
|
123
|
+
|
|
124
|
+
Safe to call twice: a workspace that already has the sample is left alone.
|
|
125
|
+
"""
|
|
126
|
+
return self.request("POST", "demo/", None)
|
|
127
|
+
|
|
98
128
|
def request(self, method, path, body=None, headers=None):
|
|
99
129
|
req = Request(self.url + path, data=json.dumps(body).encode() if body is not None else None,
|
|
100
130
|
headers={"Authorization": "Bearer " + self.token, "Content-Type": "application/json", **(headers or {})}, method=method)
|
|
@@ -17,7 +17,7 @@ sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent))
|
|
|
17
17
|
from memorydb import MemoryDB # noqa: E402
|
|
18
18
|
|
|
19
19
|
PROTOCOL_VERSION = "2024-11-05"
|
|
20
|
-
SERVER_INFO = {"name": "memorydb", "version": "0.2.
|
|
20
|
+
SERVER_INFO = {"name": "memorydb", "version": "0.2.2"}
|
|
21
21
|
|
|
22
22
|
TOOL_DEFS = [
|
|
23
23
|
{"name": "memorydb_health", "description": "Server status, identity and transaction sequence.",
|
|
@@ -62,6 +62,20 @@ TOOL_DEFS = [
|
|
|
62
62
|
"order_by": {"type": "string"}, "descending": {"type": "boolean"},
|
|
63
63
|
"projection": {"type": "array", "items": {"type": "string"}},
|
|
64
64
|
"limit": {"type": "integer", "default": 50}}, "required": ["collection"]}},
|
|
65
|
+
{"name": "memorydb_sync", "description": "Mirror a table/CSV/REST list into a collection. Only rows whose data changed are written; re-running is cheap.",
|
|
66
|
+
"inputSchema": {"type": "object", "properties": {
|
|
67
|
+
"source": {"type": "string", "description": "connector name, e.g. stripe or postgres"},
|
|
68
|
+
"collection": {"type": "string"},
|
|
69
|
+
"key_field": {"type": "string", "default": "id"},
|
|
70
|
+
"items": {"type": "array", "items": {"type": "object", "properties": {
|
|
71
|
+
"data": {"type": "object"}, "content": {"type": "string"}}},
|
|
72
|
+
"description": "up to 500 rows; each row must carry key_field"},
|
|
73
|
+
"delete_missing": {"type": "boolean", "default": False}},
|
|
74
|
+
"required": ["source", "collection", "items"]}},
|
|
75
|
+
{"name": "memorydb_list_sources", "description": "List the connectors that own rows in this workspace, with row counts and last sync time.",
|
|
76
|
+
"inputSchema": {"type": "object", "properties": {}}},
|
|
77
|
+
{"name": "memorydb_demo", "description": "Load the built-in sample workspace (four mirrored tables, notes and facts) so the console and search have data. Idempotent.",
|
|
78
|
+
"inputSchema": {"type": "object", "properties": {}}},
|
|
65
79
|
]
|
|
66
80
|
|
|
67
81
|
|
|
@@ -93,6 +107,14 @@ def handle(db, method, params):
|
|
|
93
107
|
if method == "memorydb_query_documents":
|
|
94
108
|
q = {k: v for k, v in params.items() if k != "collection"}
|
|
95
109
|
return db.request("POST", "query/" + params["collection"] + "/", q)
|
|
110
|
+
if method == "memorydb_sync":
|
|
111
|
+
return db.sources.sync(params["source"], collection=params["collection"], items=params["items"],
|
|
112
|
+
key_field=params.get("key_field", "id"),
|
|
113
|
+
delete_missing=params.get("delete_missing", False))
|
|
114
|
+
if method == "memorydb_list_sources":
|
|
115
|
+
return db.sources.all()
|
|
116
|
+
if method == "memorydb_demo":
|
|
117
|
+
return db.demo_seed()
|
|
96
118
|
raise ValueError("unknown tool " + method)
|
|
97
119
|
|
|
98
120
|
|
|
@@ -3,7 +3,7 @@ requires = ["setuptools>=77"]
|
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
[project]
|
|
5
5
|
name = "superb-memory"
|
|
6
|
-
version = "0.2.
|
|
6
|
+
version = "0.2.2"
|
|
7
7
|
description = "Python client, mdb CLI and MCP server for the MemoryDB Go document and agent memory database"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
requires-python = ">=3.10"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|