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