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