dataforge-playground / dataforge /ui /profile_view.py
Praneshrajan15's picture
feat: initial playground deployment
5143557 verified
"""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",
)
)