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