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,659 @@
1
+ """Provide PDF rendering helpers for the core resume pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any, Callable
8
+
9
+ import simple_resume.core.latex.types as latex_types
10
+ from simple_resume.core.constants import RenderMode
11
+ from simple_resume.core.effects import Effect, MakeDirectory, RenderPdf, WriteFile
12
+ from simple_resume.core.exceptions import ConfigurationError
13
+ from simple_resume.core.generate.exceptions import TemplateError
14
+ from simple_resume.core.latex.types import LatexGenerationContext
15
+ from simple_resume.core.models import RenderPlan
16
+ from simple_resume.core.protocols import EffectExecutor, LaTeXRenderer, TemplateLocator
17
+ from simple_resume.core.render import get_template_environment
18
+ from simple_resume.core.result import GenerationMetadata, GenerationResult
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class _PdfGenerationParams:
23
+ """Parameters for PDF generation to reduce function argument count."""
24
+
25
+ render_plan: RenderPlan
26
+ output_path: Path
27
+ resume_name: str
28
+ filename: str | None = None
29
+ template_locator: TemplateLocator | None = None
30
+ latex_renderer: LaTeXRenderer | None = None
31
+ effect_executor: EffectExecutor | None = None
32
+ existing_context: LatexGenerationContext | None = None
33
+
34
+
35
+ def get_latex_functions(latex_renderer: LaTeXRenderer) -> tuple[Any, Any, Any]:
36
+ """Get LaTeX functions from provided renderer.
37
+
38
+ Args:
39
+ latex_renderer: LaTeX renderer implementation
40
+
41
+ Returns:
42
+ Tuple of (LatexCompilationError, compile_tex_to_pdf,
43
+ render_resume_latex_from_data)
44
+ Returns (None, None, None) if LaTeX module is not available.
45
+
46
+ """
47
+ return latex_renderer.get_latex_functions()
48
+
49
+
50
+ class PdfGeneratorFactory:
51
+ """Factory for creating PDF generation functions with configured dependencies."""
52
+
53
+ def __init__(
54
+ self,
55
+ effect_executor: EffectExecutor | None = None,
56
+ template_locator: TemplateLocator | None = None,
57
+ latex_renderer: LaTeXRenderer | None = None,
58
+ ):
59
+ """Initialize factory with optional default dependencies.
60
+
61
+ Args:
62
+ effect_executor: Default effect executor to use when none is injected
63
+ template_locator: Default template locator to use when none is injected
64
+ latex_renderer: Default LaTeX renderer to use when none is injected
65
+
66
+ """
67
+ self._effect_executor = effect_executor
68
+ self._template_locator = template_locator
69
+ self._latex_renderer = latex_renderer
70
+
71
+ def _get_effect_executor(self, injected: EffectExecutor | None) -> EffectExecutor:
72
+ """Get effect executor, preferring injected over default."""
73
+ if injected is not None:
74
+ return injected
75
+ if self._effect_executor is not None:
76
+ return self._effect_executor
77
+ raise ConfigurationError(
78
+ "No effect executor available. "
79
+ "Either inject one or ensure factory is configured with a default."
80
+ )
81
+
82
+ def _get_template_locator(
83
+ self, injected: TemplateLocator | None
84
+ ) -> TemplateLocator:
85
+ """Get template locator, preferring injected over default."""
86
+ if injected is not None:
87
+ return injected
88
+ if self._template_locator is not None:
89
+ return self._template_locator
90
+ raise ConfigurationError(
91
+ "No template locator available. "
92
+ "Inject one or configure the factory with a default."
93
+ )
94
+
95
+ def _get_latex_renderer(self, injected: LaTeXRenderer | None) -> LaTeXRenderer:
96
+ """Get LaTeX renderer, preferring injected over default."""
97
+ if injected is not None:
98
+ return injected
99
+ if self._latex_renderer is not None:
100
+ return self._latex_renderer
101
+ raise ConfigurationError(
102
+ "No LaTeX renderer available. "
103
+ "Inject one or configure the factory with a default."
104
+ )
105
+
106
+ def create_prepare_pdf_with_weasyprint_function(
107
+ self,
108
+ ) -> Callable[..., tuple[str, list[Effect], GenerationMetadata]]:
109
+ """Create a prepare_pdf_with_weasyprint function with factory's dependencies.
110
+
111
+ Returns:
112
+ A function that takes (render_plan, output_path, **kwargs) and returns
113
+ (html_content, effects, metadata)
114
+
115
+ """
116
+ factory = self
117
+
118
+ def prepare_pdf_with_weasyprint(
119
+ render_plan: RenderPlan,
120
+ output_path: Path,
121
+ *,
122
+ resume_name: str,
123
+ filename: str | None = None,
124
+ template_locator: TemplateLocator | None = None,
125
+ ) -> tuple[str, list[Effect], GenerationMetadata]:
126
+ """Prepare PDF generation using WeasyPrint (pure function).
127
+
128
+ This function performs NO I/O operations. It prepares HTML content and
129
+ returns a list of effects that the shell layer should execute.
130
+
131
+ Args:
132
+ render_plan: Rendering configuration and context
133
+ output_path: Target PDF file path
134
+ resume_name: Name of the resume
135
+ filename: Source filename for error messages
136
+ template_locator: Optional template locator for dependency injection
137
+
138
+ Returns:
139
+ Tuple of (html_content, effects, metadata)
140
+ - html_content: Rendered HTML as string
141
+ - effects: List of effects to execute (MakeDirectory, WriteFile)
142
+ - metadata: Generation metadata
143
+
144
+ Raises:
145
+ TemplateError: If render plan is invalid or uses LaTeX mode
146
+
147
+ """
148
+ params = _PdfGenerationParams(
149
+ render_plan=render_plan,
150
+ output_path=output_path,
151
+ resume_name=resume_name,
152
+ filename=filename,
153
+ template_locator=template_locator,
154
+ )
155
+ return _prepare_pdf_with_weasyprint_impl(
156
+ params,
157
+ factory,
158
+ )
159
+
160
+ return prepare_pdf_with_weasyprint
161
+
162
+ def create_generate_pdf_with_weasyprint_function(
163
+ self,
164
+ ) -> Callable[..., tuple[GenerationResult, int | None]]:
165
+ """Create a generate_pdf_with_weasyprint function with factory's dependencies.
166
+
167
+ Returns:
168
+ A function that takes (render_plan, output_path, **kwargs) and returns
169
+ (result, page_count)
170
+
171
+ """
172
+ factory = self
173
+
174
+ def generate_pdf_with_weasyprint(
175
+ render_plan: RenderPlan,
176
+ output_path: Path,
177
+ *,
178
+ resume_name: str,
179
+ filename: str | None = None,
180
+ effect_executor: EffectExecutor | None = None,
181
+ ) -> tuple[GenerationResult, int | None]:
182
+ """Generate PDF using WeasyPrint (shell execution).
183
+
184
+ This function executes effects produced by prepare_pdf_with_weasyprint
185
+ and returns result and page count.
186
+ """
187
+ # Prepare PDF generation in core (pure)
188
+ html_content, effects, metadata = (
189
+ factory.create_prepare_pdf_with_weasyprint_function()(
190
+ render_plan=render_plan,
191
+ output_path=output_path,
192
+ resume_name=resume_name,
193
+ filename=filename,
194
+ )
195
+ )
196
+
197
+ # Execute effects using injected or default executor
198
+ executor = factory._get_effect_executor(effect_executor)
199
+ page_count: int | None = None
200
+ for effect in effects:
201
+ result = executor.execute(effect)
202
+ if isinstance(effect, RenderPdf) and isinstance(result, int):
203
+ page_count = result
204
+
205
+ # Create and return result
206
+ result = GenerationResult(
207
+ output_path=output_path,
208
+ format_type="pdf",
209
+ metadata=metadata,
210
+ )
211
+
212
+ return result, page_count
213
+
214
+ return generate_pdf_with_weasyprint
215
+
216
+ def create_prepare_pdf_with_latex_function(
217
+ self,
218
+ ) -> Callable[..., tuple[str, list[Effect], GenerationMetadata]]:
219
+ """Create a prepare_pdf_with_latex function with factory's dependencies.
220
+
221
+ Returns:
222
+ A function that takes (render_plan, output_path, **kwargs) and returns
223
+ (tex_content, effects, metadata)
224
+
225
+ """
226
+ factory = self
227
+
228
+ def prepare_pdf_with_latex(
229
+ render_plan: RenderPlan,
230
+ output_path: Path,
231
+ config: PdfGenerationConfig,
232
+ ) -> tuple[str, list[Effect], GenerationMetadata]:
233
+ """Prepare PDF generation using LaTeX (pure function).
234
+
235
+ This function performs NO I/O operations. It prepares LaTeX content and
236
+ returns a list of effects that the shell layer should execute.
237
+
238
+ Args:
239
+ render_plan: Rendering configuration and context
240
+ output_path: Target PDF file path
241
+ config: Configuration for PDF generation including resume details
242
+ and dependencies
243
+ resume_name: Name of the resume
244
+ filename: Source filename for error messages
245
+ template_locator: Optional template locator for dependency injection
246
+ latex_renderer: Optional LaTeX renderer for dependency injection
247
+
248
+ Returns:
249
+ Tuple of (tex_content, effects, metadata)
250
+ - tex_content: Rendered LaTeX as string
251
+ - effects: List of effects to execute (MakeDirectory, WriteFile)
252
+ - metadata: Generation metadata
253
+
254
+ Raises:
255
+ TemplateError: If render plan is invalid or uses HTML mode
256
+ LaTeXCompilationError: If LaTeX compilation fails
257
+ FileNotFoundError: If required files are missing
258
+
259
+ """
260
+ params = _PdfGenerationParams(
261
+ render_plan=render_plan,
262
+ output_path=output_path,
263
+ resume_name=config.resume_name,
264
+ filename=config.filename,
265
+ template_locator=config.template_locator,
266
+ latex_renderer=config.latex_renderer,
267
+ )
268
+ return _prepare_pdf_with_latex_impl(
269
+ params,
270
+ factory,
271
+ )
272
+
273
+ return prepare_pdf_with_latex
274
+
275
+ def create_generate_pdf_with_latex_function(
276
+ self,
277
+ ) -> Callable[..., tuple[GenerationResult, int | None]]:
278
+ """Create a generate_pdf_with_latex function with factory's dependencies.
279
+
280
+ Returns:
281
+ A function that takes (render_plan, output_path, **kwargs) and returns
282
+ (result, page_count)
283
+
284
+ """
285
+ factory = self
286
+
287
+ def generate_pdf_with_latex( # noqa: PLR0913
288
+ render_plan: RenderPlan,
289
+ output_path: Path,
290
+ *,
291
+ resume_name: str,
292
+ filename: str | None = None,
293
+ template_locator: TemplateLocator | None = None,
294
+ latex_renderer: LaTeXRenderer | None = None,
295
+ effect_executor: EffectExecutor | None = None,
296
+ ) -> tuple[GenerationResult, int | None]:
297
+ """Generate PDF using LaTeX (shell execution).
298
+
299
+ This function executes effects produced by prepare_pdf_with_latex
300
+ and returns result and page count.
301
+ """
302
+ # Prepare PDF generation in core (pure)
303
+ tex_content, effects, metadata = (
304
+ factory.create_prepare_pdf_with_latex_function()(
305
+ render_plan=render_plan,
306
+ output_path=output_path,
307
+ resume_name=resume_name,
308
+ filename=filename,
309
+ template_locator=template_locator,
310
+ )
311
+ )
312
+
313
+ # Execute effects using injected or default executor
314
+ executor = factory._get_effect_executor(effect_executor)
315
+ executor.execute_many(effects)
316
+
317
+ # Extract page count from prepared metadata
318
+ page_count = (
319
+ metadata.page_count if hasattr(metadata, "page_count") else None
320
+ )
321
+
322
+ # Create and return result
323
+ result = GenerationResult(
324
+ output_path=output_path,
325
+ format_type="pdf",
326
+ metadata=metadata,
327
+ )
328
+
329
+ return result, page_count
330
+
331
+ return generate_pdf_with_latex
332
+
333
+
334
+ def _prepare_pdf_with_weasyprint_impl(
335
+ params: _PdfGenerationParams,
336
+ factory: PdfGeneratorFactory,
337
+ ) -> tuple[str, list[Effect], GenerationMetadata]:
338
+ """Implement WeasyPrint PDF generation using factory for dependencies.
339
+
340
+ Args:
341
+ params: Parameters containing render_plan, output_path, resume_name,
342
+ filename, and template_locator
343
+ factory: PDF generator factory for dependency resolution
344
+
345
+ Returns:
346
+ Tuple of (html_content, effects, metadata)
347
+
348
+ Raises:
349
+ TemplateError: If render plan is invalid or uses LaTeX mode
350
+
351
+ """
352
+ if params.render_plan.mode is RenderMode.LATEX:
353
+ raise TemplateError(
354
+ "LaTeX mode not supported in WeasyPrint generation method",
355
+ template_name="latex",
356
+ filename=params.filename,
357
+ )
358
+
359
+ if not params.render_plan.context or not params.render_plan.template_name:
360
+ raise TemplateError(
361
+ "HTML plan missing context or template_name",
362
+ filename=params.filename,
363
+ )
364
+
365
+ # Resolve template location using factory
366
+ locator = factory._get_template_locator(params.template_locator)
367
+ template_loc = locator.get_template_location()
368
+ env = get_template_environment(str(template_loc))
369
+ html = (
370
+ env.get_template(params.render_plan.template_name)
371
+ .render(**params.render_plan.context)
372
+ .lstrip()
373
+ )
374
+
375
+ # Pure operations: Prepare CSS for page size
376
+ page_width = params.render_plan.config.page_width or 210
377
+ page_height = params.render_plan.config.page_height or 297
378
+ css_string = f"@page {{size: {page_width}mm {page_height}mm; margin: 0mm;}}"
379
+
380
+ # Create effects for shell execution (no PDF rendering here)
381
+ # Use static/css as base_url so font paths like "../fonts/AvenirLTStd-Light.otf"
382
+ # resolve correctly to assets/static/fonts/AvenirLTStd-Light.otf
383
+ css_base_url = template_loc.parent / "static" / "css"
384
+ effects: list[Effect] = [
385
+ MakeDirectory(path=params.output_path.parent, parents=True),
386
+ RenderPdf(
387
+ html=html,
388
+ css=css_string,
389
+ output_path=params.output_path,
390
+ base_url=str(css_base_url),
391
+ ),
392
+ ]
393
+
394
+ # Create metadata
395
+ metadata = GenerationMetadata(
396
+ format_type="pdf",
397
+ template_name=params.render_plan.template_name or "unknown",
398
+ generation_time=0.0,
399
+ file_size=0,
400
+ resume_name=params.resume_name,
401
+ palette_info=params.render_plan.palette_metadata,
402
+ page_count=None,
403
+ )
404
+
405
+ return html, effects, metadata
406
+
407
+
408
+ def _prepare_pdf_with_latex_impl(
409
+ params: _PdfGenerationParams,
410
+ factory: PdfGeneratorFactory,
411
+ ) -> tuple[str, list[Effect], GenerationMetadata]:
412
+ """Implement LaTeX PDF generation using factory for dependencies.
413
+
414
+ Args:
415
+ params: Parameters containing render_plan, output_path, resume_name,
416
+ filename, template_locator, and latex_renderer
417
+ factory: PDF generator factory for dependency resolution
418
+
419
+ Returns:
420
+ Tuple of (tex_content, effects, metadata)
421
+
422
+ Raises:
423
+ ConfigurationError: If LaTeX renderer unavailable
424
+
425
+ """
426
+ # Check latex renderer availability
427
+ renderer = factory._get_latex_renderer(params.latex_renderer)
428
+ LatexCompilationError, compile_tex_to_pdf, render_resume_latex_from_data = (
429
+ get_latex_functions(renderer)
430
+ )
431
+ if any(
432
+ func is None
433
+ for func in (
434
+ LatexCompilationError,
435
+ compile_tex_to_pdf,
436
+ render_resume_latex_from_data,
437
+ )
438
+ ):
439
+ raise ConfigurationError(
440
+ "LaTeX renderer unavailable. "
441
+ "Inject a renderer that provides compilation functions."
442
+ )
443
+
444
+ # Prepare LaTeX generation context and resolve paths.
445
+ if params.existing_context is not None:
446
+ context = params.existing_context
447
+ elif latex_types.LatexGenerationContext.last_context is not None:
448
+ context = latex_types.LatexGenerationContext.last_context
449
+ else:
450
+ context = LatexGenerationContext(
451
+ resume_data=params.render_plan.context,
452
+ processed_data=params.render_plan.context or {},
453
+ output_path=params.output_path,
454
+ base_path=params.render_plan.base_path,
455
+ filename=params.filename,
456
+ )
457
+ resolved_paths = context.paths
458
+ if resolved_paths is None:
459
+ # Strictly require resolved paths to avoid implicit template resolution.
460
+ # Tests expect a configuration error when paths are missing so we fail
461
+ # fast instead of attempting to fall back to packaged assets.
462
+ raise ConfigurationError(
463
+ "LaTeX generation requires resolved paths (templates/static). "
464
+ "Provide Paths or configure the shell layer before rendering."
465
+ )
466
+
467
+ # Generate LaTeX content
468
+ try:
469
+ resume_data = (
470
+ context.processed_data
471
+ if isinstance(context.processed_data, dict)
472
+ else context.resume_data
473
+ if isinstance(context.resume_data, dict)
474
+ else params.render_plan.context or {}
475
+ )
476
+ tex_result = render_resume_latex_from_data(
477
+ resume_data,
478
+ paths=resolved_paths,
479
+ template_name=params.render_plan.template_name or "latex/basic.tex",
480
+ )
481
+ tex_content = getattr(tex_result, "tex", tex_result)
482
+ except Exception as exc:
483
+ if "No such file or directory" in str(exc):
484
+ raise FileNotFoundError(
485
+ f"Template not found: {resolved_paths.templates}"
486
+ ) from exc
487
+ raise
488
+
489
+ # Resolve paths for effects
490
+ # resolved_paths is guaranteed by earlier guard
491
+
492
+ # Prepare effects for shell execution
493
+ effects: list[Effect] = [
494
+ MakeDirectory(path=params.output_path.parent, parents=True),
495
+ WriteFile(
496
+ path=params.output_path.with_suffix(".tex"),
497
+ content=tex_content,
498
+ encoding="utf-8",
499
+ ),
500
+ ]
501
+
502
+ # Create metadata
503
+ metadata = GenerationMetadata(
504
+ format_type="pdf",
505
+ template_name=params.render_plan.template_name or "latex",
506
+ generation_time=0.0,
507
+ file_size=len(tex_content.encode("utf-8")),
508
+ resume_name=params.resume_name,
509
+ palette_info=params.render_plan.palette_metadata,
510
+ page_count=context.metadata.page_count
511
+ if hasattr(context.metadata, "page_count")
512
+ else None,
513
+ )
514
+
515
+ return tex_content, effects, metadata
516
+
517
+
518
+ def prepare_pdf_with_weasyprint(
519
+ render_plan: RenderPlan,
520
+ output_path: Path,
521
+ *,
522
+ resume_name: str,
523
+ filename: str | None = None,
524
+ template_locator: TemplateLocator | None = None,
525
+ ) -> tuple[str, list[Effect], GenerationMetadata]:
526
+ """Prepare PDF generation using WeasyPrint (pure function).
527
+
528
+ This function performs NO I/O operations. It prepares HTML content and
529
+ returns a list of effects that the shell layer should execute.
530
+
531
+ Args:
532
+ render_plan: Rendering configuration and context
533
+ output_path: Target PDF file path
534
+ resume_name: Name of the resume
535
+ filename: Source filename for error messages
536
+ template_locator: Optional template locator for dependency injection
537
+
538
+ Returns:
539
+ Tuple of (html_content, effects, metadata)
540
+ - html_content: Rendered HTML as string
541
+ - effects: List of effects to execute (MakeDirectory, WriteFile)
542
+ - metadata: Generation metadata
543
+
544
+ Raises:
545
+ TemplateError: If render plan is invalid or uses LaTeX mode
546
+
547
+ """
548
+ factory = PdfGeneratorFactory()
549
+ params = _PdfGenerationParams(
550
+ render_plan=render_plan,
551
+ output_path=output_path,
552
+ resume_name=resume_name,
553
+ filename=filename,
554
+ template_locator=template_locator,
555
+ )
556
+ return _prepare_pdf_with_weasyprint_impl(params, factory)
557
+
558
+
559
+ class PdfGenerationConfig:
560
+ """Configuration for PDF generation functions."""
561
+
562
+ def __init__(
563
+ self,
564
+ *,
565
+ resume_name: str,
566
+ filename: str | None = None,
567
+ template_locator: TemplateLocator | None = None,
568
+ latex_renderer: LaTeXRenderer | None = None,
569
+ effect_executor: EffectExecutor | None = None,
570
+ ):
571
+ self.resume_name = resume_name
572
+ self.filename = filename
573
+ self.template_locator = template_locator
574
+ self.latex_renderer = latex_renderer
575
+ self.effect_executor = effect_executor
576
+
577
+
578
+ def prepare_pdf_with_latex(
579
+ render_plan: RenderPlan,
580
+ output_path: Path,
581
+ config: PdfGenerationConfig | LatexGenerationContext,
582
+ ) -> tuple[str, list[Effect], GenerationMetadata]:
583
+ """Prepare PDF generation using LaTeX (pure function).
584
+
585
+ This function performs NO I/O operations. It prepares TeX content and
586
+ returns a list of effects that the shell layer should execute.
587
+
588
+ Args:
589
+ render_plan: Rendering configuration and context
590
+ output_path: Target PDF file path
591
+ config: PDF generation configuration
592
+
593
+ Returns:
594
+ Tuple of (tex_content, effects, metadata)
595
+ - tex_content: Rendered TeX source code
596
+ - effects: List of effects to execute
597
+ - metadata: Generation metadata
598
+
599
+ Raises:
600
+ ConfigurationError: If paths is None or LaTeX renderer unavailable
601
+
602
+ """
603
+ factory = PdfGeneratorFactory()
604
+ if isinstance(config, LatexGenerationContext):
605
+ params = _PdfGenerationParams(
606
+ render_plan=render_plan,
607
+ output_path=output_path,
608
+ resume_name=render_plan.name,
609
+ filename=config.filename,
610
+ existing_context=config,
611
+ )
612
+ else:
613
+ params = _PdfGenerationParams(
614
+ render_plan=render_plan,
615
+ output_path=output_path,
616
+ resume_name=config.resume_name,
617
+ filename=config.filename,
618
+ template_locator=config.template_locator,
619
+ latex_renderer=config.latex_renderer,
620
+ effect_executor=config.effect_executor,
621
+ )
622
+ return _prepare_pdf_with_latex_impl(
623
+ params,
624
+ factory,
625
+ )
626
+
627
+
628
+ def generate_pdf_with_weasyprint(
629
+ render_plan: RenderPlan,
630
+ output_path: Path,
631
+ config: PdfGenerationConfig,
632
+ ) -> tuple[GenerationResult, int | None]:
633
+ """Generate PDF using WeasyPrint (shell execution).
634
+
635
+ This function executes the effects produced by prepare_pdf_with_weasyprint
636
+ and returns the result and page count.
637
+ """
638
+ factory = PdfGeneratorFactory(
639
+ effect_executor=config.effect_executor,
640
+ template_locator=config.template_locator,
641
+ latex_renderer=config.latex_renderer,
642
+ )
643
+ generator = factory.create_generate_pdf_with_weasyprint_function()
644
+ return generator(
645
+ render_plan=render_plan,
646
+ output_path=output_path,
647
+ resume_name=config.resume_name,
648
+ filename=config.filename,
649
+ effect_executor=config.effect_executor,
650
+ )
651
+
652
+
653
+ __all__ = [
654
+ "PdfGeneratorFactory",
655
+ "prepare_pdf_with_weasyprint",
656
+ "prepare_pdf_with_latex",
657
+ "generate_pdf_with_weasyprint",
658
+ "get_latex_functions",
659
+ ]