Coverage for src / sentry_tool / output.py: 89.87%
79 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-17 21:46 -0500
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-17 21:46 -0500
1"""Generic output formatting for data display."""
3import json
4from dataclasses import dataclass
5from enum import Enum
6from typing import Any
8from rich.console import Console
9from rich.table import Table
12class OutputFormat(str, Enum):
13 table = "table"
14 json = "json"
17@dataclass
18class Column:
19 header: str
20 key: str
21 style: str | None = None
22 justify: str | None = None
23 max_width: int | None = None
26def render(
27 data: list[dict[str, Any]],
28 format: OutputFormat,
29 columns: list[Column] | None = None,
30 footer: str | None = None,
31) -> None:
32 """Render list data as JSON or Rich table.
34 For JSON: prints indented JSON to stdout.
35 For table: builds a Rich table using column specs for styling (max_width, justify, style).
36 The footer (e.g. "Showing 5 issues") is only printed in table mode.
37 """
38 if format == OutputFormat.json:
39 print(json.dumps(data, indent=2))
40 return
42 console = Console()
44 if not data:
45 return
47 if columns is None:
48 columns = [Column(header=key, key=key) for key in data[0]]
50 table = Table(show_header=True, header_style="bold")
51 for col in columns:
52 kwargs: dict[str, Any] = {}
53 if col.style is not None:
54 kwargs["style"] = col.style
55 if col.justify is not None:
56 kwargs["justify"] = col.justify
57 if col.max_width is not None:
58 kwargs["max_width"] = col.max_width
59 table.add_column(col.header, **kwargs)
61 for row in data:
62 values = [str(row.get(col.key, "")) for col in columns]
63 table.add_row(*values)
65 console.print(table)
66 if footer:
67 console.print(f"\n{footer}")
70# Event-specific formatting helpers (used by `events event` command for Rich detail view)
73def print_event_context(console: Console, ctx: dict[str, Any]) -> None:
74 console.print("\n[bold]Context:[/bold]")
75 if "caller" in ctx:
76 console.print(f" [dim]Caller:[/dim] {ctx['caller']}")
77 if "stack" in ctx:
78 console.print(" [dim]Stack:[/dim]")
79 for line in ctx["stack"].split("\n"):
80 console.print(f" {line}")
83def print_exception_entry(console: Console, data: dict[str, Any]) -> None:
84 console.print("\n[bold]Exception:[/bold]")
85 for exc in data.get("values", []):
86 exc_type = exc.get("type", "Exception")
87 exc_value = exc.get("value", "")
88 console.print(f" [red]{exc_type}[/red]: {exc_value}")
90 stacktrace = exc.get("stacktrace") or {}
91 frames = stacktrace.get("frames", []) if stacktrace else []
92 if frames:
93 console.print(" [dim]Stacktrace:[/dim]")
94 for frame in frames[-5:]:
95 filename = frame.get("filename", "")
96 lineno = frame.get("lineNo", "")
97 function = frame.get("function", "")
98 console.print(f" {filename}:{lineno} in {function}")
101def render_event_basic_info(console: Console, event: dict[str, Any], short_id: str) -> None:
102 console.print(f"\n[bold cyan]=== Latest Event for {short_id} ===[/bold cyan]")
104 rows: list[dict[str, str]] = [
105 {"field": "Event ID", "value": event.get("eventID", "N/A")},
106 {"field": "Title", "value": event.get("title", "N/A")},
107 {"field": "Message", "value": event.get("message", "N/A")},
108 {"field": "Date", "value": event.get("dateCreated", "N/A")},
109 ]
111 for tag in event.get("tags", []):
112 if tag.get("key") == "server_name":
113 rows.append({"field": "Server", "value": tag.get("value", "N/A")})
114 break
116 sdk = event.get("sdk", {})
117 if sdk:
118 rows.append({"field": "SDK", "value": f"{sdk.get('name', '')} {sdk.get('version', '')}"})
120 release = event.get("release", {})
121 if release:
122 rows.append({"field": "Release", "value": release.get("version", "N/A")})
124 columns = [
125 Column("Field", "field", style="bold"),
126 Column("Value", "value"),
127 ]
128 render(rows, OutputFormat.table, columns=columns)