memwal 0.2.2.dev0__tar.gz → 0.2.2.dev1__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 (39) hide show
  1. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/CHANGELOG.md +1 -0
  2. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/PKG-INFO +1 -1
  3. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/memwal/__init__.py +1 -1
  4. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/memwal/client.py +50 -25
  5. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/pyproject.toml +1 -1
  6. memwal-0.2.2.dev1/tests/test_missing_response_fields.py +125 -0
  7. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/.gitignore +0 -0
  8. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/README.md +0 -0
  9. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/examples/.env.example +0 -0
  10. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/examples/.gitignore +0 -0
  11. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/examples/async_remember_demo.py +0 -0
  12. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/examples/interactive_demo.py +0 -0
  13. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/examples/verify_credentials.py +0 -0
  14. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/memwal/compatibility.py +0 -0
  15. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/memwal/middleware.py +0 -0
  16. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/memwal/mock.py +0 -0
  17. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/memwal/types.py +0 -0
  18. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/memwal/utils.py +0 -0
  19. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/notebooks/walrus_memory_python_sdk.ipynb +0 -0
  20. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/run_tests.py +0 -0
  21. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/tests/__init__.py +0 -0
  22. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/tests/test_auth_rejected_message.py +0 -0
  23. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/tests/test_blank_text.py +0 -0
  24. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/tests/test_client.py +0 -0
  25. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/tests/test_destroy_and_cancel_shield.py +0 -0
  26. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/tests/test_empty_namespace.py +0 -0
  27. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/tests/test_env_presets.py +0 -0
  28. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/tests/test_integration.py +0 -0
  29. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/tests/test_list_namespaces.py +0 -0
  30. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/tests/test_middleware.py +0 -0
  31. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/tests/test_mock.py +0 -0
  32. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/tests/test_nonfinite_validation.py +0 -0
  33. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/tests/test_normalize_server_url.py +0 -0
  34. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/tests/test_polling_delay.py +0 -0
  35. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/tests/test_recall_weights.py +0 -0
  36. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/tests/test_remember_derived_key.py +0 -0
  37. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/tests/test_retry_after_http_date.py +0 -0
  38. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/tests/test_signing.py +0 -0
  39. {memwal-0.2.2.dev0 → memwal-0.2.2.dev1}/tests/test_transport_retry.py +0 -0
@@ -11,6 +11,7 @@ Unreleased.
11
11
  ### Fixed
12
12
 
