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,386 @@
1
+ """Manage sessions for simple-resume operations.
2
+
3
+ This module provides `ResumeSession` for managing consistent configuration
4
+ across multiple resume operations, similar to `requests.Session`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ import uuid
11
+ from collections.abc import Generator
12
+ from contextlib import contextmanager
13
+ from dataclasses import replace as dataclass_replace
14
+ from pathlib import Path
15
+ from types import TracebackType
16
+ from typing import Any
17
+
18
+ from simple_resume.core.constants import OutputFormat
19
+ from simple_resume.core.dependencies import (
20
+ DefaultResumeConfigurator,
21
+ DefaultResumeLoader,
22
+ DIContainer,
23
+ MemoryResumeCache,
24
+ )
25
+ from simple_resume.core.exceptions import ConfigurationError, SessionError
26
+ from simple_resume.core.file_operations import find_yaml_files
27
+ from simple_resume.core.paths import Paths
28
+ from simple_resume.core.result import BatchGenerationResult, GenerationResult
29
+ from simple_resume.core.resume import Resume
30
+ from simple_resume.shell.config import resolve_paths
31
+ from simple_resume.shell.resume_extensions import to_html, to_pdf
32
+ from simple_resume.shell.session.config import SessionConfig
33
+
34
+
35
+ class ResumeSession:
36
+ """Manage resume operations with consistent configuration.
37
+
38
+ Similar to `requests.Session`, `ResumeSession` provides:
39
+ - Consistent configuration across operations.
40
+ - State management for path resolution.
41
+ - Batch operations support.
42
+ - Resource cleanup.
43
+
44
+ Usage:
45
+ with ResumeSession(data_dir="my_resumes") as session:
46
+ resume = session.resume("my_resume")
47
+ result = resume.to_pdf(open_after=True)
48
+ session.generate_all(format="html")
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ data_dir: str | Path | None = None,
54
+ *,
55
+ paths: Paths | None = None,
56
+ config: SessionConfig | None = None,
57
+ **path_overrides: str | Path,
58
+ ) -> None:
59
+ """Initialize a `ResumeSession`.
60
+
61
+ Args:
62
+ data_dir: Base data directory containing input/output folders.
63
+ paths: Pre-resolved paths object.
64
+ config: Session configuration.
65
+ **path_overrides: Path configuration overrides.
66
+
67
+ Raises:
68
+ `ConfigurationError`: If path configuration is invalid.
69
+ `SessionError`: If session initialization fails.
70
+
71
+ """
72
+ self._session_id = str(uuid.uuid4())
73
+ self._created_at = time.time()
74
+ self._is_active = True
75
+
76
+ # Configure paths
77
+ if path_overrides and paths is not None:
78
+ raise ConfigurationError("Provide either paths or path_overrides, not both")
79
+
80
+ if paths is not None:
81
+ self._paths = paths
82
+ else:
83
+ try:
84
+ self._paths = resolve_paths(data_dir, **path_overrides)
85
+ except Exception as exc:
86
+ raise ConfigurationError(f"Failed to resolve paths: {exc}") from exc
87
+
88
+ # Configure session settings
89
+ self._config = config or SessionConfig()
90
+ if self._config.paths is None:
91
+ self._config.paths = self._paths
92
+
93
+ # Apply output directory override if specified
94
+ if self._config.output_dir:
95
+ self._paths = dataclass_replace(self._paths, output=self._config.output_dir)
96
+ self._config.paths = self._paths
97
+
98
+ # Initialize dependency injection
99
+ container = DIContainer(
100
+ resume_loader=DefaultResumeLoader(),
101
+ resume_cache=MemoryResumeCache(),
102
+ resume_configurator=DefaultResumeConfigurator(),
103
+ )
104
+ self._repository = container.create_resume_repository()
105
+
106
+ # Track session statistics
107
+ self._operation_count = 0
108
+ self._generation_times: list[float] = []
109
+
110
+ @property
111
+ def session_id(self) -> str:
112
+ """Return the unique session identifier."""
113
+ return self._session_id
114
+
115
+ @property
116
+ def paths(self) -> Paths:
117
+ """Return the resolved paths for this session."""
118
+ return self._paths
119
+
120
+ @property
121
+ def config(self) -> SessionConfig:
122
+ """Return the session configuration."""
123
+ return self._config
124
+
125
+ @property
126
+ def is_active(self) -> bool:
127
+ """Check if the session is currently active."""
128
+ return self._is_active
129
+
130
+ @property
131
+ def operation_count(self) -> int:
132
+ """Return the number of operations performed in this session."""
133
+ return self._operation_count
134
+
135
+ @property
136
+ def average_generation_time(self) -> float:
137
+ """Return the average generation time for this session."""
138
+ if not self._generation_times:
139
+ return 0.0
140
+ return sum(self._generation_times) / len(self._generation_times)
141
+
142
+ def resume(self, name: str, *, use_cache: bool = True) -> Resume:
143
+ """Load a resume within this session context.
144
+
145
+ Args:
146
+ name: Resume identifier without extension.
147
+ use_cache: Whether to use cached resume data if available.
148
+
149
+ Returns:
150
+ `Resume` instance loaded with session configuration.
151
+
152
+ Raises:
153
+ `SessionError`: If the session is not active or resume loading fails.
154
+
155
+ """
156
+ if not self._is_active:
157
+ raise SessionError(
158
+ "Cannot load resume - session is not active",
159
+ session_id=self._session_id,
160
+ )
161
+
162
+ try:
163
+ # Use dependency injection for resume loading
164
+ resume = self._repository.get_resume(
165
+ name=name,
166
+ paths=self._paths,
167
+ use_cache=use_cache,
168
+ config=self._config,
169
+ )
170
+
171
+ self._operation_count += 1
172
+ return resume
173
+
174
+ except Exception as exc:
175
+ if isinstance(exc, SessionError):
176
+ raise
177
+ raise SessionError(
178
+ f"Failed to load resume '{name}': {exc}", session_id=self._session_id
179
+ ) from exc
180
+
181
+ def generate_all(
182
+ self,
183
+ format: OutputFormat | str = OutputFormat.PDF,
184
+ *,
185
+ pattern: str = "*",
186
+ open_after: bool | None = None,
187
+ parallel: bool = False,
188
+ **kwargs: Any,
189
+ ) -> BatchGenerationResult:
190
+ """Generate all resumes in the session.
191
+
192
+ Args:
193
+ format: Output format ("pdf" or "html").
194
+ pattern: Glob pattern for resume names (default: all).
195
+ open_after: Whether to open generated files (overrides session default).
196
+ parallel: Whether to generate in parallel (future enhancement).
197
+ **kwargs: Additional generation options.
198
+
199
+ Returns:
200
+ `BatchGenerationResult` with all generation results.
201
+
202
+ Raises:
203
+ `SessionError`: If the session is not active.
204
+ `ValueError`: If the format is not supported.
205
+
206
+ """
207
+ if not self._is_active:
208
+ raise SessionError(
209
+ "Cannot generate resumes - session is not active",
210
+ session_id=self._session_id,
211
+ )
212
+
213
+ try:
214
+ format_enum = OutputFormat.normalize(format)
215
+ except (ValueError, TypeError):
216
+ raise ValueError(
217
+ f"Unsupported format: {format}. Use 'pdf' or 'html'."
218
+ ) from None
219
+
220
+ if format_enum not in (OutputFormat.PDF, OutputFormat.HTML):
221
+ raise ValueError(
222
+ f"Unsupported format: {format_enum.value}. Use 'pdf' or 'html'."
223
+ )
224
+
225
+ # Use session default for open_after if not specified
226
+ if open_after is None:
227
+ open_after = self._config.auto_open
228
+
229
+ start_time = time.time()
230
+ results: dict[str, GenerationResult] = {}
231
+ errors: dict[str, Exception] = {}
232
+
233
+ # Find all YAML files in input directory
234
+ yaml_files = self._find_yaml_files(pattern)
235
+
236
+ if not yaml_files:
237
+ return BatchGenerationResult(
238
+ results={},
239
+ total_time=time.time() - start_time,
240
+ successful=0,
241
+ failed=0,
242
+ errors={},
243
+ )
244
+
245
+ # Generate each resume
246
+ for yaml_file in yaml_files:
247
+ resume_name = yaml_file.stem
248
+ try:
249
+ # Load and generate resume
250
+ resume = self.resume(resume_name, use_cache=True)
251
+
252
+ # Apply config overrides (like palette_file) to resume
253
+ if kwargs:
254
+ resume = resume.with_config(**kwargs)
255
+
256
+ if format_enum is OutputFormat.PDF:
257
+ result = to_pdf(resume, open_after=open_after)
258
+ else: # html
259
+ result = to_html(resume, open_after=open_after)
260
+
261
+ results[resume_name] = result
262
+ self._generation_times.append(time.time() - start_time)
263
+
264
+ except Exception as exc:
265
+ errors[resume_name] = exc
266
+ continue
267
+
268
+ total_time = time.time() - start_time
269
+ self._operation_count += len(yaml_files)
270
+
271
+ return BatchGenerationResult(
272
+ results=results,
273
+ total_time=total_time,
274
+ successful=len(results),
275
+ failed=len(errors),
276
+ errors=errors,
277
+ formats=[format_enum.value],
278
+ )
279
+
280
+ def _find_yaml_files(self, pattern: str = "*") -> list[Path]:
281
+ """Find YAML files matching the given pattern."""
282
+ return find_yaml_files(self._paths.input, pattern)
283
+
284
+ def invalidate_cache(self, name: str | None = None) -> None:
285
+ """Invalidate cached resume data.
286
+
287
+ Args:
288
+ name: Specific resume name to invalidate, or `None` for all.
289
+
290
+ """
291
+ self._repository.invalidate_cache(name)
292
+
293
+ def get_cache_info(self) -> dict[str, Any]:
294
+ """Return information about cached resume data.
295
+
296
+ Returns:
297
+ Dictionary with cache statistics.
298
+
299
+ """
300
+ return {
301
+ "cached_resumes": self._repository.get_cache_keys(),
302
+ "cache_size": self._repository.get_cache_size(),
303
+ "memory_usage_estimate": self._repository.get_memory_usage(),
304
+ }
305
+
306
+ def close(self) -> None:
307
+ """Close the session and clean up resources.
308
+
309
+ This method is called automatically when using the context manager.
310
+ """
311
+ if self._is_active:
312
+ # Clear cache
313
+ self._repository.clear_cache()
314
+ self._generation_times.clear()
315
+ self._is_active = False
316
+
317
+ def __enter__(self) -> ResumeSession:
318
+ """Provide context manager entry."""
319
+ if not self._is_active:
320
+ raise SessionError(
321
+ "Cannot enter inactive session", session_id=self._session_id
322
+ )
323
+ return self
324
+
325
+ def __exit__(
326
+ self,
327
+ exc_type: type[BaseException] | None,
328
+ exc_val: BaseException | None,
329
+ exc_tb: TracebackType | None,
330
+ ) -> None:
331
+ """Provide context manager exit."""
332
+ self.close()
333
+
334
+ def __repr__(self) -> str:
335
+ """Return a detailed string representation."""
336
+ return (
337
+ f"ResumeSession(id={self._session_id[:8]}..., "
338
+ f"active={self._is_active}, operations={self._operation_count})"
339
+ )
340
+
341
+ def __str__(self) -> str:
342
+ """Return a string representation of the session."""
343
+ return f"ResumeSession({self._session_id[:8]}...)"
344
+
345
+
346
+ # Convenience function for creating sessions
347
+ @contextmanager
348
+ def create_session(
349
+ data_dir: str | Path | None = None,
350
+ *,
351
+ paths: Paths | None = None,
352
+ config: SessionConfig | None = None,
353
+ **path_overrides: str | Path,
354
+ ) -> Generator[ResumeSession, None, None]:
355
+ """Create and manage a `ResumeSession` context.
356
+
357
+ This is a convenience function for creating sessions with common defaults.
358
+
359
+ Args:
360
+ data_dir: Base data directory.
361
+ paths: Optional pre-resolved `Paths` object.
362
+ config: Optional session configuration.
363
+ **path_overrides: Path configuration overrides.
364
+
365
+ Yields:
366
+ `ResumeSession` instance.
367
+
368
+ """
369
+ session = ResumeSession(
370
+ data_dir,
371
+ paths=paths,
372
+ config=config,
373
+ **path_overrides,
374
+ )
375
+ try:
376
+ with session as s:
377
+ yield s
378
+ finally:
379
+ session.close()
380
+
381
+
382
+ __all__ = [
383
+ "ResumeSession",
384
+ "SessionConfig",
385
+ "create_session",
386
+ ]
@@ -0,0 +1,181 @@
1
+ """PDF generation strategies.
2
+
3
+ This module contains the strategy implementations that coordinate
4
+ between core business logic and shell I/O operations.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import shutil
11
+ import subprocess # nosec B404
12
+ import sys
13
+ from abc import ABC, abstractmethod
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from simple_resume.core.exceptions import GenerationError
19
+ from simple_resume.core.generate.pdf import prepare_pdf_with_latex
20
+ from simple_resume.core.latex.types import LatexGenerationContext
21
+ from simple_resume.core.models import RenderPlan
22
+ from simple_resume.core.paths import Paths
23
+ from simple_resume.core.result import GenerationResult
24
+ from simple_resume.shell.effect_executor import EffectExecutor
25
+ from simple_resume.shell.render.latex import LatexCompilationError
26
+ from simple_resume.shell.render.operations import generate_pdf_with_weasyprint
27
+
28
+
29
+ class PdfGenerationStrategy(ABC):
30
+ """Abstract base class for PDF generation strategies."""
31
+
32
+ @abstractmethod
33
+ def generate_pdf(self, request: Any) -> GenerationResult:
34
+ """Generate PDF using the specific strategy."""
35
+ pass
36
+
37
+ @abstractmethod
38
+ def get_template_name(self, render_plan: RenderPlan) -> str:
39
+ """Get the template name for metadata purposes."""
40
+ pass
41
+
42
+
43
+ @dataclass(slots=True)
44
+ class PdfGenerationRequest:
45
+ """Request data for PDF generation."""
46
+
47
+ render_plan: RenderPlan
48
+ output_path: Path
49
+ open_after: bool = False
50
+ filename: str | None = None
51
+ resume_name: str = "resume"
52
+ raw_data: dict[str, Any] | None = None
53
+ processed_data: dict[str, Any] | None = None
54
+ paths: Paths | None = None
55
+
56
+
57
+ class WeasyPrintStrategy(PdfGenerationStrategy):
58
+ """PDF generation strategy using WeasyPrint backend."""
59
+
60
+ def generate_pdf(self, request: PdfGenerationRequest) -> GenerationResult:
61
+ """Generate PDF using WeasyPrint backend."""
62
+ result, _ = generate_pdf_with_weasyprint(
63
+ request.render_plan,
64
+ request.output_path,
65
+ resume_name=request.resume_name,
66
+ filename=request.filename,
67
+ )
68
+
69
+ # Open file if requested
70
+ if request.open_after and result.exists:
71
+ try:
72
+ # Robustly obtain a filesystem-safe string without instantiating
73
+ # platform-specific Path subclasses (e.g., WindowsPath) on
74
+ # non-Windows CI runners that patch os.name/sys.platform.
75
+ try:
76
+ path_to_open = os.fspath(result.output_path)
77
+ except (TypeError, AttributeError, NotImplementedError):
78
+ path_to_open = str(result.output_path)
79
+ if sys.platform.startswith("darwin"):
80
+ opener = shutil.which("open") or "open"
81
+ subprocess.Popen( # noqa: S603 # nosec B603
82
+ [opener, path_to_open],
83
+ stdout=subprocess.DEVNULL,
84
+ stderr=subprocess.DEVNULL,
85
+ )
86
+ elif os.name == "nt":
87
+ os.startfile(path_to_open) # type: ignore[attr-defined] # noqa: S606 # nosec B606
88
+ else:
89
+ opener = shutil.which("xdg-open")
90
+ if opener:
91
+ subprocess.Popen( # noqa: S603 # nosec B603
92
+ [opener, path_to_open],
93
+ stdout=subprocess.DEVNULL,
94
+ stderr=subprocess.DEVNULL,
95
+ )
96
+ except Exception as exc: # noqa: BLE001
97
+ print(f"Warning: Could not open file: {exc}", file=sys.stderr)
98
+
99
+ return result
100
+
101
+ def get_template_name(self, render_plan: RenderPlan) -> str:
102
+ """Get template name for WeasyPrint mode."""
103
+ return render_plan.template_name or "unknown"
104
+
105
+
106
+ class LatexStrategy(PdfGenerationStrategy):
107
+ """PDF generation strategy using LaTeX backend."""
108
+
109
+ def generate_pdf(self, request: PdfGenerationRequest) -> GenerationResult:
110
+ """Generate PDF using LaTeX backend."""
111
+ # Create generation context
112
+ context = LatexGenerationContext(
113
+ resume_data=request.raw_data,
114
+ processed_data=request.processed_data or {},
115
+ output_path=request.output_path,
116
+ filename=request.filename,
117
+ paths=request.paths,
118
+ )
119
+
120
+ # Prepare LaTeX generation (pure function returns effects)
121
+ try:
122
+ tex_content, effects, metadata = prepare_pdf_with_latex(
123
+ request.render_plan,
124
+ request.output_path,
125
+ context,
126
+ )
127
+
128
+ # Execute the effects to create files and run pdflatex
129
+ executor = EffectExecutor()
130
+ executor.execute_many(effects)
131
+
132
+ except LatexCompilationError as exc:
133
+ # Convert shell-layer exception to core-layer exception
134
+ raise GenerationError(
135
+ f"LaTeX compilation failed: {exc}",
136
+ output_path=request.output_path,
137
+ format_type="pdf",
138
+ resume_name=request.resume_name,
139
+ ) from exc
140
+
141
+ # Create result from metadata
142
+ generation_result = GenerationResult(
143
+ output_path=request.output_path,
144
+ format_type="pdf",
145
+ metadata=metadata,
146
+ )
147
+
148
+ # Open file if requested
149
+ if request.open_after and generation_result.output_path.exists():
150
+ try:
151
+ if sys.platform.startswith("darwin"):
152
+ opener = shutil.which("open") or "open"
153
+ subprocess.Popen( # noqa: S603 # nosec B603
154
+ [opener, str(generation_result.output_path)],
155
+ stdout=subprocess.DEVNULL,
156
+ stderr=subprocess.DEVNULL,
157
+ )
158
+ elif os.name == "nt":
159
+ os.startfile(str(generation_result.output_path)) # type: ignore[attr-defined] # noqa: S606 # nosec B606
160
+ else:
161
+ opener = shutil.which("xdg-open")
162
+ if opener:
163
+ subprocess.Popen( # noqa: S603 # nosec B603
164
+ [opener, str(generation_result.output_path)],
165
+ stdout=subprocess.DEVNULL,
166
+ stderr=subprocess.DEVNULL,
167
+ )
168
+ except Exception as exc: # noqa: BLE001
169
+ print(f"Warning: Could not open file: {exc}", file=sys.stderr)
170
+
171
+ return generation_result
172
+
173
+ def get_template_name(self, render_plan: RenderPlan) -> str:
174
+ """Get template name for LaTeX mode."""
175
+ return render_plan.template_name or "latex/basic.tex"
176
+
177
+
178
+ __all__ = [
179
+ "WeasyPrintStrategy",
180
+ "LatexStrategy",
181
+ ]
@@ -0,0 +1,35 @@
1
+ """Theme management for Simple Resume.
2
+
3
+ This module provides theme loading and application functionality,
4
+ allowing users to apply pre-built themes or create custom ones.
5
+
6
+ Usage in resume YAML:
7
+
8
+ # Apply a built-in theme
9
+ theme: modern
10
+
11
+ # Override specific theme values
12
+ theme: modern
13
+ config:
14
+ sidebar_width: 70 # Overrides theme default
15
+
16
+ Available themes: modern, classic, bold, minimal, executive
17
+ """
18
+
19
+ from simple_resume.shell.themes.loader import (
20
+ apply_theme_to_config,
21
+ clear_theme_cache,
22
+ get_themes_directory,
23
+ list_available_themes,
24
+ load_theme,
25
+ resolve_theme_in_data,
26
+ )
27
+
28
+ __all__ = [
29
+ "apply_theme_to_config",
30
+ "clear_theme_cache",
31
+ "get_themes_directory",
32
+ "list_available_themes",
33
+ "load_theme",
34
+ "resolve_theme_in_data",
35
+ ]