File size: 4,199 Bytes
5143557 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | """Rich-based terminal rendering for the ``dataforge profile`` command.
This module contains **rendering only** — no business logic, no data loading,
no detector invocation. It receives a list of :class:`Issue` objects and
renders them as a Rich table with color-coded severity.
"""
from __future__ import annotations
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
from dataforge.detectors.base import Issue, Severity
# Severity-to-color mapping for the Rich table.
_SEVERITY_STYLE = {
Severity.SAFE: "green",
Severity.REVIEW: "yellow",
Severity.UNSAFE: "bold red",
}
# Severity sort key: UNSAFE first, then REVIEW, then SAFE.
_SEVERITY_ORDER = {Severity.UNSAFE: 0, Severity.REVIEW: 1, Severity.SAFE: 2}
def render_profile_table(
issues: list[Issue],
console: Console | None = None,
*,
file_path: str = "",
) -> None:
"""Render detected issues as a rich-formatted terminal table.
The table is sorted by severity (UNSAFE first) then confidence
(highest first). Each severity level is color-coded.
Args:
issues: List of Issue objects to display.
console: Optional Rich Console instance (for testing / capture).
If None, a new Console is created.
file_path: Optional file path to display in the header panel.
Example:
>>> from dataforge.detectors.base import Issue, Severity
>>> issues = [
... Issue(row=3, column="price", issue_type="decimal_shift",
... severity=Severity.REVIEW, confidence=0.92,
... actual="1020.0", expected="102.0",
... reason="Value appears 10x too large"),
... ]
>>> render_profile_table(issues) # doctest: +SKIP
"""
if console is None:
console = Console()
# Header panel.
header_text = "DataForge Profile Report"
if file_path:
header_text += f" | {file_path}"
console.print(
Panel(
header_text,
style="bold cyan",
expand=True,
)
)
if not issues:
console.print(
Panel(
"[green]No issues detected.[/green]",
title="Result",
style="green",
)
)
return
# Sort: UNSAFE first, then REVIEW, then SAFE; highest confidence first.
sorted_issues = sorted(
issues,
key=lambda i: (_SEVERITY_ORDER[i.severity], -i.confidence),
)
# Build the table.
table = Table(
title="Detected Issues",
show_lines=True,
header_style="bold magenta",
title_style="bold white",
)
table.add_column("Row", style="dim", justify="right", width=5)
table.add_column("Column", style="cyan", min_width=12)
table.add_column("Issue Type", style="white", min_width=14)
table.add_column("Severity", justify="center", min_width=8)
table.add_column("Confidence", justify="right", min_width=10)
table.add_column("Reason", min_width=30)
for issue in sorted_issues:
severity_style = _SEVERITY_STYLE[issue.severity]
severity_text = Text(issue.severity.value.upper(), style=severity_style)
confidence_pct = f"{issue.confidence:.0%}"
table.add_row(
str(issue.row),
issue.column,
issue.issue_type,
severity_text,
confidence_pct,
issue.reason,
)
console.print(table)
# Summary panel.
total = len(sorted_issues)
by_severity: dict[Severity, int] = {}
for issue in sorted_issues:
by_severity[issue.severity] = by_severity.get(issue.severity, 0) + 1
summary_parts: list[str] = [f"[bold]{total}[/bold] issues found"]
for sev in (Severity.UNSAFE, Severity.REVIEW, Severity.SAFE):
count = by_severity.get(sev, 0)
if count > 0:
style = _SEVERITY_STYLE[sev]
summary_parts.append(f"[{style}]{sev.value.upper()}: {count}[/{style}]")
console.print(
Panel(
" | ".join(summary_parts),
title="Summary",
style="dim",
)
)
|