13
13
  - Shield shared `_build_seal_session` and `_resolve_owner` initialization tasks with `asyncio.shield`, clear the shared task slot via a completion callback that consumes exceptions on orphaned tasks, and cancel/drain in-flight initialization tasks in `close()` so cancelling one concurrent caller does not abort sibling callers or leave unretrieved task exceptions. (#1029)
14
+ - `health`, `ask`, `restore`, `list_namespaces`, `remember_manual`, and `recall` raise `MemWalError` naming the endpoint and field when a 2xx response omits a required field. They used to raise a bare `KeyError` that callers catching `MemWalError` never saw. (#1250, #1256)
14
15
 
15
16
  ## 0.2.1
16
17
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: memwal
3
- Version: 0.2.2.dev0
3
+ Version: 0.2.2.dev1
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.2.2.dev0"
131
+ __version__ = "0.2.2.dev1"
@@ -142,6 +142,24 @@ def _reject_blank_text(text: object, message: str = "Text cannot be empty") -> N
142
142
  raise ValueError(message)
143
143
 
144
144
 
145
+ def _require(data: object, field: str, endpoint: str) -> Any:
146
+ """``data[field]`` from a parsed 2xx body, or a ``MemWalError`` naming both.
147
+
148
+ A bare ``KeyError`` is not a ``MemWalError``, carries only the field name,
149
+ and is raised far from the response that caused it (#1250). Optional
150
+ fields keep using ``.get()`` with a documented default.
151
+ """
152
+
153
+ if not isinstance(data, dict):
154
+ raise MemWalError(
155
+ f"{endpoint} response was {type(data).__name__}, expected an object "
156
+ f"with {field!r}"
157
+ )
158
+ if field not in data:
159
+ raise MemWalError(f"{endpoint} response missing {field!r}")
160
+ return data[field]
161
+
162
+
145
163
  IDEMPOTENCY_BUCKET_MS = 30 * 60 * 1000
146
164
 
147
165
 
@@ -1189,9 +1207,9 @@ class MemWal:
1189
1207
  data = await self._signed_request("POST", "/api/recall", body)
1190
1208
  memories = [
1191
1209
  RecallMemory(
1192
- blob_id=m["blob_id"],
1193
- text=m["text"],
1194
- distance=m["distance"],
1210
+ blob_id=_require(m, "blob_id", "POST /api/recall"),
1211
+ text=_require(m, "text", "POST /api/recall"),
1212
+ distance=_require(m, "distance", "POST /api/recall"),
1195
1213
  score=m.get("score"),
1196
1214
  created_at=m.get("created_at"),
1197
1215
  )
@@ -1359,12 +1377,16 @@ class MemWal:
1359
1377
  "namespace": self._resolve_namespace(namespace),
1360
1378
  })
1361
1379
  memories = [
1362
- AskMemory(blob_id=m["blob_id"], text=m["text"], distance=m["distance"])
1380
+ AskMemory(
1381
+ blob_id=_require(m, "blob_id", "POST /api/ask"),
1382
+ text=_require(m, "text", "POST /api/ask"),
1383
+ distance=_require(m, "distance", "POST /api/ask"),
1384
+ )
1363
1385
  for m in data.get("memories", [])
1364
1386
  ]
1365
1387
  return AskResult(
1366
- answer=data["answer"],
1367
- memories_used=data["memories_used"],
1388
+ answer=_require(data, "answer", "POST /api/ask"),
1389
+ memories_used=_require(data, "memories_used", "POST /api/ask"),
1368
1390
  memories=memories,
1369
1391
  )
1370
1392
 
@@ -1424,12 +1446,13 @@ class MemWal:
1424
1446
  "namespace": namespace,
1425
1447
  "limit": limit,
1426
1448
  })
1449
+ endpoint = "POST /api/restore"
1427
1450
  return RestoreResult(
1428
- restored=data["restored"],
1429
- skipped=data["skipped"],
1430
- total=data["total"],
1431
- namespace=data["namespace"],
1432
- owner=data["owner"],
1451
+ restored=_require(data, "restored", endpoint),
1452
+ skipped=_require(data, "skipped", endpoint),
1453
+ total=_require(data, "total", endpoint),
1454
+ namespace=_require(data, "namespace", endpoint),
1455
+ owner=_require(data, "owner", endpoint),
1433
1456
  # Relayers older than WALM-319 omit `truncated` entirely --
1434
1457
  # treat "not present" as "not known to be truncated" rather
1435
1458
  # than require every relayer version to send it.
@@ -1492,20 +1515,21 @@ class MemWal:
1492
1515
  # against `path_and_query`, not `path`.
1493
1516
  path = f"/v1/owners/{owner}/namespaces" + (f"?{query}" if query else "")
1494
1517
  data = await self._signed_request("GET", path, {}, include_seal_session=False)
1518
+ endpoint = "GET /v1/owners/{owner}/namespaces"
1495
1519
  return NamespacesResult(
1496
1520
  namespaces=[
1497
1521
  NamespaceSummary(
1498
- id=ns["id"],
1499
- name=ns["name"],
1500
- memory_count=ns["memory_count"],
1501
- storage_used=ns["storage_used"],
1502
- updated_at=ns["updated_at"],
1522
+ id=_require(ns, "id", endpoint),
1523
+ name=_require(ns, "name", endpoint),
1524
+ memory_count=_require(ns, "memory_count", endpoint),
1525
+ storage_used=_require(ns, "storage_used", endpoint),
1526
+ updated_at=_require(ns, "updated_at", endpoint),
1503
1527
  )
1504
- for ns in data["namespaces"]
1528
+ for ns in _require(data, "namespaces", endpoint)
1505
1529
  ],
1506
1530
  next_cursor=data.get("next_cursor"),
1507
- has_more=data["has_more"],
1508
- snapshot_version=data["snapshot_version"],
1531
+ has_more=_require(data, "has_more", endpoint),
1532
+ snapshot_version=_require(data, "snapshot_version", endpoint),
1509
1533
  )
1510
1534
 
1511
1535
  async def health(self) -> HealthResult:
@@ -1523,8 +1547,8 @@ class MemWal:
1523
1547
  raise MemWalError(f"Health check failed: {response.status_code}")
1524
1548
  data = response.json()
1525
1549
  return HealthResult(
1526
- status=data["status"],
1527
- version=data["version"],
1550
+ status=_require(data, "status", "GET /health"),
1551
+ version=_require(data, "version", "GET /health"),
1528
1552
  relayer_version=data.get("relayerVersion"),
1529
1553
  api_version=data.get("apiVersion"),
1530
1554
  min_supported_sdk=data.get("minSupportedSdk"),
@@ -1569,11 +1593,12 @@ class MemWal:
1569
1593
  },
1570
1594
  include_seal_session=False,
1571
1595
  )
