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.
- {memwal-0.2.0 → memwal-0.2.0.dev0}/PKG-INFO +1 -1
- {memwal-0.2.0 → memwal-0.2.0.dev0}/memwal/__init__.py +1 -1
- {memwal-0.2.0 → memwal-0.2.0.dev0}/memwal/client.py +16 -8
- {memwal-0.2.0 → memwal-0.2.0.dev0}/memwal/mock.py +4 -2
- {memwal-0.2.0 → memwal-0.2.0.dev0}/pyproject.toml +1 -1
- memwal-0.2.0.dev0/tests/test_empty_namespace.py +144 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/.gitignore +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/CHANGELOG.md +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/README.md +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/examples/.env.example +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/examples/.gitignore +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/examples/async_remember_demo.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/examples/interactive_demo.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/examples/verify_credentials.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/memwal/compatibility.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/memwal/middleware.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/memwal/types.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/memwal/utils.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/notebooks/walrus_memory_python_sdk.ipynb +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/run_tests.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/__init__.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_auth_rejected_message.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_blank_text.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_client.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_env_presets.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_integration.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_list_namespaces.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_middleware.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_mock.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_normalize_server_url.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_polling_delay.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_recall_weights.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_remember_derived_key.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_retry_after_http_date.py +0 -0
- {memwal-0.2.0 → memwal-0.2.0.dev0}/tests/test_signing.py +0 -0
- {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
|
|
@@ -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 =
|
|
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 =
|
|
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
|
|
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":
|
|
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":
|
|
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":
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|