from datetime import timedelta from pathlib import Path from typing import Any from jinja2 import Environment, FileSystemLoader, select_autoescape BASE = Path(__file__).resolve().parent.parent def humanize(value: float | str | None) -> str: """Format a byte count as a human-readable string (e.g. "1.2 GiB"). Args: value: number of bytes (a numeric string is accepted too). Returns: e.g. "512 B", "1.2 GiB", or "—" when value is None. """ if value is None: return "—" n = float(value) for unit in ("B", "KiB", "MiB", "GiB", "TiB"): if abs(n) < 1024 or unit == "TiB": if unit == "B": return f"{int(n)} B" return f"{n:.1f} {unit}" n /= 1024 return f"{n:.1f} TiB" def rate(value: float | str | None) -> str: """Format a bytes-per-second rate as a human-readable string (e.g. "3.4 MiB/s"). Args: value: transfer rate in bytes/s (a numeric string is accepted too). Returns: e.g. "128 B/s", "3.4 MiB/s", or "—" when value is None. """ if value is None: return "—" n = float(value) for unit in ("B/s", "KiB/s", "MiB/s", "GiB/s"): if abs(n) < 1024 or unit == "GiB/s": if unit == "B/s": return f"{int(n)} B/s" return f"{n:.1f} {unit}" n /= 1024 return f"{n:.1f} GiB/s" def uptime_str(seconds: float | None) -> str: """Format a duration in seconds as a compact string (e.g. "3d 4h 12m"). Args: seconds: duration in seconds. Returns: Compact duration, or "—" when seconds is None. """ if seconds is None: return "—" td = timedelta(seconds=int(seconds)) days, rem = divmod(td.seconds, 86400) hours, rem = divmod(rem, 3600) minutes = rem // 60 parts: list[str] = [] if days: parts.append(f"{days}d") if days or hours: parts.append(f"{hours}h") parts.append(f"{minutes}m") return " ".join(parts) def pct(value: float | None) -> str: """Format a 0-100 percentage rounded to a whole number (e.g. "42%"). Args: value: percentage value. Returns: Rounded percentage string, or "—" when value is None. """ if value is None: return "—" return f"{value:.0f}%" env = Environment( loader=FileSystemLoader(BASE / "templates"), autoescape=select_autoescape(("html", "j2")), ) env.filters["humanize"] = humanize env.filters["rate"] = rate env.filters["uptime"] = uptime_str env.filters["pct"] = pct def render(name: str, **kwargs: Any) -> str: """Render a Jinja template from `templates/` with the shared environment. The environment has HTML autoescape on and the `humanize`, `rate`, `uptime`, and `pct` filters registered. Args: name: template path relative to `templates/`, e.g. "overview.html". **kwargs: template context variables. Returns: The rendered HTML as a string. """ return env.get_template(name).render(**kwargs)