memwal 0.1.11.dev11__tar.gz → 0.1.11.dev13__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 (34) hide show
  1. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/PKG-INFO +1 -1
  2. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/memwal/__init__.py +1 -1
  3. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/memwal/client.py +11 -6
  4. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/memwal/mock.py +12 -5
  5. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/memwal/types.py +4 -0
  6. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/memwal/utils.py +3 -1
  7. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/pyproject.toml +1 -1
  8. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_client.py +116 -1
  9. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_mock.py +9 -0
  10. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_signing.py +9 -0
  11. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/.gitignore +0 -0
  12. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/CHANGELOG.md +0 -0
  13. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/README.md +0 -0
  14. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/examples/.env.example +0 -0
  15. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/examples/.gitignore +0 -0
  16. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/examples/async_remember_demo.py +0 -0
  17. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/examples/interactive_demo.py +0 -0
  18. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/examples/verify_credentials.py +0 -0
  19. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/memwal/compatibility.py +0 -0
  20. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/memwal/middleware.py +0 -0
  21. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/notebooks/walrus_memory_python_sdk.ipynb +0 -0
  22. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/run_tests.py +0 -0
  23. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/__init__.py +0 -0
  24. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_auth_rejected_message.py +0 -0
  25. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_blank_text.py +0 -0
  26. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_env_presets.py +0 -0
  27. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_integration.py +0 -0
  28. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_list_namespaces.py +0 -0
  29. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_middleware.py +0 -0
  30. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_normalize_server_url.py +0 -0
  31. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_polling_delay.py +0 -0
  32. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_remember_derived_key.py +0 -0
  33. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_retry_after_http_date.py +0 -0
  34. {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_transport_retry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: memwal
3
- Version: 0.1.11.dev11
3
+ Version: 0.1.11.dev13
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
@@ -128,4 +128,4 @@ __all__ = [
128
128
  "RecallManualResult",
129
129
  ]
130
130
 
131
- __version__ = "0.1.11.dev11"
131
+ __version__ = "0.1.11.dev13"
@@ -1055,6 +1055,8 @@ class MemWal:
1055
1055
  blob_id=m["blob_id"],
1056
1056
  text=m["text"],
1057
1057
  distance=m["distance"],
1058
+ score=m.get("score"),
1059
+ created_at=m.get("created_at"),
1058
1060
  )
1059
1061
  for m in data.get("results", [])
1060
1062
  ]
@@ -1063,14 +1065,12 @@ class MemWal:
1063
1065
  dropped = int(raw_dropped)
1064
1066
  except (TypeError, ValueError):
1065
1067
  dropped = 0
1068
+ total_count = data.get("total", len(memories))
1066
1069
  if max_distance is not None:
1067
1070
  memories = [m for m in memories if m.distance < max_distance]
1068
- return RecallResult(
1069
- results=memories, total=len(memories), dropped_count=dropped
1070
- )
1071
1071
  return RecallResult(
1072
1072
  results=memories,
1073
- total=data.get("total", len(memories)),
1073
+ total=total_count,
1074
1074
  dropped_count=dropped,
1075
1075
  )
1076
1076
 
@@ -1455,7 +1455,12 @@ class MemWal:
1455
1455
  include_seal_session=False,
1456
1456
  )
1457
1457
  hits = [
1458
- RecallManualHit(blob_id=h["blob_id"], distance=h["distance"])
1458
+ RecallManualHit(
1459
+ blob_id=h["blob_id"],
1460
+ distance=h["distance"],
1461
+ created_at=h.get("created_at"),
1462
+ importance=h.get("importance"),
1463
+ )
1459
1464
  for h in data.get("results", [])
1460
1465
  ]
1461
1466
  return RecallManualResult(results=hits, total=data.get("total", len(hits)))
@@ -1817,7 +1822,7 @@ class MemWalClockDriftError(MemWalError):
1817
1822
 
1818
1823
  def _redact_internal_urls(text: str) -> str:
1819
1824
  return re.sub(
1820
- r"https?://(?:localhost|127\.0\.0\.1)(?::\d+)?[^\s)\]>'\"]*",
1825
+ r"https?://(?:(?:[\w-]+\.)*localhost|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[::1\]|0\.0\.0\.0)(?::\d+)?[^\s)\]>'\"]*",
1821
1826
  "[internal]",
1822
1827
  text,
1823
1828
  flags=re.IGNORECASE,
@@ -278,14 +278,21 @@ class MemWalMock:
278
278
  for memory in self._memories
279
279
  if memory.namespace == resolved_namespace
280
280
  ]
281
- if max_distance is not None:
282
- ranked = [item for item in ranked if item[1] < max_distance]
283
281
  ranked.sort(key=lambda item: (item[1], item[0].sequence))
282
+ page = ranked[:limit]
283
+ if max_distance is not None:
284
+ page = [item for item in page if item[1] < max_distance]
284
285
  results = [
285
- RecallMemory(blob_id=memory.blob_id, text=memory.text, distance=score)
286
- for memory, score in ranked[:limit]
286
+ RecallMemory(
287
+ blob_id=memory.blob_id,
288
+ text=memory.text,
289
+ distance=score,
290
+ score=1.0 - score,
291
+ created_at=_mock_timestamp(memory.sequence),
292
+ )
293
+ for memory, score in page
287
294
  ]
