memwal 0.2.2.dev3__py3-none-any.whl → 0.2.2.dev5__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
@@ -27,6 +27,7 @@ from .client import (
27
27
  MemWalClockDriftError,
28
28
  MemWalCompatibilityError,
29
29
  MemWalError,
30
+ MemWalJsonBodyError,
30
31
  MemWalRateLimited,
31
32
  MemWalRememberJobFailed,
32
33
  MemWalRememberJobNotFound,
@@ -43,6 +44,7 @@ from .types import (
43
44
  AskMemory,
44
45
  AskResult,
45
46
  EmbedResult,
47
+ ForgetResult,
46
48
  HealthResult,
47
49
  MemWalConfig,
48
50
  NamespacesResult,
@@ -83,6 +85,7 @@ __all__ = [
83
85
  "MemWalError",
84
86
  "MemWalCompatibilityError",
85
87
  "MemWalClockDriftError",
88
+ "MemWalJsonBodyError",
86
89
  "MemWalRateLimited",
87
90
  "MemWalRememberJobFailed",
88
91
  "MemWalRememberJobNotFound",
@@ -118,6 +121,7 @@ __all__ = [
118
121
  "AnalyzedFact",
119
122
  "HealthResult",
120
123
  "RestoreResult",
124
+ "ForgetResult",
121
125
  "NamespaceSummary",
122
126
  "NamespacesResult",
123
127
  "ScoringWeights",
@@ -128,4 +132,4 @@ __all__ = [
128
132
  "RecallManualResult",
129
133
  ]
130
134
 
131
- __version__ = "0.2.2.dev3"
135
+ __version__ = "0.2.2.dev5"
memwal/client.py CHANGED
@@ -51,6 +51,7 @@ from .types import (
51
51
  AskMemory,
52
52
  AskResult,
53
53
  EmbedResult,
54
+ ForgetResult,
54
55
  HealthResult,
55
56
  MemWalConfig,
56
57
  NamespacesResult,
@@ -675,7 +676,7 @@ class MemWal:
675
676
  if generated_key:
676
677
  self._pending_remember_keys.pop(request_identity, None)
677
678
  return RememberAcceptedResult(
678
- job_id=data["job_id"],
679
+ job_id=_require(data, "job_id", "POST /api/remember"),
679
680
  status=data.get("status", "pending"),
680
681
  )
681
682
 
@@ -717,6 +718,10 @@ class MemWal:
717
718
  # Status is metadata. No SEAL decrypt, so no session. Matches TS.
718
719
  include_seal_session=False,
719
720
  )
721
+ except MemWalJsonBodyError as err:
722
+ if err.status != 404:
723
+ raise
724
+ data = None
720
725
  except (json.JSONDecodeError, UnicodeDecodeError):
721
726
  data = None
722
727
  if not isinstance(data, dict):
@@ -800,6 +805,10 @@ class MemWal:
800
805
  )
801
806
  else:
802
807
  raise
808
+ except MemWalJsonBodyError as err:
809
+ if err.status == 404:
810
+ raise MemWalRememberJobNotFound(job_id) from None
811
+ raise
803
812
  except (json.JSONDecodeError, UnicodeDecodeError):
804
813
  raise MemWalRememberJobNotFound(job_id) from None
805
814
  except _HttpStatusError as err:
@@ -1244,6 +1253,30 @@ class MemWal:
1244
1253
  dropped_count=dropped,
1245
1254
  )
1246
1255
 
1256
+ async def forget(self, namespace: Optional[str] = None) -> ForgetResult:
1257
+ """Delete this owner's index rows for ``namespace``.
1258
+
1259
+ Wraps ``POST /api/forget``. Walrus blobs persist; the rows stop being
1260
+ retrievable. Defaults to the client namespace.
1261
+ """
1262
+ resolved_namespace = self._resolve_namespace(namespace)
1263
+ data = await self._signed_request(
1264
+ "POST",
1265
+ "/api/forget",
1266
+ {"namespace": resolved_namespace},
1267
+ include_seal_session=False,
1268
+ )
1269
+ endpoint = "POST /api/forget"
1270
+ return ForgetResult(
1271
+ deleted=int(_require(data, "deleted", endpoint)),
1272
+ namespace=_require(data, "namespace", endpoint),
1273
+ owner=_require(data, "owner", endpoint),
1274
+ )
1275
+
1276
+ async def forget_namespace(self, namespace: Optional[str] = None) -> ForgetResult:
1277
+ """Alias for :meth:`forget` matching :meth:`MemWalMock.forget_namespace`."""
1278
+ return await self.forget(namespace)
1279
+
1247
1280
  async def analyze(
1248
1281
  self,
1249
1282
  text: str,
@@ -1562,7 +1595,7 @@ class MemWal:
1562
1595
  raise _destroyed_client_error()
1563
1596
  if response.status_code != 200:
1564
1597
  raise MemWalError(f"Health check failed: {response.status_code}")
1565
- data = response.json()
1598
+ data = _parse_json_response(response, "GET /health")
1566
1599
  return HealthResult(
1567
1600
  status=_require(data, "status", "GET /health"),
1568
1601
  version=_require(data, "version", "GET /health"),
@@ -1690,7 +1723,7 @@ class MemWal:
1690
1723
  if self._destroyed:
1691
1724
  raise _destroyed_client_error()
1692
1725
  if version_response.status_code == 200:
1693
- metadata = version_response.json()
1726
+ metadata = _parse_json_response(version_response, "GET /version")
1694
1727
  elif version_response.status_code in (404, 405):
1695
1728
  health_response = await self._http.get(f"{self._server_url}/health")
1696
1729
  if self._destroyed:
@@ -1701,7 +1734,7 @@ class MemWal:
1701
1734
  f"GET /version returned {version_response.status_code}, "
1702
1735
  f"and GET /health returned {health_response.status_code}"
1703
1736
  )
1704
- metadata = health_response.json()
1737
+ metadata = _parse_json_response(health_response, "GET /health")
1705
1738
  else:
1706
1739
  raise MemWalError(
1707
1740
  "Walrus Memory compatibility check failed: "
@@ -1723,7 +1756,11 @@ class MemWal:
1723
1756
  if response.status_code != 200:
1724
1757
  raise MemWalError(f"GET /config returned {response.status_code}")
1725
1758
 
1726
- data = response.json()
1759
+ data = _parse_json_response(response, "GET /config")
1760
+ if not isinstance(data, dict):
1761
+ raise MemWalError(
1762
+ f"GET /config response was {type(data).__name__}, expected an object"
1763
+ )
1727
1764
  package_id = data.get("packageId")
1728
1765
  network = data.get("network")
1729
1766
  sui_rpc_url = data.get("suiRpcUrl")
@@ -1758,8 +1795,9 @@ class MemWal:
1758
1795
  )
1759
1796
  if response.status_code != 200:
1760
1797
  raise MemWalError(f"Sui GraphQL returned {response.status_code}")
1761
- body = response.json()
1762
- obj = (body.get("data") or {}).get("object")
1798
+ body = _parse_json_response(response, f"POST {graphql_url}")
1799
+ data = body.get("data") if isinstance(body, dict) else None
1800
+ obj = data.get("object") if isinstance(data, dict) else None
1763
1801
  if not isinstance(obj, dict) or obj.get("version") is None:
1764
1802
  raise MemWalError(f"Sui GraphQL could not resolve package {package_id}")
1765
1803
  return obj["version"]
@@ -1780,8 +1818,8 @@ class MemWal:
1780
1818
  if response.status_code != 200:
1781
1819
  raise MemWalError(f"sui_getObject returned {response.status_code}")
1782
1820
 
1783
- body = response.json()
1784
- result = body.get("result", {})
1821
+ body = _parse_json_response(response, f"POST {sui_rpc_url}")
1822
+ result = body.get("result", {}) if isinstance(body, dict) else {}
1785
1823
  version = None
1786
1824
  if isinstance(result, dict):
1787
1825
  data = result.get("data")
@@ -1904,7 +1942,7 @@ class MemWal:
1904
1942
  )
1905
1943
  if self._destroyed:
1906
1944
  raise _destroyed_client_error()
1907
- owner = data.get("owner")
1945
+ owner = data.get("owner") if isinstance(data, dict) else None
1908
1946
  if not owner:
1909
1947
  raise MemWalError(
1910
1948
  "Walrus Memory could not resolve this account's owner address "
@@ -2052,7 +2090,15 @@ class MemWal:
2052
2090
  retry_after=response.headers.get("retry-after"),
2053
2091
  )
2054
2092
 
2055
- return response.json()
2093
+ endpoint = f"{method_upper} {path}"
2094
+ data = _parse_json_response(response, endpoint)
2095
+ if response.status_code != 404 and not isinstance(data, dict):
2096
+ raise MemWalJsonBodyError(
2097
+ status=response.status_code,
2098
+ endpoint=endpoint,
2099
+ body=response.text,
2100
+ )
2101
+ return data
2056
2102
 
2057
2103
 
2058
2104
  class MemWalError(Exception):
@@ -2076,6 +2122,71 @@ class MemWalClockDriftError(MemWalError):
2076
2122
  pass
2077
2123
 
2078
2124
 
2125
+ class MemWalJsonBodyError(MemWalError):
2126
+ """Raised when an HTTP response that should carry JSON has an empty or non-JSON body."""
2127
+
2128
+ def __init__(
2129
+ self,
2130
+ status: int,
2131
+ endpoint: str,
2132
+ body: str,
2133
+ *,
2134
+ empty: bool = False,
2135
+ ) -> None:
2136
+ redacted = _redact_internal_urls(body.strip()[:200]) if body else ""
2137
+ if empty:
2138
+ message = (
2139
+ f"MemWal ({endpoint}): HTTP {status} response had an empty body; "
2140
+ "expected JSON"
2141
+ )
2142
+ elif redacted:
2143
+ message = (
2144
+ f"MemWal ({endpoint}): HTTP {status} response was not JSON: "
2145
+ f"{redacted}"
2146
+ )
2147
+ else:
2148
+ message = f"MemWal ({endpoint}): HTTP {status} response was not JSON"
2149
+ super().__init__(message)
2150
+ self.status = status
2151
+ self.endpoint = endpoint
2152
+ self.body = body
2153
+
2154
+
2155
+ def _parse_json_response(response: httpx.Response, endpoint: str) -> Any:
2156
+ """Decode a JSON response body or raise :class:`MemWalJsonBodyError` (#1219).
2157
+
2158
+ Mirrors the TypeScript SDK's ``jsonBodyError`` guard (#1201 / #1207): an
2159
+ empty or non-JSON 2xx response from a relayer or proxy in front of one
2160
+ surfaces as a :class:`MemWalError` naming the endpoint and HTTP status
2161
+ rather than Python's raw :class:`json.JSONDecodeError` or
2162
+ :class:`UnicodeDecodeError`.
2163
+ """
2164
+ try:
2165
+ return response.json()
2166
+ except UnicodeDecodeError as exc:
2167
+ raise MemWalJsonBodyError(
2168
+ status=response.status_code,
2169
+ endpoint=endpoint,
2170
+ body="",
2171
+ ) from exc
2172
+ except (json.JSONDecodeError, ValueError) as exc:
2173
+ doc = getattr(exc, "doc", None)
2174
+ if isinstance(doc, str):
2175
+ raw = doc
2176
+ else:
2177
+ try:
2178
+ raw = response.text
2179
+ except UnicodeDecodeError:
2180
+ raw = ""
2181
+ is_empty = not raw or not raw.strip()
2182
+ raise MemWalJsonBodyError(
2183
+ status=response.status_code,
2184
+ endpoint=endpoint,
2185
+ body=raw,
2186
+ empty=is_empty,
2187
+ ) from exc
2188
+
2189
+
2079
2190
  def _redact_internal_urls(text: str) -> str:
2080
2191
  return re.sub(
2081
2192
  r"https?://(?:(?:[\w-]+\.)*localhost|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[::1\]|0\.0\.0\.0)(?::\d+)?[^\s)\]>'\"]*",
@@ -2396,6 +2507,14 @@ class MemWalSync:
2396
2507
  :class:`RecallParams` for the recommended object-style call)."""
2397
2508
  return self._run(self._inner.recall(query, limit, namespace, max_distance))
2398
2509
 
2510
+ def forget(self, namespace: Optional[str] = None) -> ForgetResult:
2511
+ """Synchronous version of :meth:`MemWal.forget`."""
2512
+ return self._run(self._inner.forget(namespace))
2513
+
2514
+ def forget_namespace(self, namespace: Optional[str] = None) -> ForgetResult:
2515
+ """Synchronous version of :meth:`MemWal.forget_namespace`."""
2516
+ return self._run(self._inner.forget_namespace(namespace))
2517
+
2399
2518
  def analyze(
2400
2519
  self,
2401
2520
  text: str,
memwal/compatibility.py CHANGED
@@ -12,6 +12,12 @@ SUPPORTED_RELAYER_API_MAJOR = 1
12
12
  def compatibility_error(metadata: Dict[str, Any], server_url: str) -> Optional[str]:
13
13
  """Return an actionable error string when relayer metadata is unsupported."""
14
14
 
15
+ if not isinstance(metadata, dict):
16
+ return (
17
+ f"Walrus Memory relayer at {server_url} does not expose compatibility metadata. "
18
+ "Upgrade the relayer to a version that serves GET /version, or use an older SDK."
19
+ )
20
+
15
21
  api_version = metadata.get("apiVersion")
16
22
  relayer_version = metadata.get("relayerVersion")
17
23
  min_supported = metadata.get("minSupportedSdk")
memwal/mock.py CHANGED
@@ -20,6 +20,7 @@ from .types import (
20
20
  AskMemory,
21
21
  AskResult,
22
22
  EmbedResult,
23
+ ForgetResult,
23
24
  HealthResult,
24
25
  NamespacesResult,
25
26
  NamespaceSummary,
@@ -497,6 +498,13 @@ class MemWalMock:
497
498
  return True
498
499
  return False
499
500
 
501
+ async def forget_namespace(self, namespace: Optional[str] = None) -> ForgetResult:
502
+ resolved = self._namespace if namespace is None else namespace
503
+ if not resolved:
504
+ raise ValueError("namespace cannot be empty")
505
+ deleted = self.clear(resolved)
506
+ return ForgetResult(deleted=deleted, namespace=resolved, owner=self._owner)
507
+
500
508
  def clear(self, namespace: Optional[str] = None) -> int:
501
509
  removed = [
502
510
  memory
@@ -717,6 +725,9 @@ class MemWalMockSync:
717
725
  def forget(self, blob_id: str) -> bool:
718
726
  return self._inner.forget(blob_id)
719
727
 
728
+ def forget_namespace(self, namespace: Optional[str] = None) -> ForgetResult:
729
+ return self._run(self._inner.forget_namespace(namespace))
730
+
720
731
  def clear(self, namespace: Optional[str] = None) -> int:
721
732
  return self._inner.clear(namespace)
722
733
 
memwal/types.py CHANGED
@@ -84,6 +84,14 @@ class RememberResult:
84
84
  namespace: str
85
85
 
86
86
 
87
+ @dataclass
88
+ class ForgetResult:
89
+ """Result from POST /api/forget. Walrus blobs are not deleted."""
90
+
91
+ deleted: int
92
+ namespace: str
93
+ owner: str
94
+
87
95
  @dataclass
88
96
  class RecallMemory:
89
97
  """A single recalled memory."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: memwal
3
- Version: 0.2.2.dev3
3
+ Version: 0.2.2.dev5
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
@@ -147,7 +147,7 @@ async def test_memory_flow():
147
147
  assert "dark mode" in result.results[0].text
148
148
  ```
149
149
 
150
- The mock supports remember/job polling, bulk remember, recall, analyze, embed, ask, health, restore, list_namespaces, `forget(blob_id)`, and `clear(namespace)`. For deterministic behavior, `analyze` stores its full input as one fact instead of invoking an LLM extractor. Its simple relevance score is for application tests, not production search-quality evaluation.
150
+ The mock supports remember/job polling, bulk remember, recall, analyze, embed, ask, health, restore, list_namespaces, `forget(blob_id)` (synchronous per-blob deletion returning `bool`), `forget_namespace(namespace?)` (namespace erasure returning `ForgetResult`), and `clear(namespace?)`. For deterministic behavior, `analyze` stores its full input as one fact instead of invoking an LLM extractor. Its simple relevance score is for application tests, not production search-quality evaluation.
151
151
 
152
152
  ### Context Manager
153
153
 
@@ -254,6 +254,7 @@ Create a new async client.
254
254
  | `await analyze(text, namespace?)` | Extract and store facts |
255
255
  | `await ask(question, limit?, namespace?)` | Ask a question answered using memories |
256
256
  | `await restore(namespace, limit?)` | Restore a namespace |
257
+ | `await forget(namespace?)` | Delete index rows for the namespace. Walrus blobs stay. Returns `ForgetResult`. |
257
258
  | `await list_namespaces(cursor?, limit?)` | List namespaces that hold memories; paginate on `has_more` |
258
259
  | `await health()` | Check server health |
259
260
  | `await remember_manual(opts)` | Store encrypted payload + pre-computed vector |
@@ -0,0 +1,10 @@
1
+ memwal/__init__.py,sha256=9X3zQYVSuuy3hq96LWtxcvQxMJI0lW5XXloV-eKYG8g,3232
2
+ memwal/client.py,sha256=eCp7wnvWryITN_Z3fw_NwBV1u1_mlU_mVrdBhLUoltM,102076
3
+ memwal/compatibility.py,sha256=R3Gt1gVRjYSLNa78leNrTpGMZI1UATYo7N8Ie_44CGI,3054
4
+ memwal/middleware.py,sha256=z2xdbvaobBg4byJXnrX7amWdVYE4jfeO4t31DoDBv_o,38413
5
+ memwal/mock.py,sha256=oG0znsO-gekoCkQ0mNgT1kjQwP1ku8TjQEkaNkbRDlQ,24878
6
+ memwal/types.py,sha256=jekUFnpGhVezU_VavY4DEzty6EWb0GxAwspmszk5Cdc,14144
7
+ memwal/utils.py,sha256=RYlELBpKUHN9RuvWQu0cSGMoyXpAdTWlNKawfDpDbQ0,13361
8
+ memwal-0.2.2.dev5.dist-info/METADATA,sha256=TxTVv3b0TqieYcpLS3UxT4KXcEfdyqPZ0k-xeiT0aPg,12151
9
+ memwal-0.2.2.dev5.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
10
+ memwal-0.2.2.dev5.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.4
2
+ Generator: hatchling 1.33.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,10 +0,0 @@
1
- memwal/__init__.py,sha256=9xGvu4OTUpHjju77Hlwd8qb6oDjc6qvfQKM5nFQ1aU0,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=yPNAEIGPY3O1GiFln0zW8stTPuV5sZYXAyQqeIkdbhw,13990
7
- memwal/utils.py,sha256=RYlELBpKUHN9RuvWQu0cSGMoyXpAdTWlNKawfDpDbQ0,13361
8
- memwal-0.2.2.dev3.dist-info/METADATA,sha256=WjL3FwG7QR-MjLPYJcPUHFGPIVVGjngRp3hnltvuNek,11911
9
- memwal-0.2.2.dev3.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
10
- memwal-0.2.2.dev3.dist-info/RECORD,,