Coverage for src / rendercv / cli / render_command / run_rendercv.py: 100%
62 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 contextlib
2import pathlib
3import time
4from collections.abc import Callable
5from typing import Literal, Unpack
7import jinja2
9from rendercv.exception import RenderCVUserError, RenderCVUserValidationError
10from rendercv.renderer.html import generate_html
11from rendercv.renderer.markdown import generate_markdown
12from rendercv.renderer.pdf_png import generate_pdf, generate_png
13from rendercv.renderer.typst import generate_typst
14from rendercv.schema.rendercv_model_builder import (
15 BuildRendercvModelArguments,
16 build_rendercv_dictionary_and_model,
17 read_yaml_with_validation_errors,
18)
20from .progress_panel import ProgressPanel
23def timed_step[T, **P](
24 message: str,
25 progress_panel: ProgressPanel,
26 func: Callable[P, T],
27 *args: P.args,
28 **kwargs: P.kwargs,
29) -> T:
30 """Execute function, measure timing, and update progress panel with result.
32 Why:
33 Each generation step (Typst, PDF, PNG) returns file paths. This wrapper
34 times execution and automatically displays results in progress panel.
36 Example:
37 ```py
38 pdf_path = timed_step(
39 "Generated PDF", progress, generate_pdf, rendercv_model, typst_path
40 )
41 # Progress shows: ✓ 150 ms Generated PDF: ./cv.pdf
42 ```
44 Args:
45 message: Step description for progress display.
46 progress_panel: Progress panel to update.
47 func: Function to execute and time.
48 args: Positional arguments for func.
49 kwargs: Keyword arguments for func.
51 Returns:
52 Function result.
53 """
54 start = time.perf_counter()
55 result = func(*args, **kwargs)
56 end = time.perf_counter()
57 timing_ms = f"{(end - start) * 1000:.0f}"
59 paths: list[pathlib.Path] = []
60 if isinstance(result, pathlib.Path):
61 paths = [result]
62 elif isinstance(result, list) and result:
63 if len(result) > 1:
64 message = f"{message}s"
65 paths = [p for p in result if isinstance(p, pathlib.Path)]
67 if paths:
68 progress_panel.update_progress(
69 time_took=timing_ms, message=message, paths=paths
70 )
72 return result
75def collect_input_file_paths(
76 input_file_path: pathlib.Path,
77 design: pathlib.Path | None = None,
78 locale: pathlib.Path | None = None,
79 settings: pathlib.Path | None = None,
80) -> dict[Literal["input", "design", "locale", "settings"], pathlib.Path]:
81 """Collect all input file paths involved in a render.
83 Why:
84 A render may involve multiple files: the main YAML, plus overlay
85 files for design/locale/settings provided via CLI flags or referenced
86 in settings.render_command. Watch mode needs this complete list to
87 monitor all of them for changes, and the render pipeline needs the
88 resolved paths to read overlay file contents.
90 Args:
91 input_file_path: Path to the main YAML input file.
92 design: CLI-provided design file path.
93 locale: CLI-provided locale file path.
94 settings: CLI-provided settings file path.
96 Returns:
97 Mapping from role ("input", "design", "locale", "settings") to path.
98 """
99 files: dict[Literal["input", "design", "locale", "settings"], pathlib.Path] = {
100 "input": input_file_path
101 }
103 if design:
104 files["design"] = design
105 if locale:
106 files["locale"] = locale
107 if settings:
108 files["settings"] = settings
110 # Also include design/locale files referenced in the YAML itself
111 # (CLI flags take precedence, so skip if already provided).
112 # If YAML is invalid, watch mode should still start by watching the main file.
113 with contextlib.suppress(RenderCVUserValidationError):
114 main_dict = read_yaml_with_validation_errors(
115 input_file_path.read_text(encoding="utf-8"),
116 "main_yaml_file",
117 )
118 rc = main_dict.get("settings", {}).get("render_command", {})
119 if "design" not in files and rc.get("design"):
120 files["design"] = (input_file_path.parent / rc["design"]).resolve()
121 if "locale" not in files and rc.get("locale"):
122 files["locale"] = (input_file_path.parent / rc["locale"]).resolve()
124 return files
127def run_rendercv(
128 input_file_path: pathlib.Path,
129 progress: ProgressPanel,
130 **kwargs: Unpack[BuildRendercvModelArguments],
131) -> None:
132 """Execute complete CV generation pipeline with progress tracking and error handling.
134 Args:
135 input_file_path: Path to the main YAML input file.
136 progress: Progress panel for output display.
137 kwargs: Optional YAML overlay strings, output paths, and generation flags.
138 """
139 try:
140 main_yaml = input_file_path.read_text(encoding="utf-8")
142 _, rendercv_model = timed_step(
143 "Validated the input file",
144 progress,
145 build_rendercv_dictionary_and_model,
146 main_yaml,
147 input_file_path=input_file_path,
148 **kwargs,
149 )
150 typst_path = timed_step(
151 "Generated Typst",
152 progress,
153 generate_typst,
154 rendercv_model,
155 )
156 timed_step(
157 "Generated PDF",
158 progress,
159 generate_pdf,
160 rendercv_model,
161 typst_path,
162 )
163 timed_step(
164 "Generated PNG",
165 progress,
166 generate_png,
167 rendercv_model,
168 typst_path,
169 )
170 md_path = timed_step(
171 "Generated Markdown",
172 progress,
173 generate_markdown,
174 rendercv_model,
175 )
176 timed_step(
177 "Generated HTML",
178 progress,
179 generate_html,
180 rendercv_model,
181 md_path,
182 )
183 progress.finish_progress()
184 except RenderCVUserError as e:
185 progress.print_user_error(e)
186 except jinja2.exceptions.TemplateSyntaxError as e:
187 progress.print_user_error(
188 RenderCVUserError(
189 message=(
190 f"There is a problem with the template ({e.filename}) at line"
191 f" {e.lineno}!\n\n{e}"
192 )
193 )
194 )
195 except OSError as e:
196 progress.print_user_error(RenderCVUserError(message=f"OS Error: {e}"))
197 except RenderCVUserValidationError as e:
198 progress.print_validation_errors(e.validation_errors)