288
- return RecallResult(results=results, total=len(results))
295
+ return RecallResult(results=results, total=len(ranked[:limit]))
289
296
 
290
297
  async def analyze(
291
298
  self,
@@ -88,6 +88,8 @@ class RecallMemory:
88
88
  blob_id: str
89
89
  text: str
90
90
  distance: float
91
+ score: Optional[float] = None
92
+ created_at: Optional[str] = None
91
93
 
92
94
 
93
95
  @dataclass
@@ -328,6 +330,8 @@ class RecallManualHit:
328
330
 
329
331
  blob_id: str
330
332
  distance: float
333
+ created_at: Optional[str] = None
334
+ importance: Optional[float] = None
331
335
 
332
336
 
333
337
  @dataclass
@@ -299,7 +299,9 @@ def normalize_private_key(key: str) -> str:
299
299
  candidate = key.strip()
300
300
  if candidate.lower().startswith("suiprivkey1"):
301
301
  return bytes_to_hex(decode_sui_private_key(candidate))
302
- return candidate[2:] if candidate.startswith("0x") else candidate
302
+ if candidate.startswith(("0x", "0X")):
303
+ return candidate[2:]
304
+ return candidate
303
305
 
304
306
 
305
307
  def encode_sui_private_key(seed_bytes: bytes) -> str:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "memwal"
7
- version = "0.1.11.dev11"
7
+ version = "0.1.11.dev13"
8
8
  description = "Python SDK for Walrus Memory — Privacy-first AI memory with Ed25519 signing"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -841,6 +841,40 @@ class TestRecall:
841
841
  assert result.results == []
842
842
  assert result.dropped_count == 3
843
843
 
844
+ @respx.mock
845
+ async def test_recall_preserves_score_and_created_at(
846
+ self, memwal_client: MemWal
847
+ ) -> None:
848
+ mock_seal_session_prereqs()
849
+ respx.post(f"{_TEST_SERVER}/api/recall").mock(
850
+ return_value=httpx.Response(
851
+ 200,
852
+ json={
853
+ "results": [
854
+ {
855
+ "blob_id": "b1",
856
+ "text": "Fact with metadata",
857
+ "distance": 0.1,
858
+ "score": 0.95,
859
+ "created_at": "2026-03-30T12:00:00Z",
860
+ },
861
+ {
862
+ "blob_id": "b2",
863
+ "text": "Legacy fact",
864
+ "distance": 0.3,
865
+ },
866
+ ],
867
+ "total": 2,
868
+ },
869
+ )
870
+ )
871
+ result = await memwal_client.recall("test")
872
+ assert len(result.results) == 2
873
+ assert result.results[0].score == 0.95
874
+ assert result.results[0].created_at == "2026-03-30T12:00:00Z"
875
+ assert result.results[1].score is None
876
+ assert result.results[1].created_at is None
877
+
844
878
  @respx.mock
845
879
  async def test_accepts_recall_params_object(
846
880
  self, memwal_client: MemWal
@@ -935,9 +969,33 @@ class TestRecall:
935
969
  body = json.loads(route.calls[0].request.content)
936
970
  assert body["limit"] == 10
937
971
  assert len(result.results) == 1
938
- assert result.total == 1
972
+ assert result.total == 2
939
973
  assert result.results[0].blob_id == "b1"
940
974
 
975
+ @respx.mock
976
+ async def test_max_distance_filters_all_preserves_total(
977
+ self, memwal_client: MemWal
978
+ ) -> None:
979
+ """When all matches exceed max_distance, results is empty but total
980
+ preserves matches (#1066)."""
981
+ mock_seal_session_prereqs()
982
+ respx.post(f"{_TEST_SERVER}/api/recall").mock(
983
+ return_value=httpx.Response(
984
+ 200,
985
+ json={
986
+ "results": [
987
+ {"blob_id": "b1", "text": "I love coffee", "distance": 0.8},
988
+ {"blob_id": "b2", "text": "I live in Tokyo", "distance": 0.9},
989
+ ],
990
+ "total": 2,
991
+ },
992
+ )
993
+ )
994
+
995
+ result = await memwal_client.recall("coffee", limit=10, max_distance=0.5)
996
+ assert len(result.results) == 0
997
+ assert result.total == 2
998
+
941
999
  @respx.mock
942
1000
  async def test_get_signed_request_uses_empty_body_hash_and_no_wire_body(
943
1001
  self, memwal_client: MemWal
@@ -1451,6 +1509,41 @@ class TestManualAPI:
1451
1509
  assert len(result.results) == 1
1452
1510
  assert result.results[0].blob_id == "b1"
1453
1511
 
1512
+ @respx.mock
1513
+ async def test_recall_manual_preserves_created_at_and_importance(
1514
+ self, memwal_client: MemWal
1515
+ ) -> None:
1516
+ _mock_version()
1517
+ respx.post(f"{_TEST_SERVER}/api/recall/manual").mock(
1518
+ return_value=httpx.Response(
1519
+ 200,
1520
+ json={
1521
+ "results": [
1522
+ {
1523
+ "blob_id": "b1",
1524
+ "distance": 0.15,
1525
+ "created_at": "2026-03-30T12:00:00Z",
1526
+ "importance": 0.85,
1527
+ },
1528
+ {
1529
+ "blob_id": "b2",
1530
+ "distance": 0.4,
1531
+ },
1532
+ ],
1533
+ "total": 2,
1534
+ },
1535
+ )
1536
+ )
1537
+
1538
+ opts = RecallManualOptions(vector=[0.1, 0.2, 0.3], limit=5)
1539
+ result = await memwal_client.recall_manual(opts)
1540
+
1541
+ assert len(result.results) == 2
1542
+ assert result.results[0].created_at == "2026-03-30T12:00:00Z"
1543
+ assert result.results[0].importance == 0.85
1544
+ assert result.results[1].created_at is None
1545
+ assert result.results[1].importance is None
1546
+
1454
1547
  @respx.mock
1455
1548
  async def test_recall_manual_forwards_scoring_weights(self, memwal_client: MemWal) -> None:
1456
1549
  _mock_version()
@@ -1583,3 +1676,25 @@ class TestSealSession:
1583
1676
  assert exported["ttlMin"] == 5
1584
1677
  assert exported["sessionKey"].startswith("suiprivkey")
1585
1678
  assert exported["personalMessageSignature"]
1679
+
1680
+
1681
+ class TestRedactInternalUrls:
1682
+ @pytest.mark.parametrize(
1683
+ "raw, expected",
1684
+ [
1685
+ ("Error at http://localhost:8080/path", "Error at [internal]"),
1686
+ ("Error at http://127.0.0.1:3000/api", "Error at [internal]"),
1687
+ ("Error at http://127.0.0.2:8000/status", "Error at [internal]"),
1688
+ ("Error at http://127.128.0.1:9000/info", "Error at [internal]"),
1689
+ ("Error at http://[::1]:9000/seal/encrypt", "Error at [internal]"),
1690
+ ("Error at http://0.0.0.0:8000/health", "Error at [internal]"),
1691
+ ("Error at http://sidecar.localhost:8080/v1", "Error at [internal]"),
1692
+ ("Error at http://sub.domain.localhost:3000/test", "Error at [internal]"),
1693
+ ("Error at https://api.wal.app/v1/remember", "Error at https://api.wal.app/v1/remember"),
1694
+ ],
1695
+ )
1696
+ def test_redact_internal_urls(self, raw: str, expected: str) -> None:
1697
+ from memwal.client import _redact_internal_urls
1698
+
1699
+ assert _redact_internal_urls(raw) == expected
1700
+
@@ -39,7 +39,11 @@ async def test_mock_remember_and_recall_are_deterministic_and_offline(monkeypatc
39
39
  "I drink tea at night",
40
40
  ]
41
41
  assert recalled.results[0].distance == 0
42
+ assert recalled.results[0].score == 1.0
43
+ assert recalled.results[0].created_at == "2026-01-01T00:00:01.000Z"
42
44
  assert recalled.results[1].distance == 1
45
+ assert recalled.results[1].score == 0.0
46
+ assert recalled.results[1].created_at == "2026-01-01T00:00:02.000Z"
43
47
 
44
48
 
45
49
  @pytest.mark.asyncio
@@ -57,6 +61,11 @@ async def test_mock_isolates_namespaces_and_honors_max_distance():
57
61
  assert [memory.text for memory in alice.results] == ["Alice likes ramen"]
58
62
  assert [memory.text for memory in bob.results] == ["Bob likes tacos"]
59
63
  assert empty.total == 0
64
+ dropped = await mock.recall(
65
+ RecallParams(query="likes", namespace="user-a", max_distance=0)
66
+ )
67
+ assert dropped.results == []
68
+ assert dropped.total == 1
60
69
 
61
70
 
62
71
  @pytest.mark.asyncio
@@ -324,11 +324,20 @@ class TestPrivateKeyFormats:
324
324
  for form in (
325
325
  self._SEED_HEX,
326
326
  f"0x{self._SEED_HEX}",
327
+ f"0X{self._SEED_HEX}",
328
+ f" 0X{self._SEED_HEX} ",
327
329
  encode_sui_private_key(seed),
328
330
  f" {encode_sui_private_key(seed)} ",
329
331
  ):
330
332
  assert normalize_private_key(form) == self._SEED_HEX
331
333
 
334
+ def test_uppercase_0x_prefix_key_produces_valid_client(self) -> None:
335
+ from memwal.client import MemWal
336
+
337
+ upper_0x = f"0X{self._SEED_HEX}"
338
+ client = MemWal.create(key=upper_0x, account_id="0xtest")
339
+ assert client._private_key_hex == self._SEED_HEX
340
+
332
341
  def test_bech32_key_produces_the_same_client_identity(self) -> None:
333
342
  from memwal.client import MemWal
334
343
  from memwal.utils import encode_sui_private_key
File without changes
File without changes