Coverage for src / rendercv / cli / render_command / progress_panel.py: 97%

67 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-25 13:49 +0000

1import pathlib 

2from dataclasses import dataclass 

3 

4import rich.box 

5import rich.console 

6import rich.live 

7import rich.panel 

8import rich.table 

9import typer 

10 

11from rendercv.exception import RenderCVUserError, RenderCVValidationError 

12 

13 

14def format_validation_error_location(error_object: RenderCVValidationError) -> str: 

15 """Format schema/YAML location for validation error table rows. 

16 

17 Why: 

18 YAML parsing errors don't have schema locations, so we show source file 

19 and line/column coordinates to keep the location column actionable. 

20 

21 Args: 

22 error_object: Validation error with schema and YAML location metadata. 

23 

24 Returns: 

25 Human-readable location string for table display. 

26 """ 

27 if error_object.schema_location is not None: 

28 return ".".join(error_object.schema_location) 

29 

30 if error_object.yaml_location is None: 

31 return error_object.yaml_source 

32 

33 (start_line, _), (end_line, _) = error_object.yaml_location 

34 if start_line == end_line: 

35 return f"{error_object.yaml_source}: line {start_line}" 

36 return f"{error_object.yaml_source}: line {start_line} to line {end_line}" 

37 

38 

39class ProgressPanel(rich.live.Live): 

40 """Live-updating terminal panel showing CV generation progress with timing. 

41 

42 Example: 

43 ```py 

44 with ProgressPanel(quiet=False) as progress: 

45 progress.update_progress("50", "Generated PDF", [Path("cv.pdf")]) 

46 progress.finish_progress() 

47 # Displays: ✓ 50 ms Generated PDF: ./cv.pdf 

48 ``` 

49 

50 Args: 

51 quiet: Suppress all terminal output. 

52 """ 

53 

54 def __init__(self, quiet: bool = False): 

55 self.completed_steps: list[CompletedStep] = [] 

56 super().__init__( 

57 rich.panel.Panel( 

58 "...", 

59 title="Rendering your CV...", 

60 title_align="left", 

61 border_style="bright_black", 

62 ), 

63 console=rich.console.Console(quiet=quiet), 

64 refresh_per_second=4, 

65 ) 

66 

67 def update_progress( 

68 self, time_took: str, message: str, paths: list[pathlib.Path] 

69 ) -> None: 

70 """Add completed step to progress display. 

71 

72 Args: 

73 time_took: Execution time in milliseconds as string. 

74 message: Step description. 

75 paths: Generated file paths to display. 

76 """ 

77 self.completed_steps.append(CompletedStep(time_took, message, paths)) 

78 self.print_progress_panel(title="Rendering your CV...") 

79 

80 def finish_progress(self) -> None: 

81 """Display final success panel and clear state.""" 

82 self.print_progress_panel(title="Your CV is ready") 

83 self.completed_steps.clear() 

84 

85 def print_progress_panel(self, title: str) -> None: 

86 """Render progress panel with all completed steps. 

87 

88 Args: 

89 title: Panel title text. 

90 """ 

91 lines: list[str] = [] 

92 for step in self.completed_steps: 

93 paths_str = "" 

94 if step.paths: 

95 try: 

96 paths = [ 

97 path.relative_to(pathlib.Path.cwd()) for path in step.paths 

98 ] 

99 except ValueError: 

100 paths = step.paths 

101 paths_as_strings = [f"./{path}" for path in paths] 

102 paths_str = "; ".join(paths_as_strings) 

103 

104 timing = f"[bold green]{step.timing_ms + ' ms':<8}[/bold green]" 

105 message = step.message + (": " if paths_str else ".") 

106 paths_display = f"[purple]{paths_str}[/purple]" if paths_str else "" 

107 lines.append(f"[green]✓[/green] {timing} {message:<26} {paths_display}") 

108 

109 content = "\n".join(lines) if lines else "Rendering..." 

110 

111 self.update( 

112 rich.panel.Panel( 

113 content, 

114 title=title, 

115 title_align="left", 

116 border_style="bright_black", 

117 ) 

118 ) 

119 

120 def print_user_error(self, user_error: RenderCVUserError) -> None: 

121 """Display error panel and exit with error code. 

122 

123 Args: 

124 user_error: User-facing error to display. 

125 """ 

126 self.clear() 

127 self.update( 

128 rich.panel.Panel( 

129 user_error.message or "An unknown error occurred.", 

130 title="[bold red]Error[/bold red]", 

131 title_align="left", 

132 border_style="bold red", 

133 ) 

134 ) 

135 raise typer.Exit(code=1) 

136 

137 def print_validation_errors(self, errors: list[RenderCVValidationError]) -> None: 

138 """Display validation errors in table format and exit. 

139 

140 Why: 

141 Pydantic validation errors are parsed into user-friendly messages with 

142 YAML locations. Table shows exactly which field failed and why. 

143 

144 Args: 

145 errors: List of validation errors with location, input, and message. 

146 """ 

147 self.completed_steps.clear() 

148 table = rich.table.Table(expand=True, show_lines=True, box=rich.box.ROUNDED) 

149 table.add_column("Location", style="cyan", no_wrap=True) 

150 table.add_column("Input Value", style="magenta", no_wrap=True) 

151 table.add_column("Explanation", style="orange4") 

152 

153 for error_object in errors: 

154 table.add_row( 

155 format_validation_error_location(error_object), 

156 error_object.input, 

157 error_object.message, 

158 ) 

159 

160 self.update( 

161 rich.panel.Panel( 

162 table, 

163 title="[bold red]There are validation errors![/bold red]", 

164 title_align="left", 

165 border_style="bold red", 

166 ) 

167 ) 

168 

169 raise typer.Exit(code=1) 

170 

171 def clear(self) -> None: 

172 """Clear all completed steps and panel display.""" 

173 self.completed_steps.clear() 

174 self.update("") 

175 

176 

177@dataclass 

178class CompletedStep: 

179 timing_ms: str 

180 message: str 

181 paths: list[pathlib.Path]