memorysync-cli 1.2.0__tar.gz → 1.3.0__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.
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/PKG-INFO +1 -1
- memorysync_cli-1.3.0/src/memorysync_cli/_version.py +1 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/src/memorysync_cli/commands/memory.py +108 -6
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/src/memorysync_cli/registry.json +13 -3
- memorysync_cli-1.2.0/src/memorysync_cli/_version.py +0 -1
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/.gitignore +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/LICENSE +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/README.md +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/pyproject.toml +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/src/memorysync_cli/__init__.py +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/src/memorysync_cli/__main__.py +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/src/memorysync_cli/args.py +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/src/memorysync_cli/commands/__init__.py +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/src/memorysync_cli/commands/admin.py +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/src/memorysync_cli/commands/init.py +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/src/memorysync_cli/commands/source.py +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/src/memorysync_cli/commands/tooling.py +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/src/memorysync_cli/completions.py +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/src/memorysync_cli/config.py +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/src/memorysync_cli/credentials.py +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/src/memorysync_cli/errors.py +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/src/memorysync_cli/evaluation.py +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/src/memorysync_cli/http.py +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/src/memorysync_cli/main.py +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/src/memorysync_cli/output.py +0 -0
- {memorysync_cli-1.2.0 → memorysync_cli-1.3.0}/src/memorysync_cli/registry.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.3.0"
|
|
@@ -505,11 +505,15 @@ def import_memories(ctx: dict) -> dict:
|
|
|
505
505
|
try:
|
|
506
506
|
raw = Path(source_path).read_text(encoding="utf-8")
|
|
507
507
|
except OSError as error:
|
|
508
|
-
raise usage_error(
|
|
508
|
+
raise usage_error(
|
|
509
|
+
f"Could not read {source_path}: {_describe_read_failure(error)}"
|
|
510
|
+
) from None
|
|
509
511
|
|
|
510
512
|
records: list[dict] = []
|
|
511
513
|
errors: list[str] = []
|
|
512
514
|
|
|
515
|
+
ref_field = flags.get("ref_field") or None
|
|
516
|
+
|
|
513
517
|
def _validate(number: int, entry: Any) -> None:
|
|
514
518
|
"""Record one candidate as either usable or an error, in line order."""
|
|
515
519
|
if not isinstance(entry, dict):
|
|
@@ -526,6 +530,7 @@ def import_memories(ctx: dict) -> dict:
|
|
|
526
530
|
"text": text.strip(),
|
|
527
531
|
"source": entry.get("source") or "cli-import",
|
|
528
532
|
"line": number,
|
|
533
|
+
"ref": _pick_ref(entry, ref_field),
|
|
529
534
|
}
|
|
530
535
|
if isinstance(entry.get("metadata"), dict):
|
|
531
536
|
record["metadata"] = entry["metadata"]
|
|
@@ -562,12 +567,14 @@ def import_memories(ctx: dict) -> dict:
|
|
|
562
567
|
if flags.get("dry_run"):
|
|
563
568
|
preview = list(errors[:10])
|
|
564
569
|
trailer = len(errors) - len(preview)
|
|
570
|
+
with_ref = sum(1 for record in records if record.get("ref"))
|
|
565
571
|
return {
|
|
566
572
|
"data": {
|
|
567
573
|
"file": source_path,
|
|
568
574
|
"valid": len(records),
|
|
569
575
|
"invalid": len(errors),
|
|
570
576
|
"errors": errors,
|
|
577
|
+
"identified": with_ref,
|
|
571
578
|
},
|
|
572
579
|
"text": lambda: "\n".join(
|
|
573
580
|
line
|
|
@@ -575,6 +582,10 @@ def import_memories(ctx: dict) -> dict:
|
|
|
575
582
|
style.yellow("Dry run. Nothing was sent."),
|
|
576
583
|
f"{style.dim('valid ')} {len(records)}",
|
|
577
584
|
f"{style.dim('invalid ')} {len(errors)}",
|
|
585
|
+
# Shown always, not only under --resume, because knowing the
|
|
586
|
+
# file has usable ids is what tells someone --resume is
|
|
587
|
+
# available to them.
|
|
588
|
+
f"{style.dim('with id ')} {with_ref}",
|
|
578
589
|
*[style.dim(f" {message}") for message in preview],
|
|
579
590
|
style.dim(f" ... and {trailer} more") if trailer > 0 else "",
|
|
580
591
|
style.yellow(
|
|
@@ -597,6 +608,25 @@ def import_memories(ctx: dict) -> dict:
|
|
|
597
608
|
if not records:
|
|
598
609
|
raise usage_error(f"{source_path} contained no records.")
|
|
599
610
|
|
|
611
|
+
# --resume is only honest if every record has a stable identity. Falling back
|
|
612
|
+
# to the line number would break the moment the file is re-exported in a
|
|
613
|
+
# different order, and falling back to a hash of the text would make two
|
|
614
|
+
# genuinely identical records collide, so the second would be reported as
|
|
615
|
+
# already stored and silently dropped. Refusing names the problem instead.
|
|
616
|
+
if flags.get("resume"):
|
|
617
|
+
missing = [record for record in records if not record.get("ref")]
|
|
618
|
+
if missing:
|
|
619
|
+
shown = ", ".join(f"line {record['line']}" for record in missing[:5])
|
|
620
|
+
looked_for = f'"{ref_field}"' if ref_field else "id, _id, uuid"
|
|
621
|
+
extra = "" if ref_field else " and metadata.mem0_id"
|
|
622
|
+
raise usage_error(
|
|
623
|
+
f"--resume needs an id on every record, and {len(missing)} "
|
|
624
|
+
f"{'has' if len(missing) == 1 else 'have'} none.",
|
|
625
|
+
f"First: {shown}. Looked for {looked_for}{extra}. "
|
|
626
|
+
"Pass --ref-field <name> to use a different field, or run without "
|
|
627
|
+
"--resume.",
|
|
628
|
+
)
|
|
629
|
+
|
|
600
630
|
batch_size = _resolve_batch_size(flags)
|
|
601
631
|
batches = [records[i : i + batch_size] for i in range(0, len(records), batch_size)]
|
|
602
632
|
totals = {
|
|
@@ -604,23 +634,31 @@ def import_memories(ctx: dict) -> dict:
|
|
|
604
634
|
"imported": 0,
|
|
605
635
|
"memory_ids": [],
|
|
606
636
|
"skipped": 0,
|
|
637
|
+
# Rows the server had already stored under the same identifier. Counted
|
|
638
|
+
# apart from ``skipped`` because they mean something different to whoever
|
|
639
|
+
# is resuming: nothing was lost, this row is simply already in.
|
|
640
|
+
"already_imported": 0,
|
|
607
641
|
"failed": 0,
|
|
608
642
|
"unreported": 0,
|
|
609
643
|
"failures": [],
|
|
610
644
|
"batches": len(batches),
|
|
611
645
|
"batch_size": batch_size,
|
|
646
|
+
"resume": bool(flags.get("resume")),
|
|
612
647
|
}
|
|
613
648
|
|
|
649
|
+
resume = bool(flags.get("resume"))
|
|
614
650
|
for batch in batches:
|
|
615
651
|
before = totals["failed"]
|
|
616
|
-
items = [
|
|
617
|
-
|
|
652
|
+
items = []
|
|
653
|
+
for record in batch:
|
|
654
|
+
item = {
|
|
618
655
|
key: value
|
|
619
656
|
for key, value in record.items()
|
|
620
657
|
if key in {"text", "source", "metadata"}
|
|
621
658
|
}
|
|
622
|
-
|
|
623
|
-
|
|
659
|
+
if resume and record.get("ref"):
|
|
660
|
+
item["client_ref"] = record["ref"]
|
|
661
|
+
items.append(item)
|
|
624
662
|
try:
|
|
625
663
|
response = api.bulk_add_memories({"items": items}) or {}
|
|
626
664
|
except Exception as error: # noqa: BLE001 — reported, not raised
|
|
@@ -651,6 +689,11 @@ def import_memories(ctx: dict) -> dict:
|
|
|
651
689
|
if status == "created":
|
|
652
690
|
totals["imported"] += 1
|
|
653
691
|
totals["memory_ids"].extend(item.get("memory_ids") or [])
|
|
692
|
+
elif status == "skipped" and item.get("reason") == "already_ingested":
|
|
693
|
+
totals["already_imported"] += 1
|
|
694
|
+
# The ids the first run produced, so a resumed import still ends
|
|
695
|
+
# up knowing every memory its file corresponds to.
|
|
696
|
+
totals["memory_ids"].extend(item.get("memory_ids") or [])
|
|
654
697
|
elif status == "skipped":
|
|
655
698
|
totals["skipped"] += 1
|
|
656
699
|
else:
|
|
@@ -666,6 +709,11 @@ def import_memories(ctx: dict) -> dict:
|
|
|
666
709
|
line
|
|
667
710
|
for line in [
|
|
668
711
|
f"{style.green('Imported')} {totals['imported']} of {totals['total']}",
|
|
712
|
+
style.dim(
|
|
713
|
+
f" {totals['already_imported']} already imported on an earlier run"
|
|
714
|
+
)
|
|
715
|
+
if totals["already_imported"]
|
|
716
|
+
else "",
|
|
669
717
|
style.dim(f" {totals['skipped']} skipped as duplicate or low value")
|
|
670
718
|
if totals["skipped"]
|
|
671
719
|
else "",
|
|
@@ -691,6 +739,57 @@ def import_memories(ctx: dict) -> dict:
|
|
|
691
739
|
SERVER_BULK_MAX = 50
|
|
692
740
|
|
|
693
741
|
|
|
742
|
+
def _pick_ref(entry: dict, ref_field: str | None) -> str | None:
|
|
743
|
+
"""The record's own id, for ``--resume``.
|
|
744
|
+
|
|
745
|
+
Checked in the order an export is likely to carry one. ``metadata.mem0_id``
|
|
746
|
+
is included because the Mem0 migration guide tells people to put it there, so
|
|
747
|
+
a file built by following our own documentation resumes without extra flags.
|
|
748
|
+
Numbers are accepted and stringified; a caller-supplied id is frequently an
|
|
749
|
+
integer and refusing it would be pedantry.
|
|
750
|
+
"""
|
|
751
|
+
metadata = entry.get("metadata") if isinstance(entry.get("metadata"), dict) else {}
|
|
752
|
+
if ref_field:
|
|
753
|
+
candidates = [entry.get(ref_field), metadata.get(ref_field)]
|
|
754
|
+
else:
|
|
755
|
+
candidates = [
|
|
756
|
+
entry.get("id"),
|
|
757
|
+
entry.get("_id"),
|
|
758
|
+
entry.get("uuid"),
|
|
759
|
+
metadata.get("mem0_id"),
|
|
760
|
+
]
|
|
761
|
+
for value in candidates:
|
|
762
|
+
if isinstance(value, str) and value.strip():
|
|
763
|
+
return value.strip()
|
|
764
|
+
# bool is an int subclass and is never a usable identifier.
|
|
765
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
766
|
+
return str(value)
|
|
767
|
+
if isinstance(value, float) and value == int(value):
|
|
768
|
+
return str(int(value))
|
|
769
|
+
return None
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def _describe_read_failure(error: OSError) -> str:
|
|
773
|
+
"""Why a file could not be read, worded identically to the Node CLI.
|
|
774
|
+
|
|
775
|
+
``str(error)`` cannot be used directly: Python says
|
|
776
|
+
``[Errno 2] No such file or directory: 'relative'`` and Node says
|
|
777
|
+
``ENOENT: no such file or directory, open 'C:\\abs\\path'`` — different
|
|
778
|
+
wording, different casing, and Node leaks an absolute path the user did not
|
|
779
|
+
type. The two packages are published as interchangeable, so the common cases
|
|
780
|
+
are named here and the rest fall back to a sentence neither runtime supplies.
|
|
781
|
+
"""
|
|
782
|
+
import errno
|
|
783
|
+
|
|
784
|
+
if isinstance(error, IsADirectoryError) or error.errno == errno.EISDIR:
|
|
785
|
+
return "that is a directory, not a file."
|
|
786
|
+
if isinstance(error, PermissionError) or error.errno in {errno.EACCES, errno.EPERM}:
|
|
787
|
+
return "permission denied."
|
|
788
|
+
if isinstance(error, FileNotFoundError) or error.errno == errno.ENOENT:
|
|
789
|
+
return "no such file or directory."
|
|
790
|
+
return "the file could not be opened."
|
|
791
|
+
|
|
792
|
+
|
|
694
793
|
def _resolve_batch_size(flags: dict) -> int:
|
|
695
794
|
"""Effective batch size for this run.
|
|
696
795
|
|
|
@@ -708,9 +807,12 @@ def _resolve_batch_size(flags: dict) -> int:
|
|
|
708
807
|
except (TypeError, ValueError):
|
|
709
808
|
value = 0
|
|
710
809
|
if value < 1:
|
|
810
|
+
# Quoted by hand rather than with ``!r``. ``repr`` gives single quotes and
|
|
811
|
+
# the Node CLI's idiom gives double ones, and the two are published as
|
|
812
|
+
# interchangeable.
|
|
711
813
|
raise usage_error(
|
|
712
814
|
"--batch-size must be a whole number of at least 1.",
|
|
713
|
-
f
|
|
815
|
+
f'Got "{requested}". The server accepts at most '
|
|
714
816
|
f"{SERVER_BULK_MAX} records per request.",
|
|
715
817
|
)
|
|
716
818
|
return min(value, SERVER_BULK_MAX)
|
|
@@ -372,8 +372,8 @@
|
|
|
372
372
|
{
|
|
373
373
|
"name": "import",
|
|
374
374
|
"summary": "Bulk load memories from a JSON or JSONL file.",
|
|
375
|
-
"description": "Each record needs a text field, and may carry its own user and metadata. Validates the whole file before sending anything, and reports per-row errors with line numbers rather than failing on the first bad row.",
|
|
376
|
-
"usage": "memorysync import <file> [--user <id>] [--batch-size <n>]",
|
|
375
|
+
"description": "Each record needs a text field, and may carry its own user and metadata. Validates the whole file before sending anything, and reports per-row errors with line numbers rather than failing on the first bad row. Pass --resume to make re-running the same file safe: each record is sent with an identifier the server remembers, so rows that already landed are reported as skipped instead of being stored a second time.",
|
|
376
|
+
"usage": "memorysync import <file> [--user <id>] [--batch-size <n>] [--resume]",
|
|
377
377
|
"flags": [
|
|
378
378
|
{
|
|
379
379
|
"name": "--user",
|
|
@@ -386,6 +386,15 @@
|
|
|
386
386
|
"value": "n",
|
|
387
387
|
"description": "Records per request. Default 50."
|
|
388
388
|
},
|
|
389
|
+
{
|
|
390
|
+
"name": "--resume",
|
|
391
|
+
"description": "Send a per-record identifier so re-running the same file skips rows already stored."
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
"name": "--ref-field",
|
|
395
|
+
"value": "name",
|
|
396
|
+
"description": "Field holding each record’s own id, used by --resume. Default: id, then _id, then uuid."
|
|
397
|
+
},
|
|
389
398
|
{
|
|
390
399
|
"name": "--continue-on-error",
|
|
391
400
|
"description": "Keep going after a failed batch."
|
|
@@ -396,7 +405,8 @@
|
|
|
396
405
|
}
|
|
397
406
|
],
|
|
398
407
|
"examples": [
|
|
399
|
-
"memorysync import memories.jsonl --user alice --dry-run"
|
|
408
|
+
"memorysync import memories.jsonl --user alice --dry-run",
|
|
409
|
+
"memorysync import memories.jsonl --user alice --resume"
|
|
400
410
|
],
|
|
401
411
|
"requires_auth": true,
|
|
402
412
|
"consumes_quota": "add_requests"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.2.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|