memorysync 1.7.3__tar.gz → 1.9.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: memorysync
3
- Version: 1.7.3
3
+ Version: 1.9.0
4
4
  Summary: Official Python client for the MemorySync API.
5
5
  Project-URL: Homepage, https://memorysync.io
6
6
  Project-URL: Documentation, https://docs.memorysync.io
@@ -0,0 +1 @@
1
+ __version__ = "1.9.0"
@@ -193,6 +193,7 @@ class _RequestBuilder:
193
193
  session_id: Optional[str],
194
194
  metadata: Optional[Dict[str, Any]],
195
195
  end_user_id: Optional[str],
196
+ client_ref: Optional[str] = None,
196
197
  ) -> Dict[str, Any]:
197
198
  return _strip_none(
198
199
  {
@@ -203,6 +204,7 @@ class _RequestBuilder:
203
204
  "session_id": session_id,
204
205
  "metadata": metadata,
205
206
  "end_user_id": end_user_id,
207
+ "client_ref": client_ref,
206
208
  }
207
209
  )
208
210
 
@@ -364,7 +366,18 @@ class MemorySyncClient:
364
366
  session_id: Optional[str] = None,
365
367
  metadata: Optional[Dict[str, Any]] = None,
366
368
  end_user_id: Optional[str] = None,
369
+ client_ref: Optional[str] = None,
367
370
  ) -> AddResult:
371
+ """Store one memory candidate.
372
+
373
+ Pass ``client_ref`` -- your own identifier for the record, at most 128
374
+ characters -- and re-sending it is recognised rather than stored a second
375
+ time: the call returns an :class:`AddSkippedResponse` with reason
376
+ ``already_ingested`` and the ids the first write produced. That makes an
377
+ interrupted write safe to retry without first checking whether it landed.
378
+ Near-duplicate detection is always on, but it compares *meaning*, so it is
379
+ a filter and not a guarantee.
380
+ """
368
381
  body = _RequestBuilder.add(
369
382
  text,
370
383
  source=source,
@@ -373,6 +386,7 @@ class MemorySyncClient:
373
386
  session_id=session_id,
374
387
  metadata=metadata,
375
388
  end_user_id=end_user_id,
389
+ client_ref=client_ref,
376
390
  )
377
391
  raw = self._request("POST", "/memory/add", json=body, end_user_override=end_user_id)
378
392
  skipped = _as_skipped(raw, default_reason="no_high_value_content")
@@ -598,6 +612,81 @@ class MemorySyncClient:
598
612
  return skipped
599
613
  return Memory.from_dict(raw)
600
614
 
615
+ # ── Bulk import ──────────────────────────────────────────────────
616
+
617
+ def create_import(
618
+ self,
619
+ file: Union[bytes, "IO[bytes]"],
620
+ *,
621
+ filename: str,
622
+ end_user_id: Optional[str] = None,
623
+ resume: bool = False,
624
+ continue_on_error: bool = False,
625
+ ) -> Dict[str, Any]:
626
+ """Upload a payload and get an import job back, without waiting for it.
627
+
628
+ ``bulk_add`` accepts fifty records per call because each one runs the
629
+ extraction pipeline. For a migration out of another system, upload the
630
+ whole file here and poll :meth:`get_import`.
631
+
632
+ ``resume`` requires ``client_ref`` on every record and is what makes
633
+ re-running the same file safe: records a previous run already stored come
634
+ back counted as ``already_imported`` rather than stored again.
635
+
636
+ The payload is JSONL, or JSON when ``filename`` ends in ``.json``. The
637
+ extension is how the server picks the reader, so it is required.
638
+ """
639
+ if not filename or not filename.strip():
640
+ raise ValidationError("filename is required so the server can pick a reader")
641
+ form = _strip_none(
642
+ {
643
+ "end_user_id": end_user_id,
644
+ "resume": "true" if resume else "false",
645
+ "continue_on_error": "true" if continue_on_error else "false",
646
+ }
647
+ )
648
+ return (
649
+ self._request(
650
+ "POST",
651
+ "/imports",
652
+ files={"file": (filename, file)},
653
+ data=form,
654
+ end_user_override=end_user_id,
655
+ )
656
+ or {}
657
+ )
658
+
659
+ def get_import(self, job_id: str) -> Dict[str, Any]:
660
+ """One import job's status, progress and per-outcome counters.
661
+
662
+ ``progress_percentage`` stays below 100 until the job is genuinely
663
+ finished, so reaching 100 means done.
664
+ """
665
+ if not job_id or not str(job_id).strip():
666
+ raise ValidationError("job_id is required")
667
+ return self._request("GET", f"/imports/{job_id}") or {}
668
+
669
+ def list_imports(
670
+ self, *, status: Optional[str] = None, limit: Optional[int] = None
671
+ ) -> Dict[str, Any]:
672
+ """Recent import jobs, newest first."""
673
+ return (
674
+ self._request(
675
+ "GET", "/imports", params={"status": status, "limit": limit}
676
+ )
677
+ or {}
678
+ )
679
+
680
+ def cancel_import(self, job_id: str) -> Dict[str, Any]:
681
+ """Ask the worker to stop between batches.
682
+
683
+ Records already imported stay imported, and the counters report how far it
684
+ reached. Cancelling a finished job is a no-op rather than an error.
685
+ """
686
+ if not job_id or not str(job_id).strip():
687
+ raise ValidationError("job_id is required")
688
+ return self._request("POST", f"/imports/{job_id}/cancel") or {}
689
+
601
690
  # ── Bulk edit ────────────────────────────────────────────────────
