memwal 0.2.0__tar.gz → 0.2.0.dev0__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 (36) hide show
  1. {memwal-0.2.0 → memwal-0.2.0.dev0}/PKG-INFO +1 -1
  2. {memwal-0.2.0 → memwal-0.2.0.dev0}/memwal/__init__.py +1 -1
  3. {memwal-0.2.0 → memwal-0.2.0.dev0}/memwal/client.py +16 -8
  4. {memwal-0.2.0 → memwal-0.2.0.dev0}/memwal/mock.py +4 -2
  5. {memwal-0.2.0 → memwal-0.2.0.dev0}/pyproject.toml +1 -1
  6. memwal-0.2.0.dev0/tests/test_empty_namespace.py +144 -0
  7. {memwal-0.2.0 → memwal-0.2.0.dev0}/.gitignore +0 -0
  8. {memwal-0.2.0 → memwal-0.2.0.dev0}/CHANGELOG.md +0 -0
  9. {memwal-0.2.0 → memwal-0.2.0.dev0}/README.md +0 -0
  10. {memwal-0.2.0 → memwal-0.2.0.dev0}/examples/.env.example +0 -0
  11. {memwal-0.2.0 → memwal-0.2.0.dev0}/examples/.gitignore +0 -0
  12. {memwal-0.2.0 → memwal-0.2.0.dev0}/examples/async_remember_demo.py +0 -0
  13. {memwal-0.2.0 → memwal-0.2.0.dev0}/examples/interactive_demo.py +0 -0
  14. {memwal-0.2.0 → memwal-0.2.0.dev0}/examples/verify_credentials.py +0 -0
  15. {memwal-0.2.0 → memwal-0.2.0.dev0}/memwal/compatibility.py +0 -0
  16. {memwal-0.2.0 → memwal-0.2.0.dev0}/memwal/middleware.py +0 -0
  17. {memwal-0.2.0 → memwal-0.2.0.dev0}/memwal/types.py +0 -0
  18. {memwal-0.2.0 → memwal-0.2.0.dev0}/memwal/utils.py +0 -0
  19. {memwal-0.2.0 → memwal-0.2.0.dev0}/notebooks/walrus_memory_python_sdk.ipynb +0 -0
  20. {memwal-0.2.0 → memwal-0.2.0.dev0}/run_tests.py +0 -0
  21. {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/__init__.py +0 -0
  22. {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_auth_rejected_message.py +0 -0
  23. {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_blank_text.py +0 -0
  24. {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_client.py +0 -0
  25. {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_env_presets.py +0 -0
  26. {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_integration.py +0 -0
  27. {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_list_namespaces.py +0 -0
  28. {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_middleware.py +0 -0
  29. {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_mock.py +0 -0
  30. {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_normalize_server_url.py +0 -0
  31. {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_polling_delay.py +0 -0
  32. {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_recall_weights.py +0 -0
  33. {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_remember_derived_key.py +0 -0
  34. {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_retry_after_http_date.py +0 -0
  35. {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_signing.py +0 -0
  36. {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_transport_retry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: memwal
3
- Version: 0.2.0
3
+ Version: 0.2.0.dev0
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.0"
131
+ __version__ = "0.2.0.dev0"
@@ -435,6 +435,14 @@ class MemWal:
435
435
  # subsequent identical call then collapses onto the accepted paid job.
436
436
  self._pending_remember_keys: Dict[str, str] = {}
437
437
 
438
+ def _resolve_namespace(self, namespace: Optional[str]) -> str:
439
+ """``None`` uses the client default; do not treat ``""`` as missing."""
440
+ if namespace is None:
441
+ return self._namespace
442
+ if not namespace:
443
+ raise ValueError("namespace cannot be empty")
444
+ return namespace
445
+
438
446
  @classmethod
439
447
  def create(
440
448
  cls,
@@ -514,7 +522,7 @@ class MemWal:
514
522
  status (``"pending"``).
515
523
  """
516
524
  _reject_blank_text(text)
517
- resolved_namespace = namespace or self._namespace
525
+ resolved_namespace = self._resolve_namespace(namespace)
518
526
  request_identity = f"{resolved_namespace}\0{text}"
519
527
  generated_key = idempotency_key is None
520
528
  resolved_key = (
@@ -727,7 +735,7 @@ class MemWal:
727
735
  """
728
736
 
729
737
  _reject_blank_text(text)
730
- resolved_namespace = namespace or self._namespace
738
+ resolved_namespace = self._resolve_namespace(namespace)
731
739
  request_identity = f"{resolved_namespace}\0{text}"
732
740
  generated_key = idempotency_key is None
733
741
  resolved_key = (
@@ -773,7 +781,7 @@ class MemWal:
773
781
  payload_items: List[Dict[str, Any]] = [
774
782
  {
775
783
  "text": item.text,
776
- "namespace": item.namespace or self._namespace,
784
+ "namespace": self._resolve_namespace(item.namespace),
777
785
  }
778
786
  for item in items
779
787
  ]
@@ -1052,7 +1060,7 @@ class MemWal:
1052
1060
  body = {
1053
1061
  "query": query_text,
1054
1062
  "limit": limit,
1055
- "namespace": namespace or self._namespace,
1063
+ "namespace": self._resolve_namespace(namespace),
1056
1064
  }
1057
1065
  if scoring_weights is not None:
1058
1066
  body["scoring_weights"] = scoring_weights.to_wire()
@@ -1127,7 +1135,7 @@ class MemWal:
1127
1135
  _reject_blank_text(text)
1128
1136
  body: Dict[str, Any] = {
1129
1137
  "text": text,
1130
- "namespace": namespace or self._namespace,
1138
+ "namespace": self._resolve_namespace(namespace),
1131
1139
  }
1132
1140
  wire_occurred_at = _occurred_at_to_wire(occurred_at)
1133
1141
  if wire_occurred_at is not None:
@@ -1228,7 +1236,7 @@ class MemWal:
1228
1236
  data = await self._signed_request("POST", "/api/ask", {
1229
1237
  "question": question,
1230
1238
  "limit": limit,
1231
- "namespace": namespace or self._namespace,
1239
+ "namespace": self._resolve_namespace(namespace),
1232
1240
  })
1233
1241
  memories = [
1234
1242
  AskMemory(blob_id=m["blob_id"], text=m["text"], distance=m["distance"])
@@ -1426,7 +1434,7 @@ class MemWal:
1426
1434
  {
1427
1435
  "encrypted_data": opts.encrypted_data,
1428
1436
  "vector": opts.vector,
1429
- "namespace": opts.namespace or self._namespace,
1437
+ "namespace": self._resolve_namespace(opts.namespace),
1430
1438
  },
1431
1439
  include_seal_session=False,
1432
1440
  )
@@ -1452,7 +1460,7 @@ class MemWal:
1452
1460
  body: Dict[str, Any] = {
1453
1461
  "vector": opts.vector,
1454
1462
  "limit": opts.limit,
1455
- "namespace": opts.namespace or self._namespace,
1463
+ "namespace": self._resolve_namespace(opts.namespace),
1456
1464
  }
1457
1465
  if opts.scoring_weights is not None:
1458
1466
  body["scoring_weights"] = opts.scoring_weights.to_wire()
@@ -283,7 +283,9 @@ class MemWalMock:
283
283
  raise ValueError("limit must be a non-negative integer")
284
284
  if sort not in (None, "relevance", "recent"):
285
285
  raise ValueError("sort must be 'relevance' or 'recent'")
286
- resolved_namespace = namespace or self._namespace
286
+ resolved_namespace = self._namespace if namespace is None else namespace
287
+ if not resolved_namespace:
288
+ raise ValueError("namespace cannot be empty")
287
289
  query_tokens = _tokens(query_text)
288
290
  ranked = [
289
291
  (memory, _distance(query_tokens, memory.text))
@@ -509,7 +511,7 @@ class MemWalMock:
509
511
  blob_id: Optional[str] = None,
510
512
  ) -> _Memory:
511
513
  _validate_text(text)
512
- resolved_namespace = namespace or self._namespace
514
+ resolved_namespace = self._namespace if namespace is None else namespace
513
515
  if not resolved_namespace:
514
516
  raise ValueError("namespace cannot be empty")
515
517
  self._sequence += 1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "memwal"
7
- version = "0.2.0"
7
+ version = "0.2.0.dev0"
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,144 @@
1
+ """#1012: an explicit empty namespace must not be rewritten to the default.
2
+
3
+ The client used ``namespace or self._namespace``, so ``namespace=""`` landed
4
+ in the default partition, while the TypeScript SDK sends "" and the relayer
5
+ rejects it. Only ``None`` means "use the client default"; "" fails before
6
+ anything is signed, like the mock's empty-namespace check. Whitespace-only
7
+ names are not rejected: the relayer's validate_namespace only refuses "", a
8
+ NUL byte and over-long names, and keeps tab/newline/CR namespaces readable.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import pytest
14
+
15
+ from memwal import (
16
+ MemWal,
17
+ RecallManualOptions,
18
+ RecallParams,
19
+ RememberBulkItem,
20
+ RememberManualOptions,
21
+ )
22
+ from memwal.mock import MemWalMock
23
+
24
+
25
+ class CaptureMemWal(MemWal):
26
+ def __init__(self, *args, **kwargs):
27
+ super().__init__(*args, **kwargs)
28
+ self.captured = []
29
+
30
+ async def _ensure_compatible_relayer(self):
31
+ return {
32
+ "apiVersion": "1.0.0",
33
+ "relayerVersion": "capture",
34
+ "minSupportedSdk": {"python": "0.0.0"},
35
+ }
36
+
37
+ async def _signed_request(
38
+ self,
39
+ method,
40
+ path,
41
+ body,
42
+ accepted_statuses=(200,),
43
+ include_seal_session=True,
44
+ timeout=None,
45
+ ):
46
+ self.captured.append((method, path, body))
47
+ if path == "/api/remember":
48
+ return {"job_id": "job-1", "status": "pending"}
49
+ if path == "/api/remember/bulk":
50
+ return {"job_ids": ["job"] * len(body["items"]), "total": len(body["items"])}
51
+ if path == "/api/remember/manual":
52
+ return {"id": "1", "blob_id": "b", "owner": "0x1", "namespace": body["namespace"]}
53
+ if path == "/api/analyze":
54
+ return {"facts": [], "total": 0, "owner": "0x1"}
55
+ if path == "/api/ask":
56
+ return {"answer": "", "memories_used": 0, "memories": []}
57
+ return {"results": [], "total": 0}
58
+
59
+
60
+ def _client():
61
+ return CaptureMemWal.create(
62
+ key="11" * 32,
63
+ account_id="0x" + "ab" * 32,
64
+ server_url="https://relayer.example",
65
+ namespace="work",
66
+ )
67
+
68
+
69
+ def _sent_namespaces(client):
70
+ sent = []
71
+ for _, _, body in client.captured:
72
+ sent.extend(item["namespace"] for item in body.get("items", []))
73
+ if "namespace" in body:
74
+ sent.append(body["namespace"])
75
+ return sent
76
+
77
+
78
+ CALLS = [
79
+ ("remember", lambda c, ns: c.remember("x", namespace=ns)),
80
+ ("remember_async", lambda c, ns: c.remember_async("x", namespace=ns)),
81
+ ("remember_bulk_async", lambda c, ns: c.remember_bulk_async([RememberBulkItem("x", ns)])),
82
+ ("recall", lambda c, ns: c.recall("q", namespace=ns)),
83
+ ("recall_params", lambda c, ns: c.recall(RecallParams(query="q", namespace=ns))),
84
+ ("analyze", lambda c, ns: c.analyze("x", namespace=ns)),
85
+ ("ask", lambda c, ns: c.ask("q", namespace=ns)),
86
+ (
87
+ "remember_manual",
88
+ lambda c, ns: c.remember_manual(
89
+ RememberManualOptions(encrypted_data="AAAA", vector=[0.1], namespace=ns)
90
+ ),
91
+ ),
92
+ (
93
+ "recall_manual",
94
+ lambda c, ns: c.recall_manual(RecallManualOptions(vector=[0.1], namespace=ns)),
95
+ ),
96
+ ]
97
+
98
+
99
+ @pytest.mark.parametrize("call", CALLS, ids=lambda x: x[0])
100
+ async def test_empty_namespace_is_rejected_not_defaulted(call):
101
+ c = _client()
102
+ with pytest.raises(ValueError, match="namespace cannot be empty"):
103
+ await call[1](c, "")
104
+ assert c.captured == []
105
+
106
+
107
+ @pytest.mark.parametrize("namespace", [" ", " ", "\t", "\n", "\r"], ids=repr)
108
+ @pytest.mark.parametrize("call", CALLS, ids=lambda x: x[0])
109
+ async def test_whitespace_namespace_is_sent_as_is(call, namespace):
110
+ """Whitespace names are valid for the relayer and the TypeScript SDK."""
111
+ c = _client()
112
+ await call[1](c, namespace)
113
+ assert _sent_namespaces(c) == [namespace]
114
+
115
+
116
+ @pytest.mark.parametrize(
117
+ "name", ["recall", "recall_params", "analyze", "ask", "remember_manual", "recall_manual"]
118
+ )
119
+ async def test_none_uses_client_default_and_explicit_name_is_kept(name):
120
+ call = dict(CALLS)[name]
121
+ c = _client()
122
+ await call(c, None)
123
+ await call(c, "profile")
124
+ assert [b["namespace"] for _, _, b in c.captured] == ["work", "profile"]
125
+
126
+
127
+ async def test_mock_rejects_empty_namespace_on_recall_too():
128
+ mock = MemWalMock(namespace="work")
129
+ with pytest.raises(ValueError, match="namespace cannot be empty"):
130
+ await mock.recall("q", namespace="")
131
+
132
+
133
+ @pytest.mark.parametrize("namespace", [" ", "\t", "\n", "\r"], ids=repr)
134
+ async def test_mock_keeps_whitespace_namespaces_separate(namespace):
135
+ mock = MemWalMock(namespace="work")
136
+ await mock.remember("blank name fact", namespace=namespace)
137
+ assert len((await mock.recall("blank name fact", namespace=namespace)).results) == 1
138
+ assert (await mock.recall("blank name fact")).results == []
139
+
140
+
141
+ async def test_mock_rejects_empty_namespace_on_remember():
142
+ mock = MemWalMock(namespace="work")
143
+ with pytest.raises(ValueError, match="namespace cannot be empty"):
144
+ await mock.remember("x", namespace="")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes