mirror of
https://github.com/jmagar/unraid-mcp.git
synced 2026-03-02 08:14:43 -08:00
Addresses issues found by 4 parallel review agents (code-reviewer,
silent-failure-hunter, type-design-analyzer, pr-test-analyzer).
Source fixes:
- core/utils.py: add public safe_display_url() (moved from tools/health.py)
- core/client.py: rename _redact_sensitive → redact_sensitive (public API)
- core/types.py: add SubscriptionData.__post_init__ for tz-aware datetime
enforcement; remove 6 unused type aliases (SystemHealth, APIResponse, etc.)
- subscriptions/manager.py: add exc_info=True to both except-Exception blocks;
add except ValueError break-on-config-error before retry loop; import
redact_sensitive by new public name
- subscriptions/resources.py: re-raise in autostart_subscriptions() so
ensure_subscriptions_started() doesn't permanently set _subscriptions_started
- subscriptions/diagnostics.py: except ToolError: raise before broad except;
use safe_display_url() instead of raw URL slice
- tools/health.py: move _safe_display_url to core/utils; add exc_info=True;
raise ToolError (not return dict) on ImportError
- tools/info.py: use get_args(INFO_ACTIONS) instead of INFO_ACTIONS.__args__
- tools/{array,docker,keys,notifications,rclone,storage,virtualization}.py:
add Literal-vs-ALL_ACTIONS sync check at import time
Test fixes:
- test_health.py: import safe_display_url from core.utils; update
test_diagnose_import_error_internal to expect ToolError (not error dict)
- test_storage.py: add 3 safe_get tests for zero/False/empty-string values
- test_subscription_manager.py: add TestCapLogContentSingleMassiveLine (2 tests)
- test_client.py: rename _redact_sensitive → redact_sensitive; add tests for
new sensitive keys and is_cacheable explicit-keyword form
90 lines
2.6 KiB
Python
90 lines
2.6 KiB
Python
"""Shared utility functions for Unraid MCP tools."""
|
|
|
|
from typing import Any
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
def safe_get(data: dict[str, Any], *keys: str, default: Any = None) -> Any:
|
|
"""Safely traverse nested dict keys, handling None intermediates.
|
|
|
|
Args:
|
|
data: The root dictionary to traverse.
|
|
*keys: Sequence of keys to follow.
|
|
default: Value to return if any key is missing or None.
|
|
|
|
Returns:
|
|
The value at the end of the key chain, or default if unreachable.
|
|
Explicit ``None`` values at the final key also return ``default``.
|
|
"""
|
|
current = data
|
|
for key in keys:
|
|
if not isinstance(current, dict):
|
|
return default
|
|
current = current.get(key)
|
|
return current if current is not None else default
|
|
|
|
|
|
def format_bytes(bytes_value: int | None) -> str:
|
|
"""Format byte values into human-readable sizes.
|
|
|
|
Args:
|
|
bytes_value: Number of bytes, or None.
|
|
|
|
Returns:
|
|
Human-readable string like "1.00 GB" or "N/A" if input is None/invalid.
|
|
"""
|
|
if bytes_value is None:
|
|
return "N/A"
|
|
try:
|
|
value = float(int(bytes_value))
|
|
except (ValueError, TypeError):
|
|
return "N/A"
|
|
for unit in ["B", "KB", "MB", "GB", "TB", "PB"]:
|
|
if value < 1024.0:
|
|
return f"{value:.2f} {unit}"
|
|
value /= 1024.0
|
|
return f"{value:.2f} EB"
|
|
|
|
|
|
def safe_display_url(url: str | None) -> str | None:
|
|
"""Return a redacted URL showing only scheme + host + port.
|
|
|
|
Strips path, query parameters, credentials, and fragments to avoid
|
|
leaking internal network topology or embedded secrets (CWE-200).
|
|
"""
|
|
if not url:
|
|
return None
|
|
try:
|
|
parsed = urlparse(url)
|
|
host = parsed.hostname or "unknown"
|
|
if parsed.port:
|
|
return f"{parsed.scheme}://{host}:{parsed.port}"
|
|
return f"{parsed.scheme}://{host}"
|
|
except ValueError:
|
|
# urlparse raises ValueError for invalid URLs (e.g. contains control chars)
|
|
return "<unparseable>"
|
|
|
|
|
|
def format_kb(k: Any) -> str:
|
|
"""Format kilobyte values into human-readable sizes.
|
|
|
|
Args:
|
|
k: Number of kilobytes, or None.
|
|
|
|
Returns:
|
|
Human-readable string like "1.00 GB" or "N/A" if input is None/invalid.
|
|
"""
|
|
if k is None:
|
|
return "N/A"
|
|
try:
|
|
k = int(k)
|
|
except (ValueError, TypeError):
|
|
return "N/A"
|
|
if k >= 1024 * 1024 * 1024:
|
|
return f"{k / (1024 * 1024 * 1024):.2f} TB"
|
|
if k >= 1024 * 1024:
|
|
return f"{k / (1024 * 1024):.2f} GB"
|
|
if k >= 1024:
|
|
return f"{k / 1024:.2f} MB"
|
|
return f"{k:.2f} KB"
|