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,207 @@
1
+ """Centralized constants for simple-resume.
2
+
3
+ This package contains core constants and enums for resume generation.
4
+ Domain-specific constants are organized in separate submodules.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from enum import Enum
10
+ from typing import Final
11
+
12
+ # =============================================================================
13
+ # CLI Exit Codes
14
+ # =============================================================================
15
+
16
+ EXIT_SUCCESS: Final[int] = 0
17
+ EXIT_SIGINT: Final[int] = 130 # Ctrl+C cancellation
18
+ EXIT_FILE_SYSTEM_ERROR: Final[int] = 2
19
+ EXIT_INTERNAL_ERROR: Final[int] = 3
20
+ EXIT_RESOURCE_ERROR: Final[int] = 4
21
+ EXIT_INPUT_ERROR: Final[int] = 5
22
+ EXIT_GENERIC_ERROR: Final[int] = 1
23
+
24
+ # =============================================================================
25
+ # Error Messages
26
+ # =============================================================================
27
+
28
+ ERROR_UNKNOWN_COMMAND: Final[str] = "Unknown command"
29
+ ERROR_FILE_NOT_FOUND: Final[str] = "Resume file not found"
30
+ ERROR_INVALID_FORMAT: Final[str] = "Invalid format"
31
+ ERROR_PERMISSION_DENIED: Final[str] = "Permission denied"
32
+
33
+ # =============================================================================
34
+ # Process and Resource Limits
35
+ # =============================================================================
36
+
37
+ DEFAULT_PROCESS_TIMEOUT_SECONDS: Final[int] = 30
38
+ MAX_RESUME_SIZE_MB: Final[int] = 10
39
+ MAX_PALETTE_SIZE_MB: Final[int] = 1
40
+
41
+ # =============================================================================
42
+ # File Extensions
43
+ # =============================================================================
44
+
45
+ PDF_EXTENSION: Final[str] = ".pdf"
46
+ HTML_EXTENSION: Final[str] = ".html"
47
+ TEX_EXTENSION: Final[str] = ".tex"
48
+ MARKDOWN_EXTENSION: Final[str] = ".md"
49
+
50
+ # =============================================================================
51
+ # Default Values
52
+ # =============================================================================
53
+
54
+ # Default values will be set after enum definitions
55
+ DEFAULT_FORMAT: str
56
+ DEFAULT_TEMPLATE: str
57
+
58
+ # =============================================================================
59
+ # Configuration
60
+ # =============================================================================
61
+
62
+ MIN_FILENAME_PARTS: Final[int] = 2
63
+ ALLOWED_PATH_OVERRIDES: Final[set[str]] = {"content_dir", "templates_dir", "static_dir"}
64
+
65
+ # =============================================================================
66
+ # Validation
67
+ # =============================================================================
68
+
69
+ MAX_FILE_SIZE_MB: Final[int] = 50
70
+
71
+
72
+ class OutputFormat(str, Enum):
73
+ """Define supported output formats for resume generation.
74
+
75
+ Final formats (require rendering):
76
+ PDF: Portable Document Format
77
+ HTML: HyperText Markup Language
78
+
79
+ Intermediate formats (editable before final render):
80
+ MARKDOWN: Markdown intermediate (for HTML path)
81
+ TEX: LaTeX intermediate (for PDF path)
82
+ LATEX: Alias for TEX (deprecated, use TEX)
83
+ """
84
+
85
+ PDF = "pdf"
86
+ HTML = "html"
87
+ MARKDOWN = "markdown"
88
+ TEX = "tex"
89
+ LATEX = "latex" # Alias for TEX, kept for backwards compatibility
90
+
91
+ @classmethod
92
+ def values(cls) -> set[str]:
93
+ """Return a set of all format values including aliases."""
94
+ return {
95
+ cls.PDF.value,
96
+ cls.HTML.value,
97
+ cls.MARKDOWN.value,
98
+ cls.TEX.value,
99
+ cls.LATEX.value,
100
+ }
101
+
102
+ @classmethod
103
+ def intermediate_formats(cls) -> set[OutputFormat]:
104
+ """Return the set of intermediate (non-final) output formats."""
105
+ return {cls.MARKDOWN, cls.TEX}
106
+
107
+ @classmethod
108
+ def is_intermediate(cls, fmt: OutputFormat) -> bool:
109
+ """Check if a format is an intermediate format."""
110
+ return fmt in cls.intermediate_formats()
111
+
112
+ @classmethod
113
+ def is_valid(cls, format_str: str) -> bool:
114
+ """Check if a format string is valid."""
115
+ return format_str.lower() in cls.values()
116
+
117
+ @classmethod
118
+ def normalize(
119
+ cls, value: str | OutputFormat, *, param_name: str | None = None
120
+ ) -> OutputFormat:
121
+ """Convert arbitrary input into an `OutputFormat` enum member."""
122
+ if isinstance(value, cls):
123
+ return value
124
+ if not isinstance(value, str):
125
+ raise TypeError(
126
+ "Output format must be provided as string or OutputFormat, "
127
+ f"got {type(value)}"
128
+ )
129
+
130
+ normalized = value.strip().lower()
131
+ try:
132
+ return cls(normalized)
133
+ except ValueError as exc: # pragma: no cover - defensive path
134
+ label = f"{param_name} " if param_name else ""
135
+ raise ValueError(
136
+ f"Unsupported {label}format: {value}. "
137
+ f"Supported formats: {', '.join(sorted(cls.values()))}"
138
+ ) from exc
139
+
140
+
141
+ class TemplateType(str, Enum):
142
+ """Define available resume templates."""
143
+
144
+ NO_BARS = "resume_no_bars"
145
+ WITH_BARS = "resume_with_bars"
146
+
147
+ @classmethod
148
+ def values(cls) -> set[str]:
149
+ """Return a set of all template values."""
150
+ return {cls.NO_BARS.value, cls.WITH_BARS.value}
151
+
152
+ @classmethod
153
+ def is_valid(cls, template_str: str) -> bool:
154
+ """Check if a template string is valid."""
155
+ return template_str in cls.values()
156
+
157
+
158
+ class RenderMode(str, Enum):
159
+ """Define rendering modes for resume generation."""
160
+
161
+ HTML = "html"
162
+ LATEX = "latex"
163
+
164
+
165
+ # Set defaults using enum values
166
+ DEFAULT_FORMAT = OutputFormat.PDF.value
167
+ DEFAULT_TEMPLATE = TemplateType.NO_BARS.value
168
+ SUPPORTED_FORMATS: Final[set[str]] = OutputFormat.values()
169
+ SUPPORTED_TEMPLATES: Final[set[str]] = TemplateType.values()
170
+
171
+
172
+ __all__ = [
173
+ # Exit codes
174
+ "EXIT_SUCCESS",
175
+ "EXIT_SIGINT",
176
+ "EXIT_FILE_SYSTEM_ERROR",
177
+ "EXIT_INTERNAL_ERROR",
178
+ "EXIT_RESOURCE_ERROR",
179
+ "EXIT_INPUT_ERROR",
180
+ "EXIT_GENERIC_ERROR",
181
+ # Error messages
182
+ "ERROR_UNKNOWN_COMMAND",
183
+ "ERROR_FILE_NOT_FOUND",
184
+ "ERROR_INVALID_FORMAT",
185
+ "ERROR_PERMISSION_DENIED",
186
+ # Process and resource limits
187
+ "DEFAULT_PROCESS_TIMEOUT_SECONDS",
188
+ "MAX_RESUME_SIZE_MB",
189
+ "MAX_PALETTE_SIZE_MB",
190
+ "MAX_FILE_SIZE_MB",
191
+ # File extensions
192
+ "PDF_EXTENSION",
193
+ "HTML_EXTENSION",
194
+ "TEX_EXTENSION",
195
+ "MARKDOWN_EXTENSION",
196
+ # Defaults and configuration
197
+ "DEFAULT_FORMAT",
198
+ "DEFAULT_TEMPLATE",
199
+ "MIN_FILENAME_PARTS",
200
+ "ALLOWED_PATH_OVERRIDES",
201
+ "SUPPORTED_FORMATS",
202
+ "SUPPORTED_TEMPLATES",
203
+ # Enums
204
+ "OutputFormat",
205
+ "TemplateType",
206
+ "RenderMode",
207
+ ]
@@ -0,0 +1,98 @@
1
+ """Color-related constants for simple-resume."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final
6
+
7
+ DEFAULT_COLOR_SCHEME: Final[dict[str, str]] = {
8
+ "theme_color": "#0395DE",
9
+ "sidebar_color": "#F6F6F6",
10
+ "sidebar_text_color": "#000000",
11
+ "bar_background_color": "#DFDFDF",
12
+ "date2_color": "#616161",
13
+ "frame_color": "#757575",
14
+ "heading_icon_color": "#0395DE",
15
+ "bold_color": "#585858",
16
+ }
17
+
18
+ # WCAG 2.1 relative luminance formula constants
19
+ # Reference: https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
20
+ # These constants implement the standard relative luminance calculation
21
+ # for determining color contrast and accessibility compliance.
22
+ WCAG_LINEARIZATION_THRESHOLD: Final[float] = 0.03928
23
+ WCAG_LINEARIZATION_DIVISOR: Final[float] = 12.92
24
+ WCAG_LINEARIZATION_EXPONENT: Final[float] = 2.4
25
+ WCAG_LINEARIZATION_OFFSET: Final[float] = 0.055
26
+
27
+ # Color manipulation constants
28
+ BOLD_DARKEN_FACTOR: Final[float] = 0.75
29
+ SIDEBAR_BOLD_DARKEN_FACTOR: Final[float] = 0.8
30
+
31
+ # Luminance thresholds for color contrast calculations
32
+ LUMINANCE_VERY_DARK: Final[float] = 0.15
33
+ LUMINANCE_DARK: Final[float] = 0.5
34
+ LUMINANCE_VERY_LIGHT: Final[float] = 0.8
35
+ ICON_CONTRAST_THRESHOLD: Final[float] = 3.0
36
+
37
+ # Color Format Constants
38
+ HEX_COLOR_SHORT_LENGTH: Final[int] = 3
39
+ HEX_COLOR_FULL_LENGTH: Final[int] = 6
40
+
41
+ # UI Element Constants
42
+ DEFAULT_BOLD_COLOR: Final[str] = "#585858"
43
+
44
+ # Define color field ordering for palette application
45
+ COLOR_FIELD_ORDER: Final[tuple[str, ...]] = (
46
+ "accent_color",
47
+ "sidebar_color",
48
+ "text_color",
49
+ "emphasis_color",
50
+ "link_color",
51
+ )
52
+
53
+ # Direct color keys that can be specified in configuration
54
+ DIRECT_COLOR_KEYS: Final[set[str]] = {
55
+ "accent_color",
56
+ "sidebar_color",
57
+ "text_color",
58
+ "emphasis_color",
59
+ "link_color",
60
+ "sidebar_text_color",
61
+ }
62
+
63
+ # Resume configuration color ordering (used by palette normalization)
64
+ CONFIG_COLOR_FIELDS: Final[tuple[str, ...]] = (
65
+ "theme_color",
66
+ "sidebar_color",
67
+ "sidebar_text_color",
68
+ "bar_background_color",
69
+ "date2_color",
70
+ "frame_color",
71
+ "heading_icon_color",
72
+ )
73
+
74
+ CONFIG_DIRECT_COLOR_KEYS: Final[tuple[str, ...]] = CONFIG_COLOR_FIELDS + (
75
+ "bold_color",
76
+ "sidebar_bold_color",
77
+ )
78
+
79
+ __all__ = [
80
+ "DEFAULT_COLOR_SCHEME",
81
+ "WCAG_LINEARIZATION_THRESHOLD",
82
+ "WCAG_LINEARIZATION_DIVISOR",
83
+ "WCAG_LINEARIZATION_EXPONENT",
84
+ "WCAG_LINEARIZATION_OFFSET",
85
+ "BOLD_DARKEN_FACTOR",
86
+ "SIDEBAR_BOLD_DARKEN_FACTOR",
87
+ "LUMINANCE_VERY_DARK",
88
+ "LUMINANCE_DARK",
89
+ "LUMINANCE_VERY_LIGHT",
90
+ "ICON_CONTRAST_THRESHOLD",
91
+ "HEX_COLOR_SHORT_LENGTH",
92
+ "HEX_COLOR_FULL_LENGTH",
93
+ "DEFAULT_BOLD_COLOR",
94
+ "COLOR_FIELD_ORDER",
95
+ "DIRECT_COLOR_KEYS",
96
+ "CONFIG_COLOR_FIELDS",
97
+ "CONFIG_DIRECT_COLOR_KEYS",
98
+ ]
@@ -0,0 +1,28 @@
1
+ """Filesystem and file-format constants for simple-resume."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final
6
+
7
+ # Supported file extensions
8
+ SUPPORTED_YAML_EXTENSIONS: Final[set[str]] = {".yaml", ".yml"}
9
+ SUPPORTED_YAML_EXTENSIONS_STR: Final[str] = "yaml" # For CLI usage
10
+
11
+ # Default template paths
12
+ DEFAULT_LATEX_TEMPLATE: Final[str] = "latex/basic.tex"
13
+
14
+ # Font scaling constants
15
+ FONTAWESOME_DEFAULT_SCALE: Final[float] = 0.72
16
+
17
+ # Byte conversion constants
18
+ BYTES_PER_KB: Final[int] = 1024
19
+ BYTES_PER_MB: Final[int] = 1024 * 1024
20
+
21
+ __all__ = [
22
+ "SUPPORTED_YAML_EXTENSIONS",
23
+ "SUPPORTED_YAML_EXTENSIONS_STR",
24
+ "DEFAULT_LATEX_TEMPLATE",
25
+ "FONTAWESOME_DEFAULT_SCALE",
26
+ "BYTES_PER_KB",
27
+ "BYTES_PER_MB",
28
+ ]
@@ -0,0 +1,58 @@
1
+ """Layout and measurement constants for simple-resume."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final
6
+
7
+ # Default page dimensions in millimeters (A4 paper: 210x297mm)
8
+ DEFAULT_PAGE_WIDTH_MM: Final[int] = 210
9
+ DEFAULT_PAGE_HEIGHT_MM: Final[int] = 297
10
+
11
+ # Default sidebar width in millimeters (A4 standard layout)
12
+ DEFAULT_SIDEBAR_WIDTH_MM: Final[int] = 65
13
+
14
+ # Default padding values in points/millimeters
15
+ DEFAULT_PADDING: Final[int] = 12
16
+ DEFAULT_SIDEBAR_PADDING_ADJUSTMENT: Final[int] = -2
17
+ DEFAULT_SIDEBAR_PADDING: Final[int] = 12
18
+
19
+ # Frame padding values
20
+ DEFAULT_FRAME_PADDING: Final[int] = 10
21
+
22
+ # Cover letter specific padding
23
+ DEFAULT_COVER_PADDING_TOP: Final[int] = 10
24
+ DEFAULT_COVER_PADDING_BOTTOM: Final[int] = 20
25
+ DEFAULT_COVER_PADDING_HORIZONTAL: Final[int] = 25
26
+
27
+ # Validation constraints
28
+ MIN_PAGE_WIDTH_MM: Final[int] = 100
29
+ MAX_PAGE_WIDTH_MM: Final[int] = 300
30
+ MIN_PAGE_HEIGHT_MM: Final[int] = 150
31
+ MAX_PAGE_HEIGHT_MM: Final[int] = 400
32
+
33
+ MIN_SIDEBAR_WIDTH_MM: Final[int] = 30
34
+ MAX_SIDEBAR_WIDTH_MM: Final[int] = 100
35
+
36
+ MIN_PADDING: Final[int] = 0
37
+ MAX_PADDING: Final[int] = 50
38
+
39
+ __all__ = [
40
+ "DEFAULT_PAGE_WIDTH_MM",
41
+ "DEFAULT_PAGE_HEIGHT_MM",
42
+ "DEFAULT_SIDEBAR_WIDTH_MM",
43
+ "DEFAULT_PADDING",
44
+ "DEFAULT_SIDEBAR_PADDING_ADJUSTMENT",
45
+ "DEFAULT_SIDEBAR_PADDING",
46
+ "DEFAULT_FRAME_PADDING",
47
+ "DEFAULT_COVER_PADDING_TOP",
48
+ "DEFAULT_COVER_PADDING_BOTTOM",
49
+ "DEFAULT_COVER_PADDING_HORIZONTAL",
50
+ "MIN_PAGE_WIDTH_MM",
51
+ "MAX_PAGE_WIDTH_MM",
52
+ "MIN_PAGE_HEIGHT_MM",
53
+ "MAX_PAGE_HEIGHT_MM",
54
+ "MIN_SIDEBAR_WIDTH_MM",
55
+ "MAX_SIDEBAR_WIDTH_MM",
56
+ "MIN_PADDING",
57
+ "MAX_PADDING",
58
+ ]
@@ -0,0 +1,258 @@
1
+ """Dependency injection container and interfaces for improved testability.
2
+
3
+ This module introduces dependency injection patterns to reduce coupling between
4
+ Session and Resume classes.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, Protocol
11
+
12
+ from simple_resume.core.paths import Paths
13
+ from simple_resume.core.resume import Resume
14
+
15
+
16
+ class SessionConfigProtocol(Protocol):
17
+ """Minimal session config contract needed by the core."""
18
+
19
+ default_template: str | None
20
+ default_palette: str | None
21
+ preview_mode: bool
22
+
23
+
24
+ class NullSessionConfig:
25
+ """Fallback config used when no SessionConfig is supplied."""
26
+
27
+ default_template: str | None = None
28
+ default_palette: str | None = None
29
+ preview_mode: bool = False
30
+
31
+
32
+ class ResumeLoader(Protocol):
33
+ """Protocol for loading resume instances."""
34
+
35
+ def load_resume(
36
+ self,
37
+ name: str,
38
+ *,
39
+ paths: Paths | None = None,
40
+ transform_markdown: bool = True,
41
+ ) -> Resume:
42
+ """Load a resume by name with given configuration."""
43
+ ...
44
+
45
+
46
+ class ResumeCache(Protocol):
47
+ """Protocol for caching resume instances."""
48
+
49
+ def get_resume(self, key: str) -> Resume | None:
50
+ """Get a resume from cache."""
51
+ ...
52
+
53
+ def put_resume(self, key: str, resume: Resume) -> None:
54
+ """Put a resume into cache."""
55
+ ...
56
+
57
+ def invalidate_resume(self, key: str | None = None) -> None:
58
+ """Invalidate cached resume(s)."""
59
+ ...
60
+
61
+ def clear_cache(self) -> None:
62
+ """Clear all cached resumes."""
63
+ ...
64
+
65
+ def get_cache_keys(self) -> list[str]:
66
+ """Get list of cached resume keys."""
67
+ ...
68
+
69
+ def get_cache_size(self) -> int:
70
+ """Get number of cached resumes."""
71
+ ...
72
+
73
+ def get_memory_usage(self) -> int:
74
+ """Get estimated memory usage of cached resumes in bytes."""
75
+ ...
76
+
77
+
78
+ class ResumeConfigurator(Protocol):
79
+ """Protocol for configuring resume instances."""
80
+
81
+ def configure_resume(self, resume: Resume, config: SessionConfigProtocol) -> Resume:
82
+ """Apply session configuration to a resume."""
83
+ ...
84
+
85
+
86
+ class DefaultResumeLoader:
87
+ """Default implementation of ResumeLoader using Resume.read_yaml."""
88
+
89
+ def load_resume(
90
+ self,
91
+ name: str,
92
+ *,
93
+ paths: Paths | None = None,
94
+ transform_markdown: bool = True,
95
+ ) -> Resume:
96
+ """Load resume using Resume.read_yaml method."""
97
+ return Resume.read_yaml(
98
+ name=name,
99
+ paths=paths,
100
+ transform_markdown=transform_markdown,
101
+ )
102
+
103
+
104
+ class MemoryResumeCache:
105
+ """In-memory implementation of ResumeCache."""
106
+
107
+ def __init__(self) -> None:
108
+ """Initialize an empty in-memory cache."""
109
+ self._cache: dict[str, Resume] = {}
110
+
111
+ def get_resume(self, key: str) -> Resume | None:
112
+ """Get a resume from cache."""
113
+ return self._cache.get(key)
114
+
115
+ def put_resume(self, key: str, resume: Resume) -> None:
116
+ """Put a resume into cache."""
117
+ self._cache[key] = resume
118
+
119
+ def invalidate_resume(self, key: str | None = None) -> None:
120
+ """Invalidate cached resume(s)."""
121
+ if key is None:
122
+ self._cache.clear()
123
+ else:
124
+ self._cache.pop(key, None)
125
+
126
+ def clear_cache(self) -> None:
127
+ """Clear all cached resumes."""
128
+ self._cache.clear()
129
+
130
+ def get_cache_keys(self) -> list[str]:
131
+ """Get list of cached resume keys."""
132
+ return list(self._cache.keys())
133
+
134
+ def get_cache_size(self) -> int:
135
+ """Get number of cached resumes."""
136
+ return len(self._cache)
137
+
138
+ def get_memory_usage(self) -> int:
139
+ """Get estimated memory usage of cached resumes in bytes."""
140
+ return sum(len(str(resume._data)) for resume in self._cache.values())
141
+
142
+
143
+ class DefaultResumeConfigurator:
144
+ """Default implementation of ResumeConfigurator."""
145
+
146
+ def configure_resume(self, resume: Resume, config: SessionConfigProtocol) -> Resume:
147
+ """Apply session configuration to a resume."""
148
+ result = resume
149
+
150
+ # Apply default template if specified
151
+ if config.default_template:
152
+ result = result.with_template(config.default_template)
153
+
154
+ # Apply default palette if specified
155
+ if config.default_palette:
156
+ result = result.with_palette(config.default_palette)
157
+
158
+ # Apply preview mode if enabled
159
+ if config.preview_mode:
160
+ result = result.preview()
161
+
162
+ return result
163
+
164
+
165
+ class ResumeRepository:
166
+ """Repository for managing resume loading and caching."""
167
+
168
+ def __init__(
169
+ self,
170
+ loader: ResumeLoader,
171
+ cache: ResumeCache,
172
+ configurator: ResumeConfigurator,
173
+ ) -> None:
174
+ """Initialize repository with dependencies."""
175
+ self._loader = loader
176
+ self._cache = cache
177
+ self._configurator = configurator
178
+
179
+ def get_resume(
180
+ self,
181
+ name: str,
182
+ paths: Paths | None = None,
183
+ use_cache: bool = True,
184
+ config: SessionConfigProtocol | None = None,
185
+ ) -> Resume:
186
+ """Get a resume, loading from cache or file as needed."""
187
+ cache_key = name
188
+
189
+ # Try cache first
190
+ if use_cache and (cached_resume := self._cache.get_resume(cache_key)):
191
+ # Apply configuration to cached resume
192
+ merged_config = config or NullSessionConfig()
193
+ return self._configurator.configure_resume(cached_resume, merged_config)
194
+
195
+ # Load from file
196
+ resume = self._loader.load_resume(name, paths=paths)
197
+
198
+ # Apply configuration
199
+ if config:
200
+ resume = self._configurator.configure_resume(resume, config)
201
+
202
+ # Cache the result
203
+ if use_cache:
204
+ self._cache.put_resume(cache_key, resume)
205
+
206
+ return resume
207
+
208
+ def invalidate_cache(self, name: str | None = None) -> None:
209
+ """Invalidate cached resume(s)."""
210
+ self._cache.invalidate_resume(name)
211
+
212
+ def clear_cache(self) -> None:
213
+ """Clear all cached resumes."""
214
+ self._cache.clear_cache()
215
+
216
+ def get_cache_info(self) -> dict[str, Any]:
217
+ """Return information about cached resume data.
218
+
219
+ Returns:
220
+ Dictionary with cache statistics.
221
+
222
+ """
223
+ return {
224
+ "cached_resumes": self._cache.get_cache_keys(),
225
+ "cache_size": self._cache.get_cache_size(),
226
+ "memory_usage_estimate": self._cache.get_memory_usage(),
227
+ }
228
+
229
+ def get_cache_keys(self) -> list[str]:
230
+ """Get list of cached resume keys."""
231
+ return self._cache.get_cache_keys()
232
+
233
+ def get_cache_size(self) -> int:
234
+ """Get number of cached resumes."""
235
+ return self._cache.get_cache_size()
236
+
237
+ def get_memory_usage(self) -> int:
238
+ """Get estimated memory usage of cached resumes in bytes."""
239
+ return self._cache.get_memory_usage()
240
+
241
+
242
+ @dataclass
243
+ class DIContainer:
244
+ """Dependency injection container for creating configured objects."""
245
+
246
+ resume_loader: ResumeLoader = field(default_factory=DefaultResumeLoader)
247
+ resume_cache: ResumeCache = field(default_factory=MemoryResumeCache)
248
+ resume_configurator: ResumeConfigurator = field(
249
+ default_factory=DefaultResumeConfigurator
250
+ )
251
+
252
+ def create_resume_repository(self) -> ResumeRepository:
253
+ """Create a ResumeRepository with configured dependencies."""
254
+ return ResumeRepository(
255
+ loader=self.resume_loader,
256
+ cache=self.resume_cache,
257
+ configurator=self.resume_configurator,
258
+ )