memwal 0.2.2.dev0__py3-none-any.whl → 0.2.2.dev2__py3-none-any.whl

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.
memwal/__init__.py CHANGED
@@ -128,4 +128,4 @@ __all__ = [
128
128
  "RecallManualResult",
129
129
  ]
130
130
 
131
- __version__ = "0.2.2.dev0"
131
+ __version__ = "0.2.2.dev2"
memwal/client.py CHANGED
@@ -86,6 +86,7 @@ from .utils import (
86
86
  sha256_hex,
87
87
  sign_message,
88
88
  sign_sui_personal_message,
89
+ validate_max_distance,
89
90
  )
90
91
 
91
92
  T = TypeVar("T")
@@ -142,6 +143,36 @@ def _reject_blank_text(text: object, message: str = "Text cannot be empty") -> N
142
143
  raise ValueError(message)
143
144
 
144
145
 
146
+ def _require(data: object, field: str, endpoint: str) -> Any:
147
+ """``data[field]`` from a parsed 2xx body, or a ``MemWalError`` naming both.
148
+
149
+ A bare ``KeyError`` is not a ``MemWalError``, carries only the field name,
150
+ and is raised far from the response that caused it (#1250). Optional
151
+ fields keep using ``.get()`` with a documented default.
152
+ """
153
+
154
+ if not isinstance(data, dict):
155
+ raise MemWalError(
156
+ f"{endpoint} response was {type(data).__name__}, expected an object "
157
+ f"with {field!r}"
158
+ )
159
+ if field not in data:
160
+ raise MemWalError(f"{endpoint} response missing {field!r}")
161
+ return data[field]
162
+
163
+
164
+ def _require_positive_int(value: object, name: str) -> None:
165
+ """Reject anything but a positive ``int`` before it is signed (#1249).
166
+
167
+ ``json.dumps`` writes ``float("nan")`` as a bare ``NaN`` token, which is
168
+ not valid JSON, and negative or fractional values were forwarded as-is.
169
+ ``bool`` is an ``int`` subclass and is rejected explicitly.
170
+ """
171
+
172
+ if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
173
+ raise ValueError(f"{name} must be a positive integer, got {value!r}")
174
+
175
+
145
176
  IDEMPOTENCY_BUCKET_MS = 30 * 60 * 1000
146
177
 
147
178
 
@@ -1176,7 +1207,9 @@ class MemWal:
1176
1207
  sort = params.sort
1177
1208
  else:
1178
1209
  query_text = query
1210
+ _reject_blank_text(query_text, "query cannot be empty")
1179
1211
  _check_limit(limit)
