memwal 0.1.6.dev1__tar.gz → 0.1.6.dev2__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.
Files changed (27) hide show
  1. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/PKG-INFO +1 -1
  2. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/memwal/__init__.py +1 -1
  3. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/memwal/client.py +56 -10
  4. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/pyproject.toml +1 -1
  5. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/.gitignore +0 -0
  6. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/CHANGELOG.md +0 -0
  7. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/README.md +0 -0
  8. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/examples/.env.example +0 -0
  9. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/examples/.gitignore +0 -0
  10. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/examples/async_remember_demo.py +0 -0
  11. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/examples/interactive_demo.py +0 -0
  12. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/examples/verify_credentials.py +0 -0
  13. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/memwal/compatibility.py +0 -0
  14. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/memwal/middleware.py +0 -0
  15. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/memwal/mock.py +0 -0
  16. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/memwal/types.py +0 -0
  17. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/memwal/utils.py +0 -0
  18. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/notebooks/walrus_memory_python_sdk.ipynb +0 -0
  19. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/run_tests.py +0 -0
  20. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/tests/__init__.py +0 -0
  21. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/tests/test_auth_rejected_message.py +0 -0
  22. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/tests/test_client.py +0 -0
  23. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/tests/test_env_presets.py +0 -0
  24. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/tests/test_integration.py +0 -0
  25. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/tests/test_middleware.py +0 -0
  26. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/tests/test_mock.py +0 -0
  27. {memwal-0.1.6.dev1 → memwal-0.1.6.dev2}/tests/test_signing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: memwal
3
- Version: 0.1.6.dev1
3
+ Version: 0.1.6.dev2
4
4
  Summary: Python SDK for Walrus Memory — Privacy-first AI memory with Ed25519 signing
5
5
  Project-URL: Homepage, https://memory.walrus.xyz
6
6
  Project-URL: Documentation, https://memory.walrus.xyz
@@ -120,4 +120,4 @@ __all__ = [
120
120
  "RecallManualResult",
121
121
  ]
122
122
 
123
- __version__ = "0.1.6.dev1"
123
+ __version__ = "0.1.6.dev2"
@@ -30,6 +30,7 @@ import base64
30
30
  import json
31
31
  import random
32
32
  import time
33
+ import uuid
33
34
  from datetime import datetime, timezone
34
35
  from typing import Any, Dict, List, Optional, Sequence, Tuple, TypeVar, Union
35
36
 
@@ -223,6 +224,9 @@ class MemWal:
223
224
  self._session_build_task: Optional[asyncio.Task[str]] = None
224
225
  self._relayer_version_metadata: Optional[Dict[str, Any]] = None
225
226
  self._compatibility_lock: Optional[asyncio.Lock] = None
227
+ # Preserve a generated key across an ambiguous transport failure. A
228
+ # subsequent identical call then collapses onto the accepted paid job.
229
+ self._pending_remember_keys: Dict[str, str] = {}
226
230
 
227
231
  @classmethod
