Coverage for src / sql_tool / cli / helpers.py: 100%
81 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-14 15:28 -0500
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-14 15:28 -0500
1"""Shared CLI formatting helpers."""
3from __future__ import annotations
5import re
8def normalize_pg_interval(pg_interval: str | None) -> str:
9 """Convert PostgreSQL interval strings to human-readable form.
11 Examples: "01:00:00" → "1 hour", "00:10:00" → "10 minutes",
12 "24:00:00" → "1 day", "1 mon" → "1 month".
13 Already-readable strings like "7 days" pass through unchanged.
14 """
15 if not pg_interval:
16 return "-"
18 val = pg_interval.strip()
20 # Handle HH:MM:SS format
21 match = re.match(r"^(\d+):(\d{2}):(\d{2})$", val)
22 if match:
23 hours, minutes, seconds = (
24 int(match.group(1)),
25 int(match.group(2)),
26 int(match.group(3)),
27 )
28 total_seconds = hours * 3600 + minutes * 60 + seconds
29 if total_seconds == 0:
30 return "0 seconds"
31 parts = []
32 days, remainder = divmod(total_seconds, 86400)
33 if days:
34 parts.append(f"{days} day{'s' if days != 1 else ''}")
35 hrs, remainder = divmod(remainder, 3600)
36 if hrs:
37 parts.append(f"{hrs} hour{'s' if hrs != 1 else ''}")
38 mins, secs = divmod(remainder, 60)
39 if mins:
40 parts.append(f"{mins} minute{'s' if mins != 1 else ''}")
41 if secs:
42 parts.append(f"{secs} second{'s' if secs != 1 else ''}")
43 return " ".join(parts)
45 # Normalize "mon" → "month"
46 val = re.sub(r"\bmon\b", "month", val)
47 val = re.sub(r"\bmons\b", "months", val)
49 return val
52def format_duration_human(seconds: float | None) -> str:
53 if seconds is None:
54 return ""
56 if seconds < 60:
57 return f"{int(seconds)}s"
58 elif seconds < 3600:
59 minutes = int(seconds / 60)
60 return f"{minutes}m"
61 elif seconds < 86400:
62 hours = int(seconds / 3600)
63 return f"{hours}h"
64 else:
65 days = int(seconds / 86400)
66 return f"{days}d"
69def format_relative_time(seconds: float | None) -> str:
70 if seconds is None:
71 return ""
72 return f"{format_duration_human(seconds)} ago"
75def fmt_size(b: int | None) -> str:
76 """Format bytes as human-readable size for table output."""
77 if not b:
78 return "-"
79 units = [("TB", 1 << 40), ("GB", 1 << 30), ("MB", 1 << 20), ("KB", 1 << 10)]
80 for suffix, threshold in units:
81 if b >= threshold:
82 value = b / threshold
83 return f"{value:.0f} {suffix}" if value >= 10 else f"{value:.1f} {suffix}"
84 return f"{b}B"
87def format_size_compact(size_bytes: int | None) -> str:
88 """Format bytes as compact human-readable: 2KB, 300M, 1.0GB, 3TB."""
89 if not size_bytes:
90 return "0B"
91 units = [("TB", 1 << 40), ("GB", 1 << 30), ("M", 1 << 20), ("KB", 1 << 10)]
92 for suffix, threshold in units:
93 if size_bytes >= threshold:
94 value = size_bytes / threshold
95 if value >= 10:
96 return f"{value:.0f}{suffix}"
97 return f"{value:.1f}{suffix}"
98 return f"{size_bytes}B"
101def format_size_gb(size_bytes: int | None) -> str:
102 """Format bytes with adaptive units for CSV/JSON output."""
103 if not size_bytes:
104 return "0 bytes"
105 units = [("TB", 1 << 40), ("GB", 1 << 30), ("MB", 1 << 20), ("KB", 1 << 10)]
106 for suffix, threshold in units:
107 if size_bytes >= threshold:
108 return f"{size_bytes / threshold:.2f} {suffix}"
109 return f"{size_bytes} bytes"
112def format_timestamp(ts: str | None) -> str:
113 """Strip microseconds and timezone from PostgreSQL timestamps."""
114 if not ts or ts == "-infinity" or ts == "infinity":
115 return "-"
116 # Strip microseconds and timezone for readability
117 if "." in ts:
118 ts = ts.split(".")[0]
119 elif "+" in ts:
120 ts = ts.split("+")[0]
121 return ts