1212
+ validate_max_distance(max_distance)
1180
1213
  body = {
1181
1214
  "query": query_text,
1182
1215
  "limit": limit,
@@ -1189,9 +1222,9 @@ class MemWal:
1189
1222
  data = await self._signed_request("POST", "/api/recall", body)
1190
1223
  memories = [
1191
1224
  RecallMemory(
1192
- blob_id=m["blob_id"],
1193
- text=m["text"],
1194
- distance=m["distance"],
1225
+ blob_id=_require(m, "blob_id", "POST /api/recall"),
1226
+ text=_require(m, "text", "POST /api/recall"),
1227
+ distance=_require(m, "distance", "POST /api/recall"),
1195
1228
  score=m.get("score"),
1196
1229
  created_at=m.get("created_at"),
1197
1230
  )
@@ -1353,18 +1386,24 @@ class MemWal:
1353
1386
  Returns:
1354
1387
  :class:`AskResult` with answer, memories_used count, and memories list.
1355
1388
  """
1389
+ _reject_blank_text(question, "question cannot be empty")
1390
+ _require_positive_int(limit, "limit")
1356
1391
  data = await self._signed_request("POST", "/api/ask", {
1357
1392
  "question": question,
1358
1393
  "limit": limit,
1359
1394
  "namespace": self._resolve_namespace(namespace),
1360
1395
  })
1361
1396
  memories = [
1362
- AskMemory(blob_id=m["blob_id"], text=m["text"], distance=m["distance"])
1397
+ AskMemory(
1398
+ blob_id=_require(m, "blob_id", "POST /api/ask"),
1399
+ text=_require(m, "text", "POST /api/ask"),
1400
+ distance=_require(m, "distance", "POST /api/ask"),
1401
+ )
1363
1402
  for m in data.get("memories", [])
1364
1403
  ]
1365
1404
  return AskResult(
1366
- answer=data["answer"],
1367
- memories_used=data["memories_used"],
1405
+ answer=_require(data, "answer", "POST /api/ask"),
1406
+ memories_used=_require(data, "memories_used", "POST /api/ask"),
1368
1407
  memories=memories,
1369
1408
  )
1370
1409
 
@@ -1424,12 +1463,13 @@ class MemWal:
1424
1463
  "namespace": namespace,
1425
1464
  "limit": limit,
1426
1465
  })
1466
+ endpoint = "POST /api/restore"
1427
1467
  return RestoreResult(
1428
- restored=data["restored"],
1429
- skipped=data["skipped"],
1430
- total=data["total"],
1431
- namespace=data["namespace"],
1432
- owner=data["owner"],
1468
+ restored=_require(data, "restored", endpoint),
1469
+ skipped=_require(data, "skipped", endpoint),
1470
+ total=_require(data, "total", endpoint),
1471
+ namespace=_require(data, "namespace", endpoint),
1472
+ owner=_require(data, "owner", endpoint),
1433
1473
  # Relayers older than WALM-319 omit `truncated` entirely --
1434
1474
  # treat "not present" as "not known to be truncated" rather
1435
1475
  # than require every relayer version to send it.
@@ -1492,20 +1532,21 @@ class MemWal:
1492
1532
  # against `path_and_query`, not `path`.
1493
1533
  path = f"/v1/owners/{owner}/namespaces" + (f"?{query}" if query else "")
1494
1534
  data = await self._signed_request("GET", path, {}, include_seal_session=False)
1535
+ endpoint = "GET /v1/owners/{owner}/namespaces"
1495
1536
  return NamespacesResult(
1496
1537
  namespaces=[
1497
1538
  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"],
1539
+ id=_require(ns, "id", endpoint),
1540
+ name=_require(ns, "name", endpoint),
1541
+ memory_count=_require(ns, "memory_count", endpoint),
1542
+ storage_used=_require(ns, "storage_used", endpoint),
1543
+ updated_at=_require(ns, "updated_at", endpoint),
1503
1544
  )
1504
- for ns in data["namespaces"]
1545
+ for ns in _require(data, "namespaces", endpoint)
1505
1546
  ],
1506
1547
  next_cursor=data.get("next_cursor"),
1507
- has_more=data["has_more"],
1508
- snapshot_version=data["snapshot_version"],
1548
+ has_more=_require(data, "has_more", endpoint),
1549
+ snapshot_version=_require(data, "snapshot_version", endpoint),
1509
1550
  )
1510
1551
 
1511
1552
  async def health(self) -> HealthResult:
@@ -1523,8 +1564,8 @@ class MemWal:
1523
1564
  raise MemWalError(f"Health check failed: {response.status_code}")
1524
1565
  data = response.json()
1525
1566
  return HealthResult(
1526
- status=data["status"],
1527
- version=data["version"],
1567
+ status=_require(data, "status", "GET /health"),
1568
+ version=_require(data, "version", "GET /health"),
1528
1569
  relayer_version=data.get("relayerVersion"),
1529
1570
  api_version=data.get("apiVersion"),
1530
1571
  min_supported_sdk=data.get("minSupportedSdk"),
@@ -1569,11 +1610,12 @@ class MemWal:
1569
1610
  },
1570
1611
  include_seal_session=False,
1571
1612
  )
1613
+ endpoint = "POST /api/remember/manual"
1572
1614
  return RememberManualResult(
1573
- id=data["id"],
1574
- blob_id=data["blob_id"],
1575
- owner=data["owner"],
1576
- namespace=data["namespace"],
1615
+ id=_require(data, "id", endpoint),
1616
+ blob_id=_require(data, "blob_id", endpoint),
1617
+ owner=_require(data, "owner", endpoint),
1618
+ namespace=_require(data, "namespace", endpoint),
1577
1619
  )
1578
1620
 
1579
1621
  async def recall_manual(self, opts: RecallManualOptions) -> RecallManualResult:
memwal/mock.py CHANGED
@@ -38,6 +38,7 @@ from .types import (
38
38
  RememberResult,
39
39
  RestoreResult,
40
40
  )
41
+ from .utils import validate_max_distance
41
42
 
42
43
  _MOCK_NAMESPACE_EPOCH = datetime(2026, 1, 1, tzinfo=timezone.utc)
43
44
 
@@ -285,6 +286,7 @@ class MemWalMock:
285
286
  query_text = query
286
287
  _validate_text(query_text, "query")
287
288
  _check_limit(limit)
289
+ validate_max_distance(max_distance)
288
290
  if sort not in (None, "relevance", "recent"):
289
291
  raise ValueError("sort must be 'relevance' or 'recent'")
290
292
  resolved_namespace = self._namespace if namespace is None else namespace
memwal/utils.py CHANGED
@@ -9,8 +9,9 @@ from __future__ import annotations
9
9
 
10
10
  import base64
11
11
  import hashlib
12
+ import math
12
13
  from datetime import datetime, timezone
13
- from typing import Tuple, Union
14
+ from typing import Optional, Tuple, Union
14
15
 
15
16
  import nacl.signing
16
17
 
@@ -373,3 +374,23 @@ def sign_sui_personal_message(
373
374
  public_key_bytes = bytes(signing_key.verify_key)
374
375
  serialized = bytes([_SUI_ED25519_SCHEME_FLAG]) + signature_bytes + public_key_bytes
375
376
  return base64.b64encode(serialized).decode("ascii")
377
+
378
+
379
+ def validate_max_distance(max_distance: Optional[float]) -> None:
380
+ """Reject a ``max_distance`` that would silently drop every hit (#1248).
381
+
382
+ The recall filter keeps ``distance < max_distance``. Every comparison with
383
+ NaN is False and real distances are never negative, so NaN or a negative
384
+ threshold returns an empty page that looks like an empty namespace.
385
+ Mirrors the TypeScript ``maxDistance`` guard (#1227).
386
+ """
387
+
388
+ if max_distance is None:
389
+ return
390
+ if (
391
+ isinstance(max_distance, bool)
392
+ or not isinstance(max_distance, (int, float))
393
+ or not math.isfinite(max_distance)
394
+ or max_distance < 0
395
+ ):
396
+ raise ValueError("max_distance must be a non-negative finite number")
@@ -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.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
@@ -0,0 +1,10 @@
1
+ memwal/__init__.py,sha256=Q3MYcoT6ucCc4CtfrjKywu5hPlrHxbLJCMHsbE4VImQ,3142
2
+ memwal/client.py,sha256=3FBM1AP-YYoYPOzZFAeUA_nAYZtHWnTEuJWIdYThVas,97328
3
+ memwal/compatibility.py,sha256=FsKApCcHOFZeIeNOph3YydWVnZtMJdT0KCt0e6jylho,2799
4
+ memwal/middleware.py,sha256=z2xdbvaobBg4byJXnrX7amWdVYE4jfeO4t31DoDBv_o,38413
5
+ memwal/mock.py,sha256=JrYAkvEk_vkDDHd5RMWdyAqdE72GEgqFPlPbAFzZvkA,24347
6
+ memwal/types.py,sha256=fPvxU4k14qmj577sMtGZyVaGhwXxORtyXL-TxD0P6LI,14002
7
+ memwal/utils.py,sha256=RYlELBpKUHN9RuvWQu0cSGMoyXpAdTWlNKawfDpDbQ0,13361
8
+ memwal-0.2.2.dev2.dist-info/METADATA,sha256=CLWf7FcTs6hsaOq7PvPDWaZ2pAuQyVFLsIZ5DuWLpsw,11923
9
+ memwal-0.2.2.dev2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
10
+ memwal-0.2.2.dev2.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- memwal/__init__.py,sha256=G_2AYOMiAWMZ_G89OKlHPYV63KXxp--oMGC0cYV9r1c,3142
2
- memwal/client.py,sha256=l_V6fcUyXq9r1qV8sHw8xlSc8-Q50ebTtDuP4rscq8c,95076
3
- memwal/compatibility.py,sha256=FsKApCcHOFZeIeNOph3YydWVnZtMJdT0KCt0e6jylho,2799
4
- memwal/middleware.py,sha256=z2xdbvaobBg4byJXnrX7amWdVYE4jfeO4t31DoDBv_o,38413
5
- memwal/mock.py,sha256=cB-mu2yqycvMCVMmgqTTsIj_pGSkHufCyLNbfkPdHEE,24262
6
- memwal/types.py,sha256=fPvxU4k14qmj577sMtGZyVaGhwXxORtyXL-TxD0P6LI,14002
7
- memwal/utils.py,sha256=YyLuxPjKB0RksL1ZGPORpW7QrSID0vQQABq3L_64zKw,12596
8
- memwal-0.2.2.dev0.dist-info/METADATA,sha256=11TlxEsj6VTIT3FxMNJBr9UqstdqvYlIp8jduI83LV4,11923
9
- memwal-0.2.2.dev0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
10
- memwal-0.2.2.dev0.dist-info/RECORD,,