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.
Files changed (116) hide show
  1. simple_resume/__init__.py +132 -0
  2. simple_resume/core/__init__.py +47 -0
  3. simple_resume/core/colors.py +215 -0
  4. simple_resume/core/config.py +672 -0
  5. simple_resume/core/constants/__init__.py +207 -0
  6. simple_resume/core/constants/colors.py +98 -0
  7. simple_resume/core/constants/files.py +28 -0
  8. simple_resume/core/constants/layout.py +58 -0
  9. simple_resume/core/dependencies.py +258 -0
  10. simple_resume/core/effects.py +154 -0
  11. simple_resume/core/exceptions.py +261 -0
  12. simple_resume/core/file_operations.py +68 -0
  13. simple_resume/core/generate/__init__.py +21 -0
  14. simple_resume/core/generate/exceptions.py +69 -0
  15. simple_resume/core/generate/html.py +233 -0
  16. simple_resume/core/generate/pdf.py +659 -0
  17. simple_resume/core/generate/plan.py +131 -0
  18. simple_resume/core/hydration.py +55 -0
  19. simple_resume/core/importers/__init__.py +3 -0
  20. simple_resume/core/importers/json_resume.py +284 -0
  21. simple_resume/core/latex/__init__.py +60 -0
  22. simple_resume/core/latex/context.py +56 -0
  23. simple_resume/core/latex/conversion.py +227 -0
  24. simple_resume/core/latex/escaping.py +68 -0
  25. simple_resume/core/latex/fonts.py +93 -0
  26. simple_resume/core/latex/formatting.py +81 -0
  27. simple_resume/core/latex/sections.py +218 -0
  28. simple_resume/core/latex/types.py +84 -0
  29. simple_resume/core/markdown.py +127 -0
  30. simple_resume/core/models.py +102 -0
  31. simple_resume/core/palettes/__init__.py +38 -0
  32. simple_resume/core/palettes/common.py +73 -0
  33. simple_resume/core/palettes/data/default_palettes.json +58 -0
  34. simple_resume/core/palettes/exceptions.py +33 -0
  35. simple_resume/core/palettes/fetch_types.py +52 -0
  36. simple_resume/core/palettes/generators.py +137 -0
  37. simple_resume/core/palettes/registry.py +76 -0
  38. simple_resume/core/palettes/resolution.py +123 -0
  39. simple_resume/core/palettes/sources.py +162 -0
  40. simple_resume/core/paths.py +21 -0
  41. simple_resume/core/protocols.py +134 -0
  42. simple_resume/core/py.typed +0 -0
  43. simple_resume/core/render/__init__.py +37 -0
  44. simple_resume/core/render/manage.py +199 -0
  45. simple_resume/core/render/plan.py +405 -0
  46. simple_resume/core/result.py +226 -0
  47. simple_resume/core/resume.py +609 -0
  48. simple_resume/core/skills.py +60 -0
  49. simple_resume/core/validation.py +321 -0
  50. simple_resume/py.typed +0 -0
  51. simple_resume/shell/__init__.py +3 -0
  52. simple_resume/shell/assets/static/css/README.md +213 -0
  53. simple_resume/shell/assets/static/css/common.css +641 -0
  54. simple_resume/shell/assets/static/css/fonts.css +42 -0
  55. simple_resume/shell/assets/static/css/preview.css +82 -0
  56. simple_resume/shell/assets/static/css/print.css +99 -0
  57. simple_resume/shell/assets/static/fonts/AvenirLTStd-Book.otf +0 -0
  58. simple_resume/shell/assets/static/fonts/AvenirLTStd-Light.otf +0 -0
  59. simple_resume/shell/assets/static/fonts/AvenirLTStd-Medium.otf +0 -0
  60. simple_resume/shell/assets/static/fonts/AvenirLTStd-Oblique.otf +0 -0
  61. simple_resume/shell/assets/static/fonts/AvenirLTStd-Roman.otf +0 -0
  62. simple_resume/shell/assets/static/fonts/fontawesome/Font Awesome 6 Brands-Regular-400.otf +0 -0
  63. simple_resume/shell/assets/static/fonts/fontawesome/Font Awesome 6 Free-Solid-900.otf +0 -0
  64. simple_resume/shell/assets/static/images/default_profile_1.jpg +0 -0
  65. simple_resume/shell/assets/static/images/default_profile_2.png +0 -0
  66. simple_resume/shell/assets/static/schema.json +236 -0
  67. simple_resume/shell/assets/static/themes/README.md +208 -0
  68. simple_resume/shell/assets/static/themes/bold.yaml +64 -0
  69. simple_resume/shell/assets/static/themes/classic.yaml +64 -0
  70. simple_resume/shell/assets/static/themes/executive.yaml +64 -0
  71. simple_resume/shell/assets/static/themes/minimal.yaml +64 -0
  72. simple_resume/shell/assets/static/themes/modern.yaml +64 -0
  73. simple_resume/shell/assets/templates/html/cover.html +129 -0
  74. simple_resume/shell/assets/templates/html/demo.html +13 -0
  75. simple_resume/shell/assets/templates/html/resume_base.html +453 -0
  76. simple_resume/shell/assets/templates/html/resume_no_bars.html +316 -0
  77. simple_resume/shell/assets/templates/html/resume_with_bars.html +362 -0
  78. simple_resume/shell/cli/__init__.py +35 -0
  79. simple_resume/shell/cli/main.py +975 -0
  80. simple_resume/shell/cli/palette.py +75 -0
  81. simple_resume/shell/cli/random_palette_demo.py +407 -0
  82. simple_resume/shell/config.py +96 -0
  83. simple_resume/shell/effect_executor.py +211 -0
  84. simple_resume/shell/file_opener.py +308 -0
  85. simple_resume/shell/generate/__init__.py +37 -0
  86. simple_resume/shell/generate/core.py +650 -0
  87. simple_resume/shell/generate/lazy.py +284 -0
  88. simple_resume/shell/io_utils.py +199 -0
  89. simple_resume/shell/palettes/__init__.py +1 -0
  90. simple_resume/shell/palettes/fetch.py +63 -0
  91. simple_resume/shell/palettes/loader.py +321 -0
  92. simple_resume/shell/palettes/remote.py +179 -0
  93. simple_resume/shell/pdf_executor.py +52 -0
  94. simple_resume/shell/py.typed +0 -0
  95. simple_resume/shell/render/__init__.py +1 -0
  96. simple_resume/shell/render/latex.py +308 -0
  97. simple_resume/shell/render/operations.py +240 -0
  98. simple_resume/shell/resume_extensions.py +737 -0
  99. simple_resume/shell/runtime/__init__.py +7 -0
  100. simple_resume/shell/runtime/content.py +190 -0
  101. simple_resume/shell/runtime/generate.py +497 -0
  102. simple_resume/shell/runtime/lazy.py +138 -0
  103. simple_resume/shell/runtime/lazy_import.py +173 -0
  104. simple_resume/shell/service_locator.py +80 -0
  105. simple_resume/shell/services.py +256 -0
  106. simple_resume/shell/session/__init__.py +6 -0
  107. simple_resume/shell/session/config.py +35 -0
  108. simple_resume/shell/session/manage.py +386 -0
  109. simple_resume/shell/strategies.py +181 -0
  110. simple_resume/shell/themes/__init__.py +35 -0
  111. simple_resume/shell/themes/loader.py +230 -0
  112. simple_resume-0.1.9.dist-info/METADATA +201 -0
  113. simple_resume-0.1.9.dist-info/RECORD +116 -0
  114. simple_resume-0.1.9.dist-info/WHEEL +4 -0
  115. simple_resume-0.1.9.dist-info/entry_points.txt +5 -0
  116. 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
+ ]