open-aea-cli-ipfs 2.2.4__tar.gz → 2.2.6__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.
- {open_aea_cli_ipfs-2.2.4/open_aea_cli_ipfs.egg-info → open_aea_cli_ipfs-2.2.6}/PKG-INFO +1 -1
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/aea_cli_ipfs/ipfs_client.py +57 -4
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6/open_aea_cli_ipfs.egg-info}/PKG-INFO +1 -1
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/setup.py +1 -1
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/tests/test_ipfs_client.py +58 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/LICENSE +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/MANIFEST.in +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/README.md +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/aea_cli_ipfs/__init__.py +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/aea_cli_ipfs/core.py +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/aea_cli_ipfs/exceptions.py +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/aea_cli_ipfs/ipfs_utils.py +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/aea_cli_ipfs/py.typed +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/aea_cli_ipfs/registry.py +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/aea_cli_ipfs/test_tools/__init__.py +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/aea_cli_ipfs/test_tools/fixture_helpers.py +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/open_aea_cli_ipfs.egg-info/SOURCES.txt +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/open_aea_cli_ipfs.egg-info/dependency_links.txt +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/open_aea_cli_ipfs.egg-info/entry_points.txt +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/open_aea_cli_ipfs.egg-info/requires.txt +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/open_aea_cli_ipfs.egg-info/top_level.txt +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/pyproject.toml +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/setup.cfg +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/tests/__init__.py +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/tests/test_aea_cli_ipfs.py +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/tests/test_core.py +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/tests/test_registry.py +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/tests/test_test_tools.py +0 -0
- {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/tests/test_utils.py +0 -0
|
@@ -70,6 +70,59 @@ class ErrorResponse(StatusError):
|
|
|
70
70
|
"""IPFS daemon returned a JSON error message."""
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# Response parsing
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
_SAFE_PREVIEW_LIMIT = 80
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _safe_preview(body: Union[bytes, str]) -> str:
|
|
82
|
+
"""Render a short, printable preview of a response body for diagnostic messages.
|
|
83
|
+
|
|
84
|
+
Bytes are decoded with ``errors="replace"``, non-printable characters are
|
|
85
|
+
replaced with ``.`` (to prevent terminal-control or NUL bytes from
|
|
86
|
+
landing in logs) and the result is truncated to keep error messages and
|
|
87
|
+
logs scan-friendly.
|
|
88
|
+
|
|
89
|
+
:param body: the raw body bytes or text from the daemon.
|
|
90
|
+
:return: a short printable preview string suitable for inclusion in
|
|
91
|
+
``StatusError`` messages and logs.
|
|
92
|
+
"""
|
|
93
|
+
text = body.decode("utf-8", errors="replace") if isinstance(body, bytes) else body
|
|
94
|
+
safe = "".join(c if c.isprintable() else "." for c in text)
|
|
95
|
+
if len(safe) > _SAFE_PREVIEW_LIMIT:
|
|
96
|
+
safe = safe[:_SAFE_PREVIEW_LIMIT] + "..."
|
|
97
|
+
return safe
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _safe_json_loads(body: Union[bytes, str], context: str) -> Any:
|
|
101
|
+
"""Parse a successful IPFS response body as JSON, raising StatusError on failure.
|
|
102
|
+
|
|
103
|
+
The 200-status error-body branches already wrap ``json.loads`` in a
|
|
104
|
+
``ValueError``/``KeyError`` guard. This helper extends that guard to the
|
|
105
|
+
success path so a 200 with a non-JSON body (CDN/proxy HTML error page,
|
|
106
|
+
truncated response, gzipped payload) surfaces as a typed ``StatusError``
|
|
107
|
+
rather than an unhandled ``ValueError`` in the caller. The body preview
|
|
108
|
+
embedded in the exception message is truncated and printable-only so
|
|
109
|
+
that arbitrary remote bytes cannot inject terminal-control sequences or
|
|
110
|
+
fill logs.
|
|
111
|
+
|
|
112
|
+
:param body: the response body bytes or text from the IPFS daemon.
|
|
113
|
+
:param context: short label describing the call site for diagnostic messages.
|
|
114
|
+
:return: the decoded JSON payload.
|
|
115
|
+
:raises StatusError: if ``body`` is not valid JSON.
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
return json.loads(body)
|
|
119
|
+
except ValueError as exc:
|
|
120
|
+
raise StatusError(
|
|
121
|
+
f"IPFS API returned 200 but body is not JSON ({context}): "
|
|
122
|
+
f"{_safe_preview(body)!r}"
|
|
123
|
+
) from exc
|
|
124
|
+
|
|
125
|
+
|
|
73
126
|
# ---------------------------------------------------------------------------
|
|
74
127
|
# Multipart encoding for the IPFS /api/v0/add endpoint
|
|
75
128
|
# ---------------------------------------------------------------------------
|
|
@@ -387,10 +440,10 @@ class IPFSHTTPClient:
|
|
|
387
440
|
for line in text.strip().split("\n"):
|
|
388
441
|
line = line.strip()
|
|
389
442
|
if line:
|
|
390
|
-
results.append(
|
|
443
|
+
results.append(_safe_json_loads(line, f"{endpoint} stream"))
|
|
391
444
|
return results
|
|
392
445
|
|
|
393
|
-
return
|
|
446
|
+
return _safe_json_loads(body, endpoint)
|
|
394
447
|
|
|
395
448
|
def id(self, peer: Optional[str] = None) -> Dict:
|
|
396
449
|
"""Get node identity info."""
|
|
@@ -449,7 +502,7 @@ class IPFSHTTPClient:
|
|
|
449
502
|
for line in text.strip().split("\n"):
|
|
450
503
|
line = line.strip()
|
|
451
504
|
if line:
|
|
452
|
-
results.append(
|
|
505
|
+
results.append(_safe_json_loads(line, "/add stream"))
|
|
453
506
|
|
|
454
507
|
# Match ipfshttpclient behavior: single-item result returns a dict,
|
|
455
508
|
# multi-item result returns a list
|
|
@@ -480,7 +533,7 @@ class IPFSHTTPClient:
|
|
|
480
533
|
except (ValueError, KeyError):
|
|
481
534
|
pass
|
|
482
535
|
raise StatusError(f"IPFS API returned status {status}: {text}")
|
|
483
|
-
result =
|
|
536
|
+
result = _safe_json_loads(resp_body, "/add")
|
|
484
537
|
if "Hash" not in result:
|
|
485
538
|
raise StatusError(f"IPFS API response missing 'Hash' key: {result}")
|
|
486
539
|
return result["Hash"]
|
|
@@ -330,6 +330,64 @@ class TestAddBytesErrorHandling:
|
|
|
330
330
|
client.add_bytes(b"data")
|
|
331
331
|
|
|
332
332
|
|
|
333
|
+
class TestSuccessPathJSONSafety:
|
|
334
|
+
"""Tests for typed errors on non-JSON 200 responses.
|
|
335
|
+
|
|
336
|
+
The success-path ``json.loads`` calls in ``_api_post``, ``add``, and
|
|
337
|
+
``add_bytes`` route through ``_safe_json_loads`` so a 200 carrying a
|
|
338
|
+
non-JSON body raises a typed ``StatusError`` instead of an unhandled
|
|
339
|
+
``ValueError`` in the caller.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
@patch("aea_cli_ipfs.ipfs_client.urllib.request.urlopen")
|
|
343
|
+
def test_add_bytes_non_json_200_raises_status_error(
|
|
344
|
+
self, mock_urlopen: MagicMock
|
|
345
|
+
) -> None:
|
|
346
|
+
"""Test add_bytes raises StatusError when the 200 body is not JSON."""
|
|
347
|
+
mock_urlopen.return_value = _mock_urlopen(200, b"<html>Bad Gateway</html>")
|
|
348
|
+
client = IPFSHTTPClient("/ip4/127.0.0.1/tcp/5001")
|
|
349
|
+
with pytest.raises(StatusError, match="not JSON"):
|
|
350
|
+
client.add_bytes(b"data")
|
|
351
|
+
|
|
352
|
+
@patch("aea_cli_ipfs.ipfs_client.urllib.request.urlopen")
|
|
353
|
+
def test_add_stream_malformed_line_raises_status_error(
|
|
354
|
+
self, mock_urlopen: MagicMock
|
|
355
|
+
) -> None:
|
|
356
|
+
"""Test add raises StatusError when a stream line is not JSON."""
|
|
357
|
+
ndjson = '{"Name":"a.txt","Hash":"Qm1"}\nnot json at all\n'
|
|
358
|
+
mock_urlopen.return_value = _mock_urlopen(200, ndjson.encode())
|
|
359
|
+
|
|
360
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
361
|
+
root = Path(tmp) / "mydir"
|
|
362
|
+
root.mkdir()
|
|
363
|
+
(root / "a.txt").write_text("aaa")
|
|
364
|
+
|
|
365
|
+
client = IPFSHTTPClient("/ip4/127.0.0.1/tcp/5001")
|
|
366
|
+
with pytest.raises(StatusError, match="not JSON"):
|
|
367
|
+
client.add(str(root))
|
|
368
|
+
|
|
369
|
+
@patch("aea_cli_ipfs.ipfs_client.urllib.request.urlopen")
|
|
370
|
+
def test_api_post_non_json_200_raises_status_error(
|
|
371
|
+
self, mock_urlopen: MagicMock
|
|
372
|
+
) -> None:
|
|
373
|
+
"""Test _api_post (non-stream) raises StatusError when the 200 body is not JSON."""
|
|
374
|
+
mock_urlopen.return_value = _mock_urlopen(200, b"not json")
|
|
375
|
+
client = IPFSHTTPClient("/ip4/127.0.0.1/tcp/5001")
|
|
376
|
+
with pytest.raises(StatusError, match="not JSON"):
|
|
377
|
+
client.id()
|
|
378
|
+
|
|
379
|
+
@patch("aea_cli_ipfs.ipfs_client.urllib.request.urlopen")
|
|
380
|
+
def test_api_post_stream_malformed_line_raises_status_error(
|
|
381
|
+
self, mock_urlopen: MagicMock
|
|
382
|
+
) -> None:
|
|
383
|
+
"""Test _api_post (stream) raises StatusError when a stream line is not JSON."""
|
|
384
|
+
ndjson = '{"Key":"Qm1"}\n<html>oops</html>\n'
|
|
385
|
+
mock_urlopen.return_value = _mock_urlopen(200, ndjson.encode())
|
|
386
|
+
client = IPFSHTTPClient("/ip4/127.0.0.1/tcp/5001")
|
|
387
|
+
with pytest.raises(StatusError, match="not JSON"):
|
|
388
|
+
client.repo.gc()
|
|
389
|
+
|
|
390
|
+
|
|
333
391
|
class TestTarSafety:
|
|
334
392
|
"""Tests for tar extraction safety."""
|
|
335
393
|
|
|
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
|
{open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/aea_cli_ipfs/test_tools/fixture_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/open_aea_cli_ipfs.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/open_aea_cli_ipfs.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/open_aea_cli_ipfs.egg-info/top_level.txt
RENAMED
|
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
|