| """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_STYLE = { |
| Severity.SAFE: "green", |
| Severity.REVIEW: "yellow", |
| Severity.UNSAFE: "bold red", |
| } |
|
|
| |
| _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_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 |
|
|
| |
| sorted_issues = sorted( |
| issues, |
| key=lambda i: (_SEVERITY_ORDER[i.severity], -i.confidence), |
| ) |
|
|
| |
| 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) |
|
|
| |
| 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", |
| ) |
| ) |
|
|