Coverage for src / sentry_tool / output.py: 79.75%

79 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-02-28 19:20 -0500

1"""Generic output formatting for data display.""" 

2 

3import json 

4from dataclasses import dataclass 

5from enum import Enum 

6from typing import Any 

7 

8from rich.console import Console 

9from rich.table import Table 

10 

11 

12class OutputFormat(str, Enum): 

13 table = "table" 

14 json = "json" 

15 

16 

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 

24 

25 

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. 

33 

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 

41 

42 console = Console() 

43 

44 if not data: 

45 return 

46 

47 if columns is None: 

48 columns = [Column(header=key, key=key) for key in data[0]] 

49 

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) 

60 

61 for row in data: 

62 values = [str(row.get(col.key, "")) for col in columns] 

63 table.add_row(*values) 

64 

65 console.print(table) 

66 if footer: 

67 console.print(f"\n{footer}") 

68 

69 

70# Event-specific formatting helpers (used by `events event` command for Rich detail view) 

71 

72 

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}") 

81 

82 

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}") 

89 

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}") 

99 

100 

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]") 

103 

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 ] 

110 

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 

115 

116 sdk = event.get("sdk", {}) 

117 if sdk: 

118 rows.append({"field": "SDK", "value": f"{sdk.get('name', '')} {sdk.get('version', '')}"}) 

119 

120 release = event.get("release", {}) 

121 if release: 

122 rows.append({"field": "Release", "value": release.get("version", "N/A")}) 

123 

124 columns = [ 

125 Column("Field", "field", style="bold"), 

126 Column("Value", "value"), 

127 ] 

128 render(rows, OutputFormat.table, columns=columns)