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,211 @@
|
|
|
1
|
+
"""Effect executor for the shell layer.
|
|
2
|
+
|
|
3
|
+
The EffectExecutor performs actual I/O operations
|
|
4
|
+
described by Effect objects.
|
|
5
|
+
This is the "imperative shell" that executes
|
|
6
|
+
side effects created by the "functional core".
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
executor = EffectExecutor()
|
|
10
|
+
effects = [
|
|
11
|
+
MakeDirectory(path=Path("/tmp/output")),
|
|
12
|
+
WriteFile(path=Path("/tmp/output/file.txt"), content="data"),
|
|
13
|
+
]
|
|
14
|
+
executor.execute_many(effects)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess # nosec B404
|
|
19
|
+
import webbrowser
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Callable
|
|
22
|
+
|
|
23
|
+
import weasyprint
|
|
24
|
+
|
|
25
|
+
from simple_resume.core.effects import (
|
|
26
|
+
CopyFile,
|
|
27
|
+
DeleteFile,
|
|
28
|
+
Effect,
|
|
29
|
+
MakeDirectory,
|
|
30
|
+
OpenBrowser,
|
|
31
|
+
RenderPdf,
|
|
32
|
+
RunCommand,
|
|
33
|
+
WriteFile,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class EffectExecutor:
|
|
38
|
+
"""Executes effects in the shell layer.
|
|
39
|
+
|
|
40
|
+
This class performs actual I/O operations based on Effect descriptions.
|
|
41
|
+
It implements the "imperative shell" pattern, isolating all side effects
|
|
42
|
+
from the functional core.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def execute(self, effect: Effect) -> Any:
|
|
46
|
+
"""Execute a single effect.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
effect: The effect to execute
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Result of the operation (type depends on effect)
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ValueError: If effect type is unknown
|
|
56
|
+
Various I/O exceptions: Depending on the operation
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
# Dispatch table for effect types
|
|
60
|
+
handlers: dict[type[Effect], Callable[[Any], Any]] = {
|
|
61
|
+
WriteFile: lambda e: self._write_file(e.path, e.content, e.encoding),
|
|
62
|
+
MakeDirectory: lambda e: self._make_directory(e.path, e.parents),
|
|
63
|
+
DeleteFile: lambda e: self._delete_file(e.path),
|
|
64
|
+
CopyFile: lambda e: self._copy_file(e.source, e.destination),
|
|
65
|
+
OpenBrowser: lambda e: self._open_browser(e.url),
|
|
66
|
+
RunCommand: lambda e: self._run_command(e.command, e.cwd),
|
|
67
|
+
RenderPdf: lambda e: self._render_pdf(
|
|
68
|
+
e.html, e.css, e.output_path, e.base_url
|
|
69
|
+
),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
handler = handlers.get(type(effect))
|
|
73
|
+
if handler is None:
|
|
74
|
+
raise ValueError(f"Unknown effect type: {type(effect)}")
|
|
75
|
+
return handler(effect)
|
|
76
|
+
|
|
77
|
+
def execute_many(self, effects: list[Effect]) -> None:
|
|
78
|
+
"""Execute multiple effects in sequence.
|
|
79
|
+
|
|
80
|
+
Effects are executed in order. If any effect fails, execution stops
|
|
81
|
+
and the exception is propagated.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
effects: List of effects to execute
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
for effect in effects:
|
|
88
|
+
self.execute(effect)
|
|
89
|
+
|
|
90
|
+
def _write_file(self, path: Path, content: str | bytes, encoding: str) -> None:
|
|
91
|
+
"""Write content to a file.
|
|
92
|
+
|
|
93
|
+
Creates parent directories if they don't exist.
|
|
94
|
+
Overwrites existing file content.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
path: Target file path
|
|
98
|
+
content: Content to write (string or bytes)
|
|
99
|
+
encoding: Text encoding (used only for string content)
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
# Ensure parent directories exist
|
|
103
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
|
|
105
|
+
# Write content based on type
|
|
106
|
+
if isinstance(content, bytes):
|
|
107
|
+
path.write_bytes(content)
|
|
108
|
+
else:
|
|
109
|
+
path.write_text(content, encoding=encoding)
|
|
110
|
+
|
|
111
|
+
def _make_directory(self, path: Path, parents: bool) -> None:
|
|
112
|
+
"""Create a directory.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
path: Directory path to create
|
|
116
|
+
parents: If True, create parent directories as needed
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
FileNotFoundError: If parents=False and parent directory doesn't exist
|
|
120
|
+
|
|
121
|
+
"""
|
|
122
|
+
path.mkdir(parents=parents, exist_ok=True)
|
|
123
|
+
|
|
124
|
+
def _delete_file(self, path: Path) -> None:
|
|
125
|
+
"""Delete a file.
|
|
126
|
+
|
|
127
|
+
Does not raise an error if the file doesn't exist.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
path: File path to delete
|
|
131
|
+
|
|
132
|
+
"""
|
|
133
|
+
path.unlink(missing_ok=True)
|
|
134
|
+
|
|
135
|
+
def _copy_file(self, source: Path, destination: Path) -> None:
|
|
136
|
+
"""Copy a file from source to destination.
|
|
137
|
+
|
|
138
|
+
Creates parent directories if they don't exist.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
source: Source file path
|
|
142
|
+
destination: Destination file path
|
|
143
|
+
|
|
144
|
+
"""
|
|
145
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
shutil.copy2(source, destination)
|
|
147
|
+
|
|
148
|
+
def _open_browser(self, url: str) -> None:
|
|
149
|
+
"""Open a URL in the default web browser.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
url: URL to open (http://, https://, or file://)
|
|
153
|
+
|
|
154
|
+
"""
|
|
155
|
+
webbrowser.open(url)
|
|
156
|
+
|
|
157
|
+
def _run_command(
|
|
158
|
+
self, command: list[str], cwd: Path | None
|
|
159
|
+
) -> subprocess.CompletedProcess[bytes]:
|
|
160
|
+
"""Execute a shell command.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
command: Command to run as a list of arguments
|
|
164
|
+
cwd: Working directory for command execution
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
CompletedProcess object with execution results
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
CalledProcessError: If command exits with non-zero status
|
|
171
|
+
|
|
172
|
+
"""
|
|
173
|
+
# Validate command for security
|
|
174
|
+
if isinstance(command, (list, tuple)):
|
|
175
|
+
unsafe_chars = [";", "|", "&"]
|
|
176
|
+
if any(any(char in str(arg) for char in unsafe_chars) for arg in command):
|
|
177
|
+
raise ValueError("Unsafe command detected")
|
|
178
|
+
elif isinstance(command, str):
|
|
179
|
+
if ";" in command or "|" in command or "&" in command:
|
|
180
|
+
raise ValueError("Unsafe command detected")
|
|
181
|
+
|
|
182
|
+
return subprocess.run(command, cwd=cwd, check=True) # noqa: S603 # nosec B603
|
|
183
|
+
|
|
184
|
+
def _render_pdf(
|
|
185
|
+
self, html: str, css: str, output_path: Path, base_url: str | None
|
|
186
|
+
) -> int | None:
|
|
187
|
+
"""Render HTML+CSS to PDF using WeasyPrint."""
|
|
188
|
+
html_doc = weasyprint.HTML(string=html, base_url=base_url)
|
|
189
|
+
css_obj = weasyprint.CSS(string=css)
|
|
190
|
+
document = html_doc.render(stylesheets=[css_obj])
|
|
191
|
+
pdf_bytes = document.write_pdf()
|
|
192
|
+
if not isinstance(pdf_bytes, (bytes, bytearray)):
|
|
193
|
+
# Guard against test doubles returning non-bytes payloads.
|
|
194
|
+
try:
|
|
195
|
+
pdf_bytes = bytes(pdf_bytes)
|
|
196
|
+
except Exception as exc:
|
|
197
|
+
raise RuntimeError(
|
|
198
|
+
"WeasyPrint returned invalid output (not bytes)"
|
|
199
|
+
) from exc
|
|
200
|
+
|
|
201
|
+
if not pdf_bytes:
|
|
202
|
+
raise RuntimeError("WeasyPrint returned empty PDF output")
|
|
203
|
+
|
|
204
|
+
# Ensure parent directories and write file
|
|
205
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
206
|
+
output_path.write_bytes(pdf_bytes)
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
return len(document.pages)
|
|
210
|
+
except Exception: # pragma: no cover - defensive
|
|
211
|
+
return None
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""File opener for the shell layer.
|
|
2
|
+
|
|
3
|
+
This module handles platform-specific file opening operations.
|
|
4
|
+
All I/O operations for opening files in external applications
|
|
5
|
+
are consolidated here, following the functional core / imperative shell pattern.
|
|
6
|
+
|
|
7
|
+
The core layer should never import this module directly.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess # nosec B404
|
|
15
|
+
import sys
|
|
16
|
+
import webbrowser
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from simple_resume.core.exceptions import FileSystemError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FileOpener:
|
|
23
|
+
"""Platform-aware file opener for generated artifacts.
|
|
24
|
+
|
|
25
|
+
This class consolidates all file-opening logic in one place,
|
|
26
|
+
handling PDF, HTML, and generic file types across platforms.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def open_file(path: Path, format_type: str | None = None) -> bool:
|
|
31
|
+
"""Open a file using the appropriate system application.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
path: Path to the file to open
|
|
35
|
+
format_type: Optional format hint ('pdf', 'html', or None for generic)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
True if file was opened successfully
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
FileSystemError: If file doesn't exist or cannot be opened
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
if not path.exists():
|
|
45
|
+
raise FileSystemError(
|
|
46
|
+
f"File doesn't exist: {path}",
|
|
47
|
+
path=str(path),
|
|
48
|
+
operation="open",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if not path.is_file():
|
|
52
|
+
raise FileSystemError(
|
|
53
|
+
f"Path is not a file: {path}",
|
|
54
|
+
path=str(path),
|
|
55
|
+
operation="open",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Determine format from extension if not provided
|
|
59
|
+
if format_type is None:
|
|
60
|
+
suffix = path.suffix.lower()
|
|
61
|
+
if suffix == ".pdf":
|
|
62
|
+
format_type = "pdf"
|
|
63
|
+
elif suffix in (".html", ".htm"):
|
|
64
|
+
format_type = "html"
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
if format_type == "pdf":
|
|
68
|
+
return FileOpener._open_pdf(path)
|
|
69
|
+
elif format_type == "html":
|
|
70
|
+
return FileOpener._open_html(path)
|
|
71
|
+
else:
|
|
72
|
+
return FileOpener._open_generic(path)
|
|
73
|
+
except Exception as exc:
|
|
74
|
+
raise FileSystemError(
|
|
75
|
+
f"Failed to open file: {exc}",
|
|
76
|
+
path=str(path),
|
|
77
|
+
operation="open",
|
|
78
|
+
) from exc
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _validate_path(path: Path) -> str:
|
|
82
|
+
"""Validate path for security and return string representation.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
path: Path to validate
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
String representation of the path
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
ValueError: If path contains unsafe characters
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
path_str = str(path.resolve() if not path.is_absolute() else path)
|
|
95
|
+
|
|
96
|
+
# Check for command injection characters
|
|
97
|
+
unsafe_patterns = ["..", ";", "|", "&", "`", "$", "(", ")"]
|
|
98
|
+
if any(pattern in path_str for pattern in unsafe_patterns):
|
|
99
|
+
raise ValueError(f"Unsafe path detected: {path_str}")
|
|
100
|
+
|
|
101
|
+
return path_str
|
|
102
|
+
|
|
103
|
+
# PDF opening methods
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def _open_pdf(path: Path) -> bool:
|
|
107
|
+
"""Open PDF file using system's PDF viewer."""
|
|
108
|
+
if sys.platform == "darwin":
|
|
109
|
+
return FileOpener._open_pdf_macos(path)
|
|
110
|
+
elif sys.platform.startswith("linux"):
|
|
111
|
+
return FileOpener._open_pdf_linux(path)
|
|
112
|
+
else:
|
|
113
|
+
return FileOpener._open_pdf_windows(path)
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def _open_pdf_macos(path: Path) -> bool:
|
|
117
|
+
"""Open PDF file on macOS."""
|
|
118
|
+
path_str = FileOpener._validate_path(path)
|
|
119
|
+
subprocess.run( # noqa: S603 # nosec B603
|
|
120
|
+
["/usr/bin/open", path_str],
|
|
121
|
+
check=True,
|
|
122
|
+
capture_output=True,
|
|
123
|
+
)
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def _open_pdf_linux(path: Path) -> bool:
|
|
128
|
+
"""Open PDF file on Linux."""
|
|
129
|
+
path_str = FileOpener._validate_path(path)
|
|
130
|
+
|
|
131
|
+
# Try xdg-open first
|
|
132
|
+
xdg_open = shutil.which("xdg-open")
|
|
133
|
+
if xdg_open:
|
|
134
|
+
result = subprocess.run( # noqa: S603 # nosec B603
|
|
135
|
+
[xdg_open, path_str],
|
|
136
|
+
check=False,
|
|
137
|
+
capture_output=True,
|
|
138
|
+
)
|
|
139
|
+
if result.returncode == 0:
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
# Fallback to common PDF viewers
|
|
143
|
+
for viewer in ["evince", "okular", "acroread"]:
|
|
144
|
+
viewer_path = shutil.which(viewer)
|
|
145
|
+
if viewer_path:
|
|
146
|
+
try:
|
|
147
|
+
subprocess.run( # noqa: S603 # nosec B603
|
|
148
|
+
[viewer_path, path_str],
|
|
149
|
+
check=True,
|
|
150
|
+
capture_output=True,
|
|
151
|
+
)
|
|
152
|
+
return True
|
|
153
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _open_pdf_windows(path: Path) -> bool:
|
|
160
|
+
"""Open PDF file on Windows."""
|
|
161
|
+
path_str = FileOpener._validate_path(path)
|
|
162
|
+
cmd_path = shutil.which("cmd") or "cmd"
|
|
163
|
+
|
|
164
|
+
subprocess.run( # noqa: S603 # nosec B603
|
|
165
|
+
[cmd_path, "/c", "start", "", path_str],
|
|
166
|
+
check=True,
|
|
167
|
+
shell=False,
|
|
168
|
+
capture_output=True,
|
|
169
|
+
)
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
# HTML opening methods
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def _open_html(path: Path) -> bool:
|
|
176
|
+
"""Open HTML file using system's web browser."""
|
|
177
|
+
path_str = FileOpener._validate_path(path)
|
|
178
|
+
|
|
179
|
+
# Try webbrowser module first (cross-platform)
|
|
180
|
+
try:
|
|
181
|
+
if webbrowser.open(f"file://{path_str}"):
|
|
182
|
+
return True
|
|
183
|
+
except Exception: # noqa: BLE001
|
|
184
|
+
logging.debug("Failed to open browser with webbrowser module")
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
# Platform-specific fallbacks
|
|
188
|
+
if sys.platform == "darwin":
|
|
189
|
+
return FileOpener._open_html_macos(path)
|
|
190
|
+
elif sys.platform.startswith("linux"):
|
|
191
|
+
return FileOpener._open_html_linux(path)
|
|
192
|
+
else:
|
|
193
|
+
return FileOpener._open_html_windows(path)
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def _open_html_macos(path: Path) -> bool:
|
|
197
|
+
"""Open HTML file on macOS."""
|
|
198
|
+
path_str = FileOpener._validate_path(path)
|
|
199
|
+
result = subprocess.run( # noqa: S603 # nosec B603
|
|
200
|
+
["/usr/bin/open", path_str],
|
|
201
|
+
check=False,
|
|
202
|
+
capture_output=True,
|
|
203
|
+
)
|
|
204
|
+
return result.returncode == 0
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def _open_html_linux(path: Path) -> bool:
|
|
208
|
+
"""Open HTML file on Linux."""
|
|
209
|
+
path_str = FileOpener._validate_path(path)
|
|
210
|
+
|
|
211
|
+
xdg_open = shutil.which("xdg-open")
|
|
212
|
+
if xdg_open:
|
|
213
|
+
result = subprocess.run( # noqa: S603 # nosec B603
|
|
214
|
+
[xdg_open, path_str],
|
|
215
|
+
check=False,
|
|
216
|
+
capture_output=True,
|
|
217
|
+
)
|
|
218
|
+
return result.returncode == 0
|
|
219
|
+
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def _open_html_windows(path: Path) -> bool:
|
|
224
|
+
"""Open HTML file on Windows."""
|
|
225
|
+
path_str = FileOpener._validate_path(path)
|
|
226
|
+
cmd_path = shutil.which("cmd") or "cmd"
|
|
227
|
+
|
|
228
|
+
result = subprocess.run( # noqa: S603 # nosec B603
|
|
229
|
+
[cmd_path, "/c", "start", "", path_str],
|
|
230
|
+
check=False,
|
|
231
|
+
shell=False,
|
|
232
|
+
capture_output=True,
|
|
233
|
+
)
|
|
234
|
+
return result.returncode == 0
|
|
235
|
+
|
|
236
|
+
# Generic file opening
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def _open_generic(path: Path) -> bool:
|
|
240
|
+
"""Open file using system's default application."""
|
|
241
|
+
if sys.platform == "darwin":
|
|
242
|
+
return FileOpener._open_generic_macos(path)
|
|
243
|
+
elif sys.platform.startswith("linux"):
|
|
244
|
+
return FileOpener._open_generic_linux(path)
|
|
245
|
+
else:
|
|
246
|
+
return FileOpener._open_generic_windows(path)
|
|
247
|
+
|
|
248
|
+
@staticmethod
|
|
249
|
+
def _open_generic_macos(path: Path) -> bool:
|
|
250
|
+
"""Open file on macOS."""
|
|
251
|
+
path_str = FileOpener._validate_path(path)
|
|
252
|
+
result = subprocess.run( # noqa: S603 # nosec B603
|
|
253
|
+
["/usr/bin/open", path_str],
|
|
254
|
+
check=False,
|
|
255
|
+
capture_output=True,
|
|
256
|
+
)
|
|
257
|
+
return result.returncode == 0
|
|
258
|
+
|
|
259
|
+
@staticmethod
|
|
260
|
+
def _open_generic_linux(path: Path) -> bool:
|
|
261
|
+
"""Open file on Linux."""
|
|
262
|
+
path_str = FileOpener._validate_path(path)
|
|
263
|
+
|
|
264
|
+
xdg_open = shutil.which("xdg-open")
|
|
265
|
+
if xdg_open is None:
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
result = subprocess.run( # noqa: S603 # nosec B603
|
|
270
|
+
[xdg_open, path_str],
|
|
271
|
+
check=False,
|
|
272
|
+
capture_output=True,
|
|
273
|
+
timeout=10,
|
|
274
|
+
)
|
|
275
|
+
return result.returncode == 0
|
|
276
|
+
except subprocess.TimeoutExpired:
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
@staticmethod
|
|
280
|
+
def _open_generic_windows(path: Path) -> bool:
|
|
281
|
+
"""Open file on Windows."""
|
|
282
|
+
path_str = FileOpener._validate_path(path)
|
|
283
|
+
cmd_path = shutil.which("cmd") or "cmd"
|
|
284
|
+
|
|
285
|
+
result = subprocess.run( # noqa: S603 # nosec B603
|
|
286
|
+
[cmd_path, "/c", "start", "", path_str],
|
|
287
|
+
check=False,
|
|
288
|
+
shell=False,
|
|
289
|
+
capture_output=True,
|
|
290
|
+
)
|
|
291
|
+
return result.returncode == 0
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def open_file(path: Path, format_type: str | None = None) -> bool:
|
|
295
|
+
"""Open a file using the system default application.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
path: Path to the file to open
|
|
299
|
+
format_type: Optional format hint ('pdf', 'html', or None for generic)
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
True if file was opened successfully
|
|
303
|
+
|
|
304
|
+
"""
|
|
305
|
+
return FileOpener.open_file(path, format_type)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
__all__ = ["FileOpener", "open_file"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""High-level resume generation module in shell layer.
|
|
2
|
+
|
|
3
|
+
This module provides a clean, organized interface for generating resumes
|
|
4
|
+
in various formats. It offers both standard (eager) and lazy-loading
|
|
5
|
+
implementations to optimize for different use cases.
|
|
6
|
+
|
|
7
|
+
.. versionadded:: 0.1.0
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Direct imports from core generation modules
|
|
12
|
+
from simple_resume.core.generate.html import (
|
|
13
|
+
HtmlGeneratorFactory,
|
|
14
|
+
create_html_generator_factory,
|
|
15
|
+
)
|
|
16
|
+
from simple_resume.core.generate.pdf import (
|
|
17
|
+
PdfGeneratorFactory,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Re-export lazy loading versions for backward compatibility
|
|
21
|
+
from simple_resume.shell.generate.lazy import (
|
|
22
|
+
generate,
|
|
23
|
+
generate_all,
|
|
24
|
+
generate_html,
|
|
25
|
+
generate_pdf,
|
|
26
|
+
generate_resume,
|
|
27
|
+
preview,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"generate",
|
|
32
|
+
"generate_all",
|
|
33
|
+
"generate_html",
|
|
34
|
+
"generate_pdf",
|
|
35
|
+
"generate_resume",
|
|
36
|
+
"preview",
|
|
37
|
+
]
|