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,138 @@
|
|
|
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
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import importlib
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from types import ModuleType
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from simple_resume.core.models import GenerationConfig
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _LazyRuntimeLoader:
|
|
19
|
+
"""Lazy loader for runtime generation functions."""
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self._core: ModuleType | None = None
|
|
23
|
+
self._loaded = False
|
|
24
|
+
|
|
25
|
+
def _load_core(self) -> ModuleType:
|
|
26
|
+
"""Load runtime module if not already loaded."""
|
|
27
|
+
if not self._loaded:
|
|
28
|
+
self._core = importlib.import_module(".generate", package=__package__)
|
|
29
|
+
self._loaded = True
|
|
30
|
+
# _core is set when _loaded is True
|
|
31
|
+
return self._core # type: ignore[return-value]
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def generate_pdf(self) -> Any:
|
|
35
|
+
"""Get generate_pdf function from core module."""
|
|
36
|
+
return self._load_core().generate_pdf
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def generate_html(self) -> Any:
|
|
40
|
+
"""Get generate_html function from core module."""
|
|
41
|
+
return self._load_core().generate_html
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def generate_all(self) -> Any:
|
|
45
|
+
"""Get generate_all function from core module."""
|
|
46
|
+
return self._load_core().generate_all
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def generate_resume(self) -> Any:
|
|
50
|
+
"""Get generate_resume function from core module."""
|
|
51
|
+
return self._load_core().generate_resume
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def generate(self) -> Any:
|
|
55
|
+
"""Get generate function from core module."""
|
|
56
|
+
return self._load_core().generate
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def preview(self) -> Any:
|
|
60
|
+
"""Get preview function from core module."""
|
|
61
|
+
return self._load_core().preview
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@lru_cache(maxsize=1)
|
|
65
|
+
def _get_lazy_core() -> _LazyRuntimeLoader:
|
|
66
|
+
"""Return a shared lazy loader instance without module-level globals."""
|
|
67
|
+
return _LazyRuntimeLoader()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def generate_pdf(
|
|
71
|
+
config: GenerationConfig,
|
|
72
|
+
**config_overrides: Any,
|
|
73
|
+
) -> Any:
|
|
74
|
+
"""Generate PDF resumes using a configuration object (lazy-loaded wrapper)."""
|
|
75
|
+
return _get_lazy_core().generate_pdf(config, **config_overrides)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def generate_html(
|
|
79
|
+
config: GenerationConfig,
|
|
80
|
+
**config_overrides: Any,
|
|
81
|
+
) -> Any:
|
|
82
|
+
"""Generate HTML resumes using a configuration object (lazy-loaded wrapper)."""
|
|
83
|
+
return _get_lazy_core().generate_html(config, **config_overrides)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def generate_all(
|
|
87
|
+
config: GenerationConfig,
|
|
88
|
+
**config_overrides: Any,
|
|
89
|
+
) -> Any:
|
|
90
|
+
"""Generate resumes in all specified formats (lazy-loaded wrapper)."""
|
|
91
|
+
return _get_lazy_core().generate_all(config, **config_overrides)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def generate_resume(
|
|
95
|
+
config: GenerationConfig,
|
|
96
|
+
**config_overrides: Any,
|
|
97
|
+
) -> Any:
|
|
98
|
+
"""Generate a resume in a specific format (lazy-loaded wrapper)."""
|
|
99
|
+
return _get_lazy_core().generate_resume(config, **config_overrides)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def generate(
|
|
103
|
+
source: str | Path,
|
|
104
|
+
options: Any | None = None,
|
|
105
|
+
**overrides: Any,
|
|
106
|
+
) -> Any:
|
|
107
|
+
"""Render one or more formats for the same source (lazy-loaded wrapper)."""
|
|
108
|
+
return _get_lazy_core().generate(source, options, **overrides)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def preview(
|
|
112
|
+
source: str | Path,
|
|
113
|
+
*,
|
|
114
|
+
data_dir: str | Path | None = None,
|
|
115
|
+
template: str | None = None,
|
|
116
|
+
browser: str | None = None,
|
|
117
|
+
open_after: bool = True,
|
|
118
|
+
**overrides: Any,
|
|
119
|
+
) -> Any:
|
|
120
|
+
"""Render a single resume to HTML with preview defaults (lazy-loaded wrapper)."""
|
|
121
|
+
return _get_lazy_core().preview(
|
|
122
|
+
source,
|
|
123
|
+
data_dir=data_dir,
|
|
124
|
+
template=template,
|
|
125
|
+
browser=browser,
|
|
126
|
+
open_after=open_after,
|
|
127
|
+
**overrides,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
__all__ = [
|
|
132
|
+
"generate_pdf",
|
|
133
|
+
"generate_html",
|
|
134
|
+
"generate_all",
|
|
135
|
+
"generate_resume",
|
|
136
|
+
"generate",
|
|
137
|
+
"preview",
|
|
138
|
+
]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Lazy loading utilities for optimizing import performance.
|
|
2
|
+
|
|
3
|
+
This module provides utilities to defer heavy imports until actually needed,
|
|
4
|
+
reducing initial memory footprint and improving startup performance.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import importlib
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
from typing import Any, Callable, TypeVar, cast
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LazyModule:
|
|
17
|
+
"""A module that loads on first attribute access."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, module_name: str) -> None:
|
|
20
|
+
"""Initialize lazy module.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
module_name: Full module path to import when needed
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
self._module_name = module_name
|
|
27
|
+
self._module: Any = None
|
|
28
|
+
self._loaded = False
|
|
29
|
+
|
|
30
|
+
def _load(self) -> Any:
|
|
31
|
+
"""Load the module if not already loaded."""
|
|
32
|
+
if not self._loaded:
|
|
33
|
+
self._module = importlib.import_module(self._module_name)
|
|
34
|
+
self._loaded = True
|
|
35
|
+
return self._module
|
|
36
|
+
|
|
37
|
+
def __getattr__(self, name: str) -> Any:
|
|
38
|
+
"""Get attribute from the lazily loaded module."""
|
|
39
|
+
module = self._load()
|
|
40
|
+
return getattr(module, name)
|
|
41
|
+
|
|
42
|
+
def __dir__(self) -> list[str]:
|
|
43
|
+
"""Return directory listing of the module when loaded."""
|
|
44
|
+
module = self._load()
|
|
45
|
+
return dir(module)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class LazyFunction:
|
|
49
|
+
"""A function that loads its implementation on first call."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, module_path: str, function_name: str) -> None:
|
|
52
|
+
"""Initialize lazy function.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
module_path: Full module path containing the function
|
|
56
|
+
function_name: Name of the function to lazy-load
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
self._module_path = module_path
|
|
60
|
+
self._function_name = function_name
|
|
61
|
+
self._function: Callable[..., Any] | None = None
|
|
62
|
+
self._loaded = False
|
|
63
|
+
|
|
64
|
+
def _load(self) -> Callable[..., Any]:
|
|
65
|
+
"""Load the function if not already loaded."""
|
|
66
|
+
if not self._loaded:
|
|
67
|
+
module = importlib.import_module(self._module_path)
|
|
68
|
+
self._function = getattr(module, self._function_name)
|
|
69
|
+
self._loaded = True
|
|
70
|
+
# After loading, _function is guaranteed to be set
|
|
71
|
+
return cast(Callable[..., Any], self._function)
|
|
72
|
+
|
|
73
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
74
|
+
"""Call the lazily loaded function."""
|
|
75
|
+
function = self._load()
|
|
76
|
+
return function(*args, **kwargs)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@lru_cache(maxsize=128)
|
|
80
|
+
def lazy_import(module_path: str) -> LazyModule:
|
|
81
|
+
"""Create a cached lazy module instance.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
module_path: Full module path to import lazily
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
LazyModule instance that loads on first access
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
return LazyModule(module_path)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@lru_cache(maxsize=64)
|
|
94
|
+
def lazy_function(module_path: str, function_name: str) -> LazyFunction:
|
|
95
|
+
"""Create a cached lazy function instance.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
module_path: Full module path containing the function
|
|
99
|
+
function_name: Name of the function to lazy-load
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
LazyFunction instance that loads on first call
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
return LazyFunction(module_path, function_name)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Common lazy-loaded modules
|
|
109
|
+
_lazy_session = lazy_import("simple_resume.shell.session")
|
|
110
|
+
_lazy_result = lazy_import("simple_resume.core.result")
|
|
111
|
+
_lazy_validation = lazy_import("simple_resume.core.validation")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# Public lazy API functions
|
|
115
|
+
def lazy_create_session(*args: Any, **kwargs: Any) -> Any:
|
|
116
|
+
"""Lazily loaded create_session function."""
|
|
117
|
+
return _lazy_session.create_session(*args, **kwargs)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def lazy_ResumeSession(*args: Any, **kwargs: Any) -> Any:
|
|
121
|
+
"""Lazily loaded ResumeSession class."""
|
|
122
|
+
return _lazy_session.ResumeSession(*args, **kwargs)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def lazy_SessionConfig(*args: Any, **kwargs: Any) -> Any:
|
|
126
|
+
"""Lazily loaded SessionConfig class."""
|
|
127
|
+
return _lazy_session.SessionConfig(*args, **kwargs)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def lazy_GenerationResult(*args: Any, **kwargs: Any) -> Any:
|
|
131
|
+
"""Lazily loaded GenerationResult class."""
|
|
132
|
+
return _lazy_result.GenerationResult(*args, **kwargs)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def lazy_BatchGenerationResult(*args: Any, **kwargs: Any) -> Any:
|
|
136
|
+
"""Lazily loaded BatchGenerationResult class."""
|
|
137
|
+
return _lazy_result.BatchGenerationResult(*args, **kwargs)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def lazy_GenerationMetadata(*args: Any, **kwargs: Any) -> Any:
|
|
141
|
+
"""Lazily loaded GenerationMetadata class."""
|
|
142
|
+
return _lazy_result.GenerationMetadata(*args, **kwargs)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def lazy_validate_directory_path(*args: Any, **kwargs: Any) -> Any:
|
|
146
|
+
"""Lazily loaded validate_directory_path function."""
|
|
147
|
+
return _lazy_validation.validate_directory_path(*args, **kwargs)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def lazy_validate_format(*args: Any, **kwargs: Any) -> Any:
|
|
151
|
+
"""Lazily loaded validate_format function."""
|
|
152
|
+
return _lazy_validation.validate_format(*args, **kwargs)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def lazy_validate_template_name(*args: Any, **kwargs: Any) -> Any:
|
|
156
|
+
"""Lazily loaded validate_template_name function."""
|
|
157
|
+
return _lazy_validation.validate_template_name(*args, **kwargs)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
__all__ = [
|
|
161
|
+
"LazyModule",
|
|
162
|
+
"LazyFunction",
|
|
163
|
+
"lazy_import",
|
|
164
|
+
"lazy_function",
|
|
165
|
+
"lazy_create_session",
|
|
166
|
+
"lazy_ResumeSession",
|
|
167
|
+
"lazy_SessionConfig",
|
|
168
|
+
"lazy_GenerationResult",
|
|
169
|
+
"lazy_BatchGenerationResult",
|
|
170
|
+
"lazy_validate_directory_path",
|
|
171
|
+
"lazy_validate_format",
|
|
172
|
+
"lazy_validate_template_name",
|
|
173
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Service locator pattern for dependency injection.
|
|
2
|
+
|
|
3
|
+
This module provides a clean way to inject shell-layer dependencies
|
|
4
|
+
into core functions without using late-bound imports.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from functools import lru_cache
|
|
10
|
+
from typing import Any, Callable, TypeVar
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ServiceLocator:
|
|
16
|
+
"""Simple service locator for dependency injection."""
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
"""Initialize the service locator."""
|
|
20
|
+
self._services: dict[str, Any] = {}
|
|
21
|
+
self._factories: dict[str, Callable[[], Any]] = {}
|
|
22
|
+
|
|
23
|
+
def register(self, name: str, service: Any) -> None:
|
|
24
|
+
"""Register a service instance."""
|
|
25
|
+
self._services[name] = service
|
|
26
|
+
|
|
27
|
+
def register_factory(self, name: str, factory: Callable[[], Any]) -> None:
|
|
28
|
+
"""Register a factory function for lazy service creation."""
|
|
29
|
+
self._factories[name] = factory
|
|
30
|
+
|
|
31
|
+
def get(self, name: str, service_type: type[T]) -> T:
|
|
32
|
+
"""Get a service by name, ensuring it matches the expected type."""
|
|
33
|
+
if name in self._services:
|
|
34
|
+
service = self._services[name]
|
|
35
|
+
if not isinstance(service, service_type):
|
|
36
|
+
raise TypeError(f"Service {name} is not of type {service_type}")
|
|
37
|
+
return service
|
|
38
|
+
|
|
39
|
+
if name in self._factories:
|
|
40
|
+
service = self._factories[name]()
|
|
41
|
+
if not isinstance(service, service_type):
|
|
42
|
+
raise TypeError(f"Service {name} is not of type {service_type}")
|
|
43
|
+
self._services[name] = service # Cache the instance
|
|
44
|
+
return service
|
|
45
|
+
|
|
46
|
+
raise ValueError(f"Service {name} not registered")
|
|
47
|
+
|
|
48
|
+
def has(self, name: str) -> bool:
|
|
49
|
+
"""Check if a service is registered."""
|
|
50
|
+
return name in self._services or name in self._factories
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@lru_cache(maxsize=1)
|
|
54
|
+
def get_service_locator() -> ServiceLocator:
|
|
55
|
+
"""Get the global service locator instance without module globals."""
|
|
56
|
+
return ServiceLocator()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def register_service(name: str, service: Any) -> None:
|
|
60
|
+
"""Register a service with the global locator."""
|
|
61
|
+
get_service_locator().register(name, service)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def register_service_factory(name: str, factory: Callable[[], Any]) -> None:
|
|
65
|
+
"""Register a service factory with the global locator."""
|
|
66
|
+
get_service_locator().register_factory(name, factory)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_service(name: str, service_type: type[T]) -> T:
|
|
70
|
+
"""Get a service from the global locator."""
|
|
71
|
+
return get_service_locator().get(name, service_type)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
"ServiceLocator",
|
|
76
|
+
"get_service_locator",
|
|
77
|
+
"register_service",
|
|
78
|
+
"register_service_factory",
|
|
79
|
+
"get_service",
|
|
80
|
+
]
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Shell layer implementations of core protocols.
|
|
2
|
+
|
|
3
|
+
This module provides concrete implementations of the protocols defined
|
|
4
|
+
in the core layer, enabling dependency injection without late-bound imports.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import copy
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
from simple_resume.core.generate.html import create_html_generator_factory
|
|
17
|
+
from simple_resume.core.generate.pdf import PdfGeneratorFactory
|
|
18
|
+
from simple_resume.core.markdown import render_markdown_content
|
|
19
|
+
from simple_resume.core.protocols import (
|
|
20
|
+
ContentLoader,
|
|
21
|
+
EffectExecutor,
|
|
22
|
+
FileOpenerService,
|
|
23
|
+
HtmlGenerator,
|
|
24
|
+
LaTeXRenderer,
|
|
25
|
+
PaletteLoader,
|
|
26
|
+
PathResolver,
|
|
27
|
+
PdfGenerationStrategy,
|
|
28
|
+
TemplateLocator,
|
|
29
|
+
)
|
|
30
|
+
from simple_resume.core.resume import set_default_loaders
|
|
31
|
+
from simple_resume.shell.config import TEMPLATE_LOC
|
|
32
|
+
from simple_resume.shell.effect_executor import EffectExecutor as ShellEffectExecutor
|
|
33
|
+
from simple_resume.shell.file_opener import open_file as shell_open_file
|
|
34
|
+
from simple_resume.shell.io_utils import candidate_yaml_path, resolve_paths_for_read
|
|
35
|
+
from simple_resume.shell.palettes.loader import get_palette_registry
|
|
36
|
+
from simple_resume.shell.render.latex import (
|
|
37
|
+
LatexCompilationError,
|
|
38
|
+
compile_tex_to_pdf,
|
|
39
|
+
render_resume_latex_from_data,
|
|
40
|
+
)
|
|
41
|
+
from simple_resume.shell.render.operations import (
|
|
42
|
+
generate_html_with_jinja as shell_generate,
|
|
43
|
+
)
|
|
44
|
+
from simple_resume.shell.runtime.content import get_content, load_palette_from_file
|
|
45
|
+
from simple_resume.shell.service_locator import register_service
|
|
46
|
+
from simple_resume.shell.strategies import (
|
|
47
|
+
LatexStrategy,
|
|
48
|
+
PdfGenerationRequest,
|
|
49
|
+
WeasyPrintStrategy,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DefaultTemplateLocator(TemplateLocator):
|
|
54
|
+
"""Default template locator implementation."""
|
|
55
|
+
|
|
56
|
+
def get_template_location(self) -> Path:
|
|
57
|
+
"""Get the template directory path."""
|
|
58
|
+
return TEMPLATE_LOC
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class DefaultEffectExecutor(EffectExecutor):
|
|
62
|
+
"""Default effect executor implementation."""
|
|
63
|
+
|
|
64
|
+
def __init__(self) -> None:
|
|
65
|
+
"""Initialize the default effect executor."""
|
|
66
|
+
self._executor = ShellEffectExecutor()
|
|
67
|
+
|
|
68
|
+
def execute(self, effect: Any) -> None:
|
|
69
|
+
"""Execute a single effect."""
|
|
70
|
+
self._executor.execute(effect)
|
|
71
|
+
|
|
72
|
+
def execute_many(self, effects: list[Any]) -> None:
|
|
73
|
+
"""Execute multiple effects."""
|
|
74
|
+
self._executor.execute_many(effects)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class DefaultPathResolver(PathResolver):
|
|
78
|
+
"""Default path resolver implementation."""
|
|
79
|
+
|
|
80
|
+
def candidate_yaml_path(self, name: str) -> Path:
|
|
81
|
+
"""Get candidate YAML path for a name."""
|
|
82
|
+
result = candidate_yaml_path(name)
|
|
83
|
+
if result is None:
|
|
84
|
+
# Fallback to creating a Path from name
|
|
85
|
+
return Path(name)
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
def resolve_paths_for_read(
|
|
89
|
+
self,
|
|
90
|
+
paths: Any,
|
|
91
|
+
overrides: dict[str, Any],
|
|
92
|
+
candidate_path: Path,
|
|
93
|
+
) -> Any:
|
|
94
|
+
"""Resolve paths for reading operations."""
|
|
95
|
+
return resolve_paths_for_read(paths, overrides, candidate_path)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class DefaultContentLoader(ContentLoader):
|
|
99
|
+
"""Default content loader implementation."""
|
|
100
|
+
|
|
101
|
+
def __init__(self) -> None:
|
|
102
|
+
"""Initialize the default content loader."""
|
|
103
|
+
self._path_resolver = DefaultPathResolver()
|
|
104
|
+
|
|
105
|
+
def load(
|
|
106
|
+
self,
|
|
107
|
+
name: str,
|
|
108
|
+
paths: Any,
|
|
109
|
+
transform_markdown: bool,
|
|
110
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
111
|
+
"""Load content from a YAML file."""
|
|
112
|
+
candidate_path = self._path_resolver.candidate_yaml_path(name)
|
|
113
|
+
resolved_paths = self._path_resolver.resolve_paths_for_read(
|
|
114
|
+
paths, {}, candidate_path
|
|
115
|
+
)
|
|
116
|
+
raw_data = get_content(name, paths=resolved_paths, transform_markdown=False)
|
|
117
|
+
|
|
118
|
+
if transform_markdown:
|
|
119
|
+
processed_data = render_markdown_content(raw_data)
|
|
120
|
+
else:
|
|
121
|
+
processed_data = copy.deepcopy(raw_data)
|
|
122
|
+
return processed_data, raw_data
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class DefaultPdfGenerationStrategy(PdfGenerationStrategy):
|
|
126
|
+
"""Default PDF generation strategy implementation."""
|
|
127
|
+
|
|
128
|
+
def __init__(self, mode: str = "weasyprint") -> None:
|
|
129
|
+
"""Initialize the PDF generation strategy.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
mode: PDF generation mode ('latex' or 'weasyprint')
|
|
133
|
+
|
|
134
|
+
"""
|
|
135
|
+
if mode == "latex":
|
|
136
|
+
self._strategy: LatexStrategy | WeasyPrintStrategy = LatexStrategy()
|
|
137
|
+
else:
|
|
138
|
+
self._strategy = WeasyPrintStrategy()
|
|
139
|
+
|
|
140
|
+
def generate(
|
|
141
|
+
self,
|
|
142
|
+
render_plan: Any,
|
|
143
|
+
output_path: Path,
|
|
144
|
+
resume_name: str,
|
|
145
|
+
filename: str | None = None,
|
|
146
|
+
) -> tuple[Any, int | None]:
|
|
147
|
+
"""Generate a PDF file."""
|
|
148
|
+
if not isinstance(render_plan, PdfGenerationRequest):
|
|
149
|
+
raise TypeError(
|
|
150
|
+
"render_plan must be a PdfGenerationRequest; "
|
|
151
|
+
"legacy inputs are not supported"
|
|
152
|
+
)
|
|
153
|
+
request = render_plan
|
|
154
|
+
result = self._strategy.generate_pdf(request)
|
|
155
|
+
return result, None if hasattr(result, "page_count") else None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class DefaultHtmlGenerator(HtmlGenerator):
|
|
159
|
+
"""Default HTML generator implementation."""
|
|
160
|
+
|
|
161
|
+
def generate(
|
|
162
|
+
self,
|
|
163
|
+
render_plan: Any,
|
|
164
|
+
output_path: Path,
|
|
165
|
+
filename: str | None = None,
|
|
166
|
+
) -> Any:
|
|
167
|
+
"""Generate HTML content."""
|
|
168
|
+
# Use shell's render operations directly since this service is in shell layer
|
|
169
|
+
return shell_generate(render_plan, output_path, filename)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class DefaultFileOpenerService(FileOpenerService):
|
|
173
|
+
"""Default file opener service implementation."""
|
|
174
|
+
|
|
175
|
+
def open_file(self, path: Path, format_type: str | None = None) -> bool:
|
|
176
|
+
"""Open a file with the system default application."""
|
|
177
|
+
return shell_open_file(path, format_type)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class DefaultPaletteLoader(PaletteLoader):
|
|
181
|
+
"""Default palette loader implementation."""
|
|
182
|
+
|
|
183
|
+
def load_palette_from_file(self, path: str | Path) -> dict[str, Any]:
|
|
184
|
+
"""Load a palette from a file."""
|
|
185
|
+
return load_palette_from_file(path)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class DefaultLaTeXRenderer(LaTeXRenderer):
|
|
189
|
+
"""Default LaTeX renderer implementation."""
|
|
190
|
+
|
|
191
|
+
def get_latex_functions(self) -> tuple[Any, Any, Any]:
|
|
192
|
+
"""Get LaTeX compilation functions."""
|
|
193
|
+
try:
|
|
194
|
+
return (
|
|
195
|
+
LatexCompilationError,
|
|
196
|
+
compile_tex_to_pdf,
|
|
197
|
+
render_resume_latex_from_data,
|
|
198
|
+
)
|
|
199
|
+
except ImportError:
|
|
200
|
+
return None, None, None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def register_default_services() -> None:
|
|
204
|
+
"""Register all default services with the service locator."""
|
|
205
|
+
# Create default implementations
|
|
206
|
+
content_loader = DefaultContentLoader()
|
|
207
|
+
path_resolver = DefaultPathResolver()
|
|
208
|
+
palette_loader = DefaultPaletteLoader()
|
|
209
|
+
template_locator = DefaultTemplateLocator()
|
|
210
|
+
effect_executor = DefaultEffectExecutor()
|
|
211
|
+
latex_renderer = DefaultLaTeXRenderer()
|
|
212
|
+
|
|
213
|
+
# Set default dependencies for core HTML generation
|
|
214
|
+
html_factory = create_html_generator_factory(template_locator)
|
|
215
|
+
|
|
216
|
+
# Set default dependencies for core PDF generation
|
|
217
|
+
pdf_factory = PdfGeneratorFactory(
|
|
218
|
+
effect_executor=effect_executor,
|
|
219
|
+
template_locator=template_locator,
|
|
220
|
+
latex_renderer=latex_renderer,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Register with service locator (for legacy compatibility)
|
|
224
|
+
register_service("html_generator_factory", html_factory)
|
|
225
|
+
register_service("pdf_generator_factory", pdf_factory)
|
|
226
|
+
register_service("file_opener", DefaultFileOpenerService())
|
|
227
|
+
register_service("palette_loader", palette_loader)
|
|
228
|
+
register_service("latex_renderer", latex_renderer)
|
|
229
|
+
register_service("html_generator", DefaultHtmlGenerator())
|
|
230
|
+
register_service("pdf_generation_strategy", DefaultPdfGenerationStrategy())
|
|
231
|
+
|
|
232
|
+
# Set default loaders for core Resume class
|
|
233
|
+
set_default_loaders(
|
|
234
|
+
content_loader=content_loader,
|
|
235
|
+
palette_loader=palette_loader,
|
|
236
|
+
path_resolver=path_resolver,
|
|
237
|
+
palette_registry_provider=get_palette_registry,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Warm the palette registry once at startup to avoid expensive discovery
|
|
241
|
+
# during latency-sensitive operations (e.g., concurrent renders in tests).
|
|
242
|
+
get_palette_registry()
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
__all__ = [
|
|
246
|
+
"DefaultTemplateLocator",
|
|
247
|
+
"DefaultEffectExecutor",
|
|
248
|
+
"DefaultPathResolver",
|
|
249
|
+
"DefaultContentLoader",
|
|
250
|
+
"DefaultPdfGenerationStrategy",
|
|
251
|
+
"DefaultHtmlGenerator",
|
|
252
|
+
"DefaultFileOpenerService",
|
|
253
|
+
"DefaultPaletteLoader",
|
|
254
|
+
"DefaultLaTeXRenderer",
|
|
255
|
+
"register_default_services",
|
|
256
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Session configuration for simple-resume operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from simple_resume.core.constants import OutputFormat
|
|
10
|
+
from simple_resume.core.exceptions import ConfigurationError
|
|
11
|
+
from simple_resume.core.paths import Paths
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class SessionConfig:
|
|
16
|
+
"""Configuration for a `ResumeSession`."""
|
|
17
|
+
|
|
18
|
+
paths: Paths | None = None
|
|
19
|
+
default_template: str | None = None
|
|
20
|
+
default_palette: str | None = None
|
|
21
|
+
default_format: OutputFormat | str = OutputFormat.PDF
|
|
22
|
+
auto_open: bool = False
|
|
23
|
+
preview_mode: bool = False
|
|
24
|
+
output_dir: Path | None = None
|
|
25
|
+
# Additional session-wide settings
|
|
26
|
+
session_metadata: dict[str, Any] = field(default_factory=dict)
|
|
27
|
+
|
|
28
|
+
def __post_init__(self) -> None:
|
|
29
|
+
"""Normalize enum-backed fields."""
|
|
30
|
+
try:
|
|
31
|
+
self.default_format = OutputFormat.normalize(self.default_format)
|
|
32
|
+
except (ValueError, TypeError) as exc:
|
|
33
|
+
raise ConfigurationError(
|
|
34
|
+
f"Invalid default format: {self.default_format}"
|
|
35
|
+
) from exc
|