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.
Files changed (29) hide show
  1. {open_aea_cli_ipfs-2.2.4/open_aea_cli_ipfs.egg-info → open_aea_cli_ipfs-2.2.6}/PKG-INFO +1 -1
  2. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/aea_cli_ipfs/ipfs_client.py +57 -4
  3. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6/open_aea_cli_ipfs.egg-info}/PKG-INFO +1 -1
  4. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/setup.py +1 -1
  5. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/tests/test_ipfs_client.py +58 -0
  6. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/LICENSE +0 -0
  7. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/MANIFEST.in +0 -0
  8. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/README.md +0 -0
  9. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/aea_cli_ipfs/__init__.py +0 -0
  10. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/aea_cli_ipfs/core.py +0 -0
  11. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/aea_cli_ipfs/exceptions.py +0 -0
  12. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/aea_cli_ipfs/ipfs_utils.py +0 -0
  13. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/aea_cli_ipfs/py.typed +0 -0
  14. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/aea_cli_ipfs/registry.py +0 -0
  15. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/aea_cli_ipfs/test_tools/__init__.py +0 -0
  16. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/aea_cli_ipfs/test_tools/fixture_helpers.py +0 -0
  17. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/open_aea_cli_ipfs.egg-info/SOURCES.txt +0 -0
  18. {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
  19. {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
  20. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/open_aea_cli_ipfs.egg-info/requires.txt +0 -0
  21. {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
  22. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/pyproject.toml +0 -0
  23. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/setup.cfg +0 -0
  24. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/tests/__init__.py +0 -0
  25. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/tests/test_aea_cli_ipfs.py +0 -0
  26. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/tests/test_core.py +0 -0
  27. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/tests/test_registry.py +0 -0
  28. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/tests/test_test_tools.py +0 -0
  29. {open_aea_cli_ipfs-2.2.4 → open_aea_cli_ipfs-2.2.6}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: open-aea-cli-ipfs
3
- Version: 2.2.4
3
+ Version: 2.2.6
4
4
  Summary: CLI extension for open AEA framework wrapping IPFS functionality.
5
5
  Author: Valory AG
6
6
  License: Apache-2.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(json.loads(line))
443
+ results.append(_safe_json_loads(line, f"{endpoint} stream"))
391
444
  return results
392
445
 
393
- return json.loads(body)
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(json.loads(line))
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 = json.loads(resp_body)
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"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: open-aea-cli-ipfs
3
- Version: 2.2.4
3
+ Version: 2.2.6
4
4
  Summary: CLI extension for open AEA framework wrapping IPFS functionality.
5
5
  Author: Valory AG
6
6
  License: Apache-2.0
@@ -34,7 +34,7 @@ def _read_long_description() -> str:
34
34
 
35
35
  setup(
36
36
  name="open-aea-cli-ipfs",
37
- version="2.2.4",
37
+ version="2.2.6",
38
38
  author="Valory AG",
39
39
  license="Apache-2.0",
40
40
  description="CLI extension for open AEA framework wrapping IPFS functionality.",
@@ -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