|
| 1 | +"""Small, dependency-free text-formatting helpers for the fleet reporter. |
| 2 | +
|
| 3 | +Adapted from lafleur's observability layer (``lafleur/utils.py``, ``lafleur/report.py``) -- |
| 4 | +copied rather than imported so fusil takes no dependency on lafleur. Pure stdlib. |
| 5 | +""" |
| 6 | + |
| 7 | +from __future__ import annotations |
| 8 | + |
| 9 | +import os |
| 10 | + |
| 11 | + |
| 12 | +def human_bytes(n: float | None) -> str: |
| 13 | + """1536 -> '1.5K', 1234567890 -> '1.1G'. None/negative -> '-'.""" |
| 14 | + if n is None or n < 0: |
| 15 | + return "-" |
| 16 | + n = float(n) |
| 17 | + for unit in ("B", "K", "M", "G", "T", "P"): |
| 18 | + if n < 1024 or unit == "P": |
| 19 | + if unit == "B": |
| 20 | + return "%dB" % int(n) |
| 21 | + return "%.1f%s" % (n, unit) |
| 22 | + n /= 1024 |
| 23 | + return "%.1fP" % n |
| 24 | + |
| 25 | + |
| 26 | +def format_duration(seconds: float | None) -> str: |
| 27 | + """90061 -> '1d 1h', 305 -> '5m 5s', None -> '-'. Two most-significant units.""" |
| 28 | + if seconds is None or seconds < 0: |
| 29 | + return "-" |
| 30 | + seconds = int(seconds) |
| 31 | + days, rem = divmod(seconds, 86400) |
| 32 | + hours, rem = divmod(rem, 3600) |
| 33 | + minutes, secs = divmod(rem, 60) |
| 34 | + parts = [] |
| 35 | + if days: |
| 36 | + parts += ["%dd" % days, "%dh" % hours] |
| 37 | + elif hours: |
| 38 | + parts += ["%dh" % hours, "%dm" % minutes] |
| 39 | + elif minutes: |
| 40 | + parts += ["%dm" % minutes, "%ds" % secs] |
| 41 | + else: |
| 42 | + parts = ["%ds" % secs] |
| 43 | + return " ".join(parts[:2]) |
| 44 | + |
| 45 | + |
| 46 | +def format_rate(count: int, seconds: float | None) -> str: |
| 47 | + """Per-minute rate, e.g. 3092 sessions over 2h -> '25.8/m'. '-' if no elapsed time.""" |
| 48 | + if not seconds or seconds <= 0: |
| 49 | + return "-" |
| 50 | + return "%.1f/m" % (count / (seconds / 60.0)) |
| 51 | + |
| 52 | + |
| 53 | +def dir_size_bytes(path: str) -> int: |
| 54 | + """Total size of a directory tree in bytes (best-effort; unreadable entries skipped).""" |
| 55 | + total = 0 |
| 56 | + for root, _dirs, files in os.walk(path, onerror=lambda _e: None): |
| 57 | + for name in files: |
| 58 | + try: |
| 59 | + total += os.lstat(os.path.join(root, name)).st_size |
| 60 | + except OSError: |
| 61 | + pass |
| 62 | + return total |
0 commit comments