simple-resume 0.1.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simple_resume/__init__.py +132 -0
- simple_resume/core/__init__.py +47 -0
- simple_resume/core/colors.py +215 -0
- simple_resume/core/config.py +672 -0
- simple_resume/core/constants/__init__.py +207 -0
- simple_resume/core/constants/colors.py +98 -0
- simple_resume/core/constants/files.py +28 -0
- simple_resume/core/constants/layout.py +58 -0
- simple_resume/core/dependencies.py +258 -0
- simple_resume/core/effects.py +154 -0
- simple_resume/core/exceptions.py +261 -0
- simple_resume/core/file_operations.py +68 -0
- simple_resume/core/generate/__init__.py +21 -0
- simple_resume/core/generate/exceptions.py +69 -0
- simple_resume/core/generate/html.py +233 -0
- simple_resume/core/generate/pdf.py +659 -0
- simple_resume/core/generate/plan.py +131 -0
- simple_resume/core/hydration.py +55 -0
- simple_resume/core/importers/__init__.py +3 -0
- simple_resume/core/importers/json_resume.py +284 -0
- simple_resume/core/latex/__init__.py +60 -0
- simple_resume/core/latex/context.py +56 -0
- simple_resume/core/latex/conversion.py +227 -0
- simple_resume/core/latex/escaping.py +68 -0
- simple_resume/core/latex/fonts.py +93 -0
- simple_resume/core/latex/formatting.py +81 -0
- simple_resume/core/latex/sections.py +218 -0
- simple_resume/core/latex/types.py +84 -0
- simple_resume/core/markdown.py +127 -0
- simple_resume/core/models.py +102 -0
- simple_resume/core/palettes/__init__.py +38 -0
- simple_resume/core/palettes/common.py +73 -0
- simple_resume/core/palettes/data/default_palettes.json +58 -0
- simple_resume/core/palettes/exceptions.py +33 -0
- simple_resume/core/palettes/fetch_types.py +52 -0
- simple_resume/core/palettes/generators.py +137 -0
- simple_resume/core/palettes/registry.py +76 -0
- simple_resume/core/palettes/resolution.py +123 -0
- simple_resume/core/palettes/sources.py +162 -0
- simple_resume/core/paths.py +21 -0
- simple_resume/core/protocols.py +134 -0
- simple_resume/core/py.typed +0 -0
- simple_resume/core/render/__init__.py +37 -0
- simple_resume/core/render/manage.py +199 -0
- simple_resume/core/render/plan.py +405 -0
- simple_resume/core/result.py +226 -0
- simple_resume/core/resume.py +609 -0
- simple_resume/core/skills.py +60 -0
- simple_resume/core/validation.py +321 -0
- simple_resume/py.typed +0 -0
- simple_resume/shell/__init__.py +3 -0
- simple_resume/shell/assets/static/css/README.md +213 -0
- simple_resume/shell/assets/static/css/common.css +641 -0
- simple_resume/shell/assets/static/css/fonts.css +42 -0
- simple_resume/shell/assets/static/css/preview.css +82 -0
- simple_resume/shell/assets/static/css/print.css +99 -0
- simple_resume/shell/assets/static/fonts/AvenirLTStd-Book.otf +0 -0
- simple_resume/shell/assets/static/fonts/AvenirLTStd-Light.otf +0 -0
- simple_resume/shell/assets/static/fonts/AvenirLTStd-Medium.otf +0 -0
- simple_resume/shell/assets/static/fonts/AvenirLTStd-Oblique.otf +0 -0
- simple_resume/shell/assets/static/fonts/AvenirLTStd-Roman.otf +0 -0
- simple_resume/shell/assets/static/fonts/fontawesome/Font Awesome 6 Brands-Regular-400.otf +0 -0
- simple_resume/shell/assets/static/fonts/fontawesome/Font Awesome 6 Free-Solid-900.otf +0 -0
- simple_resume/shell/assets/static/images/default_profile_1.jpg +0 -0
- simple_resume/shell/assets/static/images/default_profile_2.png +0 -0
- simple_resume/shell/assets/static/schema.json +236 -0
- simple_resume/shell/assets/static/themes/README.md +208 -0
- simple_resume/shell/assets/static/themes/bold.yaml +64 -0
- simple_resume/shell/assets/static/themes/classic.yaml +64 -0
- simple_resume/shell/assets/static/themes/executive.yaml +64 -0
- simple_resume/shell/assets/static/themes/minimal.yaml +64 -0
- simple_resume/shell/assets/static/themes/modern.yaml +64 -0
- simple_resume/shell/assets/templates/html/cover.html +129 -0
- simple_resume/shell/assets/templates/html/demo.html +13 -0
- simple_resume/shell/assets/templates/html/resume_base.html +453 -0
- simple_resume/shell/assets/templates/html/resume_no_bars.html +316 -0
- simple_resume/shell/assets/templates/html/resume_with_bars.html +362 -0
- simple_resume/shell/cli/__init__.py +35 -0
- simple_resume/shell/cli/main.py +975 -0
- simple_resume/shell/cli/palette.py +75 -0
- simple_resume/shell/cli/random_palette_demo.py +407 -0
- simple_resume/shell/config.py +96 -0
- simple_resume/shell/effect_executor.py +211 -0
- simple_resume/shell/file_opener.py +308 -0
- simple_resume/shell/generate/__init__.py +37 -0
- simple_resume/shell/generate/core.py +650 -0
- simple_resume/shell/generate/lazy.py +284 -0
- simple_resume/shell/io_utils.py +199 -0
- simple_resume/shell/palettes/__init__.py +1 -0
- simple_resume/shell/palettes/fetch.py +63 -0
- simple_resume/shell/palettes/loader.py +321 -0
- simple_resume/shell/palettes/remote.py +179 -0
- simple_resume/shell/pdf_executor.py +52 -0
- simple_resume/shell/py.typed +0 -0
- simple_resume/shell/render/__init__.py +1 -0
- simple_resume/shell/render/latex.py +308 -0
- simple_resume/shell/render/operations.py +240 -0
- simple_resume/shell/resume_extensions.py +737 -0
- simple_resume/shell/runtime/__init__.py +7 -0
- simple_resume/shell/runtime/content.py +190 -0
- simple_resume/shell/runtime/generate.py +497 -0
- simple_resume/shell/runtime/lazy.py +138 -0
- simple_resume/shell/runtime/lazy_import.py +173 -0
- simple_resume/shell/service_locator.py +80 -0
- simple_resume/shell/services.py +256 -0
- simple_resume/shell/session/__init__.py +6 -0
- simple_resume/shell/session/config.py +35 -0
- simple_resume/shell/session/manage.py +386 -0
- simple_resume/shell/strategies.py +181 -0
- simple_resume/shell/themes/__init__.py +35 -0
- simple_resume/shell/themes/loader.py +230 -0
- simple_resume-0.1.9.dist-info/METADATA +201 -0
- simple_resume-0.1.9.dist-info/RECORD +116 -0
- simple_resume-0.1.9.dist-info/WHEEL +4 -0
- simple_resume-0.1.9.dist-info/entry_points.txt +5 -0
- simple_resume-0.1.9.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Render resumes as LaTeX documents (shell layer with I/O operations)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import shutil
|
|
7
|
+
|
|
8
|
+
# Bandit: subprocess usage is confined to vetted TeX commands.
|
|
9
|
+
import subprocess # nosec B404
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
15
|
+
|
|
16
|
+
from simple_resume.core.latex import (
|
|
17
|
+
LatexRenderResult,
|
|
18
|
+
build_latex_context_pure,
|
|
19
|
+
fontawesome_support_block,
|
|
20
|
+
)
|
|
21
|
+
from simple_resume.core.paths import Paths
|
|
22
|
+
from simple_resume.shell import config
|
|
23
|
+
from simple_resume.shell.runtime.content import get_content
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LatexCompilationError(RuntimeError):
|
|
27
|
+
"""Raise when LaTeX compilation fails."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, message: str, *, log: str | None = None) -> None:
|
|
30
|
+
"""Initialize the exception.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
message: The error message.
|
|
34
|
+
log: The compilation log.
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
super().__init__(message)
|
|
38
|
+
self.log = log
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _jinja_environment(template_root: Path) -> Environment:
|
|
42
|
+
"""Return a Jinja2 environment for LaTeX templates.
|
|
43
|
+
|
|
44
|
+
This is an I/O operation that loads templates from the file system.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
template_root: Path to template directory.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Configured Jinja2 environment.
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
loader = FileSystemLoader(str(template_root))
|
|
54
|
+
env = Environment(loader=loader, autoescape=select_autoescape(("html", "xml")))
|
|
55
|
+
return env
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def build_latex_context(
|
|
59
|
+
data: dict[str, Any],
|
|
60
|
+
*,
|
|
61
|
+
static_dir: Path | None = None,
|
|
62
|
+
) -> dict[str, Any]:
|
|
63
|
+
"""Prepare the LaTeX template context from raw resume data (with I/O).
|
|
64
|
+
|
|
65
|
+
This shell function wraps the pure core function and adds file system
|
|
66
|
+
operations for FontAwesome font detection.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
data: Raw resume data dictionary.
|
|
70
|
+
static_dir: Path to static assets directory for font detection.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Dictionary of context variables for LaTeX template rendering.
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
# Get pure context from core
|
|
77
|
+
context = build_latex_context_pure(data)
|
|
78
|
+
|
|
79
|
+
# Add fontawesome_block with file system check (I/O operation)
|
|
80
|
+
fontawesome_dir: str | None = None
|
|
81
|
+
if static_dir is not None:
|
|
82
|
+
candidate = Path(static_dir) / "fonts" / "fontawesome"
|
|
83
|
+
if candidate.is_dir(): # I/O: file system check
|
|
84
|
+
fontawesome_dir = f"{candidate.resolve().as_posix()}/"
|
|
85
|
+
|
|
86
|
+
context["fontawesome_block"] = fontawesome_support_block(fontawesome_dir)
|
|
87
|
+
|
|
88
|
+
return context
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def render_resume_latex_from_data(
|
|
92
|
+
data: dict[str, Any],
|
|
93
|
+
*,
|
|
94
|
+
paths: Paths | None = None,
|
|
95
|
+
template_name: str = "latex/basic.tex",
|
|
96
|
+
) -> LatexRenderResult:
|
|
97
|
+
"""Render a LaTeX template with the prepared context.
|
|
98
|
+
|
|
99
|
+
This function performs I/O operations: template loading and rendering.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
data: Resume data dictionary.
|
|
103
|
+
paths: Path configuration (resolved if not provided).
|
|
104
|
+
template_name: Template file name to render.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
LatexRenderResult with rendered tex and context.
|
|
108
|
+
|
|
109
|
+
"""
|
|
110
|
+
resolved_paths = paths or config.resolve_paths()
|
|
111
|
+
context = build_latex_context(data, static_dir=resolved_paths.static)
|
|
112
|
+
|
|
113
|
+
# I/O operations: load and render template
|
|
114
|
+
env = _jinja_environment(resolved_paths.templates)
|
|
115
|
+
template = env.get_template(template_name)
|
|
116
|
+
tex = template.render(**context)
|
|
117
|
+
|
|
118
|
+
return LatexRenderResult(tex=tex, context=context)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def render_resume_latex(
|
|
122
|
+
name: str,
|
|
123
|
+
*,
|
|
124
|
+
paths: Paths | None = None,
|
|
125
|
+
template_name: str = "latex/basic.tex",
|
|
126
|
+
) -> LatexRenderResult:
|
|
127
|
+
"""Read resume data and render it to a LaTeX string.
|
|
128
|
+
|
|
129
|
+
This function performs I/O operations: reading resume data from file system.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
name: Resume name to load.
|
|
133
|
+
paths: Path configuration (resolved if not provided).
|
|
134
|
+
template_name: Template file name to render.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
LatexRenderResult with rendered tex and context.
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
resolved_paths = paths or config.resolve_paths()
|
|
141
|
+
|
|
142
|
+
# I/O operation: read resume data
|
|
143
|
+
data = get_content(name, paths=resolved_paths, transform_markdown=False)
|
|
144
|
+
|
|
145
|
+
return render_resume_latex_from_data(
|
|
146
|
+
data, paths=resolved_paths, template_name=template_name
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def compile_tex_to_pdf(
|
|
151
|
+
tex_path: Path,
|
|
152
|
+
*,
|
|
153
|
+
engines: Iterable[str] = ("xelatex", "pdflatex"),
|
|
154
|
+
) -> Path:
|
|
155
|
+
"""Compile a `.tex` file to PDF using an available LaTeX engine.
|
|
156
|
+
|
|
157
|
+
This function performs I/O operations: subprocess execution and file system access.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
tex_path: Path to .tex file to compile.
|
|
161
|
+
engines: LaTeX engines to try (in order).
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Path to generated PDF file.
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
LatexCompilationError: If compilation fails.
|
|
168
|
+
|
|
169
|
+
"""
|
|
170
|
+
# I/O: Check which LaTeX engine is available
|
|
171
|
+
available_engine = None
|
|
172
|
+
for engine in engines:
|
|
173
|
+
if shutil.which(engine):
|
|
174
|
+
available_engine = engine
|
|
175
|
+
break
|
|
176
|
+
|
|
177
|
+
if available_engine is None:
|
|
178
|
+
raise LatexCompilationError(
|
|
179
|
+
"No LaTeX engine found. Install xelatex or pdflatex to render PDFs."
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
tex_argument = str(tex_path) if tex_path.is_absolute() else tex_path.name
|
|
183
|
+
|
|
184
|
+
command = [
|
|
185
|
+
available_engine,
|
|
186
|
+
"-interaction=nonstopmode",
|
|
187
|
+
"-halt-on-error",
|
|
188
|
+
"-output-directory",
|
|
189
|
+
str(tex_path.parent.resolve()),
|
|
190
|
+
tex_argument,
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
# I/O: Execute subprocess
|
|
194
|
+
# Bandit: command arguments are constructed from vetted engine names and paths.
|
|
195
|
+
result = subprocess.run( # noqa: S603 # nosec B603
|
|
196
|
+
command,
|
|
197
|
+
cwd=str(tex_path.parent),
|
|
198
|
+
capture_output=True,
|
|
199
|
+
check=False,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if result.returncode != 0:
|
|
203
|
+
log = (result.stdout or b"") + b"\n" + (result.stderr or b"")
|
|
204
|
+
message = f"LaTeX compilation failed with exit code {result.returncode}"
|
|
205
|
+
raise LatexCompilationError(
|
|
206
|
+
message,
|
|
207
|
+
log=log.decode("utf-8", errors="ignore"),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
pdf_path = tex_path.with_suffix(".pdf")
|
|
211
|
+
return pdf_path
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def compile_tex_to_html(
|
|
215
|
+
tex_path: Path,
|
|
216
|
+
*,
|
|
217
|
+
tools: Iterable[str] = ("pandoc", "htlatex"),
|
|
218
|
+
) -> Path:
|
|
219
|
+
"""Compile a `.tex` file to HTML using an available tool.
|
|
220
|
+
|
|
221
|
+
This function performs I/O operations: subprocess execution and file system access.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
tex_path: Path to .tex file to compile.
|
|
225
|
+
tools: Conversion tools to try (in order).
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Path to generated HTML file.
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
LatexCompilationError: If compilation fails.
|
|
232
|
+
|
|
233
|
+
"""
|
|
234
|
+
html_path = tex_path.with_suffix(".html")
|
|
235
|
+
tex_argument = str(tex_path) if tex_path.is_absolute() else tex_path.name
|
|
236
|
+
|
|
237
|
+
last_error: LatexCompilationError | None = None
|
|
238
|
+
|
|
239
|
+
# I/O: Try each tool
|
|
240
|
+
for tool in tools:
|
|
241
|
+
if shutil.which(tool) is None:
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
if tool == "pandoc":
|
|
245
|
+
command = [
|
|
246
|
+
tool,
|
|
247
|
+
tex_argument,
|
|
248
|
+
"-f",
|
|
249
|
+
"latex",
|
|
250
|
+
"-t",
|
|
251
|
+
"html5",
|
|
252
|
+
"-s",
|
|
253
|
+
"-o",
|
|
254
|
+
str(html_path),
|
|
255
|
+
]
|
|
256
|
+
elif tool == "htlatex":
|
|
257
|
+
command = [
|
|
258
|
+
tool,
|
|
259
|
+
tex_argument,
|
|
260
|
+
"html",
|
|
261
|
+
]
|
|
262
|
+
else:
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
# I/O: Execute subprocess
|
|
266
|
+
# Bandit: conversion command uses whitelisted tool names and the
|
|
267
|
+
# rendered tex path.
|
|
268
|
+
result = subprocess.run( # noqa: S603 # nosec B603
|
|
269
|
+
command,
|
|
270
|
+
cwd=str(tex_path.parent),
|
|
271
|
+
capture_output=True,
|
|
272
|
+
check=False,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
if result.returncode == 0:
|
|
276
|
+
# I/O: Check/rename generated file
|
|
277
|
+
if tool == "htlatex":
|
|
278
|
+
generated = tex_path.with_suffix(".html")
|
|
279
|
+
if generated.exists():
|
|
280
|
+
generated.rename(html_path)
|
|
281
|
+
if not html_path.exists():
|
|
282
|
+
html_path.write_text("", encoding="utf-8")
|
|
283
|
+
return html_path
|
|
284
|
+
|
|
285
|
+
log = (result.stdout or b"") + b"\n" + (result.stderr or b"")
|
|
286
|
+
last_error = LatexCompilationError(
|
|
287
|
+
f"LaTeX to HTML conversion via {tool} "
|
|
288
|
+
f"failed with exit code {result.returncode}",
|
|
289
|
+
log=log.decode("utf-8", errors="ignore"),
|
|
290
|
+
)
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
if last_error is not None:
|
|
294
|
+
raise last_error
|
|
295
|
+
raise LatexCompilationError(
|
|
296
|
+
"No LaTeX-to-HTML tool found. Install pandoc or htlatex to render HTML output."
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
__all__ = [
|
|
301
|
+
"LatexCompilationError",
|
|
302
|
+
"LatexRenderResult",
|
|
303
|
+
"build_latex_context",
|
|
304
|
+
"compile_tex_to_html",
|
|
305
|
+
"compile_tex_to_pdf",
|
|
306
|
+
"render_resume_latex",
|
|
307
|
+
"render_resume_latex_from_data",
|
|
308
|
+
]
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Shell layer for rendering operations with external dependencies.
|
|
2
|
+
|
|
3
|
+
This module contains all the external dependencies and rendering logic
|
|
4
|
+
that should be isolated from the pure core functionality.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import subprocess # nosec B404
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from types import ModuleType
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
# Import internal modules that will receive injected dependencies
|
|
15
|
+
from simple_resume.core.effects import CopyFile, MakeDirectory
|
|
16
|
+
from simple_resume.core.generate import html as _html_generation
|
|
17
|
+
from simple_resume.core.generate import pdf as _pdf_generation
|
|
18
|
+
from simple_resume.core.generate.html import create_html_generator_factory
|
|
19
|
+
from simple_resume.core.generate.pdf import PdfGenerationConfig
|
|
20
|
+
from simple_resume.core.models import RenderPlan
|
|
21
|
+
from simple_resume.core.protocols import EffectExecutor as EffectExecutorProtocol
|
|
22
|
+
from simple_resume.core.protocols import TemplateLocator
|
|
23
|
+
from simple_resume.core.render import get_template_environment
|
|
24
|
+
from simple_resume.core.result import GenerationMetadata, GenerationResult
|
|
25
|
+
from simple_resume.shell.config import ASSETS_ROOT, TEMPLATE_LOC
|
|
26
|
+
from simple_resume.shell.effect_executor import EffectExecutor
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def create_backend_injector(module: ModuleType, **overrides: Any) -> Any:
|
|
30
|
+
"""Create a context manager for temporarily overriding module attributes.
|
|
31
|
+
|
|
32
|
+
This is a factory function that returns a context manager, keeping the
|
|
33
|
+
core module pure while allowing dependency injection in the shell layer.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
class _BackendInjector:
|
|
37
|
+
def __init__(self, module: ModuleType, **overrides: Any) -> None:
|
|
38
|
+
self.module = module
|
|
39
|
+
self.overrides = overrides
|
|
40
|
+
self.originals: dict[str, Any] = {}
|
|
41
|
+
|
|
42
|
+
def __enter__(self) -> None:
|
|
43
|
+
for name, value in self.overrides.items():
|
|
44
|
+
self.originals[name] = getattr(self.module, name, None)
|
|
45
|
+
setattr(self.module, name, value)
|
|
46
|
+
|
|
47
|
+
def __exit__(
|
|
48
|
+
self,
|
|
49
|
+
exc_type: type[BaseException] | None,
|
|
50
|
+
exc_val: BaseException | None,
|
|
51
|
+
exc_tb: object,
|
|
52
|
+
) -> None:
|
|
53
|
+
for name, original in self.originals.items():
|
|
54
|
+
setattr(self.module, name, original)
|
|
55
|
+
|
|
56
|
+
return _BackendInjector(module, **overrides)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def generate_pdf_with_weasyprint(
|
|
60
|
+
render_plan: RenderPlan,
|
|
61
|
+
output_path: Path,
|
|
62
|
+
resume_name: str,
|
|
63
|
+
filename: str | None = None,
|
|
64
|
+
effect_executor: EffectExecutorProtocol | None = None,
|
|
65
|
+
) -> tuple[GenerationResult, int | None]:
|
|
66
|
+
"""Delegate to the HTML-to-PDF backend with patchable dependencies."""
|
|
67
|
+
|
|
68
|
+
class _TemplateLocator(TemplateLocator):
|
|
69
|
+
def get_template_location(self) -> Path:
|
|
70
|
+
return TEMPLATE_LOC
|
|
71
|
+
|
|
72
|
+
locator = _TemplateLocator()
|
|
73
|
+
executor = effect_executor or EffectExecutor()
|
|
74
|
+
|
|
75
|
+
backend_injector = create_backend_injector(
|
|
76
|
+
_pdf_generation,
|
|
77
|
+
get_template_environment=get_template_environment,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
with backend_injector:
|
|
81
|
+
config = PdfGenerationConfig(
|
|
82
|
+
resume_name=resume_name,
|
|
83
|
+
filename=filename,
|
|
84
|
+
template_locator=locator,
|
|
85
|
+
effect_executor=executor,
|
|
86
|
+
)
|
|
87
|
+
return _pdf_generation.generate_pdf_with_weasyprint(
|
|
88
|
+
render_plan,
|
|
89
|
+
output_path,
|
|
90
|
+
config,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _get_asset_copy_effects(output_dir: Path) -> list[CopyFile | MakeDirectory]:
|
|
95
|
+
"""Generate effects to copy CSS and font files to output directory.
|
|
96
|
+
|
|
97
|
+
This ensures HTML files work standalone without needing a base tag
|
|
98
|
+
or server. Assets are copied to output_dir/static/css/ and output_dir/static/fonts/.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
output_dir: The directory where HTML output is being written
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List of effects to create directories and copy files
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
effects: list[CopyFile | MakeDirectory] = []
|
|
108
|
+
static_src = ASSETS_ROOT / "static"
|
|
109
|
+
|
|
110
|
+
# CSS files
|
|
111
|
+
css_src = static_src / "css"
|
|
112
|
+
css_dest = output_dir / "static" / "css"
|
|
113
|
+
effects.append(MakeDirectory(path=css_dest, parents=True))
|
|
114
|
+
|
|
115
|
+
for css_file in css_src.glob("*.css"):
|
|
116
|
+
effects.append(CopyFile(source=css_file, destination=css_dest / css_file.name))
|
|
117
|
+
|
|
118
|
+
# Font files
|
|
119
|
+
fonts_src = static_src / "fonts"
|
|
120
|
+
fonts_dest = output_dir / "static" / "fonts"
|
|
121
|
+
if fonts_src.exists():
|
|
122
|
+
effects.append(MakeDirectory(path=fonts_dest, parents=True))
|
|
123
|
+
for font_file in fonts_src.glob("*"):
|
|
124
|
+
if font_file.is_file():
|
|
125
|
+
effects.append(
|
|
126
|
+
CopyFile(source=font_file, destination=fonts_dest / font_file.name)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return effects
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def generate_html_with_jinja(
|
|
133
|
+
render_plan: RenderPlan,
|
|
134
|
+
output_path: Path,
|
|
135
|
+
filename: str | None = None,
|
|
136
|
+
effect_executor: EffectExecutorProtocol | None = None,
|
|
137
|
+
) -> GenerationResult:
|
|
138
|
+
"""Render HTML via Jinja with injectable template environment."""
|
|
139
|
+
|
|
140
|
+
class _TemplateLocator(TemplateLocator):
|
|
141
|
+
def get_template_location(self) -> Path:
|
|
142
|
+
return TEMPLATE_LOC
|
|
143
|
+
|
|
144
|
+
locator = _TemplateLocator()
|
|
145
|
+
|
|
146
|
+
# Create HTML generator factory with explicit locator
|
|
147
|
+
html_factory = create_html_generator_factory(default_template_locator=locator)
|
|
148
|
+
prepare_html_func = html_factory.create_prepare_html_function()
|
|
149
|
+
|
|
150
|
+
backend_injector = create_backend_injector(
|
|
151
|
+
_html_generation,
|
|
152
|
+
get_template_environment=get_template_environment,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
with backend_injector:
|
|
156
|
+
html_content, effects, metadata = prepare_html_func(
|
|
157
|
+
render_plan=render_plan,
|
|
158
|
+
output_path=output_path,
|
|
159
|
+
resume_name=filename or "resume",
|
|
160
|
+
filename=filename,
|
|
161
|
+
template_locator=locator,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Add effects to copy static assets (CSS, fonts) to output directory
|
|
165
|
+
output_dir = output_path.parent
|
|
166
|
+
asset_effects = _get_asset_copy_effects(output_dir)
|
|
167
|
+
all_effects = list(effects) + asset_effects
|
|
168
|
+
|
|
169
|
+
# Execute the effects to actually create the files
|
|
170
|
+
executor = effect_executor or EffectExecutor()
|
|
171
|
+
executor.execute_many(all_effects)
|
|
172
|
+
|
|
173
|
+
# Create and return GenerationResult
|
|
174
|
+
return GenerationResult(
|
|
175
|
+
output_path=output_path,
|
|
176
|
+
format_type="html",
|
|
177
|
+
metadata=metadata,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def open_file_in_browser(
|
|
182
|
+
file_path: Path,
|
|
183
|
+
browser: str | None = None,
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Open a file in the default or specified browser.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
file_path: Path to the file to open.
|
|
189
|
+
browser: Optional browser command.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
None
|
|
193
|
+
|
|
194
|
+
"""
|
|
195
|
+
if browser:
|
|
196
|
+
# Use specified browser command for opening the file
|
|
197
|
+
subprocess.Popen( # noqa: S603 # nosec B603
|
|
198
|
+
[browser, str(file_path)],
|
|
199
|
+
stdout=subprocess.DEVNULL,
|
|
200
|
+
stderr=subprocess.DEVNULL,
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
# Use default system opener
|
|
204
|
+
subprocess.run( # noqa: S603 # nosec B603
|
|
205
|
+
["xdg-open", str(file_path)]
|
|
206
|
+
if Path("/usr/bin/xdg-open").exists()
|
|
207
|
+
else ["open", str(file_path)]
|
|
208
|
+
if Path("/usr/bin/open").exists()
|
|
209
|
+
else ["start", str(file_path)],
|
|
210
|
+
check=False,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def create_generation_result(
|
|
215
|
+
output_path: Path,
|
|
216
|
+
format_type: str,
|
|
217
|
+
generation_time: float,
|
|
218
|
+
**metadata_kwargs: Any,
|
|
219
|
+
) -> GenerationResult:
|
|
220
|
+
"""Create a GenerationResult with metadata."""
|
|
221
|
+
# Explicitly construct metadata with proper types to satisfy type checkers
|
|
222
|
+
metadata = GenerationMetadata(
|
|
223
|
+
format_type=format_type,
|
|
224
|
+
template_name=str(metadata_kwargs.get("template_name", "unknown")),
|
|
225
|
+
generation_time=generation_time,
|
|
226
|
+
file_size=int(metadata_kwargs.get("file_size", 0)),
|
|
227
|
+
resume_name=str(metadata_kwargs.get("resume_name", "resume")),
|
|
228
|
+
palette_info=metadata_kwargs.get("palette_info"),
|
|
229
|
+
page_count=metadata_kwargs.get("page_count"),
|
|
230
|
+
)
|
|
231
|
+
return GenerationResult(output_path, format_type, metadata)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
__all__ = [
|
|
235
|
+
"create_backend_injector",
|
|
236
|
+
"generate_pdf_with_weasyprint",
|
|
237
|
+
"generate_html_with_jinja",
|
|
238
|
+
"open_file_in_browser",
|
|
239
|
+
"create_generation_result",
|
|
240
|
+
]
|