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,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
|
+
]
|