602
691
 
603
692
  def batch_update(
@@ -1057,7 +1146,18 @@ class AsyncMemorySyncClient:
1057
1146
  session_id: Optional[str] = None,
1058
1147
  metadata: Optional[Dict[str, Any]] = None,
1059
1148
  end_user_id: Optional[str] = None,
1149
+ client_ref: Optional[str] = None,
1060
1150
  ) -> AddResult:
1151
+ """Store one memory candidate.
1152
+
1153
+ Pass ``client_ref`` -- your own identifier for the record, at most 128
1154
+ characters -- and re-sending it is recognised rather than stored a second
1155
+ time: the call returns an :class:`AddSkippedResponse` with reason
1156
+ ``already_ingested`` and the ids the first write produced. That makes an
1157
+ interrupted write safe to retry without first checking whether it landed.
1158
+ Near-duplicate detection is always on, but it compares *meaning*, so it is
1159
+ a filter and not a guarantee.
1160
+ """
1061
1161
  body = _RequestBuilder.add(
1062
1162
  text,
1063
1163
  source=source,
@@ -1066,6 +1166,7 @@ class AsyncMemorySyncClient:
1066
1166
  session_id=session_id,
1067
1167
  metadata=metadata,
1068
1168
  end_user_id=end_user_id,
1169
+ client_ref=client_ref,
1069
1170
  )
1070
1171
  raw = await self._request("POST", "/memory/add", json=body, end_user_override=end_user_id)
1071
1172
  skipped = _as_skipped(raw, default_reason="no_high_value_content")
@@ -1270,6 +1371,61 @@ class AsyncMemorySyncClient:
1270
1371
 
1271
1372
  # ── Bulk edit ────────────────────────────────────────────────────
1272
1373
 
1374
+ # ── Bulk import ──────────────────────────────────────────────────
1375
+
1376
+ async def create_import(
1377
+ self,
1378
+ file: Union[bytes, "IO[bytes]"],
1379
+ *,
1380
+ filename: str,
1381
+ end_user_id: Optional[str] = None,
1382
+ resume: bool = False,
1383
+ continue_on_error: bool = False,
1384
+ ) -> Dict[str, Any]:
1385
+ """Async twin of :meth:`MemorySyncClient.create_import`."""
1386
+ if not filename or not filename.strip():
1387
+ raise ValidationError("filename is required so the server can pick a reader")
1388
+ form = _strip_none(
1389
+ {
1390
+ "end_user_id": end_user_id,
1391
+ "resume": "true" if resume else "false",
1392
+ "continue_on_error": "true" if continue_on_error else "false",
1393
+ }
1394
+ )
1395
+ return (
1396
+ await self._request(
1397
+ "POST",
1398
+ "/imports",
1399
+ files={"file": (filename, file)},
1400
+ data=form,
1401
+ end_user_override=end_user_id,
1402
+ )
1403
+ or {}
1404
+ )
1405
+
1406
+ async def get_import(self, job_id: str) -> Dict[str, Any]:
1407
+ """Async twin of :meth:`MemorySyncClient.get_import`."""
1408
+ if not job_id or not str(job_id).strip():
1409
+ raise ValidationError("job_id is required")
1410
+ return await self._request("GET", f"/imports/{job_id}") or {}
1411
+
1412
+ async def list_imports(
1413
+ self, *, status: Optional[str] = None, limit: Optional[int] = None
1414
+ ) -> Dict[str, Any]:
1415
+ """Async twin of :meth:`MemorySyncClient.list_imports`."""
1416
+ return (
1417
+ await self._request(
1418
+ "GET", "/imports", params={"status": status, "limit": limit}
1419
+ )
1420
+ or {}
1421
+ )
1422
+
1423
+ async def cancel_import(self, job_id: str) -> Dict[str, Any]:
1424
+ """Async twin of :meth:`MemorySyncClient.cancel_import`."""
1425
+ if not job_id or not str(job_id).strip():
1426
+ raise ValidationError("job_id is required")
1427
+ return await self._request("POST", f"/imports/{job_id}/cancel") or {}
1428
+
1273
1429
  async def batch_update(
1274
1430
  self,
1275
1431
  items: Sequence[Union[BatchUpdateItem, Dict[str, Any]]],
@@ -79,6 +79,14 @@ class BulkAddItem:
79
79
  metadata: Optional[Dict[str, Any]] = None
80
80
  importance: Optional[float] = None
81
81
  end_user_id: Optional[str] = None
82
+ #: Your own identifier for this record, at most 128 characters.
83
+ #:
84
+ #: Send it and re-submitting the record is recognised rather than stored a
85
+ #: second time: the item comes back ``skipped`` with reason
86
+ #: ``already_ingested`` and the ids the first submission produced. This is
87
+ #: what makes a large import safe to re-run. Near-duplicate detection is
88
+ #: always on but compares *meaning*, so it is a filter and not a guarantee.
89
+ client_ref: Optional[str] = None
82
90
 
83
91
  def to_dict(self) -> Dict[str, Any]:
84
92
  out: Dict[str, Any] = {"text": self.text}
@@ -94,6 +102,8 @@ class BulkAddItem:
94
102
  out["importance"] = self.importance
95
103
  if self.end_user_id is not None:
96
104
  out["end_user_id"] = self.end_user_id
105
+ if self.client_ref is not None:
106
+ out["client_ref"] = self.client_ref
97
107
  return out
98
108
 
99
109
 
@@ -1 +0,0 @@
1
- __version__ = "1.7.3"
File without changes
File without changes
File without changes
File without changes