1596
+ endpoint = "POST /api/remember/manual"
1572
1597
  return RememberManualResult(
1573
- id=data["id"],
1574
- blob_id=data["blob_id"],
1575
- owner=data["owner"],
1576
- namespace=data["namespace"],
1598
+ id=_require(data, "id", endpoint),
1599
+ blob_id=_require(data, "blob_id", endpoint),
1600
+ owner=_require(data, "owner", endpoint),
1601
+ namespace=_require(data, "namespace", endpoint),
1577
1602
  )
1578
1603
 
1579
1604
  async def recall_manual(self, opts: RecallManualOptions) -> RecallManualResult:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "memwal"
7
- version = "0.2.2.dev0"
7
+ version = "0.2.2.dev1"
8
8
  description = "Python SDK for Walrus Memory — Privacy-first AI memory with Ed25519 signing"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,125 @@
1
+ """A 2xx body missing a required field raises ``MemWalError`` (#1250).
2
+
3
+ #1219 covers 2xx bodies that are not JSON. This is the valid-JSON sibling:
4
+ six methods subscripted response fields directly, so a relayer that drifted
5
+ or a proxy rewriting the body surfaced as a bare ``KeyError`` that callers
6
+ catching ``MemWalError`` (the documented error type) never saw.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Dict
12
+
13
+ import httpx
14
+ import nacl.signing
15
+ import pytest
16
+ import respx
17
+
18
+ from memwal import MemWal, MemWalError
19
+ from memwal.types import RememberManualOptions
20
+ from memwal.utils import bytes_to_hex
21
+
22
+ _SERVER = "https://relayer.example"
23
+ _KEY_HEX = bytes_to_hex(bytes(nacl.signing.SigningKey(b"\x01" * 32)))
24
+
25
+
26
+ def _client(body: Any) -> MemWal:
27
+ client = MemWal.create(key=_KEY_HEX, account_id="0x1", server_url=_SERVER)
28
+
29
+ async def fake_signed_request(method, path, req_body, **kwargs): # type: ignore[no-untyped-def]
30
+ return body
31
+
32
+ async def fake_owner() -> str:
33
+ return "0xowner"
34
+
35
+ client._signed_request = fake_signed_request # type: ignore[method-assign]
36
+ client._resolve_owner = fake_owner # type: ignore[method-assign]
37
+ return client
38
+
39
+
40
+ def _assert_names(exc: pytest.ExceptionInfo[MemWalError], endpoint: str, field: str) -> None:
41
+ assert not isinstance(exc.value, KeyError)
42
+ message = str(exc.value)
43
+ assert endpoint in message, message
44
+ assert field in message, message
45
+
46
+
47
+ @respx.mock
48
+ async def test_health_missing_status() -> None:
49
+ respx.get(f"{_SERVER}/health").mock(return_value=httpx.Response(200, json={}))
50
+ client = MemWal.create(key=_KEY_HEX, account_id="0x1", server_url=_SERVER)
51
+ with pytest.raises(MemWalError) as exc:
52
+ await client.health()
53
+ _assert_names(exc, "GET /health", "status")
54
+
55
+
56
+ @respx.mock
57
+ async def test_health_non_object_body() -> None:
58
+ respx.get(f"{_SERVER}/health").mock(return_value=httpx.Response(200, json=["ok"]))
59
+ client = MemWal.create(key=_KEY_HEX, account_id="0x1", server_url=_SERVER)
60
+ with pytest.raises(MemWalError) as exc:
61
+ await client.health()
62
+ _assert_names(exc, "GET /health", "status")
63
+
64
+
65
+ async def test_ask_missing_memories_used() -> None:
66
+ with pytest.raises(MemWalError) as exc:
67
+ await _client({"answer": "hi"}).ask("q?")
68
+ _assert_names(exc, "POST /api/ask", "memories_used")
69
+
70
+
71
+ async def test_ask_memory_entry_missing_text() -> None:
72
+ body = {"answer": "hi", "memories_used": 1, "memories": [{"blob_id": "b", "distance": 0.1}]}
73
+ with pytest.raises(MemWalError) as exc:
74
+ await _client(body).ask("q?")
75
+ _assert_names(exc, "POST /api/ask", "text")
76
+
77
+
78
+ async def test_restore_missing_restored() -> None:
79
+ with pytest.raises(MemWalError) as exc:
80
+ await _client({}).restore("ns")
81
+ _assert_names(exc, "POST /api/restore", "restored")
82
+
83
+
84
+ async def test_list_namespaces_missing_namespaces() -> None:
85
+ with pytest.raises(MemWalError) as exc:
86
+ await _client({}).list_namespaces()
87
+ _assert_names(exc, "namespaces", "namespaces")
88
+
89
+
90
+ async def test_list_namespaces_entry_missing_name() -> None:
91
+ body: Dict[str, Any] = {
92
+ "namespaces": [{"id": "ns-1"}],
93
+ "has_more": False,
94
+ "snapshot_version": 1,
95
+ }
96
+ with pytest.raises(MemWalError) as exc:
97
+ await _client(body).list_namespaces()
98
+ _assert_names(exc, "GET /v1/owners/", "name")
99
+
100
+
101
+ async def test_remember_manual_missing_id() -> None:
102
+ opts = RememberManualOptions(encrypted_data="ZGF0YQ==", vector=[0.1])
103
+ with pytest.raises(MemWalError) as exc:
104
+ await _client({}).remember_manual(opts)
105
+ _assert_names(exc, "POST /api/remember/manual", "id")
106
+
107
+
108
+ async def test_recall_entry_missing_text() -> None:
109
+ body = {"results": [{"blob_id": "b", "distance": 0.1}], "total": 1}
110
+ with pytest.raises(MemWalError) as exc:
111
+ await _client(body).recall("coffee")
112
+ _assert_names(exc, "POST /api/recall", "text")
113
+
114
+
115
+ async def test_well_formed_bodies_still_parse() -> None:
116
+ ask = await _client({"answer": "hi", "memories_used": 0}).ask("q?")
117
+ assert ask.answer == "hi" and ask.memories == []
118
+ recall = await _client(
119
+ {"results": [{"blob_id": "b", "text": "t", "distance": 0.1}], "total": 1}
120
+ ).recall("coffee")
121
+ assert recall.results[0].text == "t"
122
+ restore = await _client(
123
+ {"restored": 1, "skipped": 0, "total": 1, "namespace": "ns", "owner": "0xo"}
124
+ ).restore("ns")
125
+ assert restore.truncated is False and restore.failed == 0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes