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,137 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Provide procedural palette generators."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import colorsys
|
|
7
|
+
import hashlib
|
|
8
|
+
import math
|
|
9
|
+
|
|
10
|
+
# Default deterministic seed for consistent color palette generation
|
|
11
|
+
# Format: YYYYMMDD (November 1, 2025) - ensures reproducible palettes across runs
|
|
12
|
+
DEFAULT_SEED = 20251101
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DeterministicRNG:
|
|
16
|
+
"""Define a deterministic random number generator using hash-based seeding."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, seed: int):
|
|
19
|
+
"""Initialize the deterministic RNG with a seed.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
seed: The seed for the random number generator.
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
self.seed = seed
|
|
26
|
+
self.state = seed
|
|
27
|
+
|
|
28
|
+
def random(self) -> float:
|
|
29
|
+
"""Generate a deterministic random float between 0 and 1."""
|
|
30
|
+
self.state += 1
|
|
31
|
+
hash_input = f"{self.seed}-{self.state}".encode()
|
|
32
|
+
# 64-bit digest keeps deterministic behavior without wasting work
|
|
33
|
+
hash_bytes = hashlib.blake2s(hash_input, digest_size=8).digest()
|
|
34
|
+
hash_int = int.from_bytes(hash_bytes, "big")
|
|
35
|
+
return hash_int / (2**64 - 1)
|
|
36
|
+
|
|
37
|
+
def uniform(self, a: float, b: float) -> float:
|
|
38
|
+
"""Generate a deterministic random float between a and b."""
|
|
39
|
+
return a + self.random() * (b - a)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _clamp(value: float, low: float, high: float) -> float:
|
|
43
|
+
"""Restrict a value to the [low, high] interval."""
|
|
44
|
+
return max(low, min(value, high))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _wrap_hue(value: float) -> float:
|
|
48
|
+
"""Wrap a hue value into [0, 360) degrees."""
|
|
49
|
+
return value % 360.0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _generate_hues(
|
|
53
|
+
*,
|
|
54
|
+
start: float,
|
|
55
|
+
end: float,
|
|
56
|
+
count: int,
|
|
57
|
+
rng: DeterministicRNG,
|
|
58
|
+
) -> list[float]:
|
|
59
|
+
"""Return evenly distributed hues between start and end (inclusive)."""
|
|
60
|
+
start = _wrap_hue(start)
|
|
61
|
+
end = _wrap_hue(end)
|
|
62
|
+
|
|
63
|
+
if count == 1:
|
|
64
|
+
return [start]
|
|
65
|
+
|
|
66
|
+
span = (end - start) % 360.0
|
|
67
|
+
if math.isclose(span, 0.0):
|
|
68
|
+
span = 360.0
|
|
69
|
+
|
|
70
|
+
step = span / (count - 1)
|
|
71
|
+
return [
|
|
72
|
+
_wrap_hue(start + index * step + rng.uniform(-step * 0.05, step * 0.05))
|
|
73
|
+
for index in range(count)
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _generate_luminance_values(
|
|
78
|
+
*,
|
|
79
|
+
start: float,
|
|
80
|
+
end: float,
|
|
81
|
+
count: int,
|
|
82
|
+
) -> list[float]:
|
|
83
|
+
"""Return interpolated luminance values."""
|
|
84
|
+
if count == 1:
|
|
85
|
+
return [_clamp(start, 0.0, 1.0)]
|
|
86
|
+
step = (end - start) / (count - 1)
|
|
87
|
+
return [_clamp(start + index * step, 0.0, 1.0) for index in range(count)]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _hsl_to_hex(hue: float, saturation: float, luminance: float) -> str:
|
|
91
|
+
"""Convert HSL values to a hex string."""
|
|
92
|
+
r, g, b = colorsys.hls_to_rgb(hue / 360.0, luminance, saturation)
|
|
93
|
+
r_hex = f"{int(_clamp(r, 0.0, 1.0) * 255 + 0.5):02X}"
|
|
94
|
+
g_hex = f"{int(_clamp(g, 0.0, 1.0) * 255 + 0.5):02X}"
|
|
95
|
+
b_hex = f"{int(_clamp(b, 0.0, 1.0) * 255 + 0.5):02X}"
|
|
96
|
+
return f"#{r_hex}{g_hex}{b_hex}"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def generate_hcl_palette(
|
|
100
|
+
size: int,
|
|
101
|
+
*,
|
|
102
|
+
seed: int | None = None,
|
|
103
|
+
hue_range: tuple[float, float] = (0.0, 360.0),
|
|
104
|
+
chroma: float = 0.12,
|
|
105
|
+
luminance_range: tuple[float, float] = (0.35, 0.85),
|
|
106
|
+
) -> list[str]:
|
|
107
|
+
"""Generate a deterministic palette in an HCL-inspired fashion.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
size: Number of swatches to produce.
|
|
111
|
+
seed: Optional deterministic seed. Defaults to a project seed.
|
|
112
|
+
hue_range: Inclusive range of hue values (degrees).
|
|
113
|
+
chroma: Saturation component (0-1, approximated using HSL saturation).
|
|
114
|
+
luminance_range: Inclusive range of luminance/lightness values.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List of hex color strings.
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
if size <= 0:
|
|
121
|
+
raise ValueError("size must be a positive integer")
|
|
122
|
+
|
|
123
|
+
rng = DeterministicRNG(seed if seed is not None else DEFAULT_SEED)
|
|
124
|
+
hue_start, hue_end = hue_range
|
|
125
|
+
lum_start, lum_end = luminance_range
|
|
126
|
+
|
|
127
|
+
hues = _generate_hues(start=hue_start, end=hue_end, count=size, rng=rng)
|
|
128
|
+
luminances = _generate_luminance_values(start=lum_start, end=lum_end, count=size)
|
|
129
|
+
|
|
130
|
+
saturation = _clamp(chroma, 0.0, 1.0)
|
|
131
|
+
colors: list[str] = []
|
|
132
|
+
for hue, luminance in zip(hues, luminances):
|
|
133
|
+
colors.append(_hsl_to_hex(hue, saturation, _clamp(luminance, 0.0, 1.0)))
|
|
134
|
+
return colors
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
__all__ = ["generate_hcl_palette"]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Provide a palette registry that aggregates multiple providers."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from typing import Callable
|
|
8
|
+
|
|
9
|
+
from simple_resume.core.palettes.common import Palette
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PaletteRegistry:
|
|
13
|
+
"""Define an in-memory registry of named palettes."""
|
|
14
|
+
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
"""Initialize an empty palette registry."""
|
|
17
|
+
self._palettes: dict[str, Palette] = {}
|
|
18
|
+
|
|
19
|
+
def register(self, palette: Palette) -> None:
|
|
20
|
+
"""Register or overwrite a palette."""
|
|
21
|
+
key = palette.name.lower()
|
|
22
|
+
self._palettes[key] = palette
|
|
23
|
+
|
|
24
|
+
def get(self, name: str) -> Palette:
|
|
25
|
+
"""Return a palette by name."""
|
|
26
|
+
key = name.lower()
|
|
27
|
+
try:
|
|
28
|
+
return self._palettes[key]
|
|
29
|
+
except KeyError as exc:
|
|
30
|
+
raise KeyError(f"Palette not found: {name}") from exc
|
|
31
|
+
|
|
32
|
+
def list(self) -> list[Palette]:
|
|
33
|
+
"""Return all registered palettes sorted by name."""
|
|
34
|
+
return [self._palettes[key] for key in sorted(self._palettes)]
|
|
35
|
+
|
|
36
|
+
def to_json(self) -> str:
|
|
37
|
+
"""Serialize the registry to JSON."""
|
|
38
|
+
return json.dumps([palette.to_dict() for palette in self.list()], indent=2)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_CACHE_ENV = "SIMPLE_RESUME_PALETTE_CACHE"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_palette_registry(
|
|
45
|
+
*,
|
|
46
|
+
default_loader: Callable[[], list[Palette]] | None = None,
|
|
47
|
+
palettable_loader: Callable[[], list[Palette]] | None = None,
|
|
48
|
+
) -> PaletteRegistry:
|
|
49
|
+
"""Build a palette registry with custom loader functions.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
default_loader: Function to load default palettes
|
|
53
|
+
palettable_loader: Function to load palettable palettes
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
PaletteRegistry populated with palettes from the specified loaders
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
registry = PaletteRegistry()
|
|
60
|
+
|
|
61
|
+
if default_loader:
|
|
62
|
+
for palette in default_loader():
|
|
63
|
+
registry.register(palette)
|
|
64
|
+
|
|
65
|
+
if palettable_loader:
|
|
66
|
+
for palette in palettable_loader():
|
|
67
|
+
registry.register(palette)
|
|
68
|
+
|
|
69
|
+
return registry
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
"Palette",
|
|
74
|
+
"PaletteRegistry",
|
|
75
|
+
"build_palette_registry",
|
|
76
|
+
]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Pure palette resolution logic without network I/O.
|
|
2
|
+
|
|
3
|
+
This module contains pure functions that resolve palette configurations
|
|
4
|
+
into either colors or fetch requests, without performing any I/O operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from simple_resume.core.constants.colors import CONFIG_COLOR_FIELDS
|
|
10
|
+
from simple_resume.core.palettes.common import PaletteSource
|
|
11
|
+
from simple_resume.core.palettes.exceptions import (
|
|
12
|
+
PaletteError,
|
|
13
|
+
PaletteGenerationError,
|
|
14
|
+
PaletteLookupError,
|
|
15
|
+
)
|
|
16
|
+
from simple_resume.core.palettes.fetch_types import (
|
|
17
|
+
PaletteFetchRequest,
|
|
18
|
+
PaletteResolution,
|
|
19
|
+
)
|
|
20
|
+
from simple_resume.core.palettes.generators import generate_hcl_palette
|
|
21
|
+
from simple_resume.core.palettes.registry import PaletteRegistry
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def resolve_palette_config(
|
|
25
|
+
block: dict[str, Any], *, registry: PaletteRegistry
|
|
26
|
+
) -> PaletteResolution:
|
|
27
|
+
"""Pure palette resolution - returns colors OR fetch request.
|
|
28
|
+
|
|
29
|
+
This function performs pure logic to determine what colors are needed
|
|
30
|
+
and how to obtain them. It never performs I/O operations.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
block: Palette configuration block from resume config.
|
|
34
|
+
registry: Palette registry to look up named palettes (injected dependency).
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
PaletteResolution with either colors (for local sources) or
|
|
38
|
+
a fetch request (for remote sources).
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
PaletteError: If palette configuration is invalid.
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
source = PaletteSource.normalize(block.get("source"), param_name="palette")
|
|
46
|
+
except (TypeError, ValueError) as exc:
|
|
47
|
+
raise PaletteError(
|
|
48
|
+
f"Unsupported palette source: {block.get('source')}"
|
|
49
|
+
) from exc
|
|
50
|
+
|
|
51
|
+
if source is PaletteSource.REGISTRY:
|
|
52
|
+
"""Pure lookup from local registry (no I/O)."""
|
|
53
|
+
name = block.get("name")
|
|
54
|
+
if not name:
|
|
55
|
+
raise PaletteLookupError("registry source requires 'name'")
|
|
56
|
+
|
|
57
|
+
palette = registry.get(str(name))
|
|
58
|
+
|
|
59
|
+
colors = list(palette.swatches)
|
|
60
|
+
metadata = {
|
|
61
|
+
"source": source.value,
|
|
62
|
+
"name": palette.name,
|
|
63
|
+
"size": len(palette.swatches),
|
|
64
|
+
"attribution": palette.metadata,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return PaletteResolution(colors=colors, metadata=metadata)
|
|
68
|
+
|
|
69
|
+
elif source is PaletteSource.GENERATOR:
|
|
70
|
+
"""Pure generation - no I/O."""
|
|
71
|
+
|
|
72
|
+
size = int(block.get("size", len(CONFIG_COLOR_FIELDS)))
|
|
73
|
+
seed = block.get("seed")
|
|
74
|
+
hue_range = tuple(block.get("hue_range", (0, 360)))
|
|
75
|
+
luminance_range = tuple(block.get("luminance_range", (0.35, 0.85)))
|
|
76
|
+
chroma = float(block.get("chroma", 0.12))
|
|
77
|
+
|
|
78
|
+
REQUIRED_RANGE_LENGTH = 2
|
|
79
|
+
if (
|
|
80
|
+
len(hue_range) != REQUIRED_RANGE_LENGTH
|
|
81
|
+
or len(luminance_range) != REQUIRED_RANGE_LENGTH
|
|
82
|
+
):
|
|
83
|
+
raise PaletteGenerationError(
|
|
84
|
+
"hue_range and luminance_range must have two values"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
colors = generate_hcl_palette(
|
|
88
|
+
size,
|
|
89
|
+
seed=int(seed) if seed is not None else None,
|
|
90
|
+
hue_range=(float(hue_range[0]), float(hue_range[1])),
|
|
91
|
+
chroma=chroma,
|
|
92
|
+
luminance_range=(float(luminance_range[0]), float(luminance_range[1])),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
metadata = {
|
|
96
|
+
"source": source.value,
|
|
97
|
+
"size": len(colors),
|
|
98
|
+
"seed": int(seed) if seed is not None else None,
|
|
99
|
+
"hue_range": [float(hue_range[0]), float(hue_range[1])],
|
|
100
|
+
"luminance_range": [float(luminance_range[0]), float(luminance_range[1])],
|
|
101
|
+
"chroma": chroma,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return PaletteResolution(colors=colors, metadata=metadata)
|
|
105
|
+
|
|
106
|
+
elif source is PaletteSource.REMOTE:
|
|
107
|
+
"""Return request for shell to execute - no network I/O here."""
|
|
108
|
+
fetch_request = PaletteFetchRequest(
|
|
109
|
+
source=source.value,
|
|
110
|
+
keywords=block.get("keywords"),
|
|
111
|
+
num_results=int(block.get("num_results", 1)),
|
|
112
|
+
order_by=str(block.get("order_by", "score")),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return PaletteResolution(fetch_request=fetch_request)
|
|
116
|
+
|
|
117
|
+
else:
|
|
118
|
+
raise PaletteError(f"Unsupported palette source: {source.value}")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
__all__ = [
|
|
122
|
+
"resolve_palette_config",
|
|
123
|
+
]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Provide palette sources: bundled datasets, palettable integration, remote APIs."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from collections.abc import Iterable, Mapping
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from importlib import import_module
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from simple_resume.core.palettes.common import Palette
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# NOTE: Network-related functions (_validate_url, _create_safe_request) and
|
|
18
|
+
# ColourLoversClient have been moved to shell/palettes/remote.py
|
|
19
|
+
|
|
20
|
+
DEFAULT_DATA_FILENAME = "default_palettes.json"
|
|
21
|
+
PALETTABLE_CACHE = "palettable_registry.json"
|
|
22
|
+
PALETTE_MODULE_CATEGORY_INDEX = 2
|
|
23
|
+
MIN_MODULE_NAME_PARTS = 2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class PalettableRecord:
|
|
28
|
+
"""Define metadata describing a palette provided by `palettable`."""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
module: str
|
|
32
|
+
attribute: str
|
|
33
|
+
category: str
|
|
34
|
+
palette_type: str
|
|
35
|
+
size: int
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict[str, object]:
|
|
38
|
+
"""Convert a record to dictionary representation."""
|
|
39
|
+
return {
|
|
40
|
+
"name": self.name,
|
|
41
|
+
"module": self.module,
|
|
42
|
+
"attribute": self.attribute,
|
|
43
|
+
"category": self.category,
|
|
44
|
+
"palette_type": self.palette_type,
|
|
45
|
+
"size": self.size,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_dict(cls, data: Mapping[str, object]) -> PalettableRecord:
|
|
50
|
+
"""Create a record from a dictionary."""
|
|
51
|
+
return cls(
|
|
52
|
+
name=str(data["name"]),
|
|
53
|
+
module=str(data["module"]),
|
|
54
|
+
attribute=str(data["attribute"]),
|
|
55
|
+
category=str(data["category"]),
|
|
56
|
+
palette_type=str(data["palette_type"]),
|
|
57
|
+
size=int(data["size"])
|
|
58
|
+
if isinstance(data["size"], (int, float, str))
|
|
59
|
+
else 0,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _data_dir() -> Path:
|
|
64
|
+
"""Return the data directory."""
|
|
65
|
+
return Path(__file__).resolve().parent / "data"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _default_file() -> Path:
|
|
69
|
+
"""Return the default palette file path (no I/O)."""
|
|
70
|
+
return _data_dir() / "default_palettes.json"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def parse_palette_data(payload: list[dict[str, Any]]) -> list[Palette]:
|
|
74
|
+
"""Parse palette JSON data into Palette objects (pure function).
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
payload: List of palette dictionaries with 'name', 'colors', etc.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
List of Palette objects.
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
palettes: list[Palette] = []
|
|
84
|
+
for entry in payload:
|
|
85
|
+
palettes.append(
|
|
86
|
+
Palette(
|
|
87
|
+
name=entry["name"],
|
|
88
|
+
swatches=tuple(entry["colors"]),
|
|
89
|
+
source=entry.get("source", "default"),
|
|
90
|
+
metadata=entry.get("metadata", {}),
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
return palettes
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def load_palettable_palette(record: PalettableRecord) -> Palette | None:
|
|
97
|
+
"""Resolve a `palettable` palette into our `Palette` type.
|
|
98
|
+
|
|
99
|
+
This remains in the core layer because it transforms library objects
|
|
100
|
+
into pure data structures; dynamic import is the only side effect.
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
module = import_module(record.module)
|
|
104
|
+
palette_obj = getattr(module, record.attribute)
|
|
105
|
+
raw_colors = getattr(palette_obj, "hex_colors", None) or getattr(
|
|
106
|
+
palette_obj, "colors", []
|
|
107
|
+
)
|
|
108
|
+
colors = tuple(
|
|
109
|
+
str(color if str(color).startswith("#") else f"#{color}")
|
|
110
|
+
for color in raw_colors
|
|
111
|
+
)
|
|
112
|
+
if not colors:
|
|
113
|
+
return None
|
|
114
|
+
metadata = {
|
|
115
|
+
"category": record.category,
|
|
116
|
+
"palette_type": record.palette_type,
|
|
117
|
+
"size": record.size,
|
|
118
|
+
}
|
|
119
|
+
return Palette(
|
|
120
|
+
name=record.name,
|
|
121
|
+
swatches=colors,
|
|
122
|
+
source="palettable",
|
|
123
|
+
metadata=metadata,
|
|
124
|
+
)
|
|
125
|
+
except Exception as exc: # noqa: BLE001
|
|
126
|
+
logger.debug(
|
|
127
|
+
"Unable to load palettable palette %s.%s: %s",
|
|
128
|
+
record.module,
|
|
129
|
+
record.attribute,
|
|
130
|
+
exc,
|
|
131
|
+
)
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _cache_path(filename: str) -> Path:
|
|
136
|
+
"""Return the cache file path.
|
|
137
|
+
|
|
138
|
+
NOTE: Assumes cache directory already exists. Directory creation
|
|
139
|
+
should be handled by the shell layer before calling core functions.
|
|
140
|
+
"""
|
|
141
|
+
return Path.home() / ".cache" / "simple_resume" / filename
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def parse_palettable_cache(payload: list[dict[str, Any]]) -> list[PalettableRecord]:
|
|
145
|
+
"""Parse palettable cache JSON into records (pure function)."""
|
|
146
|
+
return [PalettableRecord.from_dict(item) for item in payload]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def serialize_palettable_records(
|
|
150
|
+
records: Iterable[PalettableRecord],
|
|
151
|
+
) -> list[dict[str, Any]]:
|
|
152
|
+
"""Serialize palettable records to JSON-serializable dicts (pure function)."""
|
|
153
|
+
return [record.to_dict() for record in records]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
__all__ = [
|
|
157
|
+
"PalettableRecord",
|
|
158
|
+
"parse_palette_data",
|
|
159
|
+
"parse_palettable_cache",
|
|
160
|
+
"serialize_palettable_records",
|
|
161
|
+
"load_palettable_palette",
|
|
162
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Core filesystem path dataclasses used across the project."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class Paths:
|
|
11
|
+
"""Resolved filesystem locations for resume data and assets."""
|
|
12
|
+
|
|
13
|
+
data: Path
|
|
14
|
+
input: Path
|
|
15
|
+
output: Path
|
|
16
|
+
content: Path
|
|
17
|
+
templates: Path
|
|
18
|
+
static: Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = ["Paths"]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Protocol definitions for shell layer dependencies.
|
|
2
|
+
|
|
3
|
+
These protocols define the interfaces that shell layer implementations
|
|
4
|
+
must provide to the core layer, enabling dependency injection without
|
|
5
|
+
late-bound imports.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Protocol, runtime_checkable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class TemplateLocator(Protocol):
|
|
16
|
+
"""Protocol for locating template directories."""
|
|
17
|
+
|
|
18
|
+
def get_template_location(self) -> Path:
|
|
19
|
+
"""Get the template directory path."""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@runtime_checkable
|
|
24
|
+
class EffectExecutor(Protocol):
|
|
25
|
+
"""Protocol for executing effects."""
|
|
26
|
+
|
|
27
|
+
def execute(self, effect: Any) -> Any:
|
|
28
|
+
"""Execute a single effect and return its result (type varies)."""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
def execute_many(self, effects: list[Any]) -> None:
|
|
32
|
+
"""Execute multiple effects."""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@runtime_checkable
|
|
37
|
+
class ContentLoader(Protocol):
|
|
38
|
+
"""Protocol for loading resume content."""
|
|
39
|
+
|
|
40
|
+
def load(
|
|
41
|
+
self,
|
|
42
|
+
name: str,
|
|
43
|
+
paths: Any,
|
|
44
|
+
transform_markdown: bool,
|
|
45
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
46
|
+
"""Load content from a YAML file."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@runtime_checkable
|
|
51
|
+
class PdfGenerationStrategy(Protocol):
|
|
52
|
+
"""Protocol for PDF generation strategies."""
|
|
53
|
+
|
|
54
|
+
def generate(
|
|
55
|
+
self,
|
|
56
|
+
render_plan: Any,
|
|
57
|
+
output_path: Path,
|
|
58
|
+
resume_name: str,
|
|
59
|
+
filename: str | None = None,
|
|
60
|
+
) -> tuple[Any, int | None]:
|
|
61
|
+
"""Generate a PDF file."""
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@runtime_checkable
|
|
66
|
+
class HtmlGenerator(Protocol):
|
|
67
|
+
"""Protocol for HTML generation."""
|
|
68
|
+
|
|
69
|
+
def generate(
|
|
70
|
+
self,
|
|
71
|
+
render_plan: Any,
|
|
72
|
+
output_path: Path,
|
|
73
|
+
filename: str | None = None,
|
|
74
|
+
) -> Any:
|
|
75
|
+
"""Generate HTML content."""
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@runtime_checkable
|
|
80
|
+
class FileOpenerService(Protocol):
|
|
81
|
+
"""Protocol for opening files."""
|
|
82
|
+
|
|
83
|
+
def open_file(self, path: Path, format_type: str | None = None) -> bool:
|
|
84
|
+
"""Open a file with the system default application."""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@runtime_checkable
|
|
89
|
+
class PaletteLoader(Protocol):
|
|
90
|
+
"""Protocol for loading color palettes."""
|
|
91
|
+
|
|
92
|
+
def load_palette_from_file(self, path: str | Path) -> dict[str, Any]:
|
|
93
|
+
"""Load a palette from a file."""
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@runtime_checkable
|
|
98
|
+
class PathResolver(Protocol):
|
|
99
|
+
"""Protocol for resolving file paths."""
|
|
100
|
+
|
|
101
|
+
def candidate_yaml_path(self, name: str) -> Path:
|
|
102
|
+
"""Get candidate YAML path for a name."""
|
|
103
|
+
...
|
|
104
|
+
|
|
105
|
+
def resolve_paths_for_read(
|
|
106
|
+
self,
|
|
107
|
+
paths: Any,
|
|
108
|
+
overrides: dict[str, Any],
|
|
109
|
+
candidate_path: Path,
|
|
110
|
+
) -> Any:
|
|
111
|
+
"""Resolve paths for reading operations."""
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@runtime_checkable
|
|
116
|
+
class LaTeXRenderer(Protocol):
|
|
117
|
+
"""Protocol for LaTeX rendering."""
|
|
118
|
+
|
|
119
|
+
def get_latex_functions(self) -> tuple[Any, Any, Any]:
|
|
120
|
+
"""Get LaTeX compilation functions."""
|
|
121
|
+
...
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
__all__ = [
|
|
125
|
+
"TemplateLocator",
|
|
126
|
+
"EffectExecutor",
|
|
127
|
+
"ContentLoader",
|
|
128
|
+
"PdfGenerationStrategy",
|
|
129
|
+
"HtmlGenerator",
|
|
130
|
+
"FileOpenerService",
|
|
131
|
+
"PaletteLoader",
|
|
132
|
+
"PathResolver",
|
|
133
|
+
"LaTeXRenderer",
|
|
134
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Core rendering functionality for resumes.
|
|
2
|
+
|
|
3
|
+
This module provides pure functions for template rendering and coordination
|
|
4
|
+
between different rendering backends without any I/O side effects.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from simple_resume.core.render.manage import (
|
|
10
|
+
get_template_environment,
|
|
11
|
+
prepare_html_generation_request,
|
|
12
|
+
prepare_pdf_generation_request,
|
|
13
|
+
validate_render_plan,
|
|
14
|
+
)
|
|
15
|
+
from simple_resume.core.render.plan import (
|
|
16
|
+
RenderPlanConfig,
|
|
17
|
+
build_render_plan,
|
|
18
|
+
normalize_with_palette_fallback,
|
|
19
|
+
prepare_render_data,
|
|
20
|
+
transform_for_mode,
|
|
21
|
+
validate_resume_config,
|
|
22
|
+
validate_resume_config_or_raise,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"get_template_environment",
|
|
27
|
+
"prepare_html_generation_request",
|
|
28
|
+
"prepare_pdf_generation_request",
|
|
29
|
+
"validate_render_plan",
|
|
30
|
+
"build_render_plan",
|
|
31
|
+
"normalize_with_palette_fallback",
|
|
32
|
+
"prepare_render_data",
|
|
33
|
+
"RenderPlanConfig",
|
|
34
|
+
"transform_for_mode",
|
|
35
|
+
"validate_resume_config",
|
|
36
|
+
"validate_resume_config_or_raise",
|
|
37
|
+
]
|