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,226 @@
|
|
|
1
|
+
"""Pure result objects for simple-resume operations.
|
|
2
|
+
|
|
3
|
+
This module contains immutable data classes that describe generation results.
|
|
4
|
+
These are pure data structures with NO I/O operations - all file operations
|
|
5
|
+
should be performed by the shell layer.
|
|
6
|
+
|
|
7
|
+
Following the functional core / imperative shell pattern:
|
|
8
|
+
- Core: Describes what was generated (this module)
|
|
9
|
+
- Shell: Performs I/O operations on the results (shell/file_opener.py)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import time
|
|
16
|
+
from collections.abc import Iterator
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import PosixPath, PurePath
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class GenerationMetadata:
|
|
24
|
+
"""Metadata describing a generation operation (pure data)."""
|
|
25
|
+
|
|
26
|
+
format_type: str
|
|
27
|
+
template_name: str
|
|
28
|
+
generation_time: float
|
|
29
|
+
file_size: int
|
|
30
|
+
resume_name: str
|
|
31
|
+
palette_info: dict[str, Any] | None = None
|
|
32
|
+
page_count: int | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class GenerationResult:
|
|
37
|
+
"""Pure generation result - describes the artifact location.
|
|
38
|
+
|
|
39
|
+
This is a pure data class that only holds information about a generated file.
|
|
40
|
+
It does NOT perform any I/O operations. For file operations like opening,
|
|
41
|
+
deleting, or reading files, use the shell layer (shell/file_opener.py).
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
output_path: Path where the file was generated
|
|
45
|
+
format_type: Type of the generated file ('pdf', 'html', etc.)
|
|
46
|
+
metadata: Optional metadata about the generation process
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
output_path: Any
|
|
51
|
+
_normalized_path: PosixPath = field(init=False, repr=False, compare=False)
|
|
52
|
+
format_type: str
|
|
53
|
+
metadata: GenerationMetadata | None = None
|
|
54
|
+
|
|
55
|
+
def __post_init__(self) -> None:
|
|
56
|
+
"""Initialize the GenerationResult after dataclass creation."""
|
|
57
|
+
original_path = self.output_path
|
|
58
|
+
try:
|
|
59
|
+
if isinstance(original_path, PurePath):
|
|
60
|
+
normalized_path = PosixPath(os.fspath(original_path))
|
|
61
|
+
else:
|
|
62
|
+
# Force POSIX concrete path to avoid WindowsPath instantiation
|
|
63
|
+
# when platform is mocked during CI strategy tests.
|
|
64
|
+
normalized_path = PosixPath(os.fspath(original_path))
|
|
65
|
+
except Exception:
|
|
66
|
+
# Final safety net: force a POSIX path from string.
|
|
67
|
+
normalized_path = PosixPath(str(original_path))
|
|
68
|
+
|
|
69
|
+
object.__setattr__(self, "_normalized_path", normalized_path)
|
|
70
|
+
|
|
71
|
+
# Normalize format_type to lowercase
|
|
72
|
+
normalized_format = self.format_type.lower()
|
|
73
|
+
object.__setattr__(self, "format_type", normalized_format)
|
|
74
|
+
|
|
75
|
+
# Create default metadata if none provided
|
|
76
|
+
if self.metadata is None:
|
|
77
|
+
default_metadata = GenerationMetadata(
|
|
78
|
+
format_type=normalized_format,
|
|
79
|
+
template_name="unknown",
|
|
80
|
+
generation_time=time.time(),
|
|
81
|
+
file_size=0,
|
|
82
|
+
resume_name="unknown",
|
|
83
|
+
palette_info=None,
|
|
84
|
+
page_count=None,
|
|
85
|
+
)
|
|
86
|
+
object.__setattr__(self, "metadata", default_metadata)
|
|
87
|
+
|
|
88
|
+
def __repr__(self) -> str: # pragma: no cover - debugging helper
|
|
89
|
+
"""Return a detailed string representation for debugging."""
|
|
90
|
+
return (
|
|
91
|
+
f"GenerationResult(output_path={self.output_path}, "
|
|
92
|
+
f"format_type={self.format_type})"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def __str__(self) -> str:
|
|
96
|
+
"""Return a string representation of the GenerationResult."""
|
|
97
|
+
return f"GenerationResult(format={self.format_type}, path={self.output_path})"
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def name(self) -> str:
|
|
101
|
+
"""Return the filename of the output file."""
|
|
102
|
+
return self._normalized_path.name
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def stem(self) -> str:
|
|
106
|
+
"""Return the stem (filename without extension) of the output file."""
|
|
107
|
+
return self._normalized_path.stem
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def suffix(self) -> str:
|
|
111
|
+
"""Return the suffix (extension) of the output file."""
|
|
112
|
+
return self._normalized_path.suffix
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def exists(self) -> bool:
|
|
116
|
+
"""Check if the output file exists and is a file (not a directory).
|
|
117
|
+
|
|
118
|
+
Note: This is a read-only property that checks file existence.
|
|
119
|
+
It's acceptable in core because it's a pure query with no side effects.
|
|
120
|
+
"""
|
|
121
|
+
return self._normalized_path.exists() and self._normalized_path.is_file()
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def size(self) -> int:
|
|
125
|
+
"""Return file size in bytes (0 if file doesn't exist).
|
|
126
|
+
|
|
127
|
+
Note: This is a read-only property that queries file metadata.
|
|
128
|
+
It's acceptable in core because it's a pure query with no side effects.
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
return self._normalized_path.stat().st_size if self.exists else 0
|
|
132
|
+
except OSError:
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def size_human(self) -> str:
|
|
137
|
+
"""Return file size in human-readable format."""
|
|
138
|
+
size = self.size
|
|
139
|
+
KB_FACTOR = 1024
|
|
140
|
+
MB_FACTOR = 1024 * 1024
|
|
141
|
+
GB_FACTOR = 1024 * 1024 * 1024
|
|
142
|
+
if size < KB_FACTOR:
|
|
143
|
+
return f"{size} B"
|
|
144
|
+
elif size < MB_FACTOR:
|
|
145
|
+
return f"{size / KB_FACTOR:.1f} KB"
|
|
146
|
+
elif size < GB_FACTOR:
|
|
147
|
+
return f"{size / MB_FACTOR:.1f} MB"
|
|
148
|
+
else:
|
|
149
|
+
return f"{size / GB_FACTOR:.1f} GB"
|
|
150
|
+
|
|
151
|
+
def __bool__(self) -> bool:
|
|
152
|
+
"""Return True if the output file exists."""
|
|
153
|
+
return self.exists
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass(frozen=True)
|
|
157
|
+
class BatchGenerationResult:
|
|
158
|
+
"""Pure batch result with aggregate reporting information.
|
|
159
|
+
|
|
160
|
+
This is a pure data class that holds multiple GenerationResults.
|
|
161
|
+
It does NOT perform any I/O operations directly.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
results: dict[str, GenerationResult] = field(default_factory=dict)
|
|
165
|
+
resume_name: str | None = None
|
|
166
|
+
formats: list[str] | None = None
|
|
167
|
+
total_time: float = 0.0
|
|
168
|
+
successful: int = 0
|
|
169
|
+
failed: int = 0
|
|
170
|
+
errors: dict[str, Exception] = field(default_factory=dict)
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def total(self) -> int:
|
|
174
|
+
"""Return total number of operations."""
|
|
175
|
+
return self.successful + self.failed
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def success_rate(self) -> float:
|
|
179
|
+
"""Return success rate as a percentage."""
|
|
180
|
+
total = self.total
|
|
181
|
+
if total == 0:
|
|
182
|
+
return 0.0
|
|
183
|
+
return (self.successful / total) * 100.0
|
|
184
|
+
|
|
185
|
+
def __iter__(self) -> Iterator[GenerationResult]:
|
|
186
|
+
"""Return an iterator over the results."""
|
|
187
|
+
if self.results is None:
|
|
188
|
+
return iter([])
|
|
189
|
+
return iter(self.results.values())
|
|
190
|
+
|
|
191
|
+
def __len__(self) -> int:
|
|
192
|
+
"""Return the number of results."""
|
|
193
|
+
return len(self.results) if self.results is not None else 0
|
|
194
|
+
|
|
195
|
+
def __getitem__(self, key: str) -> GenerationResult:
|
|
196
|
+
"""Return the result with the given key."""
|
|
197
|
+
if self.results is None:
|
|
198
|
+
raise KeyError(f"No results available - key '{key}' not found")
|
|
199
|
+
return self.results[key]
|
|
200
|
+
|
|
201
|
+
def __contains__(self, key: str) -> bool:
|
|
202
|
+
"""Check if a result with the given key exists."""
|
|
203
|
+
return self.results is not None and key in self.results
|
|
204
|
+
|
|
205
|
+
def get(
|
|
206
|
+
self, key: str, default: GenerationResult | None = None
|
|
207
|
+
) -> GenerationResult | None:
|
|
208
|
+
"""Get a result by key, returning default if not found."""
|
|
209
|
+
if self.results is None:
|
|
210
|
+
return default
|
|
211
|
+
return self.results.get(key, default)
|
|
212
|
+
|
|
213
|
+
def get_successful(self) -> list[GenerationResult]:
|
|
214
|
+
"""Return the list of successful results."""
|
|
215
|
+
return list(self.results.values()) if self.results is not None else []
|
|
216
|
+
|
|
217
|
+
def get_failed(self) -> dict[str, Exception]:
|
|
218
|
+
"""Return the dictionary of failed results."""
|
|
219
|
+
return self.errors.copy() if self.errors is not None else {}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
__all__ = [
|
|
223
|
+
"GenerationMetadata",
|
|
224
|
+
"GenerationResult",
|
|
225
|
+
"BatchGenerationResult",
|
|
226
|
+
]
|