228
232
  def create(
@@ -281,7 +285,10 @@ class MemWal:
281
285
  # ============================================================
282
286
 
283
287
  async def remember(
284
- self, text: str, namespace: Optional[str] = None
288
+ self,
289
+ text: str,
290
+ namespace: Optional[str] = None,
291
+ idempotency_key: Optional[str] = None,
285
292
  ) -> RememberAcceptedResult:
286
293
  """Submit a remember request and return as soon as the server accepts it.
287
294
 
@@ -299,12 +306,27 @@ class MemWal:
299
306
  :class:`RememberAcceptedResult` with ``job_id`` and initial
300
307
  status (``"pending"``).
301
308
  """
309
+ resolved_namespace = namespace or self._namespace
310
+ request_identity = f"{resolved_namespace}\0{text}"
311
+ generated_key = idempotency_key is None
312
+ resolved_key = idempotency_key or self._pending_remember_keys.get(request_identity)
313
+ if resolved_key is None:
314
+ resolved_key = str(uuid.uuid4())
315
+ if generated_key:
316
+ self._pending_remember_keys[request_identity] = resolved_key
317
+
302
318
  data = await self._signed_request(
303
319
  "POST",
304
320
  "/api/remember",
305
- {"text": text, "namespace": namespace or self._namespace},
321
+ {
322
+ "text": text,
323
+ "namespace": resolved_namespace,
324
+ "idempotency_key": resolved_key,
325
+ },
306
326
  accepted_statuses=(200, 202),
307
327
  )
328
+ if generated_key:
329
+ self._pending_remember_keys.pop(request_identity, None)
308
330
  return RememberAcceptedResult(
309
331
  job_id=data["job_id"],
310
332
  status=data.get("status", "pending"),
@@ -312,9 +334,12 @@ class MemWal:
312
334
 
313
335
  # Alias for parity with TS SDK ``rememberAsync``.
314
336
  async def remember_async(
315
- self, text: str, namespace: Optional[str] = None
337
+ self,
338
+ text: str,
339
+ namespace: Optional[str] = None,
340
+ idempotency_key: Optional[str] = None,
316
341
  ) -> RememberAcceptedResult:
317
- return await self.remember(text, namespace)
342
+ return await self.remember(text, namespace, idempotency_key)
318
343
 
319
344
  async def wait_for_remember_job(
320
345
  self,
@@ -380,6 +405,7 @@ class MemWal:
380
405
  namespace: Optional[str] = None,
381
406
  poll_interval_ms: int = 1500,
382
407
  timeout_ms: int = 60_000,
408
+ idempotency_key: Optional[str] = None,
383
409
  ) -> RememberResult:
384
410
  """Submit a remember and wait for the background worker to finish.
385
411
 
@@ -388,12 +414,24 @@ class MemWal:
388
414
  ``failed``). Mirrors TS ``rememberAndWait``.
389
415
  """
390
416
 
391
- accepted = await self.remember(text, namespace)
392
- return await self.wait_for_remember_job(
417
+ resolved_namespace = namespace or self._namespace
418
+ request_identity = f"{resolved_namespace}\0{text}"
419
+ generated_key = idempotency_key is None
420
+ resolved_key = idempotency_key or self._pending_remember_keys.get(request_identity)
421
+ if resolved_key is None:
422
+ resolved_key = str(uuid.uuid4())
423
+ if generated_key:
424
+ self._pending_remember_keys[request_identity] = resolved_key
425
+
426
+ accepted = await self.remember(text, resolved_namespace, resolved_key)
427
+ result = await self.wait_for_remember_job(
393
428
  accepted.job_id,
394
429
  poll_interval_ms=poll_interval_ms,
395
430
  timeout_ms=timeout_ms,
396
431
  )
432
+ if generated_key:
433
+ self._pending_remember_keys.pop(request_identity, None)
434
+ return result
397
435
 
398
436
  # ============================================================
399
437
  # Bulk remember (ENG-1408)
@@ -1285,16 +1323,22 @@ class MemWalSync:
1285
1323
  return asyncio.run(coro)
1286
1324
 
1287
1325
  def remember(
1288
- self, text: str, namespace: Optional[str] = None
1326
+ self,
1327
+ text: str,
1328
+ namespace: Optional[str] = None,
1329
+ idempotency_key: Optional[str] = None,
1289
1330
  ) -> RememberAcceptedResult:
1290
1331
  """Synchronous version of :meth:`MemWal.remember` (async accept)."""
1291
- return self._run(self._inner.remember(text, namespace))
1332
+ return self._run(self._inner.remember(text, namespace, idempotency_key))
1292
1333
 
1293
1334
  # Alias for parity with TS SDK ``rememberAsync``.
1294
1335
  def remember_async(
1295
- self, text: str, namespace: Optional[str] = None
1336
+ self,
1337
+ text: str,
1338
+ namespace: Optional[str] = None,
1339
+ idempotency_key: Optional[str] = None,
1296
1340
  ) -> RememberAcceptedResult:
1297
- return self._run(self._inner.remember_async(text, namespace))
1341
+ return self._run(self._inner.remember_async(text, namespace, idempotency_key))
1298
1342
 
1299
1343
  def wait_for_remember_job(
1300
1344
  self,
@@ -1317,6 +1361,7 @@ class MemWalSync:
1317
1361
  namespace: Optional[str] = None,
1318
1362
  poll_interval_ms: int = 1500,
1319
1363
  timeout_ms: int = 60_000,
1364
+ idempotency_key: Optional[str] = None,
1320
1365
  ) -> RememberResult:
1321
1366
  """Synchronous version of :meth:`MemWal.remember_and_wait`."""
1322
1367
  return self._run(
@@ -1325,6 +1370,7 @@ class MemWalSync:
1325
1370
  namespace,
1326
1371
  poll_interval_ms=poll_interval_ms,
1327
1372
  timeout_ms=timeout_ms,
1373
+ idempotency_key=idempotency_key,
1328
1374
  )
1329
1375
  )
1330
1376
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "memwal"
7
- version = "0.1.6.dev1"
7
+ version = "0.1.6.dev2"
8
8
  description = "Python SDK for Walrus Memory — Privacy-first AI memory with Ed25519 signing"
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes