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,650 @@
|
|
|
1
|
+
"""Core generation functions with immediate imports.
|
|
2
|
+
|
|
3
|
+
This module provides high-level functions for generating resumes with immediate
|
|
4
|
+
import loading. These functions are ideal for:
|
|
5
|
+
- Applications that will definitely use generation functionality
|
|
6
|
+
- Situations where predictability is preferred over optimization
|
|
7
|
+
- Web applications where import time is less critical than request time
|
|
8
|
+
|
|
9
|
+
For lazy-loaded versions with better startup performance, see `generate.lazy`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
from collections.abc import Iterable, Sequence
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from simple_resume.core.constants import DEFAULT_FORMAT, OutputFormat
|
|
21
|
+
from simple_resume.core.exceptions import (
|
|
22
|
+
ConfigurationError,
|
|
23
|
+
FileSystemError,
|
|
24
|
+
GenerationError,
|
|
25
|
+
ValidationError,
|
|
26
|
+
)
|
|
27
|
+
from simple_resume.core.generate.plan import (
|
|
28
|
+
CommandType,
|
|
29
|
+
GeneratePlanOptions,
|
|
30
|
+
GenerationCommand,
|
|
31
|
+
build_generation_plan,
|
|
32
|
+
)
|
|
33
|
+
from simple_resume.core.models import GenerationConfig
|
|
34
|
+
from simple_resume.core.result import BatchGenerationResult, GenerationResult
|
|
35
|
+
from simple_resume.core.validation import (
|
|
36
|
+
validate_directory_path,
|
|
37
|
+
validate_format,
|
|
38
|
+
validate_template_name,
|
|
39
|
+
)
|
|
40
|
+
from simple_resume.shell import session as session_mod
|
|
41
|
+
from simple_resume.shell.resume_extensions import to_html, to_markdown, to_pdf, to_tex
|
|
42
|
+
|
|
43
|
+
_YAML_SUFFIXES = {".yaml", ".yml"}
|
|
44
|
+
CommandResult = (
|
|
45
|
+
GenerationResult
|
|
46
|
+
| BatchGenerationResult
|
|
47
|
+
| dict[str, GenerationResult | BatchGenerationResult]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _to_optional_path(value: str | Path | None) -> Path | None:
|
|
52
|
+
"""Convert value to optional `Path` object."""
|
|
53
|
+
if value is None:
|
|
54
|
+
return None
|
|
55
|
+
return value if isinstance(value, Path) else Path(value)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _normalize_format_sequence(
|
|
59
|
+
formats: Sequence[OutputFormat | str],
|
|
60
|
+
) -> list[OutputFormat]:
|
|
61
|
+
"""Normalize a sequence of format strings to `OutputFormat` enums."""
|
|
62
|
+
return [OutputFormat.normalize(fmt, param_name="format") for fmt in formats]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _plan_options_from_config(
|
|
66
|
+
config: GenerationConfig,
|
|
67
|
+
overrides: dict[str, Any],
|
|
68
|
+
*,
|
|
69
|
+
formats: Sequence[OutputFormat],
|
|
70
|
+
) -> GeneratePlanOptions:
|
|
71
|
+
"""Create `GeneratePlanOptions` from `GenerationConfig` and overrides."""
|
|
72
|
+
return GeneratePlanOptions(
|
|
73
|
+
name=config.name,
|
|
74
|
+
data_dir=_to_optional_path(config.data_dir),
|
|
75
|
+
template=config.template,
|
|
76
|
+
output_path=_to_optional_path(config.output_path),
|
|
77
|
+
output_dir=_to_optional_path(config.output_dir),
|
|
78
|
+
preview=config.preview,
|
|
79
|
+
open_after=config.open_after,
|
|
80
|
+
browser=config.browser,
|
|
81
|
+
formats=formats,
|
|
82
|
+
overrides=overrides,
|
|
83
|
+
paths=config.paths,
|
|
84
|
+
pattern=config.pattern,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _build_plan_for_config(
|
|
89
|
+
config: GenerationConfig,
|
|
90
|
+
overrides: dict[str, Any],
|
|
91
|
+
*,
|
|
92
|
+
formats: Sequence[OutputFormat],
|
|
93
|
+
) -> list[GenerationCommand]:
|
|
94
|
+
"""Build a list of `GenerationCommand` objects from config."""
|
|
95
|
+
options = _plan_options_from_config(config, overrides, formats=formats)
|
|
96
|
+
return build_generation_plan(options)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _generate_with_format(
|
|
100
|
+
config: GenerationConfig,
|
|
101
|
+
*,
|
|
102
|
+
format_type: OutputFormat,
|
|
103
|
+
browser: str | None = None,
|
|
104
|
+
overrides: dict[str, Any] | None = None,
|
|
105
|
+
) -> GenerationResult | BatchGenerationResult:
|
|
106
|
+
"""Generate output in the requested format using a unified pipeline."""
|
|
107
|
+
# Touch time() to ensure monotonic clock import is kept (for parity with previous
|
|
108
|
+
# implementation that relied on time for side effects).
|
|
109
|
+
time.time()
|
|
110
|
+
format_type = OutputFormat.normalize(format_type, param_name="format_type")
|
|
111
|
+
|
|
112
|
+
normalized_overrides: dict[str, Any] = dict(overrides or {})
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
# Validate inputs using configuration object.
|
|
116
|
+
template = config.template
|
|
117
|
+
if template:
|
|
118
|
+
template = validate_template_name(template)
|
|
119
|
+
|
|
120
|
+
if config.data_dir and config.paths is None:
|
|
121
|
+
validate_directory_path(config.data_dir, must_exist=False)
|
|
122
|
+
|
|
123
|
+
if config.output_dir and config.paths is None:
|
|
124
|
+
validate_directory_path(
|
|
125
|
+
config.output_dir,
|
|
126
|
+
must_exist=False,
|
|
127
|
+
create_if_missing=False,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Create session with consistent configuration.
|
|
131
|
+
session_config = session_mod.SessionConfig(
|
|
132
|
+
default_template=template,
|
|
133
|
+
default_format=format_type,
|
|
134
|
+
auto_open=config.open_after,
|
|
135
|
+
preview_mode=config.preview,
|
|
136
|
+
output_dir=Path(config.output_dir) if config.output_dir else None,
|
|
137
|
+
session_metadata=normalized_overrides,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
with session_mod.ResumeSession(
|
|
141
|
+
data_dir=config.data_dir,
|
|
142
|
+
paths=config.paths,
|
|
143
|
+
config=session_config,
|
|
144
|
+
) as session:
|
|
145
|
+
if config.name:
|
|
146
|
+
# Generate single resume.
|
|
147
|
+
resume = session.resume(config.name)
|
|
148
|
+
if normalized_overrides:
|
|
149
|
+
resume = resume.with_config(**normalized_overrides)
|
|
150
|
+
|
|
151
|
+
if format_type is OutputFormat.PDF:
|
|
152
|
+
return to_pdf(resume, open_after=config.open_after)
|
|
153
|
+
|
|
154
|
+
if format_type is OutputFormat.HTML:
|
|
155
|
+
return to_html(
|
|
156
|
+
resume,
|
|
157
|
+
open_after=config.open_after,
|
|
158
|
+
browser=browser,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
raise GenerationError(
|
|
162
|
+
f"Unsupported format requested: {format_type}",
|
|
163
|
+
format_type=format_type,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Generate multiple resumes.
|
|
167
|
+
batch_kwargs = dict(normalized_overrides)
|
|
168
|
+
if format_type is OutputFormat.HTML and browser is not None:
|
|
169
|
+
batch_kwargs.setdefault("browser", browser)
|
|
170
|
+
|
|
171
|
+
return session.generate_all(
|
|
172
|
+
format=format_type,
|
|
173
|
+
pattern=config.pattern,
|
|
174
|
+
open_after=config.open_after,
|
|
175
|
+
**batch_kwargs,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
except Exception as exc:
|
|
179
|
+
if isinstance(
|
|
180
|
+
exc, (GenerationError, ValidationError, ConfigurationError, FileSystemError)
|
|
181
|
+
):
|
|
182
|
+
raise
|
|
183
|
+
|
|
184
|
+
error_label = (
|
|
185
|
+
"PDFs" if format_type is OutputFormat.PDF else format_type.value.upper()
|
|
186
|
+
)
|
|
187
|
+
raise GenerationError(
|
|
188
|
+
f"Failed to generate {error_label}: {exc}",
|
|
189
|
+
format_type=format_type,
|
|
190
|
+
) from exc
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _generate_single_format(
|
|
194
|
+
session: session_mod.ResumeSession,
|
|
195
|
+
config: GenerationConfig,
|
|
196
|
+
format_type: OutputFormat,
|
|
197
|
+
overrides: dict[str, Any],
|
|
198
|
+
) -> GenerationResult | BatchGenerationResult:
|
|
199
|
+
"""Generate a single format for a batch operation."""
|
|
200
|
+
if not config.name:
|
|
201
|
+
return session.generate_all(
|
|
202
|
+
format=format_type,
|
|
203
|
+
pattern=config.pattern,
|
|
204
|
+
open_after=config.open_after,
|
|
205
|
+
**overrides,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
resume = session.resume(config.name)
|
|
209
|
+
if format_type is OutputFormat.PDF:
|
|
210
|
+
return to_pdf(resume, open_after=config.open_after)
|
|
211
|
+
return to_html(resume, open_after=config.open_after, browser=config.browser)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _execute_batch_all(
|
|
215
|
+
config: GenerationConfig,
|
|
216
|
+
overrides: dict[str, Any],
|
|
217
|
+
) -> dict[str, GenerationResult | BatchGenerationResult]:
|
|
218
|
+
"""Execute a multi-format batch command."""
|
|
219
|
+
formats = config.formats or [OutputFormat.PDF, OutputFormat.HTML]
|
|
220
|
+
normalized_formats = _normalize_format_sequence(formats)
|
|
221
|
+
|
|
222
|
+
template = validate_template_name(config.template) if config.template else None
|
|
223
|
+
if config.data_dir and config.paths is None:
|
|
224
|
+
validate_directory_path(config.data_dir, must_exist=False)
|
|
225
|
+
if config.output_dir and config.paths is None:
|
|
226
|
+
validate_directory_path(
|
|
227
|
+
config.output_dir, must_exist=False, create_if_missing=False
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
results: dict[str, GenerationResult | BatchGenerationResult] = {}
|
|
231
|
+
default_fmt = normalized_formats[0] if normalized_formats else OutputFormat.PDF
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
session_config = session_mod.SessionConfig(
|
|
235
|
+
default_template=template,
|
|
236
|
+
default_format=default_fmt,
|
|
237
|
+
auto_open=config.open_after,
|
|
238
|
+
preview_mode=config.preview,
|
|
239
|
+
output_dir=_to_optional_path(config.output_dir),
|
|
240
|
+
session_metadata=overrides,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
with session_mod.ResumeSession(
|
|
244
|
+
data_dir=config.data_dir,
|
|
245
|
+
paths=config.paths,
|
|
246
|
+
config=session_config,
|
|
247
|
+
) as session:
|
|
248
|
+
for format_type in normalized_formats:
|
|
249
|
+
results[format_type.value] = _generate_single_format(
|
|
250
|
+
session, config, format_type, overrides
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
except (GenerationError, ValidationError, ConfigurationError, FileSystemError):
|
|
254
|
+
raise
|
|
255
|
+
except Exception as exc:
|
|
256
|
+
raise GenerationError(
|
|
257
|
+
f"Failed to generate resumes: {exc}",
|
|
258
|
+
format_type=", ".join(fmt.value for fmt in normalized_formats),
|
|
259
|
+
) from exc
|
|
260
|
+
|
|
261
|
+
return results
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def execute_generation_commands(
|
|
265
|
+
commands: Sequence[GenerationCommand],
|
|
266
|
+
) -> list[tuple[GenerationCommand, CommandResult]]:
|
|
267
|
+
"""Execute planner commands and return their results."""
|
|
268
|
+
executed: list[tuple[GenerationCommand, CommandResult]] = []
|
|
269
|
+
for command in commands:
|
|
270
|
+
result: CommandResult
|
|
271
|
+
if command.kind is CommandType.BATCH_ALL:
|
|
272
|
+
result = _execute_batch_all(command.config, command.overrides)
|
|
273
|
+
else:
|
|
274
|
+
format_type = command.format
|
|
275
|
+
if format_type is None:
|
|
276
|
+
raise GenerationError("Planner command missing required format")
|
|
277
|
+
result = _generate_with_format(
|
|
278
|
+
command.config,
|
|
279
|
+
format_type=format_type,
|
|
280
|
+
browser=command.config.browser,
|
|
281
|
+
overrides=command.overrides,
|
|
282
|
+
)
|
|
283
|
+
executed.append((command, result))
|
|
284
|
+
return executed
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _execute_plan_for_formats(
|
|
288
|
+
config: GenerationConfig,
|
|
289
|
+
overrides: dict[str, Any],
|
|
290
|
+
formats: Sequence[OutputFormat],
|
|
291
|
+
) -> list[tuple[GenerationCommand, CommandResult]]:
|
|
292
|
+
plan = _build_plan_for_config(config, dict(overrides), formats=formats)
|
|
293
|
+
return execute_generation_commands(plan)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _unwrap_generation_result(
|
|
297
|
+
result: CommandResult,
|
|
298
|
+
) -> GenerationResult | BatchGenerationResult:
|
|
299
|
+
"""Unwrap a single `GenerationResult` from a `CommandResult`."""
|
|
300
|
+
if isinstance(result, dict):
|
|
301
|
+
raise TypeError(
|
|
302
|
+
"Planner returned batch-all result where single output was expected"
|
|
303
|
+
)
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _collect_generate_all_results(
|
|
308
|
+
executions: Iterable[tuple[GenerationCommand, CommandResult]],
|
|
309
|
+
) -> dict[str, GenerationResult | BatchGenerationResult]:
|
|
310
|
+
aggregated: dict[str, GenerationResult | BatchGenerationResult] = {}
|
|
311
|
+
for command, result in executions:
|
|
312
|
+
if command.kind is CommandType.BATCH_ALL:
|
|
313
|
+
if not isinstance(result, dict):
|
|
314
|
+
raise TypeError("Batch-all command must return a dictionary result")
|
|
315
|
+
aggregated.update(result)
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
if command.format is None:
|
|
319
|
+
raise GenerationError("Planner command missing format information")
|
|
320
|
+
aggregated[command.format.value] = _unwrap_generation_result(result)
|
|
321
|
+
return aggregated
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def generate_pdf(
|
|
325
|
+
config: GenerationConfig,
|
|
326
|
+
**config_overrides: Any,
|
|
327
|
+
) -> GenerationResult | BatchGenerationResult:
|
|
328
|
+
"""Generate PDF resumes using a configuration object.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
config: Configuration describing what to render and where to write output.
|
|
332
|
+
**config_overrides: Keyword overrides applied to the resume configuration.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
A generation result for a single resume or a batch when multiple
|
|
336
|
+
files are rendered.
|
|
337
|
+
|
|
338
|
+
Raises:
|
|
339
|
+
`ConfigurationError`: Raised on invalid path configuration.
|
|
340
|
+
`GenerationError`: Raised when PDF rendering fails.
|
|
341
|
+
`ValidationError`: Raised when resume data fails validation.
|
|
342
|
+
`FileSystemError`: Raised on filesystem errors during rendering.
|
|
343
|
+
|
|
344
|
+
Examples:
|
|
345
|
+
Generate all resumes in a directory::
|
|
346
|
+
|
|
347
|
+
cfg = GenerationConfig(data_dir="my_resumes")
|
|
348
|
+
results = generate_pdf(cfg)
|
|
349
|
+
|
|
350
|
+
Render a single resume with overrides::
|
|
351
|
+
|
|
352
|
+
cfg = GenerationConfig(
|
|
353
|
+
name="casey",
|
|
354
|
+
template="resume_with_bars",
|
|
355
|
+
open_after=True,
|
|
356
|
+
)
|
|
357
|
+
result = generate_pdf(cfg, theme_color="#0066CC")
|
|
358
|
+
|
|
359
|
+
"""
|
|
360
|
+
executions = _execute_plan_for_formats(
|
|
361
|
+
config,
|
|
362
|
+
config_overrides,
|
|
363
|
+
formats=[OutputFormat.PDF],
|
|
364
|
+
)
|
|
365
|
+
if not executions:
|
|
366
|
+
raise GenerationError("Planner produced no commands for generate_pdf")
|
|
367
|
+
return _unwrap_generation_result(executions[0][1])
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def generate_html(
|
|
371
|
+
config: GenerationConfig,
|
|
372
|
+
**config_overrides: Any,
|
|
373
|
+
) -> GenerationResult | BatchGenerationResult:
|
|
374
|
+
"""Generate HTML resumes using a configuration object.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
config: Configuration describing what to render and where to write output.
|
|
378
|
+
**config_overrides: Keyword overrides applied to the resume configuration.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
A generation result for a single resume or a batch when multiple
|
|
382
|
+
files are rendered.
|
|
383
|
+
|
|
384
|
+
Raises:
|
|
385
|
+
`ConfigurationError`: Raised on invalid path configuration.
|
|
386
|
+
`GenerationError`: Raised when HTML rendering fails.
|
|
387
|
+
`ValidationError`: Raised when resume data fails validation.
|
|
388
|
+
`FileSystemError`: Raised on filesystem errors during rendering.
|
|
389
|
+
|
|
390
|
+
Examples:
|
|
391
|
+
Generate HTML with preview enabled::
|
|
392
|
+
|
|
393
|
+
cfg = GenerationConfig(data_dir="my_resumes", preview=True)
|
|
394
|
+
results = generate_html(cfg)
|
|
395
|
+
|
|
396
|
+
Render a single resume in the browser of choice::
|
|
397
|
+
|
|
398
|
+
cfg = GenerationConfig(
|
|
399
|
+
name="casey",
|
|
400
|
+
template="resume_no_bars",
|
|
401
|
+
browser="firefox",
|
|
402
|
+
)
|
|
403
|
+
result = generate_html(cfg)
|
|
404
|
+
|
|
405
|
+
"""
|
|
406
|
+
executions = _execute_plan_for_formats(
|
|
407
|
+
config,
|
|
408
|
+
config_overrides,
|
|
409
|
+
formats=[OutputFormat.HTML],
|
|
410
|
+
)
|
|
411
|
+
if not executions:
|
|
412
|
+
raise GenerationError("Planner produced no commands for generate_html")
|
|
413
|
+
return _unwrap_generation_result(executions[0][1])
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def generate_all(
|
|
417
|
+
config: GenerationConfig,
|
|
418
|
+
**config_overrides: Any,
|
|
419
|
+
) -> dict[str, GenerationResult | BatchGenerationResult]:
|
|
420
|
+
"""Generate resumes in all specified formats.
|
|
421
|
+
|
|
422
|
+
Generates resumes in multiple formats (e.g., PDF and HTML) from a single
|
|
423
|
+
configuration.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
config: Configuration describing what to render and which formats to include.
|
|
427
|
+
**config_overrides: Keyword overrides applied to individual resume renders.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Dictionary mapping format names to `GenerationResult` or
|
|
431
|
+
`BatchGenerationResult`.
|
|
432
|
+
|
|
433
|
+
Raises:
|
|
434
|
+
`ValueError`: If any requested format is not supported.
|
|
435
|
+
`ConfigurationError`: If path configuration is invalid.
|
|
436
|
+
`GenerationError`: If generation fails for any format.
|
|
437
|
+
|
|
438
|
+
Examples:
|
|
439
|
+
# Generate all resumes in both PDF and HTML formats
|
|
440
|
+
results = generate_all("my_resumes")
|
|
441
|
+
|
|
442
|
+
# Generate specific resume in multiple formats
|
|
443
|
+
results = generate_all(
|
|
444
|
+
GenerationConfig(
|
|
445
|
+
name="my_resume",
|
|
446
|
+
formats=["pdf", "html"],
|
|
447
|
+
template="professional",
|
|
448
|
+
)
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
"""
|
|
452
|
+
target_formats = config.formats or [OutputFormat.PDF, OutputFormat.HTML]
|
|
453
|
+
normalized_formats = _normalize_format_sequence(target_formats)
|
|
454
|
+
executions = _execute_plan_for_formats(
|
|
455
|
+
config,
|
|
456
|
+
config_overrides,
|
|
457
|
+
formats=normalized_formats,
|
|
458
|
+
)
|
|
459
|
+
return _collect_generate_all_results(executions)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def generate_resume(
|
|
463
|
+
config: GenerationConfig,
|
|
464
|
+
**config_overrides: Any,
|
|
465
|
+
) -> GenerationResult:
|
|
466
|
+
"""Generate a single resume.
|
|
467
|
+
|
|
468
|
+
This function is designed for generating a single resume file, as opposed
|
|
469
|
+
to batch operations.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
config: Configuration describing the resume to render.
|
|
473
|
+
**config_overrides: Keyword overrides applied to the resume configuration.
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
`GenerationResult` with metadata and operations.
|
|
477
|
+
|
|
478
|
+
Examples:
|
|
479
|
+
# Simple generation
|
|
480
|
+
result = generate_resume(GenerationConfig(name="my_resume"))
|
|
481
|
+
|
|
482
|
+
# With template and output path
|
|
483
|
+
result = generate_resume(
|
|
484
|
+
GenerationConfig(
|
|
485
|
+
name="my_resume",
|
|
486
|
+
format="pdf",
|
|
487
|
+
template="professional",
|
|
488
|
+
output_path="output/my_resume.pdf",
|
|
489
|
+
)
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
"""
|
|
493
|
+
format_enum = validate_format(config.format, param_name="format")
|
|
494
|
+
plan_config = GenerationConfig(
|
|
495
|
+
data_dir=config.data_dir,
|
|
496
|
+
output_dir=config.output_dir,
|
|
497
|
+
output_path=config.output_path,
|
|
498
|
+
paths=config.paths,
|
|
499
|
+
template=config.template,
|
|
500
|
+
format=format_enum,
|
|
501
|
+
open_after=config.open_after,
|
|
502
|
+
preview=config.preview,
|
|
503
|
+
name=config.name,
|
|
504
|
+
pattern=config.pattern,
|
|
505
|
+
browser=config.browser,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
executions = _execute_plan_for_formats(
|
|
509
|
+
plan_config,
|
|
510
|
+
config_overrides,
|
|
511
|
+
formats=[format_enum],
|
|
512
|
+
)
|
|
513
|
+
if not executions:
|
|
514
|
+
raise GenerationError("Planner produced no commands for generate_resume")
|
|
515
|
+
result = _unwrap_generation_result(executions[0][1])
|
|
516
|
+
if isinstance(result, BatchGenerationResult):
|
|
517
|
+
raise GenerationError(
|
|
518
|
+
"Planner returned batch output when generate_resume expected a single "
|
|
519
|
+
"resume"
|
|
520
|
+
)
|
|
521
|
+
return result
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _infer_data_dir_and_name(
|
|
525
|
+
source: str | Path,
|
|
526
|
+
data_dir: str | Path | None,
|
|
527
|
+
) -> tuple[Path, str | None]:
|
|
528
|
+
"""Infer a data directory and optional resume name from user-friendly inputs."""
|
|
529
|
+
source_path = Path(source)
|
|
530
|
+
|
|
531
|
+
if data_dir is not None:
|
|
532
|
+
base_dir = Path(data_dir)
|
|
533
|
+
if source_path.exists() and source_path.is_dir():
|
|
534
|
+
return source_path, None
|
|
535
|
+
if source_path.suffix.lower() in _YAML_SUFFIXES:
|
|
536
|
+
return base_dir, source_path.stem
|
|
537
|
+
return base_dir, str(source)
|
|
538
|
+
|
|
539
|
+
if source_path.exists():
|
|
540
|
+
if source_path.is_dir():
|
|
541
|
+
return source_path, None
|
|
542
|
+
if source_path.suffix.lower() in _YAML_SUFFIXES:
|
|
543
|
+
return source_path.parent, source_path.stem
|
|
544
|
+
|
|
545
|
+
raise ValueError(
|
|
546
|
+
"Unable to infer data_dir from source. Provide a YAML path, directory, "
|
|
547
|
+
"or set data_dir explicitly."
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
@dataclass
|
|
552
|
+
class GenerateOptions:
|
|
553
|
+
"""Configuration options for resume generation."""
|
|
554
|
+
|
|
555
|
+
formats: Sequence[str | OutputFormat] | None = None
|
|
556
|
+
data_dir: str | Path | None = None
|
|
557
|
+
output_dir: str | Path | None = None
|
|
558
|
+
template: str | None = None
|
|
559
|
+
preview: bool = False
|
|
560
|
+
open_after: bool = False
|
|
561
|
+
browser: str | None = None
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def generate(
|
|
565
|
+
source: str | Path,
|
|
566
|
+
options: GenerateOptions | None = None,
|
|
567
|
+
**overrides: Any,
|
|
568
|
+
) -> dict[str, GenerationResult | BatchGenerationResult]:
|
|
569
|
+
"""Render one or more formats for the same source."""
|
|
570
|
+
opts = options or GenerateOptions()
|
|
571
|
+
|
|
572
|
+
target_formats = tuple(opts.formats or (DEFAULT_FORMAT,))
|
|
573
|
+
normalized_targets = tuple(
|
|
574
|
+
validate_format(fmt, param_name="format") for fmt in target_formats
|
|
575
|
+
)
|
|
576
|
+
base_dir, resume_name = _infer_data_dir_and_name(source, opts.data_dir)
|
|
577
|
+
|
|
578
|
+
if len(normalized_targets) == 1:
|
|
579
|
+
fmt = normalized_targets[0]
|
|
580
|
+
cfg = GenerationConfig(
|
|
581
|
+
data_dir=base_dir,
|
|
582
|
+
name=resume_name,
|
|
583
|
+
output_dir=opts.output_dir,
|
|
584
|
+
template=opts.template,
|
|
585
|
+
open_after=opts.open_after,
|
|
586
|
+
preview=opts.preview or (fmt is OutputFormat.HTML),
|
|
587
|
+
browser=opts.browser if fmt is OutputFormat.HTML else None,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
if fmt is OutputFormat.PDF:
|
|
591
|
+
return {fmt.value: generate_pdf(cfg, **overrides)}
|
|
592
|
+
|
|
593
|
+
if fmt is OutputFormat.HTML:
|
|
594
|
+
return {fmt.value: generate_html(cfg, **overrides)}
|
|
595
|
+
|
|
596
|
+
raise ValueError(f"Unsupported format requested: {fmt.value}")
|
|
597
|
+
|
|
598
|
+
cfg = GenerationConfig(
|
|
599
|
+
data_dir=base_dir,
|
|
600
|
+
name=resume_name,
|
|
601
|
+
output_dir=opts.output_dir,
|
|
602
|
+
template=opts.template,
|
|
603
|
+
formats=list(normalized_targets),
|
|
604
|
+
open_after=opts.open_after,
|
|
605
|
+
preview=opts.preview,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
return generate_all(cfg, **overrides)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def preview(
|
|
612
|
+
source: str | Path,
|
|
613
|
+
*,
|
|
614
|
+
data_dir: str | Path | None = None,
|
|
615
|
+
template: str | None = None,
|
|
616
|
+
browser: str | None = None,
|
|
617
|
+
open_after: bool = True,
|
|
618
|
+
**overrides: Any,
|
|
619
|
+
) -> GenerationResult | BatchGenerationResult:
|
|
620
|
+
"""Render a single resume to HTML with preview defaults."""
|
|
621
|
+
base_dir, resume_name = _infer_data_dir_and_name(source, data_dir)
|
|
622
|
+
if resume_name is None:
|
|
623
|
+
raise ValueError("preview() requires a specific resume name or YAML path.")
|
|
624
|
+
|
|
625
|
+
cfg = GenerationConfig(
|
|
626
|
+
data_dir=base_dir,
|
|
627
|
+
name=resume_name,
|
|
628
|
+
template=template,
|
|
629
|
+
browser=browser,
|
|
630
|
+
preview=True,
|
|
631
|
+
open_after=open_after,
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
return generate_html(cfg, **overrides)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
__all__ = [
|
|
638
|
+
"GenerationConfig",
|
|
639
|
+
"execute_generation_commands",
|
|
640
|
+
"generate_pdf",
|
|
641
|
+
"generate_html",
|
|
642
|
+
"generate_all",
|
|
643
|
+
"generate_resume",
|
|
644
|
+
"generate",
|
|
645
|
+
"preview",
|
|
646
|
+
"to_html",
|
|
647
|
+
"to_markdown",
|
|
648
|
+
"to_pdf",
|
|
649
|
+
"to_tex",
|
|
650
|
+
]
|