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,672 @@
|
|
|
1
|
+
"""Provide pure configuration normalization helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import copy
|
|
7
|
+
from contextlib import ExitStack
|
|
8
|
+
from importlib import resources
|
|
9
|
+
from itertools import cycle
|
|
10
|
+
from typing import Any, Callable
|
|
11
|
+
|
|
12
|
+
from simple_resume.core.colors import (
|
|
13
|
+
ColorCalculationService,
|
|
14
|
+
darken_color,
|
|
15
|
+
get_contrasting_text_color,
|
|
16
|
+
is_valid_color,
|
|
17
|
+
)
|
|
18
|
+
from simple_resume.core.constants.colors import (
|
|
19
|
+
BOLD_DARKEN_FACTOR,
|
|
20
|
+
CONFIG_COLOR_FIELDS,
|
|
21
|
+
CONFIG_DIRECT_COLOR_KEYS,
|
|
22
|
+
DEFAULT_BOLD_COLOR,
|
|
23
|
+
DEFAULT_COLOR_SCHEME,
|
|
24
|
+
)
|
|
25
|
+
from simple_resume.core.palettes import resolve_palette_config
|
|
26
|
+
from simple_resume.core.palettes.exceptions import (
|
|
27
|
+
PaletteError,
|
|
28
|
+
PaletteLookupError,
|
|
29
|
+
)
|
|
30
|
+
from simple_resume.core.palettes.fetch_types import PaletteFetchRequest
|
|
31
|
+
from simple_resume.core.palettes.registry import PaletteRegistry
|
|
32
|
+
|
|
33
|
+
# Keep an open handle to package resources so they're available even when the
|
|
34
|
+
# distribution is zipped (e.g., installed from a wheel).
|
|
35
|
+
_asset_stack = ExitStack()
|
|
36
|
+
PACKAGE_ROOT = _asset_stack.enter_context(
|
|
37
|
+
resources.as_file(resources.files("simple_resume"))
|
|
38
|
+
)
|
|
39
|
+
atexit.register(_asset_stack.close)
|
|
40
|
+
|
|
41
|
+
PATH_CONTENT = PACKAGE_ROOT
|
|
42
|
+
TEMPLATE_LOC = PACKAGE_ROOT / "templates"
|
|
43
|
+
STATIC_LOC = PACKAGE_ROOT / "static"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _coerce_number(value: Any, *, field: str, prefix: str) -> float | int | None:
|
|
47
|
+
"""Coerce a value to a number (float or int) or None.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
value: The value to coerce.
|
|
51
|
+
field: The name of the field being coerced, for error messages.
|
|
52
|
+
prefix: A prefix for error messages.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The coerced numeric value, or None if the input is None.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
ValueError: If the value cannot be coerced to a number.
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
if value is None:
|
|
62
|
+
return None
|
|
63
|
+
if isinstance(value, bool):
|
|
64
|
+
raise ValueError(f"{prefix}{field} must be numeric. Got bool value {value!r}")
|
|
65
|
+
if isinstance(value, (int, float)):
|
|
66
|
+
return value
|
|
67
|
+
if isinstance(value, str):
|
|
68
|
+
stripped = value.strip()
|
|
69
|
+
if not stripped:
|
|
70
|
+
raise ValueError(f"{prefix}{field} must be numeric. Got empty string.")
|
|
71
|
+
try:
|
|
72
|
+
number = float(stripped)
|
|
73
|
+
return int(number) if number.is_integer() else number
|
|
74
|
+
except ValueError as exc:
|
|
75
|
+
raise ValueError(f"{prefix}{field} must be numeric. Got {value!r}") from exc
|
|
76
|
+
raise ValueError(f"{prefix}{field} must be numeric. Got {type(value).__name__}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def apply_config_defaults(config: dict[str, Any]) -> None:
|
|
80
|
+
"""Apply default layout, padding, and font weight settings to the configuration.
|
|
81
|
+
|
|
82
|
+
This function centralizes ALL default values to prevent silent failures.
|
|
83
|
+
Templates should only use .get() as a safety net with matching defaults.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
config: The configuration dictionary to modify in-place.
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
# ==========================================================================
|
|
90
|
+
# Page Dimensions (A4 default)
|
|
91
|
+
# ==========================================================================
|
|
92
|
+
config.setdefault("page_width", 210)
|
|
93
|
+
config.setdefault("page_height", 297)
|
|
94
|
+
config.setdefault("sidebar_width", 65)
|
|
95
|
+
|
|
96
|
+
# ==========================================================================
|
|
97
|
+
# Base Padding
|
|
98
|
+
# ==========================================================================
|
|
99
|
+
config.setdefault("padding", 12)
|
|
100
|
+
base_padding = config["padding"]
|
|
101
|
+
|
|
102
|
+
# ==========================================================================
|
|
103
|
+
# Sidebar Padding (derived from base padding)
|
|
104
|
+
# ==========================================================================
|
|
105
|
+
config.setdefault("sidebar_padding_left", base_padding - 2)
|
|
106
|
+
config.setdefault("sidebar_padding_right", base_padding - 2)
|
|
107
|
+
config.setdefault("sidebar_padding_top", 0)
|
|
108
|
+
config.setdefault("sidebar_padding_bottom", base_padding)
|
|
109
|
+
|
|
110
|
+
# ==========================================================================
|
|
111
|
+
# Pitch Section Padding
|
|
112
|
+
# ==========================================================================
|
|
113
|
+
config.setdefault("pitch_padding_top", 10)
|
|
114
|
+
config.setdefault("pitch_padding_bottom", 8)
|
|
115
|
+
config.setdefault("pitch_padding_left", 6)
|
|
116
|
+
|
|
117
|
+
# ==========================================================================
|
|
118
|
+
# Heading Padding
|
|
119
|
+
# ==========================================================================
|
|
120
|
+
config.setdefault("h2_padding_left", 6)
|
|
121
|
+
config.setdefault("h2_padding_top", 8)
|
|
122
|
+
config.setdefault("h3_padding_top", 7)
|
|
123
|
+
|
|
124
|
+
# ==========================================================================
|
|
125
|
+
# Section Layout
|
|
126
|
+
# ==========================================================================
|
|
127
|
+
config.setdefault("section_heading_margin_top", 7)
|
|
128
|
+
config.setdefault("section_heading_margin_bottom", 1)
|
|
129
|
+
config.setdefault("section_heading_text_margin", -6)
|
|
130
|
+
config.setdefault("section_icon_design_scale", 1)
|
|
131
|
+
config.setdefault("entry_margin_bottom", 4)
|
|
132
|
+
config.setdefault("tech_stack_margin_bottom", 2)
|
|
133
|
+
|
|
134
|
+
# ==========================================================================
|
|
135
|
+
# Section Icon Settings
|
|
136
|
+
# ==========================================================================
|
|
137
|
+
config.setdefault("section_icon_circle_size", 7.8)
|
|
138
|
+
config.setdefault("section_icon_circle_x_offset", 0)
|
|
139
|
+
config.setdefault("section_icon_design_size", 3.5)
|
|
140
|
+
config.setdefault("section_icon_design_x_offset", 0)
|
|
141
|
+
config.setdefault("section_icon_design_y_offset", 0)
|
|
142
|
+
|
|
143
|
+
# ==========================================================================
|
|
144
|
+
# Container Widths & Padding
|
|
145
|
+
# ==========================================================================
|
|
146
|
+
config.setdefault("date_container_width", 15)
|
|
147
|
+
config.setdefault("description_container_padding_left", 4)
|
|
148
|
+
config.setdefault("skill_container_padding_top", 3)
|
|
149
|
+
config.setdefault("skill_spacer_padding_top", 3)
|
|
150
|
+
config.setdefault("profile_image_padding_bottom", 8)
|
|
151
|
+
|
|
152
|
+
# ==========================================================================
|
|
153
|
+
# Frame (Preview Mode)
|
|
154
|
+
# ==========================================================================
|
|
155
|
+
config.setdefault("frame_padding", 15)
|
|
156
|
+
|
|
157
|
+
# ==========================================================================
|
|
158
|
+
# Cover Page Padding
|
|
159
|
+
# ==========================================================================
|
|
160
|
+
config.setdefault("cover_padding_top", 15)
|
|
161
|
+
config.setdefault("cover_padding_bottom", 15)
|
|
162
|
+
config.setdefault("cover_padding_h", 20)
|
|
163
|
+
|
|
164
|
+
# ==========================================================================
|
|
165
|
+
# Contact Section
|
|
166
|
+
# ==========================================================================
|
|
167
|
+
config.setdefault("contact_icon_size", 5)
|
|
168
|
+
config.setdefault("contact_icon_margin_top", 0.5)
|
|
169
|
+
config.setdefault("contact_text_padding_left", 2)
|
|
170
|
+
|
|
171
|
+
# ==========================================================================
|
|
172
|
+
# Font Settings
|
|
173
|
+
# ==========================================================================
|
|
174
|
+
config.setdefault("bold_font_weight", 600)
|
|
175
|
+
config.setdefault("description_font_size", 8.5)
|
|
176
|
+
config.setdefault("date_font_size", 9)
|
|
177
|
+
config.setdefault("sidebar_font_size", 8.5)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def validate_dimensions(config: dict[str, Any], filename_prefix: str) -> None:
|
|
181
|
+
"""Validate page and sidebar dimensions in the configuration.
|
|
182
|
+
|
|
183
|
+
Ensures that width and height are positive, and sidebar width is less
|
|
184
|
+
than page width.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
config: The configuration dictionary containing dimension settings.
|
|
188
|
+
filename_prefix: A prefix for error messages, typically the source filename.
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
ValueError: If any dimension is invalid.
|
|
192
|
+
|
|
193
|
+
"""
|
|
194
|
+
page_width = _coerce_number(
|
|
195
|
+
config.get("page_width"), field="page_width", prefix=filename_prefix
|
|
196
|
+
)
|
|
197
|
+
page_height = _coerce_number(
|
|
198
|
+
config.get("page_height"), field="page_height", prefix=filename_prefix
|
|
199
|
+
)
|
|
200
|
+
sidebar_width = _coerce_number(
|
|
201
|
+
config.get("sidebar_width"), field="sidebar_width", prefix=filename_prefix
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if page_width is not None and page_width <= 0:
|
|
205
|
+
raise ValueError(
|
|
206
|
+
f"{filename_prefix}Invalid resume config: page_width must be positive. "
|
|
207
|
+
f"Got page_width={config.get('page_width')}"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if page_height is not None and page_height <= 0:
|
|
211
|
+
raise ValueError(
|
|
212
|
+
f"{filename_prefix}Invalid resume config: page_height must be positive. "
|
|
213
|
+
f"Got page_height={config.get('page_height')}"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if page_width is not None:
|
|
217
|
+
config["page_width"] = page_width
|
|
218
|
+
if page_height is not None:
|
|
219
|
+
config["page_height"] = page_height
|
|
220
|
+
if sidebar_width is not None:
|
|
221
|
+
config["sidebar_width"] = sidebar_width
|
|
222
|
+
|
|
223
|
+
if sidebar_width is not None:
|
|
224
|
+
if sidebar_width <= 0:
|
|
225
|
+
raise ValueError(
|
|
226
|
+
f"{filename_prefix}Sidebar width must be positive. "
|
|
227
|
+
f"Got {config.get('sidebar_width')}"
|
|
228
|
+
)
|
|
229
|
+
if page_width is not None and sidebar_width >= page_width:
|
|
230
|
+
raise ValueError(
|
|
231
|
+
f"{filename_prefix}Sidebar width ({config.get('sidebar_width')}mm) "
|
|
232
|
+
f"must be less than page width ({config.get('page_width')}mm)"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _normalize_color_scheme(config: dict[str, Any]) -> None:
|
|
237
|
+
"""Normalize the 'color_scheme' field in the configuration.
|
|
238
|
+
|
|
239
|
+
Ensures 'color_scheme' is a string and defaults to "default" if empty or
|
|
240
|
+
not a string.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
config: The configuration dictionary to modify in-place.
|
|
244
|
+
|
|
245
|
+
"""
|
|
246
|
+
raw_scheme = config.get("color_scheme", "")
|
|
247
|
+
if isinstance(raw_scheme, str):
|
|
248
|
+
config["color_scheme"] = raw_scheme.strip() or "default"
|
|
249
|
+
else:
|
|
250
|
+
config["color_scheme"] = "default"
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _validate_color_fields(config: dict[str, Any], filename_prefix: str) -> None:
|
|
254
|
+
"""Validate and set default values for color-related fields in the configuration.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
config: The configuration dictionary to modify in-place.
|
|
258
|
+
filename_prefix: A prefix for error messages, typically the source filename.
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
ValueError: If a color value is invalid or not a hex string.
|
|
262
|
+
|
|
263
|
+
"""
|
|
264
|
+
for field in CONFIG_COLOR_FIELDS:
|
|
265
|
+
value = config.get(field)
|
|
266
|
+
if not value:
|
|
267
|
+
default_value = DEFAULT_COLOR_SCHEME.get(field)
|
|
268
|
+
if default_value:
|
|
269
|
+
config[field] = default_value
|
|
270
|
+
value = config.get(field)
|
|
271
|
+
if value is None:
|
|
272
|
+
continue
|
|
273
|
+
if not isinstance(value, str):
|
|
274
|
+
raise ValueError(
|
|
275
|
+
f"{filename_prefix}Invalid color format for '{field}': {value}. "
|
|
276
|
+
"Expected hex color string."
|
|
277
|
+
)
|
|
278
|
+
if not is_valid_color(value):
|
|
279
|
+
raise ValueError(
|
|
280
|
+
f"{filename_prefix}Invalid color format for '{field}': {value}. "
|
|
281
|
+
"Expected hex color like '#0395DE' or '#FFF'"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _auto_calculate_sidebar_text_color(config: dict[str, Any]) -> None:
|
|
286
|
+
"""Automatically calculate and set the sidebar text color for contrast.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
config: The configuration dictionary to modify in-place.
|
|
290
|
+
|
|
291
|
+
"""
|
|
292
|
+
config["sidebar_text_color"] = ColorCalculationService.calculate_sidebar_text_color(
|
|
293
|
+
config
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _handle_sidebar_bold_color(config: dict[str, Any], filename_prefix: str) -> None:
|
|
298
|
+
"""Handle explicit or automatically calculated sidebar bold color.
|
|
299
|
+
|
|
300
|
+
If 'sidebar_bold_color' is explicitly set, validates it.
|
|
301
|
+
Otherwise, calculates it using ColorCalculationService.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
config: The configuration dictionary to modify in-place.
|
|
305
|
+
filename_prefix: A prefix for error messages.
|
|
306
|
+
|
|
307
|
+
Raises:
|
|
308
|
+
ValueError: If an explicitly set color is invalid.
|
|
309
|
+
|
|
310
|
+
"""
|
|
311
|
+
explicit_color = config.get("sidebar_bold_color")
|
|
312
|
+
if explicit_color:
|
|
313
|
+
if not isinstance(explicit_color, str):
|
|
314
|
+
raise ValueError(
|
|
315
|
+
f"{filename_prefix}Invalid color format for 'sidebar_bold_color': "
|
|
316
|
+
f"{explicit_color}. Expected hex color string."
|
|
317
|
+
)
|
|
318
|
+
if not is_valid_color(explicit_color):
|
|
319
|
+
raise ValueError(
|
|
320
|
+
f"{filename_prefix}Invalid color format for 'sidebar_bold_color': "
|
|
321
|
+
f"{explicit_color}. Expected hex color like '#0395DE' or '#FFF'"
|
|
322
|
+
)
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
config["sidebar_bold_color"] = ColorCalculationService.calculate_sidebar_bold_color(
|
|
326
|
+
config
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _handle_icon_color(config: dict[str, Any], filename_prefix: str) -> None:
|
|
331
|
+
"""Handle explicit or automatically calculated icon colors.
|
|
332
|
+
|
|
333
|
+
If 'heading_icon_color' is explicitly set, validates it.
|
|
334
|
+
Otherwise, calculates heading and sidebar icon colors using ColorCalculationService.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
config: The configuration dictionary to modify in-place.
|
|
338
|
+
filename_prefix: A prefix for error messages.
|
|
339
|
+
|
|
340
|
+
Raises:
|
|
341
|
+
ValueError: If an explicitly set heading icon color is invalid.
|
|
342
|
+
|
|
343
|
+
"""
|
|
344
|
+
heading_icon_color = config.get("heading_icon_color")
|
|
345
|
+
if heading_icon_color:
|
|
346
|
+
if not isinstance(heading_icon_color, str):
|
|
347
|
+
raise ValueError(
|
|
348
|
+
f"{filename_prefix}Invalid color format for 'heading_icon_color': "
|
|
349
|
+
f"{heading_icon_color}. Expected hex color string."
|
|
350
|
+
)
|
|
351
|
+
if not is_valid_color(heading_icon_color):
|
|
352
|
+
raise ValueError(
|
|
353
|
+
f"{filename_prefix}Invalid color format for 'heading_icon_color': "
|
|
354
|
+
f"{heading_icon_color}. Expected hex color like '#0395DE' or '#FFF'"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
config["heading_icon_color"] = ColorCalculationService.calculate_heading_icon_color(
|
|
358
|
+
config
|
|
359
|
+
)
|
|
360
|
+
config["sidebar_icon_color"] = ColorCalculationService.calculate_sidebar_icon_color(
|
|
361
|
+
config
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _handle_bold_color(config: dict[str, Any], filename_prefix: str) -> None:
|
|
366
|
+
"""Handle explicit or automatically calculated bold text color.
|
|
367
|
+
|
|
368
|
+
If 'bold_color' is explicitly set, validates it.
|
|
369
|
+
Otherwise, derives it from 'frame_color' or uses a default.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
config: The configuration dictionary to modify in-place.
|
|
373
|
+
filename_prefix: A prefix for error messages.
|
|
374
|
+
|
|
375
|
+
Raises:
|
|
376
|
+
ValueError: If an explicitly set bold color is invalid.
|
|
377
|
+
|
|
378
|
+
"""
|
|
379
|
+
bold_color = config.get("bold_color")
|
|
380
|
+
if bold_color:
|
|
381
|
+
if not isinstance(bold_color, str):
|
|
382
|
+
raise ValueError(
|
|
383
|
+
f"{filename_prefix}Invalid color format for 'bold_color': "
|
|
384
|
+
f"{bold_color}. Expected hex color string."
|
|
385
|
+
)
|
|
386
|
+
if not is_valid_color(bold_color):
|
|
387
|
+
raise ValueError(
|
|
388
|
+
f"{filename_prefix}Invalid color format for 'bold_color': "
|
|
389
|
+
f"{bold_color}. Expected hex color like '#0395DE' or '#FFF'"
|
|
390
|
+
)
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
frame_color = config.get("frame_color")
|
|
394
|
+
if isinstance(frame_color, str) and is_valid_color(frame_color):
|
|
395
|
+
config["bold_color"] = darken_color(frame_color, BOLD_DARKEN_FACTOR)
|
|
396
|
+
return
|
|
397
|
+
config["bold_color"] = DEFAULT_COLOR_SCHEME.get("bold_color", DEFAULT_BOLD_COLOR)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def prepare_config(config: dict[str, Any], *, filename: str = "") -> bool:
|
|
401
|
+
"""Apply defaults and validate dimensions prior to palette resolution."""
|
|
402
|
+
filename_prefix = f"{filename}: " if filename else ""
|
|
403
|
+
apply_config_defaults(config)
|
|
404
|
+
validate_dimensions(config, filename_prefix)
|
|
405
|
+
return bool(config.get("sidebar_text_color"))
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def finalize_config(
|
|
409
|
+
config: dict[str, Any],
|
|
410
|
+
*,
|
|
411
|
+
filename: str = "",
|
|
412
|
+
sidebar_text_locked: bool = False,
|
|
413
|
+
) -> None:
|
|
414
|
+
"""Finalize color fields after palette resolution."""
|
|
415
|
+
filename_prefix = f"{filename}: " if filename else ""
|
|
416
|
+
_normalize_color_scheme(config)
|
|
417
|
+
_validate_color_fields(config, filename_prefix)
|
|
418
|
+
if not sidebar_text_locked:
|
|
419
|
+
_auto_calculate_sidebar_text_color(config)
|
|
420
|
+
_handle_icon_color(config, filename_prefix)
|
|
421
|
+
_handle_bold_color(config, filename_prefix)
|
|
422
|
+
_handle_sidebar_bold_color(config, filename_prefix)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
# ============================================================================
|
|
426
|
+
# Configuration Processing (Consolidated from configuration_processor.py)
|
|
427
|
+
# ============================================================================
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _is_direct_color_block(block: dict[str, Any]) -> bool:
|
|
431
|
+
"""Check if block contains direct color definitions (not palette config)."""
|
|
432
|
+
if not isinstance(block, dict):
|
|
433
|
+
return False
|
|
434
|
+
|
|
435
|
+
has_direct_colors = any(field in block for field in CONFIG_DIRECT_COLOR_KEYS)
|
|
436
|
+
palette_block_keys = {
|
|
437
|
+
"source",
|
|
438
|
+
"name",
|
|
439
|
+
"colors",
|
|
440
|
+
"size",
|
|
441
|
+
"seed",
|
|
442
|
+
"hue_range",
|
|
443
|
+
"luminance_range",
|
|
444
|
+
"chroma",
|
|
445
|
+
"keywords",
|
|
446
|
+
"num_results",
|
|
447
|
+
"order_by",
|
|
448
|
+
}
|
|
449
|
+
has_palette_config = any(key in block for key in palette_block_keys)
|
|
450
|
+
return has_direct_colors and not has_palette_config
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _process_direct_colors(
|
|
454
|
+
config: dict[str, Any],
|
|
455
|
+
block: dict[str, Any],
|
|
456
|
+
) -> dict[str, Any] | None:
|
|
457
|
+
"""Process direct color definitions by merging into config."""
|
|
458
|
+
# Direct color definitions: merge into config directly
|
|
459
|
+
for field in CONFIG_DIRECT_COLOR_KEYS:
|
|
460
|
+
if field in block:
|
|
461
|
+
config[field] = block[field]
|
|
462
|
+
|
|
463
|
+
# Automatically calculate sidebar text color based on sidebar background
|
|
464
|
+
if config.get("sidebar_color"):
|
|
465
|
+
config["sidebar_text_color"] = get_contrasting_text_color(
|
|
466
|
+
config["sidebar_color"]
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Return metadata indicating a direct color definition
|
|
470
|
+
return {
|
|
471
|
+
"source": "direct",
|
|
472
|
+
"fields": [f for f in CONFIG_DIRECT_COLOR_KEYS if f in block],
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _resolve_palette_block(
|
|
477
|
+
block: dict[str, Any],
|
|
478
|
+
*,
|
|
479
|
+
registry: PaletteRegistry,
|
|
480
|
+
palette_fetcher: Callable[[PaletteFetchRequest], tuple[list[str], dict[str, Any]]]
|
|
481
|
+
| None = None,
|
|
482
|
+
) -> tuple[list[str] | None, dict[str, Any]]:
|
|
483
|
+
"""Resolve palette block to color swatches and metadata.
|
|
484
|
+
|
|
485
|
+
This function uses dependency injection for both registry lookup and
|
|
486
|
+
network operations. The registry is required for local palette lookups.
|
|
487
|
+
If a palette_fetcher is provided, it will be used for remote palettes.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
block: Palette configuration block.
|
|
491
|
+
registry: Palette registry for looking up named palettes (required).
|
|
492
|
+
palette_fetcher: Optional callable that executes PaletteFetchRequest.
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
Tuple of (colors, metadata).
|
|
496
|
+
|
|
497
|
+
Raises:
|
|
498
|
+
PaletteLookupError: If palette cannot be resolved.
|
|
499
|
+
PaletteError: If palette configuration is invalid.
|
|
500
|
+
|
|
501
|
+
"""
|
|
502
|
+
# Use the new pure resolution system with injected registry
|
|
503
|
+
resolution = resolve_palette_config(block, registry=registry)
|
|
504
|
+
|
|
505
|
+
if resolution.has_colors:
|
|
506
|
+
# Local sources (registry, generator) - already have colors
|
|
507
|
+
return resolution.colors, resolution.metadata or {}
|
|
508
|
+
|
|
509
|
+
elif resolution.needs_fetch:
|
|
510
|
+
# Remote source - need to execute fetch request via injected dependency
|
|
511
|
+
if palette_fetcher is None:
|
|
512
|
+
raise PaletteLookupError(
|
|
513
|
+
"Remote palette fetching requires palette_fetcher parameter. "
|
|
514
|
+
"Provide a fetch function from the shell layer."
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
# Execute the network operation using the injected fetcher
|
|
518
|
+
if resolution.fetch_request is None:
|
|
519
|
+
raise PaletteLookupError(
|
|
520
|
+
"Internal error: fetch_request is None despite needs_fetch being True"
|
|
521
|
+
)
|
|
522
|
+
return palette_fetcher(resolution.fetch_request)
|
|
523
|
+
|
|
524
|
+
else:
|
|
525
|
+
raise PaletteLookupError(f"Unable to resolve palette: {block}")
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _process_palette_colors(
|
|
529
|
+
config: dict[str, Any],
|
|
530
|
+
block: dict[str, Any],
|
|
531
|
+
*,
|
|
532
|
+
registry: PaletteRegistry,
|
|
533
|
+
palette_fetcher: Callable[[PaletteFetchRequest], tuple[list[str], dict[str, Any]]]
|
|
534
|
+
| None = None,
|
|
535
|
+
) -> dict[str, Any] | None:
|
|
536
|
+
"""Process palette block by resolving colors and applying to config."""
|
|
537
|
+
try:
|
|
538
|
+
swatches, palette_meta = _resolve_palette_block(
|
|
539
|
+
block, registry=registry, palette_fetcher=palette_fetcher
|
|
540
|
+
)
|
|
541
|
+
except PaletteError:
|
|
542
|
+
raise
|
|
543
|
+
except (TypeError, ValueError, KeyError, AttributeError) as exc:
|
|
544
|
+
# Common errors when palette configuration is malformed
|
|
545
|
+
raise PaletteError(f"Invalid palette block: {exc}") from exc
|
|
546
|
+
|
|
547
|
+
if not swatches:
|
|
548
|
+
return None
|
|
549
|
+
|
|
550
|
+
# Cycle through swatches to cover all required fields
|
|
551
|
+
iterator = cycle(swatches)
|
|
552
|
+
for field in CONFIG_COLOR_FIELDS:
|
|
553
|
+
if field not in config or not config[field]:
|
|
554
|
+
config[field] = next(iterator)
|
|
555
|
+
|
|
556
|
+
# Automatically calculate sidebar text color based on sidebar background
|
|
557
|
+
if config.get("sidebar_color"):
|
|
558
|
+
config["sidebar_text_color"] = get_contrasting_text_color(
|
|
559
|
+
config["sidebar_color"]
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
# Set color scheme name if provided
|
|
563
|
+
if "color_scheme" not in config and "name" in block:
|
|
564
|
+
config["color_scheme"] = str(block["name"])
|
|
565
|
+
|
|
566
|
+
return palette_meta
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def apply_palette_block(
|
|
570
|
+
config: dict[str, Any],
|
|
571
|
+
*,
|
|
572
|
+
registry: PaletteRegistry,
|
|
573
|
+
palette_fetcher: Callable[[PaletteFetchRequest], tuple[list[str], dict[str, Any]]]
|
|
574
|
+
| None = None,
|
|
575
|
+
) -> dict[str, Any] | None:
|
|
576
|
+
"""Apply a palette block to the configuration using simplified logic.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
config: Configuration dictionary to modify.
|
|
580
|
+
registry: Palette registry for looking up named palettes (required).
|
|
581
|
+
palette_fetcher: Optional callable to handle remote palette fetching.
|
|
582
|
+
|
|
583
|
+
"""
|
|
584
|
+
block = config.get("palette")
|
|
585
|
+
if not isinstance(block, dict):
|
|
586
|
+
return None
|
|
587
|
+
|
|
588
|
+
# Simple conditional logic instead of complex Strategy pattern
|
|
589
|
+
if _is_direct_color_block(block):
|
|
590
|
+
return _process_direct_colors(config, block)
|
|
591
|
+
else:
|
|
592
|
+
return _process_palette_colors(
|
|
593
|
+
config, block, registry=registry, palette_fetcher=palette_fetcher
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def normalize_config(
|
|
598
|
+
raw_config: dict[str, Any],
|
|
599
|
+
filename: str = "",
|
|
600
|
+
*,
|
|
601
|
+
registry: PaletteRegistry,
|
|
602
|
+
palette_fetcher: Callable[[PaletteFetchRequest], tuple[list[str], dict[str, Any]]]
|
|
603
|
+
| None = None,
|
|
604
|
+
) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
|
605
|
+
"""Return a normalized copy of the configuration.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
raw_config: Raw configuration dictionary.
|
|
609
|
+
filename: Source filename for error messages.
|
|
610
|
+
registry: Palette registry for looking up named palettes (required).
|
|
611
|
+
palette_fetcher: Optional callable to handle remote palette fetching.
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
Tuple of (normalized_config, palette_metadata).
|
|
615
|
+
|
|
616
|
+
"""
|
|
617
|
+
working = copy.deepcopy(raw_config)
|
|
618
|
+
sidebar_locked = prepare_config(working, filename=filename)
|
|
619
|
+
palette_meta = apply_palette_block(
|
|
620
|
+
working,
|
|
621
|
+
registry=registry,
|
|
622
|
+
palette_fetcher=palette_fetcher,
|
|
623
|
+
)
|
|
624
|
+
finalize_config(
|
|
625
|
+
working,
|
|
626
|
+
filename=filename,
|
|
627
|
+
sidebar_text_locked=sidebar_locked,
|
|
628
|
+
)
|
|
629
|
+
return working, palette_meta
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def validate_config(
|
|
633
|
+
config: dict[str, Any],
|
|
634
|
+
filename: str = "",
|
|
635
|
+
*,
|
|
636
|
+
registry: PaletteRegistry,
|
|
637
|
+
palette_fetcher: Callable[[PaletteFetchRequest], tuple[list[str], dict[str, Any]]]
|
|
638
|
+
| None = None,
|
|
639
|
+
) -> None:
|
|
640
|
+
"""Normalize the configuration in-place if present (pure operation).
|
|
641
|
+
|
|
642
|
+
Args:
|
|
643
|
+
config: Configuration dictionary to normalize in-place.
|
|
644
|
+
filename: Source filename for error messages.
|
|
645
|
+
registry: Palette registry for looking up named palettes (required).
|
|
646
|
+
palette_fetcher: Optional callable to handle remote palette fetching.
|
|
647
|
+
|
|
648
|
+
"""
|
|
649
|
+
if not config:
|
|
650
|
+
return
|
|
651
|
+
normalized, _ = normalize_config(
|
|
652
|
+
config,
|
|
653
|
+
filename=filename,
|
|
654
|
+
registry=registry,
|
|
655
|
+
palette_fetcher=palette_fetcher,
|
|
656
|
+
)
|
|
657
|
+
config.clear()
|
|
658
|
+
config.update(normalized)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
__all__ = [
|
|
662
|
+
"CONFIG_COLOR_FIELDS",
|
|
663
|
+
"DEFAULT_BOLD_COLOR",
|
|
664
|
+
"DEFAULT_COLOR_SCHEME",
|
|
665
|
+
"CONFIG_DIRECT_COLOR_KEYS",
|
|
666
|
+
"apply_palette_block",
|
|
667
|
+
"finalize_config",
|
|
668
|
+
"prepare_config",
|
|
669
|
+
"_resolve_palette_block",
|
|
670
|
+
"normalize_config",
|
|
671
|
+
"validate_config",
|
|
672
|
+
]
|