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,284 @@
|
|
|
1
|
+
"""Lazy-loaded generation functions for optimal import performance.
|
|
2
|
+
|
|
3
|
+
This module provides thin wrappers around the core generation functions
|
|
4
|
+
with lazy loading to reduce startup memory footprint.
|
|
5
|
+
|
|
6
|
+
.. versionadded:: 0.1.0
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib
|
|
12
|
+
from functools import lru_cache
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from types import ModuleType
|
|
15
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
16
|
+
|
|
17
|
+
from simple_resume.core.models import GenerationConfig
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from simple_resume.core.result import BatchGenerationResult, GenerationResult
|
|
21
|
+
from simple_resume.shell.generate.core import GenerateOptions
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _LazyCoreLoader:
|
|
25
|
+
"""Lazy loader for core generation functions."""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self._core: ModuleType | None = None
|
|
29
|
+
self._loaded = False
|
|
30
|
+
|
|
31
|
+
def _load_core(self) -> ModuleType:
|
|
32
|
+
"""Load core module if not already loaded."""
|
|
33
|
+
if not self._loaded:
|
|
34
|
+
self._core = importlib.import_module(".core", package=__package__)
|
|
35
|
+
self._loaded = True
|
|
36
|
+
if self._core is None: # pragma: no cover
|
|
37
|
+
raise RuntimeError("Failed to load core module")
|
|
38
|
+
return self._core
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def generate_pdf(self) -> Any:
|
|
42
|
+
"""Get generate_pdf function from core module."""
|
|
43
|
+
return self._load_core().generate_pdf
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def generate_html(self) -> Any:
|
|
47
|
+
"""Get generate_html function from core module."""
|
|
48
|
+
return self._load_core().generate_html
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def generate_all(self) -> Any:
|
|
52
|
+
"""Get generate_all function from core module."""
|
|
53
|
+
return self._load_core().generate_all
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def generate_resume(self) -> Any:
|
|
57
|
+
"""Get generate_resume function from core module."""
|
|
58
|
+
return self._load_core().generate_resume
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def generate(self) -> Any:
|
|
62
|
+
"""Get generate function from core module."""
|
|
63
|
+
return self._load_core().generate
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def preview(self) -> Any:
|
|
67
|
+
"""Get preview function from core module."""
|
|
68
|
+
return self._load_core().preview
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@lru_cache(maxsize=1)
|
|
72
|
+
def _get_lazy_core_loader() -> _LazyCoreLoader:
|
|
73
|
+
"""Provide a lazily created singleton loader without module globals."""
|
|
74
|
+
return _LazyCoreLoader()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def generate_pdf(
|
|
78
|
+
config: GenerationConfig,
|
|
79
|
+
**config_overrides: Any,
|
|
80
|
+
) -> BatchGenerationResult:
|
|
81
|
+
"""Generate PDF resumes using a configuration object.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
config: Generation configuration specifying sources, formats, and options.
|
|
85
|
+
**config_overrides: Additional configuration overrides.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
BatchGenerationResult containing generated PDF files and metadata.
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
>>> from simple_resume import generate_pdf
|
|
92
|
+
>>> from simple_resume.core.models import GenerationConfig
|
|
93
|
+
>>> config = GenerationConfig(sources=["resume.yaml"], formats=["pdf"])
|
|
94
|
+
>>> result = generate_pdf(config)
|
|
95
|
+
>>> print(result.successful)
|
|
96
|
+
|
|
97
|
+
.. versionadded:: 0.1.0
|
|
98
|
+
|
|
99
|
+
"""
|
|
100
|
+
lazy_core = _get_lazy_core_loader()
|
|
101
|
+
result: BatchGenerationResult = cast(
|
|
102
|
+
"BatchGenerationResult", lazy_core.generate_pdf(config, **config_overrides)
|
|
103
|
+
)
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def generate_html(
|
|
108
|
+
config: GenerationConfig,
|
|
109
|
+
**config_overrides: Any,
|
|
110
|
+
) -> BatchGenerationResult:
|
|
111
|
+
"""Generate HTML resumes using a configuration object.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
config: Generation configuration specifying sources, formats, and options.
|
|
115
|
+
**config_overrides: Additional configuration overrides.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
BatchGenerationResult containing generated HTML files and metadata.
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
>>> from simple_resume import generate_html
|
|
122
|
+
>>> from simple_resume.core.models import GenerationConfig
|
|
123
|
+
>>> config = GenerationConfig(sources=["resume.yaml"], formats=["html"])
|
|
124
|
+
>>> result = generate_html(config)
|
|
125
|
+
|
|
126
|
+
.. versionadded:: 0.1.0
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
lazy_core = _get_lazy_core_loader()
|
|
130
|
+
result: BatchGenerationResult = cast(
|
|
131
|
+
"BatchGenerationResult", lazy_core.generate_html(config, **config_overrides)
|
|
132
|
+
)
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def generate_all(
|
|
137
|
+
config: GenerationConfig,
|
|
138
|
+
**config_overrides: Any,
|
|
139
|
+
) -> BatchGenerationResult:
|
|
140
|
+
"""Generate resumes in all specified formats.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
config: Generation configuration specifying sources, formats, and options.
|
|
144
|
+
**config_overrides: Additional configuration overrides.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
BatchGenerationResult containing all generated files and metadata.
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
>>> from simple_resume import generate_all
|
|
151
|
+
>>> from simple_resume.core.models import GenerationConfig
|
|
152
|
+
>>> config = GenerationConfig(sources=["resume.yaml"], formats=["pdf", "html"])
|
|
153
|
+
>>> result = generate_all(config)
|
|
154
|
+
|
|
155
|
+
.. versionadded:: 0.1.0
|
|
156
|
+
|
|
157
|
+
"""
|
|
158
|
+
lazy_core = _get_lazy_core_loader()
|
|
159
|
+
result: BatchGenerationResult = cast(
|
|
160
|
+
"BatchGenerationResult", lazy_core.generate_all(config, **config_overrides)
|
|
161
|
+
)
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def generate_resume(
|
|
166
|
+
config: GenerationConfig,
|
|
167
|
+
**config_overrides: Any,
|
|
168
|
+
) -> GenerationResult:
|
|
169
|
+
"""Generate a single resume in a specific format.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
config: Generation configuration specifying source and format.
|
|
173
|
+
**config_overrides: Additional configuration overrides.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
GenerationResult for the generated resume.
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
>>> from simple_resume import generate_resume
|
|
180
|
+
>>> from simple_resume.core.models import GenerationConfig
|
|
181
|
+
>>> config = GenerationConfig(sources=["resume.yaml"], formats=["pdf"])
|
|
182
|
+
>>> result = generate_resume(config)
|
|
183
|
+
|
|
184
|
+
.. versionadded:: 0.1.0
|
|
185
|
+
|
|
186
|
+
"""
|
|
187
|
+
lazy_core = _get_lazy_core_loader()
|
|
188
|
+
result: GenerationResult = cast(
|
|
189
|
+
"GenerationResult", lazy_core.generate_resume(config, **config_overrides)
|
|
190
|
+
)
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def generate(
|
|
195
|
+
source: str | Path,
|
|
196
|
+
options: GenerateOptions | None = None,
|
|
197
|
+
**overrides: Any,
|
|
198
|
+
) -> dict[str, GenerationResult | BatchGenerationResult]:
|
|
199
|
+
"""Render one or more formats for the same source.
|
|
200
|
+
|
|
201
|
+
This is the primary entry point for generating resumes. It accepts a
|
|
202
|
+
source file path and optional configuration.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
source: Path to the resume YAML file.
|
|
206
|
+
options: Optional GenerateOptions with formats and settings.
|
|
207
|
+
**overrides: Additional configuration overrides.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Dictionary mapping format names to GenerationResult or BatchGenerationResult.
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
>>> from simple_resume import generate
|
|
214
|
+
>>> result = generate("resume.yaml")
|
|
215
|
+
>>> for fmt, r in result.items():
|
|
216
|
+
... print(f"Generated {fmt}: {r}")
|
|
217
|
+
|
|
218
|
+
.. versionadded:: 0.1.0
|
|
219
|
+
|
|
220
|
+
"""
|
|
221
|
+
lazy_core = _get_lazy_core_loader()
|
|
222
|
+
result: dict[str, GenerationResult | BatchGenerationResult] = cast(
|
|
223
|
+
"dict[str, GenerationResult | BatchGenerationResult]",
|
|
224
|
+
lazy_core.generate(source, options, **overrides),
|
|
225
|
+
)
|
|
226
|
+
return result
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def preview(
|
|
230
|
+
source: str | Path,
|
|
231
|
+
*,
|
|
232
|
+
data_dir: str | Path | None = None,
|
|
233
|
+
template: str | None = None,
|
|
234
|
+
browser: str | None = None,
|
|
235
|
+
open_after: bool = True,
|
|
236
|
+
**overrides: Any,
|
|
237
|
+
) -> GenerationResult | BatchGenerationResult:
|
|
238
|
+
"""Render a single resume to HTML and open in browser.
|
|
239
|
+
|
|
240
|
+
This convenience function generates an HTML preview and optionally
|
|
241
|
+
opens it in the default web browser.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
source: Path to the resume YAML file.
|
|
245
|
+
data_dir: Optional data directory override.
|
|
246
|
+
template: Optional template name override.
|
|
247
|
+
browser: Optional browser command (e.g., "firefox", "chrome").
|
|
248
|
+
open_after: Whether to open the file after generation (default: True).
|
|
249
|
+
**overrides: Additional configuration overrides.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
GenerationResult for the generated HTML preview.
|
|
253
|
+
|
|
254
|
+
Example:
|
|
255
|
+
>>> from simple_resume import preview
|
|
256
|
+
>>> result = preview("resume.yaml") # Opens in browser
|
|
257
|
+
>>> result = preview("resume.yaml", open_after=False) # Just generate
|
|
258
|
+
|
|
259
|
+
.. versionadded:: 0.1.0
|
|
260
|
+
|
|
261
|
+
"""
|
|
262
|
+
lazy_core = _get_lazy_core_loader()
|
|
263
|
+
result: GenerationResult | BatchGenerationResult = cast(
|
|
264
|
+
"GenerationResult | BatchGenerationResult",
|
|
265
|
+
lazy_core.preview(
|
|
266
|
+
source,
|
|
267
|
+
data_dir=data_dir,
|
|
268
|
+
template=template,
|
|
269
|
+
browser=browser,
|
|
270
|
+
open_after=open_after,
|
|
271
|
+
**overrides,
|
|
272
|
+
),
|
|
273
|
+
)
|
|
274
|
+
return result
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
__all__ = [
|
|
278
|
+
"generate_pdf",
|
|
279
|
+
"generate_html",
|
|
280
|
+
"generate_all",
|
|
281
|
+
"generate_resume",
|
|
282
|
+
"generate",
|
|
283
|
+
"preview",
|
|
284
|
+
]
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Consolidated path and file handling utilities.
|
|
2
|
+
|
|
3
|
+
This module provides centralized path handling following the Path-first principle:
|
|
4
|
+
- Accept str | Path at API boundaries for flexibility
|
|
5
|
+
- Normalize to Path immediately after receiving input
|
|
6
|
+
- Use Path objects internally throughout the codebase
|
|
7
|
+
- Convert to str only when required by external APIs
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from oyaml import safe_load
|
|
17
|
+
|
|
18
|
+
from simple_resume.core.paths import Paths
|
|
19
|
+
from simple_resume.shell.config import FILE_DEFAULT, resolve_paths
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def candidate_yaml_path(name: str | os.PathLike[str]) -> Path | None:
|
|
23
|
+
"""Return a Path if ``name`` resembles a supported resume file, else ``None``.
|
|
24
|
+
|
|
25
|
+
Notes:
|
|
26
|
+
We treat ``.json`` specially: a bare "resume.json" is often meant as a resume
|
|
27
|
+
*name* (looked up under the data-dir input folder). To avoid surprising
|
|
28
|
+
"file not found" errors, we only treat ``.json`` as a direct path when it
|
|
29
|
+
looks like a real path (has directory components or is absolute).
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
if isinstance(name, (str, os.PathLike)):
|
|
33
|
+
maybe_path = Path(name)
|
|
34
|
+
suffix = maybe_path.suffix.lower()
|
|
35
|
+
if suffix in {".yaml", ".yml"}:
|
|
36
|
+
return maybe_path
|
|
37
|
+
if suffix == ".json":
|
|
38
|
+
if maybe_path.is_absolute() or len(maybe_path.parts) > 1:
|
|
39
|
+
return maybe_path
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def resolve_paths_for_read(
|
|
44
|
+
supplied_paths: Paths | None,
|
|
45
|
+
overrides: dict[str, Any],
|
|
46
|
+
candidate: Path | None,
|
|
47
|
+
) -> Paths:
|
|
48
|
+
"""Resolve path configuration for read operations."""
|
|
49
|
+
if supplied_paths is not None:
|
|
50
|
+
return supplied_paths
|
|
51
|
+
|
|
52
|
+
if overrides:
|
|
53
|
+
return resolve_paths(**overrides)
|
|
54
|
+
|
|
55
|
+
if candidate is not None:
|
|
56
|
+
if candidate.parent.name == "input":
|
|
57
|
+
base_dir = candidate.parent.parent
|
|
58
|
+
else:
|
|
59
|
+
base_dir = candidate.parent
|
|
60
|
+
|
|
61
|
+
base_paths = resolve_paths(data_dir=base_dir)
|
|
62
|
+
return Paths(
|
|
63
|
+
data=base_paths.data,
|
|
64
|
+
input=candidate.parent,
|
|
65
|
+
output=base_paths.output,
|
|
66
|
+
content=base_paths.content,
|
|
67
|
+
templates=base_paths.templates,
|
|
68
|
+
static=base_paths.static,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return resolve_paths(**overrides)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def normalize_resume_name(name: str | os.PathLike[str]) -> str:
|
|
75
|
+
"""Normalize resume identifiers by stripping extensions and defaults.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
name: Resume identifier (filename, stem, or path)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Normalized resume name without extension
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
if not name:
|
|
85
|
+
return FILE_DEFAULT
|
|
86
|
+
if isinstance(name, (str, os.PathLike)):
|
|
87
|
+
candidate = Path(name)
|
|
88
|
+
suffix = candidate.suffix.lower()
|
|
89
|
+
if suffix in {".yaml", ".yml", ".json"}:
|
|
90
|
+
return candidate.stem
|
|
91
|
+
return candidate.name or str(name)
|
|
92
|
+
return str(name)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def find_resume_file(
|
|
96
|
+
resume_name: str, input_path: Path, *, include_uppercase: bool = True
|
|
97
|
+
) -> Path:
|
|
98
|
+
"""Find a resume file in the input directory by name.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
resume_name: Name of the resume (without extension)
|
|
102
|
+
input_path: Directory to search for resume files
|
|
103
|
+
include_uppercase: Whether to search for uppercase extensions
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Path to the found resume file
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
candidates: list[Path] = []
|
|
110
|
+
extensions = ["yaml", "yml", "json"]
|
|
111
|
+
|
|
112
|
+
# Search for files with both lowercase and uppercase extensions
|
|
113
|
+
for ext in extensions:
|
|
114
|
+
candidates.extend(input_path.glob(f"{resume_name}.{ext}"))
|
|
115
|
+
if include_uppercase:
|
|
116
|
+
candidates.extend(input_path.glob(f"{resume_name}.{ext.upper()}"))
|
|
117
|
+
|
|
118
|
+
if candidates:
|
|
119
|
+
return candidates[0]
|
|
120
|
+
|
|
121
|
+
# Fallback to default yaml file
|
|
122
|
+
return input_path / f"{resume_name}.yaml"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def read_yaml_file(path: str | Path) -> dict[str, Any]:
|
|
126
|
+
"""Read and parse a YAML file.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
path: Path to the YAML file
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Parsed YAML content as dictionary
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ValueError: If YAML content is not a dictionary
|
|
136
|
+
FileNotFoundError: If file doesn't exist
|
|
137
|
+
|
|
138
|
+
"""
|
|
139
|
+
path_obj = Path(path)
|
|
140
|
+
if not path_obj.exists():
|
|
141
|
+
raise FileNotFoundError(f"YAML file not found: {path}")
|
|
142
|
+
|
|
143
|
+
with open(path_obj, encoding="utf-8") as file:
|
|
144
|
+
content = safe_load(file)
|
|
145
|
+
|
|
146
|
+
if content is None:
|
|
147
|
+
return {}
|
|
148
|
+
|
|
149
|
+
if not isinstance(content, dict):
|
|
150
|
+
raise ValueError(
|
|
151
|
+
f"YAML file must contain a dictionary at the root level, "
|
|
152
|
+
f"but found {type(content).__name__}: {path_obj}"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return content
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def ensure_directory_exists(path: Path) -> Path:
|
|
159
|
+
"""Ensure a directory exists, creating it if necessary.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
path: Directory path to ensure exists
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
The same path for convenience
|
|
166
|
+
|
|
167
|
+
"""
|
|
168
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
return path
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def resolve_output_path(base_dir: Path, filename: str, extension: str = ".pdf") -> Path:
|
|
173
|
+
"""Resolve an output file path with proper extension.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
base_dir: Base output directory
|
|
177
|
+
filename: Base filename (without extension)
|
|
178
|
+
extension: Desired file extension
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Resolved output file path
|
|
182
|
+
|
|
183
|
+
"""
|
|
184
|
+
if not filename.endswith(extension):
|
|
185
|
+
filename = f"{filename}{extension}"
|
|
186
|
+
|
|
187
|
+
ensure_directory_exists(base_dir)
|
|
188
|
+
return base_dir / filename
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
__all__ = [
|
|
192
|
+
"candidate_yaml_path",
|
|
193
|
+
"resolve_paths_for_read",
|
|
194
|
+
"normalize_resume_name",
|
|
195
|
+
"find_resume_file",
|
|
196
|
+
"read_yaml_file",
|
|
197
|
+
"ensure_directory_exists",
|
|
198
|
+
"resolve_output_path",
|
|
199
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Palette-related functionality for the shell layer."""
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Shell layer palette fetching with network I/O.
|
|
2
|
+
|
|
3
|
+
This module executes the network operations described by PaletteFetchRequest
|
|
4
|
+
objects created by the pure core logic. This isolates all network I/O
|
|
5
|
+
to the shell layer.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from simple_resume.core.palettes import PaletteFetchRequest
|
|
11
|
+
from simple_resume.core.palettes.exceptions import PaletteLookupError
|
|
12
|
+
from simple_resume.shell.palettes.remote import ColourLoversClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def execute_palette_fetch(
|
|
16
|
+
request: PaletteFetchRequest,
|
|
17
|
+
) -> tuple[list[str], dict[str, Any]]:
|
|
18
|
+
"""Shell operation - performs network I/O to fetch palette.
|
|
19
|
+
|
|
20
|
+
This function executes the network operation described by the request
|
|
21
|
+
and returns the actual color data.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
request: Palette fetch request describing the network operation
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Tuple of (colors, metadata) from the remote source
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
PaletteLookupError: If palette fetch fails or returns no results
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
if request.source not in {"colourlovers", "remote"}:
|
|
34
|
+
raise PaletteLookupError(f"Unsupported remote source: {request.source}")
|
|
35
|
+
|
|
36
|
+
client = ColourLoversClient()
|
|
37
|
+
# Convert list of keywords to comma-separated string for the API
|
|
38
|
+
keywords_str = ",".join(request.keywords) if request.keywords else None
|
|
39
|
+
palettes = client.fetch(
|
|
40
|
+
keywords=keywords_str,
|
|
41
|
+
num_results=request.num_results,
|
|
42
|
+
order_by=request.order_by,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if not palettes:
|
|
46
|
+
raise PaletteLookupError(f"No palettes found for keywords: {request.keywords}")
|
|
47
|
+
|
|
48
|
+
palette = palettes[0]
|
|
49
|
+
colors = list(palette.swatches)
|
|
50
|
+
|
|
51
|
+
metadata = {
|
|
52
|
+
"source": request.source,
|
|
53
|
+
"name": palette.name,
|
|
54
|
+
"attribution": palette.metadata,
|
|
55
|
+
"size": len(colors),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return colors, metadata
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"execute_palette_fetch",
|
|
63
|
+
]
|