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,84 @@
|
|
|
1
|
+
"""LaTeX document data types (pure, immutable)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, ClassVar, Literal, TypedDict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ParagraphBlock(TypedDict):
|
|
11
|
+
"""Define a paragraph text block."""
|
|
12
|
+
|
|
13
|
+
kind: Literal["paragraph"]
|
|
14
|
+
text: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ListBlock(TypedDict):
|
|
18
|
+
"""Define a bullet or enumerated list block."""
|
|
19
|
+
|
|
20
|
+
kind: Literal["itemize", "enumerate"]
|
|
21
|
+
items: list[str]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
Block = ParagraphBlock | ListBlock
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class LatexEntry:
|
|
29
|
+
"""Define a single entry in a resume section."""
|
|
30
|
+
|
|
31
|
+
title: str
|
|
32
|
+
subtitle: str | None
|
|
33
|
+
date_range: str | None
|
|
34
|
+
blocks: list[Block]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class LatexSection:
|
|
39
|
+
"""Define a top-level resume section."""
|
|
40
|
+
|
|
41
|
+
title: str
|
|
42
|
+
entries: list[LatexEntry]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class LatexRenderResult:
|
|
47
|
+
"""Define the result of a LaTeX render operation."""
|
|
48
|
+
|
|
49
|
+
tex: str
|
|
50
|
+
context: dict[str, Any]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class LatexGenerationContext:
|
|
55
|
+
"""Context object for LaTeX PDF generation, grouping related parameters."""
|
|
56
|
+
|
|
57
|
+
last_context: ClassVar[LatexGenerationContext | None] = None
|
|
58
|
+
resume_data: dict[str, Any] | None
|
|
59
|
+
processed_data: dict[str, Any]
|
|
60
|
+
output_path: Path
|
|
61
|
+
base_path: Path | str | None = None
|
|
62
|
+
filename: str | None = None
|
|
63
|
+
paths: Any = None
|
|
64
|
+
metadata: Any = None
|
|
65
|
+
|
|
66
|
+
def __post_init__(self) -> None:
|
|
67
|
+
"""Cache the most recent context for fallback use."""
|
|
68
|
+
type(self).last_context = self
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def raw_data(self) -> dict[str, Any] | None:
|
|
72
|
+
"""Backward-compatible accessor used by some tests."""
|
|
73
|
+
return self.resume_data
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
__all__ = [
|
|
77
|
+
"Block",
|
|
78
|
+
"LatexEntry",
|
|
79
|
+
"LatexGenerationContext",
|
|
80
|
+
"LatexRenderResult",
|
|
81
|
+
"LatexSection",
|
|
82
|
+
"ListBlock",
|
|
83
|
+
"ParagraphBlock",
|
|
84
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Pure helpers for transforming resume Markdown into HTML."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from markdown import markdown
|
|
10
|
+
|
|
11
|
+
from simple_resume.core.colors import darken_color, is_valid_color
|
|
12
|
+
from simple_resume.core.constants.colors import (
|
|
13
|
+
BOLD_DARKEN_FACTOR,
|
|
14
|
+
DEFAULT_BOLD_COLOR,
|
|
15
|
+
DEFAULT_COLOR_SCHEME,
|
|
16
|
+
)
|
|
17
|
+
from simple_resume.core.hydration import build_skill_group_payload
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def derive_bold_color(frame_color: str | None) -> str:
|
|
21
|
+
"""Return a darkened color for bold text."""
|
|
22
|
+
if isinstance(frame_color, str) and is_valid_color(frame_color):
|
|
23
|
+
return darken_color(frame_color, BOLD_DARKEN_FACTOR)
|
|
24
|
+
return DEFAULT_COLOR_SCHEME.get("bold_color", DEFAULT_BOLD_COLOR)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _apply_bold_color(html: str, color: str, font_weight: int = 600) -> str:
|
|
28
|
+
"""Apply color styling to `<strong>` tags in an HTML string."""
|
|
29
|
+
if not html or "<strong" not in html:
|
|
30
|
+
return html
|
|
31
|
+
|
|
32
|
+
strong_style = f"color: {color}; font-weight: {font_weight} !important;"
|
|
33
|
+
replacements = {
|
|
34
|
+
"<strong>": f'<strong class="markdown-strong" style="{strong_style}">',
|
|
35
|
+
"<strong >": f'<strong class="markdown-strong" style="{strong_style}">',
|
|
36
|
+
}
|
|
37
|
+
for needle, replacement in replacements.items():
|
|
38
|
+
html = html.replace(needle, replacement)
|
|
39
|
+
return html
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def transform_markdown_blocks(
|
|
43
|
+
data: dict[str, Any],
|
|
44
|
+
*,
|
|
45
|
+
bold_color: str = DEFAULT_BOLD_COLOR,
|
|
46
|
+
bold_font_weight: int = 600,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Convert Markdown fields in-place."""
|
|
49
|
+
extensions = [
|
|
50
|
+
"fenced_code",
|
|
51
|
+
"tables",
|
|
52
|
+
"codehilite",
|
|
53
|
+
"nl2br",
|
|
54
|
+
"attr_list",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
description = data.get("description")
|
|
58
|
+
if isinstance(description, str):
|
|
59
|
+
data["description"] = _apply_bold_color(
|
|
60
|
+
markdown(description, extensions=extensions),
|
|
61
|
+
bold_color,
|
|
62
|
+
bold_font_weight,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
body = data.get("body")
|
|
66
|
+
if isinstance(body, dict):
|
|
67
|
+
for block_data in body.values():
|
|
68
|
+
for element in block_data:
|
|
69
|
+
if isinstance(element, dict):
|
|
70
|
+
desc = element.get("description")
|
|
71
|
+
if isinstance(desc, str):
|
|
72
|
+
element["description"] = _apply_bold_color(
|
|
73
|
+
markdown(desc, extensions=extensions),
|
|
74
|
+
bold_color,
|
|
75
|
+
bold_font_weight,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _determine_bold_color(config: Mapping[str, Any] | None) -> str:
|
|
80
|
+
"""Derive the effective bold color from configuration data."""
|
|
81
|
+
if not config:
|
|
82
|
+
return DEFAULT_COLOR_SCHEME.get("bold_color", DEFAULT_BOLD_COLOR)
|
|
83
|
+
|
|
84
|
+
# First check for explicit bold_color
|
|
85
|
+
bold_color = config.get("bold_color")
|
|
86
|
+
if isinstance(bold_color, str) and is_valid_color(bold_color):
|
|
87
|
+
return bold_color
|
|
88
|
+
|
|
89
|
+
# Fall back to frame_color (use directly, not derived)
|
|
90
|
+
frame_color = config.get("frame_color")
|
|
91
|
+
if isinstance(frame_color, str) and is_valid_color(frame_color):
|
|
92
|
+
return frame_color
|
|
93
|
+
|
|
94
|
+
# Check other color candidates
|
|
95
|
+
color_candidates = [
|
|
96
|
+
config.get("heading_icon_color"),
|
|
97
|
+
config.get("theme_color"),
|
|
98
|
+
]
|
|
99
|
+
for candidate in color_candidates:
|
|
100
|
+
if isinstance(candidate, str) and is_valid_color(candidate):
|
|
101
|
+
return candidate
|
|
102
|
+
|
|
103
|
+
return DEFAULT_COLOR_SCHEME.get("bold_color", DEFAULT_BOLD_COLOR)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def render_markdown_content(resume_data: dict[str, Any]) -> dict[str, Any]:
|
|
107
|
+
"""Return a copy of resume data with Markdown converted to HTML."""
|
|
108
|
+
transformed_resume = copy.deepcopy(resume_data)
|
|
109
|
+
|
|
110
|
+
config = transformed_resume.get("config")
|
|
111
|
+
bold_color = _determine_bold_color(config if isinstance(config, Mapping) else None)
|
|
112
|
+
bold_font_weight = 600 # default
|
|
113
|
+
if isinstance(config, Mapping):
|
|
114
|
+
bold_font_weight = int(config.get("bold_font_weight", 600))
|
|
115
|
+
|
|
116
|
+
transform_markdown_blocks(
|
|
117
|
+
transformed_resume, bold_color=bold_color, bold_font_weight=bold_font_weight
|
|
118
|
+
)
|
|
119
|
+
transformed_resume.update(build_skill_group_payload(transformed_resume))
|
|
120
|
+
return transformed_resume
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
__all__ = [
|
|
124
|
+
"derive_bold_color",
|
|
125
|
+
"render_markdown_content",
|
|
126
|
+
"transform_markdown_blocks",
|
|
127
|
+
]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Core data models for resume rendering."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from simple_resume.core.constants import OutputFormat, RenderMode
|
|
10
|
+
from simple_resume.core.paths import Paths
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class ResumeConfig:
|
|
15
|
+
"""A normalized resume configuration with validated fields."""
|
|
16
|
+
|
|
17
|
+
page_width: int | None = None
|
|
18
|
+
page_height: int | None = None
|
|
19
|
+
sidebar_width: int | None = None
|
|
20
|
+
output_mode: str = "markdown"
|
|
21
|
+
template: str = "resume_no_bars"
|
|
22
|
+
color_scheme: str = "default"
|
|
23
|
+
|
|
24
|
+
# Color fields
|
|
25
|
+
theme_color: str = "#0395DE"
|
|
26
|
+
sidebar_color: str = "#F6F6F6"
|
|
27
|
+
sidebar_text_color: str = "#000000"
|
|
28
|
+
sidebar_bold_color: str = "#000000"
|
|
29
|
+
bar_background_color: str = "#DFDFDF"
|
|
30
|
+
date2_color: str = "#616161"
|
|
31
|
+
frame_color: str = "#757575"
|
|
32
|
+
heading_icon_color: str = "#0395DE"
|
|
33
|
+
bold_color: str = "#585858"
|
|
34
|
+
|
|
35
|
+
# Layout customization fields (section heading icons)
|
|
36
|
+
section_icon_circle_size: float = 7.8
|
|
37
|
+
section_icon_circle_x_offset: float = 0
|
|
38
|
+
section_icon_design_size: float = 3.5
|
|
39
|
+
section_icon_design_x_offset: float = 0
|
|
40
|
+
section_icon_design_y_offset: float = 0
|
|
41
|
+
section_heading_text_margin: float = -6
|
|
42
|
+
|
|
43
|
+
# Contact icon customization
|
|
44
|
+
contact_icon_size: float = 5
|
|
45
|
+
contact_icon_margin_top: float = 0.5
|
|
46
|
+
contact_icon_margin_right: float = 2
|
|
47
|
+
contact_icon_gap: float = 4
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class RenderPlan:
|
|
52
|
+
"""A pure data structure describing how to render a resume."""
|
|
53
|
+
|
|
54
|
+
name: str
|
|
55
|
+
mode: RenderMode
|
|
56
|
+
config: ResumeConfig
|
|
57
|
+
template_name: str | None = None
|
|
58
|
+
context: dict[str, Any] | None = None
|
|
59
|
+
tex: str | None = None
|
|
60
|
+
palette_metadata: dict[str, Any] | None = None
|
|
61
|
+
base_path: Path | str = ""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class ValidationResult:
|
|
66
|
+
"""The result of validating resume data."""
|
|
67
|
+
|
|
68
|
+
is_valid: bool
|
|
69
|
+
errors: list[str]
|
|
70
|
+
warnings: list[str]
|
|
71
|
+
normalized_config: ResumeConfig | None = None
|
|
72
|
+
palette_metadata: dict[str, Any] | None = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class GenerationConfig:
|
|
77
|
+
"""A complete configuration for generation operations."""
|
|
78
|
+
|
|
79
|
+
# Path configuration
|
|
80
|
+
data_dir: str | Path | None = None
|
|
81
|
+
output_dir: str | Path | None = None
|
|
82
|
+
output_path: str | Path | None = None
|
|
83
|
+
paths: Paths | None = None
|
|
84
|
+
|
|
85
|
+
# Generation options
|
|
86
|
+
template: str | None = None
|
|
87
|
+
format: OutputFormat | str = OutputFormat.PDF
|
|
88
|
+
open_after: bool = False
|
|
89
|
+
preview: bool = False
|
|
90
|
+
name: str | None = None
|
|
91
|
+
pattern: str = "*"
|
|
92
|
+
browser: str | None = None
|
|
93
|
+
formats: list[OutputFormat | str] | None = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
__all__ = [
|
|
97
|
+
"GenerationConfig",
|
|
98
|
+
"RenderMode",
|
|
99
|
+
"RenderPlan",
|
|
100
|
+
"ResumeConfig",
|
|
101
|
+
"ValidationResult",
|
|
102
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Palette discovery utilities and registries (pure core)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from simple_resume.core.palettes.exceptions import (
|
|
7
|
+
PaletteError,
|
|
8
|
+
PaletteGenerationError,
|
|
9
|
+
PaletteLookupError,
|
|
10
|
+
PaletteRemoteDisabled,
|
|
11
|
+
PaletteRemoteError,
|
|
12
|
+
)
|
|
13
|
+
from simple_resume.core.palettes.fetch_types import (
|
|
14
|
+
PaletteFetchRequest,
|
|
15
|
+
PaletteResolution,
|
|
16
|
+
)
|
|
17
|
+
from simple_resume.core.palettes.generators import generate_hcl_palette
|
|
18
|
+
from simple_resume.core.palettes.registry import (
|
|
19
|
+
Palette,
|
|
20
|
+
PaletteRegistry,
|
|
21
|
+
build_palette_registry,
|
|
22
|
+
)
|
|
23
|
+
from simple_resume.core.palettes.resolution import resolve_palette_config
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Palette",
|
|
27
|
+
"PaletteRegistry",
|
|
28
|
+
"build_palette_registry",
|
|
29
|
+
"generate_hcl_palette",
|
|
30
|
+
"PaletteError",
|
|
31
|
+
"PaletteLookupError",
|
|
32
|
+
"PaletteGenerationError",
|
|
33
|
+
"PaletteRemoteDisabled",
|
|
34
|
+
"PaletteRemoteError",
|
|
35
|
+
"PaletteFetchRequest",
|
|
36
|
+
"PaletteResolution",
|
|
37
|
+
"resolve_palette_config",
|
|
38
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Define common types and utilities for palette modules."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
_CACHE_ENV = "SIMPLE_RESUME_PALETTE_CACHE_DIR"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Palette:
|
|
16
|
+
"""Define palette metadata and resolved swatches."""
|
|
17
|
+
|
|
18
|
+
name: str
|
|
19
|
+
swatches: tuple[str, ...]
|
|
20
|
+
source: str
|
|
21
|
+
metadata: dict[str, object] = field(default_factory=dict)
|
|
22
|
+
|
|
23
|
+
def to_dict(self) -> dict[str, object]:
|
|
24
|
+
"""Serialize palette to a JSON-friendly structure."""
|
|
25
|
+
return {
|
|
26
|
+
"name": self.name,
|
|
27
|
+
"swatches": list(self.swatches),
|
|
28
|
+
"source": self.source,
|
|
29
|
+
"metadata": dict(self.metadata),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PaletteSource(str, Enum):
|
|
34
|
+
"""Define supported palette sources for resume configuration."""
|
|
35
|
+
|
|
36
|
+
REGISTRY = "registry"
|
|
37
|
+
GENERATOR = "generator"
|
|
38
|
+
REMOTE = "remote"
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def normalize(
|
|
42
|
+
cls, value: str | PaletteSource | None, *, param_name: str | None = None
|
|
43
|
+
) -> PaletteSource:
|
|
44
|
+
"""Convert arbitrary input into a `PaletteSource` enum member."""
|
|
45
|
+
if value is None:
|
|
46
|
+
return cls.REGISTRY
|
|
47
|
+
if isinstance(value, cls):
|
|
48
|
+
return value
|
|
49
|
+
if not isinstance(value, str):
|
|
50
|
+
raise TypeError(
|
|
51
|
+
f"Palette source must be string or PaletteSource, got {type(value)}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
normalized = value.strip().lower()
|
|
55
|
+
try:
|
|
56
|
+
return cls(normalized)
|
|
57
|
+
except ValueError as exc:
|
|
58
|
+
label = f"{param_name} " if param_name else ""
|
|
59
|
+
supported_sources = ", ".join(
|
|
60
|
+
sorted(member.value for member in cls.__members__.values())
|
|
61
|
+
)
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"Unsupported {label}source: {value}. Supported sources: "
|
|
64
|
+
f"{supported_sources}"
|
|
65
|
+
) from exc
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_cache_dir() -> Path:
|
|
69
|
+
"""Return palette cache directory."""
|
|
70
|
+
custom = os.environ.get(_CACHE_ENV)
|
|
71
|
+
if custom:
|
|
72
|
+
return Path(custom).expanduser()
|
|
73
|
+
return Path.home() / ".cache" / "simple-resume" / "palettes"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "default",
|
|
4
|
+
"colors": [
|
|
5
|
+
"#0395DE",
|
|
6
|
+
"#F6F6F6",
|
|
7
|
+
"#DFDFDF",
|
|
8
|
+
"#616161",
|
|
9
|
+
"#757575"
|
|
10
|
+
],
|
|
11
|
+
"source": "default",
|
|
12
|
+
"metadata": {
|
|
13
|
+
"description": "Professional blue default palette"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "modern_teal",
|
|
18
|
+
"colors": [
|
|
19
|
+
"#0891B2",
|
|
20
|
+
"#F0FDFA",
|
|
21
|
+
"#CCFBF1",
|
|
22
|
+
"#134E4A",
|
|
23
|
+
"#0E7490"
|
|
24
|
+
],
|
|
25
|
+
"source": "default",
|
|
26
|
+
"metadata": {
|
|
27
|
+
"description": "Default teal palette"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "ocean",
|
|
32
|
+
"colors": [
|
|
33
|
+
"#005B96",
|
|
34
|
+
"#E6F7FF",
|
|
35
|
+
"#A7C6ED",
|
|
36
|
+
"#013A63",
|
|
37
|
+
"#0A2463"
|
|
38
|
+
],
|
|
39
|
+
"source": "default",
|
|
40
|
+
"metadata": {
|
|
41
|
+
"description": "Classic ocean blues used by legacy templates"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "ocean_blue",
|
|
46
|
+
"colors": [
|
|
47
|
+
"#0275D8",
|
|
48
|
+
"#F0F8FF",
|
|
49
|
+
"#CFE2FF",
|
|
50
|
+
"#023E8A",
|
|
51
|
+
"#03045E"
|
|
52
|
+
],
|
|
53
|
+
"source": "default",
|
|
54
|
+
"metadata": {
|
|
55
|
+
"description": "Ocean-inspired palette for CLI demos"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Exception types used by the palette subsystem."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PaletteError(RuntimeError):
|
|
8
|
+
"""Base class for palette-related failures."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PaletteLookupError(PaletteError):
|
|
12
|
+
"""Raised when a named palette cannot be located."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PaletteGenerationError(PaletteError):
|
|
16
|
+
"""Raised when a generator cannot produce the requested swatches."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PaletteRemoteDisabled(PaletteError):
|
|
20
|
+
"""Raised when remote palette access is disabled by configuration."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PaletteRemoteError(PaletteError):
|
|
24
|
+
"""Raised when a remote palette provider returns an error."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"PaletteError",
|
|
29
|
+
"PaletteGenerationError",
|
|
30
|
+
"PaletteLookupError",
|
|
31
|
+
"PaletteRemoteDisabled",
|
|
32
|
+
"PaletteRemoteError",
|
|
33
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Palette request and response types for pure core operations.
|
|
2
|
+
|
|
3
|
+
These types allow core functions to describe what network operations
|
|
4
|
+
are needed without actually performing them, keeping the core pure.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class PaletteFetchRequest:
|
|
13
|
+
"""Request to fetch palette from remote source.
|
|
14
|
+
|
|
15
|
+
This describes a network operation that should be executed by the shell layer.
|
|
16
|
+
The core layer creates these requests but never executes them directly.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
source: str # e.g., "colourlovers"
|
|
20
|
+
keywords: list[str] | None = None
|
|
21
|
+
num_results: int = 1
|
|
22
|
+
order_by: str = "score"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class PaletteResolution:
|
|
27
|
+
"""Result of palette resolution - either colors or fetch request.
|
|
28
|
+
|
|
29
|
+
This represents the result of pure palette resolution logic.
|
|
30
|
+
It either contains resolved colors (for local sources) or a
|
|
31
|
+
fetch request (for remote sources) that the shell should execute.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
colors: list[str] | None = None
|
|
35
|
+
metadata: dict[str, Any] | None = None
|
|
36
|
+
fetch_request: PaletteFetchRequest | None = None
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def needs_fetch(self) -> bool:
|
|
40
|
+
"""Check if this resolution requires network fetching."""
|
|
41
|
+
return self.fetch_request is not None
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def has_colors(self) -> bool:
|
|
45
|
+
"""Check if this resolution already contains colors."""
|
|
46
|
+
return self.colors is not None and len(self.colors) > 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"PaletteFetchRequest",
|
|
51
|
+
"PaletteResolution",
|
|
52
|
+
]
|