memwal 0.1.11.dev3__tar.gz → 0.1.11.dev5__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.1.11.dev3 → memwal-0.1.11.dev5}/CHANGELOG.md +8 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/PKG-INFO +1 -1
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/memwal/__init__.py +3 -1
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/memwal/client.py +113 -26
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/pyproject.toml +1 -1
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/tests/test_auth_rejected_message.py +1 -0
- memwal-0.1.11.dev5/tests/test_blank_text.py +22 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/tests/test_client.py +96 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/tests/test_polling_delay.py +2 -1
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/.gitignore +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/README.md +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/examples/.env.example +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/examples/.gitignore +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/examples/async_remember_demo.py +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/examples/interactive_demo.py +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/examples/verify_credentials.py +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/memwal/compatibility.py +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/memwal/middleware.py +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/memwal/mock.py +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/memwal/types.py +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/memwal/utils.py +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/notebooks/walrus_memory_python_sdk.ipynb +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/run_tests.py +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/tests/__init__.py +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/tests/test_env_presets.py +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/tests/test_integration.py +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/tests/test_list_namespaces.py +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/tests/test_middleware.py +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/tests/test_mock.py +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/tests/test_normalize_server_url.py +0 -0
- {memwal-0.1.11.dev3 → memwal-0.1.11.dev5}/tests/test_signing.py +0 -0
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# memwal
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- `remember`, `remember_bulk`, `analyze`, and `embed` reject whitespace-only text before the request. A blank body used to be accepted and then sit until the caller timed out. (#1129)
|
|
8
|
+
- A 401 `AUTH_REJECTED` now says a wallet address is not a MemWal account ID. (#1132)
|
|
9
|
+
- `wait_for_remember_jobs` / `remember_bulk_and_wait` no longer return a batch they could not read as if it were still uploading. When every status poll is rate-limited, the wait makes one confirming read and raises `MemWalRateLimited` (`status` 429, `job_ids`, `retry_after`) if that is refused too. An item still unsettled at the deadline names its last known status, or says no read got through. (WALM-671, #967)
|
|
10
|
+
|
|
3
11
|
## 0.1.12
|
|
4
12
|
|
|
5
13
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: memwal
|
|
3
|
-
Version: 0.1.11.
|
|
3
|
+
Version: 0.1.11.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
|
|
@@ -27,6 +27,7 @@ from .client import (
|
|
|
27
27
|
MemWalClockDriftError,
|
|
28
28
|
MemWalCompatibilityError,
|
|
29
29
|
MemWalError,
|
|
30
|
+
MemWalRateLimited,
|
|
30
31
|
MemWalRememberJobFailed,
|
|
31
32
|
MemWalRememberJobNotFound,
|
|
32
33
|
MemWalRememberJobTimeout,
|
|
@@ -82,6 +83,7 @@ __all__ = [
|
|
|
82
83
|
"MemWalError",
|
|
83
84
|
"MemWalCompatibilityError",
|
|
84
85
|
"MemWalClockDriftError",
|
|
86
|
+
"MemWalRateLimited",
|
|
85
87
|
"MemWalRememberJobFailed",
|
|
86
88
|
"MemWalRememberJobNotFound",
|
|
87
89
|
"MemWalRememberJobTimeout",
|
|
@@ -126,4 +128,4 @@ __all__ = [
|
|
|
126
128
|
"RecallManualResult",
|
|
127
129
|
]
|
|
128
130
|
|
|
129
|
-
__version__ = "0.1.11.
|
|
131
|
+
__version__ = "0.1.11.dev5"
|
|
@@ -96,7 +96,8 @@ PACKAGE_VERSION_CHECK_TIMEOUT_S = 10.0
|
|
|
96
96
|
SEAL_SESSION_SAFETY_MARGIN_MS = 30_000
|
|
97
97
|
AUTH_REJECTED_MESSAGE = (
|
|
98
98
|
"401 from relayer: typically wrong private key, key not registered on this "
|
|
99
|
-
"account, account ID mismatch
|
|
99
|
+
"account, account ID mismatch (a wallet address is not a MemWal account ID), "
|
|
100
|
+
"or staging/mainnet mismatch. Check .env.local "
|
|
100
101
|
"and dashboard credentials. Full troubleshooting: "
|
|
101
102
|
"https://docs.wal.app/walrus-memory/troubleshooting/overview#401-auth_rejected-errors"
|
|
102
103
|
)
|
|
@@ -121,6 +122,18 @@ def _server_url_for_log(parsed: ParseResult) -> str:
|
|
|
121
122
|
return f"{parsed.scheme}://{host}"
|
|
122
123
|
|
|
123
124
|
|
|
125
|
+
def _reject_blank_text(text: object, message: str = "Text cannot be empty") -> None:
|
|
126
|
+
"""Reject empty and whitespace-only text before a request is sent.
|
|
127
|
+
|
|
128
|
+
The relayer does the same (#1129). Failing here means an older relayer
|
|
129
|
+
cannot accept the job and leave the caller polling until timeout.
|
|
130
|
+
Non-blank text is not trimmed.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
if not isinstance(text, str) or not text.strip():
|
|
134
|
+
raise ValueError(message)
|
|
135
|
+
|
|
136
|
+
|
|
124
137
|
def normalize_server_url(url: str) -> str:
|
|
125
138
|
"""Strip a trailing slash and warn on plaintext HTTP to a remote host.
|
|
126
139
|
|
|
@@ -414,6 +427,7 @@ class MemWal:
|
|
|
414
427
|
:class:`RememberAcceptedResult` with ``job_id`` and initial
|
|
415
428
|
status (``"pending"``).
|
|
416
429
|
"""
|
|
430
|
+
_reject_blank_text(text)
|
|
417
431
|
resolved_namespace = namespace or self._namespace
|
|
418
432
|
request_identity = f"{resolved_namespace}\0{text}"
|
|
419
433
|
generated_key = idempotency_key is None
|
|
@@ -569,6 +583,8 @@ class MemWal:
|
|
|
569
583
|
|
|
570
584
|
if not items:
|
|
571
585
|
raise ValueError("remember_bulk_async: items must be a non-empty array")
|
|
586
|
+
for index, item in enumerate(items):
|
|
587
|
+
_reject_blank_text(item.text, f"items[{index}].text cannot be empty")
|
|
572
588
|
|
|
573
589
|
payload_items: List[Dict[str, Any]] = [
|
|
574
590
|
{
|
|
@@ -662,6 +678,31 @@ class MemWal:
|
|
|
662
678
|
attempt = 0
|
|
663
679
|
saw_rate_limit = False
|
|
664
680
|
retry_after_ms = 0
|
|
681
|
+
last_seen: Dict[str, RememberBulkStatusItem] = {}
|
|
682
|
+
last_refusal: Optional[int] = None
|
|
683
|
+
# Whether any status read got through. An item missing from an answer
|
|
684
|
+
# was not refused, so it must not be reported as if it had been.
|
|
685
|
+
answered = False
|
|
686
|
+
|
|
687
|
+
def settle(item: RememberBulkStatusItem) -> bool:
|
|
688
|
+
last_seen[item.job_id] = item
|
|
689
|
+
if item.status == "done":
|
|
690
|
+
results[item.job_id] = RememberBulkItemResult(
|
|
691
|
+
id=item.job_id,
|
|
692
|
+
blob_id=item.blob_id or "",
|
|
693
|
+
status="done",
|
|
694
|
+
error=None,
|
|
695
|
+
)
|
|
696
|
+
return True
|
|
697
|
+
if item.status in ("failed", "not_found"):
|
|
698
|
+
results[item.job_id] = RememberBulkItemResult(
|
|
699
|
+
id=item.job_id,
|
|
700
|
+
blob_id=item.blob_id or "",
|
|
701
|
+
status="failed",
|
|
702
|
+
error=item.error,
|
|
703
|
+
)
|
|
704
|
+
return True
|
|
705
|
+
return False
|
|
665
706
|
|
|
666
707
|
while pending and _now_ms() < deadline_ms:
|
|
667
708
|
if retry_after_ms > 0:
|
|
@@ -675,40 +716,66 @@ class MemWal:
|
|
|
675
716
|
batch = await self.get_remember_bulk_status(pending)
|
|
676
717
|
except _HttpStatusError as err:
|
|
677
718
|
if _is_transient_polling_status(err.status):
|
|
719
|
+
last_refusal = err.status
|
|
678
720
|
if err.status == 429:
|
|
679
721
|
saw_rate_limit = True
|
|
680
722
|
retry_after_ms = _clamped_retry_after_ms(err, deadline_ms)
|
|
681
723
|
continue
|
|
682
724
|
raise
|
|
725
|
+
answered = True
|
|
683
726
|
|
|
684
|
-
terminal_ids: Set[str] =
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
blob_id=item.blob_id or "",
|
|
690
|
-
status="done",
|
|
691
|
-
error=None,
|
|
692
|
-
)
|
|
693
|
-
terminal_ids.add(item.job_id)
|
|
694
|
-
elif item.status in ("failed", "not_found"):
|
|
695
|
-
results[item.job_id] = RememberBulkItemResult(
|
|
696
|
-
id=item.job_id,
|
|
697
|
-
blob_id=item.blob_id or "",
|
|
698
|
-
status="failed",
|
|
699
|
-
error=item.error,
|
|
700
|
-
)
|
|
701
|
-
terminal_ids.add(item.job_id)
|
|
727
|
+
terminal_ids: Set[str] = {
|
|
728
|
+
item.job_id
|
|
729
|
+
for item in batch.results
|
|
730
|
+
if item.job_id in pending and settle(item)
|
|
731
|
+
}
|
|
702
732
|
pending = [jid for jid in pending if jid not in terminal_ids]
|
|
703
733
|
|
|
734
|
+
if opts.timeout_ms > 0 and len(pending) == len(job_ids) and not last_seen:
|
|
735
|
+
# Nothing settled and nothing was seen: the polls may all have been
|
|
736
|
+
# refused. Confirm with one direct read before reporting anything.
|
|
737
|
+
try:
|
|
738
|
+
probed = await self.get_remember_bulk_status(list(job_ids))
|
|
739
|
+
except _HttpStatusError as err:
|
|
740
|
+
if err.status == 429:
|
|
741
|
+
# Header first, else the body's retry_after_seconds, so a
|
|
742
|
+
# proxy that strips Retry-After does not lose the hint.
|
|
743
|
+
wait_ms = _retry_after_ms(err)
|
|
744
|
+
retry_after = f"{wait_ms / 1000:g}" if wait_ms else None
|
|
745
|
+
raise MemWalRateLimited(job_ids, retry_after) from err
|
|
746
|
+
# Any other probe failure is not evidence about the jobs.
|
|
747
|
+
else:
|
|
748
|
+
answered = True
|
|
749
|
+
settled = {
|
|
750
|
+
item.job_id
|
|
751
|
+
for item in probed.results
|
|
752
|
+
if item.job_id in pending and settle(item)
|
|
753
|
+
}
|
|
754
|
+
pending = [jid for jid in pending if jid not in settled]
|
|
755
|
+
|
|
756
|
+
for job_id in pending:
|
|
757
|
+
seen = last_seen.get(job_id)
|
|
758
|
+
if seen is not None:
|
|
759
|
+
suffix = f": {_redact_internal_urls(seen.error)}" if seen.error else ""
|
|
760
|
+
error = f"still {seen.status} after {opts.timeout_ms}ms{suffix}"
|
|
761
|
+
elif answered:
|
|
762
|
+
error = "not in the relayer's status answer; this item may not be stored"
|
|
763
|
+
elif last_refusal is not None:
|
|
764
|
+
error = (
|
|
765
|
+
f"no status read got through (last poll: HTTP {last_refusal}); "
|
|
766
|
+
"this item may not be stored"
|
|
767
|
+
)
|
|
768
|
+
else:
|
|
769
|
+
error = f"polling timed out after {opts.timeout_ms}ms"
|
|
770
|
+
# Only the "no status read got through" message already names the 429.
|
|
771
|
+
names_rate_limit = seen is None and not answered and last_refusal == 429
|
|
772
|
+
if saw_rate_limit and not names_rate_limit:
|
|
773
|
+
error += "; wait hit a rate limit (429)"
|
|
774
|
+
results[job_id] = RememberBulkItemResult(
|
|
775
|
+
id=job_id, blob_id="", status="timeout", error=error
|
|
776
|
+
)
|
|
777
|
+
|
|
704
778
|
ordered = [results[job_id] for job_id in job_ids]
|
|
705
|
-
if saw_rate_limit:
|
|
706
|
-
for item in ordered:
|
|
707
|
-
if item.status == "timeout":
|
|
708
|
-
item.error = (
|
|
709
|
-
f"polling timed out after {opts.timeout_ms}ms; "
|
|
710
|
-
"wait hit a rate limit (429)"
|
|
711
|
-
)
|
|
712
779
|
succeeded = sum(1 for r in ordered if r.status == "done")
|
|
713
780
|
failed = sum(1 for r in ordered if r.status == "failed")
|
|
714
781
|
timed_out = sum(1 for r in ordered if r.status == "timeout")
|
|
@@ -847,6 +914,7 @@ class MemWal:
|
|
|
847
914
|
:class:`AnalyzeResult` with extracted ``facts`` + per-fact
|
|
848
915
|
``job_ids`` for downstream polling.
|
|
849
916
|
"""
|
|
917
|
+
_reject_blank_text(text)
|
|
850
918
|
body: Dict[str, Any] = {
|
|
851
919
|
"text": text,
|
|
852
920
|
"namespace": namespace or self._namespace,
|
|
@@ -919,6 +987,7 @@ class MemWal:
|
|
|
919
987
|
:meth:`remember` (server handles embed + encrypt + upload).
|
|
920
988
|
"""
|
|
921
989
|
|
|
990
|
+
_reject_blank_text(text, "text cannot be empty")
|
|
922
991
|
data = await self._signed_request(
|
|
923
992
|
"POST",
|
|
924
993
|
"/api/embed",
|
|
@@ -1601,6 +1670,24 @@ class MemWalRememberJobFailed(MemWalError):
|
|
|
1601
1670
|
self.error = error
|
|
1602
1671
|
|
|
1603
1672
|
|
|
1673
|
+
class MemWalRateLimited(MemWalError):
|
|
1674
|
+
"""Every status read for a bulk wait was rate-limited, the confirming read
|
|
1675
|
+
included, so none of its writes could be confirmed (GH #967). Status reads
|
|
1676
|
+
count against the same delegate-key budget as writes."""
|
|
1677
|
+
|
|
1678
|
+
def __init__(self, job_ids: Sequence[str], retry_after: Optional[str] = None) -> None:
|
|
1679
|
+
wait = f" after ~{retry_after}s" if retry_after else ""
|
|
1680
|
+
super().__init__(
|
|
1681
|
+
f"Walrus Memory rate-limited every status read for this batch, so none of its "
|
|
1682
|
+
f"{len(job_ids)} writes could be confirmed. Do not treat them as stored: check them "
|
|
1683
|
+
f"with get_remember_bulk_status(job_ids){wait}. Status reads count against the same "
|
|
1684
|
+
"delegate-key budget as writes."
|
|
1685
|
+
)
|
|
1686
|
+
self.status = 429
|
|
1687
|
+
self.job_ids = list(job_ids)
|
|
1688
|
+
self.retry_after = retry_after
|
|
1689
|
+
|
|
1690
|
+
|
|
1604
1691
|
class MemWalRememberJobTimeout(MemWalError):
|
|
1605
1692
|
"""Polling loop exceeded the configured timeout."""
|
|
1606
1693
|
|
|
@@ -12,6 +12,7 @@ from memwal.client import (
|
|
|
12
12
|
|
|
13
13
|
def test_auth_rejected_message_points_to_troubleshooting_guide() -> None:
|
|
14
14
|
assert "docs.wal.app/walrus-memory/troubleshooting/overview" in AUTH_REJECTED_MESSAGE
|
|
15
|
+
assert "wallet address" in AUTH_REJECTED_MESSAGE
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def test_auth_503_is_retryable_not_a_credential_failure() -> None:
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Whitespace-only text is rejected before a request is sent (#1129)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from memwal.client import _reject_blank_text
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.parametrize("text", ["", " ", "\t", "\n", " \t\n "])
|
|
11
|
+
def test_blank_text_rejected(text: str) -> None:
|
|
12
|
+
with pytest.raises(ValueError, match="Text cannot be empty"):
|
|
13
|
+
_reject_blank_text(text)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_padded_text_is_not_trimmed() -> None:
|
|
17
|
+
_reject_blank_text(" hello ")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_bulk_item_message_names_the_index() -> None:
|
|
21
|
+
with pytest.raises(ValueError, match=r"items\[0\].text cannot be empty"):
|
|
22
|
+
_reject_blank_text(" \t", "items[0].text cannot be empty")
|
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import base64
|
|
11
11
|
import json
|
|
12
|
+
import time
|
|
12
13
|
from datetime import datetime, timedelta, timezone
|
|
13
14
|
from typing import Any, Sequence
|
|
14
15
|
|
|
@@ -22,7 +23,9 @@ from memwal.client import (
|
|
|
22
23
|
MemWalClockDriftError,
|
|
23
24
|
MemWalCompatibilityError,
|
|
24
25
|
MemWalError,
|
|
26
|
+
MemWalRateLimited,
|
|
25
27
|
MemWalSync,
|
|
28
|
+
_HttpStatusError,
|
|
26
29
|
)
|
|
27
30
|
from memwal.types import (
|
|
28
31
|
RecallManualOptions,
|
|
@@ -462,6 +465,99 @@ class TestRememberBulkAsync:
|
|
|
462
465
|
assert [item.status for item in result.results] == ["done", "done"]
|
|
463
466
|
|
|
464
467
|
|
|
468
|
+
class TestBulkWaitUnderRateLimit:
|
|
469
|
+
async def test_every_read_refused_raises_rate_limited(
|
|
470
|
+
self, memwal_client: MemWal
|
|
471
|
+
) -> None:
|
|
472
|
+
ids = [f"job-{i}" for i in range(12)]
|
|
473
|
+
|
|
474
|
+
async def refused(job_ids: Sequence[str]) -> RememberBulkStatusResult:
|
|
475
|
+
raise _HttpStatusError(429, '{"error":"rate limited"}', retry_after="30")
|
|
476
|
+
|
|
477
|
+
memwal_client.get_remember_bulk_status = refused # type: ignore[method-assign]
|
|
478
|
+
with pytest.raises(MemWalRateLimited) as exc:
|
|
479
|
+
await memwal_client.wait_for_remember_jobs(
|
|
480
|
+
ids, RememberBulkOptions(poll_interval_ms=1, timeout_ms=300)
|
|
481
|
+
)
|
|
482
|
+
assert exc.value.status == 429
|
|
483
|
+
assert exc.value.job_ids == ids
|
|
484
|
+
assert exc.value.retry_after == "30"
|
|
485
|
+
assert "none of its 12 writes could be confirmed" in str(exc.value)
|
|
486
|
+
|
|
487
|
+
async def test_rate_limited_reads_the_body_when_the_header_is_stripped(
|
|
488
|
+
self, memwal_client: MemWal
|
|
489
|
+
) -> None:
|
|
490
|
+
async def refused(job_ids: Sequence[str]) -> RememberBulkStatusResult:
|
|
491
|
+
raise _HttpStatusError(429, '{"retry_after_seconds":45}')
|
|
492
|
+
|
|
493
|
+
memwal_client.get_remember_bulk_status = refused # type: ignore[method-assign]
|
|
494
|
+
with pytest.raises(MemWalRateLimited) as exc:
|
|
495
|
+
await memwal_client.wait_for_remember_jobs(
|
|
496
|
+
["job-1"], RememberBulkOptions(poll_interval_ms=1, timeout_ms=300)
|
|
497
|
+
)
|
|
498
|
+
assert exc.value.retry_after == "45"
|
|
499
|
+
assert "after ~45s" in str(exc.value)
|
|
500
|
+
|
|
501
|
+
async def test_confirming_read_settles_a_refused_wait(
|
|
502
|
+
self, memwal_client: MemWal
|
|
503
|
+
) -> None:
|
|
504
|
+
start = time.monotonic()
|
|
505
|
+
|
|
506
|
+
async def probe_answers(job_ids: Sequence[str]) -> RememberBulkStatusResult:
|
|
507
|
+
# Refused for the whole 250ms wait; the read after the deadline is
|
|
508
|
+
# the probe. Time-based because jitter varies the poll count.
|
|
509
|
+
if time.monotonic() - start < 0.25:
|
|
510
|
+
raise _HttpStatusError(429, "{}")
|
|
511
|
+
return RememberBulkStatusResult(
|
|
512
|
+
results=[
|
|
513
|
+
RememberBulkStatusItem(job_id="job-a", status="done", blob_id="blob-a"),
|
|
514
|
+
RememberBulkStatusItem(
|
|
515
|
+
job_id="job-b", status="failed", error="walrus upload failed"
|
|
516
|
+
),
|
|
517
|
+
]
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
memwal_client.get_remember_bulk_status = probe_answers # type: ignore[method-assign]
|
|
521
|
+
out = await memwal_client.wait_for_remember_jobs(
|
|
522
|
+
["job-a", "job-b", "job-c"],
|
|
523
|
+
RememberBulkOptions(poll_interval_ms=1, timeout_ms=250),
|
|
524
|
+
)
|
|
525
|
+
assert [r.status for r in out.results] == ["done", "failed", "timeout"]
|
|
526
|
+
# A read did get through; it just left job-c out, so saying no read
|
|
527
|
+
# got through would be false.
|
|
528
|
+
assert "not in the relayer's status answer" in (out.results[2].error or "")
|
|
529
|
+
|
|
530
|
+
async def test_partly_settled_batch_is_returned(self, memwal_client: MemWal) -> None:
|
|
531
|
+
reads = 0
|
|
532
|
+
|
|
533
|
+
async def then_refused(job_ids: Sequence[str]) -> RememberBulkStatusResult:
|
|
534
|
+
nonlocal reads
|
|
535
|
+
reads += 1
|
|
536
|
+
if reads == 1:
|
|
537
|
+
return RememberBulkStatusResult(
|
|
538
|
+
results=[
|
|
539
|
+
RememberBulkStatusItem(
|
|
540
|
+
job_id="job-a", status="done", blob_id="blob-a"
|
|
541
|
+
),
|
|
542
|
+
RememberBulkStatusItem(
|
|
543
|
+
job_id="job-b",
|
|
544
|
+
status="running",
|
|
545
|
+
error="still retrying this upload",
|
|
546
|
+
),
|
|
547
|
+
]
|
|
548
|
+
)
|
|
549
|
+
raise _HttpStatusError(429, "{}")
|
|
550
|
+
|
|
551
|
+
memwal_client.get_remember_bulk_status = then_refused # type: ignore[method-assign]
|
|
552
|
+
out = await memwal_client.wait_for_remember_jobs(
|
|
553
|
+
["job-a", "job-b"], RememberBulkOptions(poll_interval_ms=1, timeout_ms=300)
|
|
554
|
+
)
|
|
555
|
+
assert [r.status for r in out.results] == ["done", "timeout"]
|
|
556
|
+
assert "still running after 300ms: still retrying this upload" in (
|
|
557
|
+
out.results[1].error or ""
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
|
|
465
561
|
# ============================================================
|
|
466
562
|
# recall() tests
|
|
467
563
|
# ============================================================
|
|
@@ -139,5 +139,6 @@ async def test_bulk_timeout_names_429_and_keeps_unreturned_jobs_pending(
|
|
|
139
139
|
assert settled.results[0].status == "done"
|
|
140
140
|
assert settled.results[1].status == "timeout"
|
|
141
141
|
assert settled.results[1].error == (
|
|
142
|
-
"
|
|
142
|
+
"not in the relayer's status answer; this item may not be stored; "
|
|
143
|
+
"wait hit a rate limit (429)"
|
|
143
144
|
)
|
|
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
|