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
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-25 13:49 +0000
1import pathlib
2from dataclasses import dataclass
4import rich.box
5import rich.console
6import rich.live
7import rich.panel
8import rich.table
9import typer
11from rendercv.exception import RenderCVUserError, RenderCVValidationError
14def format_validation_error_location(error_object: RenderCVValidationError) -> str:
15 """Format schema/YAML location for validation error table rows.
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.
21 Args:
22 error_object: Validation error with schema and YAML location metadata.
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)
30 if error_object.yaml_location is None:
31 return error_object.yaml_source
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}"
39class ProgressPanel(rich.live.Live):
40 """Live-updating terminal panel showing CV generation progress with timing.
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 ```
50 Args:
51 quiet: Suppress all terminal output.
52 """
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 )
67 def update_progress(
68 self, time_took: str, message: str, paths: list[pathlib.Path]
69 ) -> None:
70 """Add completed step to progress display.
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...")
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()
85 def print_progress_panel(self, title: str) -> None:
86 """Render progress panel with all completed steps.
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)
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}")
109 content = "\n".join(lines) if lines else "Rendering..."
111 self.update(
112 rich.panel.Panel(
113 content,
114 title=title,
115 title_align="left",
116 border_style="bright_black",
117 )
118 )
120 def print_user_error(self, user_error: RenderCVUserError) -> None:
121 """Display error panel and exit with error code.
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)
137 def print_validation_errors(self, errors: list[RenderCVValidationError]) -> None:
138 """Display validation errors in table format and exit.
140 Why:
141 Pydantic validation errors are parsed into user-friendly messages with
142 YAML locations. Table shows exactly which field failed and why.
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")
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 )
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 )
169 raise typer.Exit(code=1)
171 def clear(self) -> None:
172 """Clear all completed steps and panel display."""
173 self.completed_steps.clear()
174 self.update("")
177@dataclass
178class CompletedStep:
179 timing_ms: str
180 message: str
181 paths: list[pathlib.Path]