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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superb-memory
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Python client, mdb CLI and MCP server for the MemoryDB Go document and agent memory database
5
5
  Author: humaniods
6
6
  License-Expression: MIT
@@ -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.1" # the release tag mdb serve downloads by default
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": json.loads(args.where), "limit": args.limit}
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)["results"])
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.1"}
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.1"
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superb-memory
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Python client, mdb CLI and MCP server for the MemoryDB Go document and agent memory database
5
5
  Author: humaniods
6
6
  License-Expression: MIT
File without changes
File without changes