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,131 @@
1
+ """Provide pure generation planning helpers for CLI and session shells."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ from collections.abc import Sequence
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from simple_resume.core.constants import OutputFormat
13
+ from simple_resume.core.models import GenerationConfig
14
+ from simple_resume.core.paths import Paths
15
+
16
+
17
+ class CommandType(str, Enum):
18
+ """Define kinds of generation commands the shell can execute."""
19
+
20
+ SINGLE = "single"
21
+ BATCH_SINGLE = "batch_single"
22
+ BATCH_ALL = "batch_all"
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class GenerationCommand:
27
+ """Define a pure description of a generation step."""
28
+
29
+ kind: CommandType
30
+ format: OutputFormat | None
31
+ config: GenerationConfig
32
+ overrides: dict[str, Any]
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class GeneratePlanOptions:
37
+ """Define normalized inputs for planning CLI/session work."""
38
+
39
+ name: str | None
40
+ data_dir: Path | None
41
+ template: str | None
42
+ output_path: Path | None
43
+ output_dir: Path | None
44
+ preview: bool
45
+ open_after: bool
46
+ browser: str | None
47
+ formats: Sequence[OutputFormat]
48
+ overrides: dict[str, Any]
49
+ paths: Paths | None = None
50
+ pattern: str = "*"
51
+
52
+
53
+ def build_generation_plan(options: GeneratePlanOptions) -> list[GenerationCommand]:
54
+ """Return the deterministic commands needed to satisfy the request."""
55
+ if not options.formats:
56
+ raise ValueError("At least one output format must be specified")
57
+
58
+ plan: list[GenerationCommand] = []
59
+ overrides = copy.deepcopy(options.overrides)
60
+
61
+ if options.name:
62
+ for format_type in options.formats:
63
+ plan.append(
64
+ GenerationCommand(
65
+ kind=CommandType.SINGLE,
66
+ format=format_type,
67
+ config=GenerationConfig(
68
+ name=options.name,
69
+ data_dir=options.data_dir,
70
+ template=options.template,
71
+ format=format_type,
72
+ output_path=options.output_path,
73
+ open_after=options.open_after,
74
+ preview=options.preview,
75
+ browser=options.browser,
76
+ paths=options.paths,
77
+ pattern=options.pattern,
78
+ ),
79
+ overrides=copy.deepcopy(overrides),
80
+ )
81
+ )
82
+ return plan
83
+
84
+ if len(options.formats) == 1:
85
+ format_type = options.formats[0]
86
+ plan.append(
87
+ GenerationCommand(
88
+ kind=CommandType.BATCH_SINGLE,
89
+ format=format_type,
90
+ config=GenerationConfig(
91
+ data_dir=options.data_dir,
92
+ template=options.template,
93
+ output_dir=options.output_dir,
94
+ open_after=options.open_after,
95
+ preview=options.preview,
96
+ browser=options.browser,
97
+ paths=options.paths,
98
+ pattern=options.pattern,
99
+ ),
100
+ overrides=copy.deepcopy(overrides),
101
+ )
102
+ )
103
+ return plan
104
+
105
+ plan.append(
106
+ GenerationCommand(
107
+ kind=CommandType.BATCH_ALL,
108
+ format=None,
109
+ config=GenerationConfig(
110
+ data_dir=options.data_dir,
111
+ template=options.template,
112
+ output_dir=options.output_dir,
113
+ open_after=options.open_after,
114
+ preview=options.preview,
115
+ browser=options.browser,
116
+ formats=list(options.formats),
117
+ paths=options.paths,
118
+ pattern=options.pattern,
119
+ ),
120
+ overrides=copy.deepcopy(overrides),
121
+ )
122
+ )
123
+ return plan
124
+
125
+
126
+ __all__ = [
127
+ "CommandType",
128
+ "GenerationCommand",
129
+ "GeneratePlanOptions",
130
+ "build_generation_plan",
131
+ ]
@@ -0,0 +1,55 @@
1
+ """Provide pure helpers for transforming hydrated resume data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ from collections.abc import Mapping
7
+ from typing import Any, Callable
8
+
9
+ from simple_resume.core.skills import format_skill_groups
10
+
11
+ NormalizeConfigFn = Callable[
12
+ [dict[str, Any], str], tuple[dict[str, Any], dict[str, Any] | None]
13
+ ]
14
+ RenderMarkdownFn = Callable[[dict[str, Any]], dict[str, Any]]
15
+
16
+
17
+ def build_skill_group_payload(resume_data: Mapping[str, Any]) -> dict[str, Any]:
18
+ """Return the computed skill group payload for sidebar sections."""
19
+ return {
20
+ "expertise_groups": format_skill_groups(resume_data.get("expertise")),
21
+ "programming_groups": format_skill_groups(resume_data.get("programming")),
22
+ "keyskills_groups": format_skill_groups(resume_data.get("keyskills")),
23
+ "certification_groups": format_skill_groups(resume_data.get("certification")),
24
+ }
25
+
26
+
27
+ def hydrate_resume_structure(
28
+ source_yaml: dict[str, Any],
29
+ *,
30
+ filename: str = "",
31
+ transform_markdown: bool = True,
32
+ normalize_config_fn: NormalizeConfigFn,
33
+ render_markdown_fn: RenderMarkdownFn,
34
+ ) -> dict[str, Any]:
35
+ """Return normalized resume data using injected pure helpers."""
36
+ processed_resume = copy.deepcopy(source_yaml)
37
+
38
+ config = processed_resume.get("config")
39
+ if isinstance(config, dict):
40
+ normalized_config, palette_meta = normalize_config_fn(config, filename)
41
+ processed_resume["config"] = normalized_config
42
+ if palette_meta:
43
+ meta = dict(processed_resume.get("meta", {}))
44
+ meta["palette"] = palette_meta
45
+ processed_resume["meta"] = meta
46
+
47
+ if transform_markdown:
48
+ processed_resume = render_markdown_fn(processed_resume)
49
+ else:
50
+ processed_resume.update(build_skill_group_payload(processed_resume))
51
+
52
+ return processed_resume
53
+
54
+
55
+ __all__ = ["build_skill_group_payload", "hydrate_resume_structure"]
@@ -0,0 +1,3 @@
1
+ """Importers for external resume formats."""
2
+
3
+ __all__ = []
@@ -0,0 +1,284 @@
1
+ """Import helpers for the JSON Resume (jsonresume.org) open resume format.
2
+
3
+ This module intentionally does *not* attempt a 1:1 mapping of every JSON Resume
4
+ field to simple-resume. Instead, it provides a pragmatic conversion that:
5
+
6
+ - Produces a valid simple-resume payload (including a non-empty ``config`` block)
7
+ - Preserves key content (basics, work, education, projects)
8
+ - Uses markdown-friendly formatting for highlights
9
+
10
+ Schema reference (v1.0.x): https://github.com/jsonresume/resume-schema
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from collections.abc import Mapping
16
+ from typing import Any
17
+
18
+
19
+ def looks_like_json_resume(payload: Any) -> bool:
20
+ """Return True if payload resembles a JSON Resume document."""
21
+ if not isinstance(payload, Mapping):
22
+ return False
23
+ basics = payload.get("basics")
24
+ return isinstance(basics, Mapping) and "full_name" not in payload
25
+
26
+
27
+ def _strip_url_prefix(url: str, prefix: str) -> str:
28
+ if url.startswith(prefix):
29
+ return url[len(prefix) :].lstrip("/")
30
+ return url
31
+
32
+
33
+ def _to_markdown_bullets(items: Any) -> str:
34
+ if not items:
35
+ return ""
36
+ if isinstance(items, str):
37
+ return items
38
+ if not isinstance(items, list):
39
+ return str(items)
40
+ cleaned = [str(item).strip() for item in items if item]
41
+ if not cleaned:
42
+ return ""
43
+ return "\n".join(f"- {line}" for line in cleaned)
44
+
45
+
46
+ def _join_nonempty(*parts: Any, sep: str = "\n\n") -> str:
47
+ chunks = [str(p).strip() for p in parts if isinstance(p, str) and p.strip()]
48
+ return sep.join(chunks)
49
+
50
+
51
+ def _get_str(data: Mapping[str, Any], key: str) -> str | None:
52
+ """Extract a non-empty stripped string or return None."""
53
+ val = data.get(key)
54
+ if isinstance(val, str) and val.strip():
55
+ return val.strip()
56
+ return None
57
+
58
+
59
+ def _convert_basics_simple(basics: Mapping[str, Any], result: dict[str, Any]) -> None:
60
+ """Extract simple scalar fields from basics into result."""
61
+ field_map = {
62
+ "name": "full_name",
63
+ "email": "email",
64
+ "phone": "phone",
65
+ "url": "web",
66
+ "image": "image_uri",
67
+ "label": "headline",
68
+ "summary": "description",
69
+ }
70
+ for src_key, dst_key in field_map.items():
71
+ val = _get_str(basics, src_key)
72
+ if val:
73
+ result[dst_key] = val
74
+
75
+
76
+ def _convert_location(basics: Mapping[str, Any], result: dict[str, Any]) -> None:
77
+ """Extract location from basics into result['address']."""
78
+ location = basics.get("location")
79
+ if not isinstance(location, Mapping):
80
+ return
81
+
82
+ address_lines: list[str] = []
83
+ addr = _get_str(location, "address")
84
+ if addr:
85
+ address_lines.append(addr)
86
+
87
+ city, region, postal = (
88
+ location.get("city"),
89
+ location.get("region"),
90
+ location.get("postalCode"),
91
+ )
92
+ line2_parts = [
93
+ str(x).strip()
94
+ for x in (city, region, postal)
95
+ if isinstance(x, str) and x.strip()
96
+ ]
97
+ if line2_parts:
98
+ address_lines.append(" ".join(line2_parts))
99
+
100
+ country = _get_str(location, "countryCode")
101
+ if country:
102
+ address_lines.append(country)
103
+
104
+ if address_lines:
105
+ result["address"] = address_lines
106
+
107
+
108
+ def _convert_profiles(basics: Mapping[str, Any], result: dict[str, Any]) -> None:
109
+ """Extract linkedin/github from profiles into result."""
110
+ profiles = basics.get("profiles")
111
+ if not isinstance(profiles, list):
112
+ return
113
+
114
+ for profile in profiles:
115
+ if not isinstance(profile, Mapping):
116
+ continue
117
+ network = str(profile.get("network", "")).strip().lower()
118
+ purl = _get_str(profile, "url")
119
+ username = _get_str(profile, "username")
120
+
121
+ if network == "linkedin":
122
+ if purl:
123
+ result["linkedin"] = _strip_url_prefix(
124
+ purl, "https://www.linkedin.com/"
125
+ )
126
+ elif username:
127
+ result["linkedin"] = username
128
+ elif network == "github":
129
+ if purl:
130
+ result["github"] = _strip_url_prefix(purl, "https://github.com/")
131
+ elif username:
132
+ result["github"] = username
133
+
134
+
135
+ def _convert_work(payload: Mapping[str, Any]) -> list[dict[str, Any]]:
136
+ """Convert work entries to simple-resume format."""
137
+ work = payload.get("work")
138
+ if not isinstance(work, list):
139
+ return []
140
+
141
+ entries: list[dict[str, Any]] = []
142
+ for item in work:
143
+ if not isinstance(item, Mapping):
144
+ continue
145
+ highlights = _to_markdown_bullets(item.get("highlights"))
146
+ raw_summary = item.get("summary")
147
+ summary_str = str(raw_summary).strip() if raw_summary else ""
148
+ desc = _join_nonempty(summary_str, highlights)
149
+ entries.append(
150
+ {
151
+ "start": item.get("startDate") or "",
152
+ "end": item.get("endDate") or "",
153
+ "title": item.get("position") or "",
154
+ "company": item.get("name") or "",
155
+ "company_link": item.get("url") or "",
156
+ "description": desc or "",
157
+ }
158
+ )
159
+ return entries
160
+
161
+
162
+ def _convert_education(payload: Mapping[str, Any]) -> list[dict[str, Any]]:
163
+ """Convert education entries to simple-resume format."""
164
+ education = payload.get("education")
165
+ if not isinstance(education, list):
166
+ return []
167
+
168
+ entries: list[dict[str, Any]] = []
169
+ for item in education:
170
+ if not isinstance(item, Mapping):
171
+ continue
172
+ study = str(item.get("studyType", "")).strip()
173
+ area = str(item.get("area", "")).strip()
174
+ title = " ".join(x for x in (study, area) if x)
175
+
176
+ desc_parts: list[str] = []
177
+ gpa = item.get("gpa")
178
+ if gpa:
179
+ desc_parts.append(f"GPA: {gpa}")
180
+ courses_md = _to_markdown_bullets(item.get("courses"))
181
+ if courses_md:
182
+ desc_parts.append("Courses:\n" + courses_md)
183
+
184
+ entries.append(
185
+ {
186
+ "start": item.get("startDate") or "",
187
+ "end": item.get("endDate") or "",
188
+ "title": title,
189
+ "company": item.get("institution") or "",
190
+ "description": _join_nonempty(*desc_parts) or "",
191
+ }
192
+ )
193
+ return entries
194
+
195
+
196
+ def _convert_projects(payload: Mapping[str, Any]) -> list[dict[str, Any]]:
197
+ """Convert project entries to simple-resume format."""
198
+ projects = payload.get("projects")
199
+ if not isinstance(projects, list):
200
+ return []
201
+
202
+ entries: list[dict[str, Any]] = []
203
+ for item in projects:
204
+ if not isinstance(item, Mapping):
205
+ continue
206
+ highlights = _to_markdown_bullets(item.get("highlights"))
207
+ raw_desc = item.get("description")
208
+ desc_str = str(raw_desc).strip() if raw_desc else ""
209
+ desc = _join_nonempty(desc_str, highlights)
210
+ entries.append(
211
+ {
212
+ "start": item.get("startDate") or "",
213
+ "end": item.get("endDate") or "",
214
+ "title": item.get("name") or "",
215
+ "title_link": item.get("url") or "",
216
+ "company": item.get("entity") or "",
217
+ "description": desc or "",
218
+ }
219
+ )
220
+ return entries
221
+
222
+
223
+ def _convert_skills(payload: Mapping[str, Any], result: dict[str, Any]) -> None:
224
+ """Extract skills into expertise and keyskills."""
225
+ skills = payload.get("skills")
226
+ if not isinstance(skills, list):
227
+ return
228
+
229
+ expertise: list[str] = []
230
+ keyskills: list[str] = []
231
+ for group in skills:
232
+ if not isinstance(group, Mapping):
233
+ continue
234
+ group_name = _get_str(group, "name")
235
+ if group_name:
236
+ expertise.append(group_name)
237
+ keywords = group.get("keywords")
238
+ if isinstance(keywords, list):
239
+ for kw in keywords:
240
+ if isinstance(kw, str) and kw.strip():
241
+ keyskills.append(kw.strip())
242
+
243
+ if expertise:
244
+ result["expertise"] = sorted(set(expertise))
245
+ if keyskills:
246
+ result["keyskills"] = sorted(set(keyskills))
247
+
248
+
249
+ def json_resume_to_simple_resume(payload: Mapping[str, Any]) -> dict[str, Any]:
250
+ """Convert a JSON Resume payload into a simple-resume YAML-shaped dict."""
251
+ basics = payload.get("basics")
252
+ basics = basics if isinstance(basics, Mapping) else {}
253
+
254
+ result: dict[str, Any] = {
255
+ "template": "resume_no_bars",
256
+ "config": {"template": "resume_no_bars"},
257
+ }
258
+
259
+ # Convert basics section
260
+ _convert_basics_simple(basics, result)
261
+ _convert_location(basics, result)
262
+ _convert_profiles(basics, result)
263
+
264
+ # Build body sections
265
+ body: dict[str, list[dict[str, Any]]] = {}
266
+ work_entries = _convert_work(payload)
267
+ if work_entries:
268
+ body["Experience"] = work_entries
269
+ education_entries = _convert_education(payload)
270
+ if education_entries:
271
+ body["Education"] = education_entries
272
+ project_entries = _convert_projects(payload)
273
+ if project_entries:
274
+ body["Projects"] = project_entries
275
+ if body:
276
+ result["body"] = body
277
+
278
+ # Convert skills
279
+ _convert_skills(payload, result)
280
+
281
+ return result
282
+
283
+
284
+ __all__ = ["json_resume_to_simple_resume", "looks_like_json_resume"]
@@ -0,0 +1,60 @@
1
+ """Core LaTeX functionality (pure, deterministic, no side effects).
2
+
3
+ This package contains pure business logic for LaTeX document generation.
4
+ All functions are deterministic and have no side effects (no file I/O,
5
+ no network access, no randomness).
6
+
7
+ The shell layer (simple_resume.shell.render.latex) handles all I/O operations
8
+ including template loading, file system access, and LaTeX compilation.
9
+ """
10
+
11
+ from simple_resume.core.latex.context import build_latex_context_pure
12
+ from simple_resume.core.latex.conversion import (
13
+ collect_blocks,
14
+ convert_inline,
15
+ normalize_iterable,
16
+ )
17
+ from simple_resume.core.latex.escaping import escape_latex, escape_url
18
+ from simple_resume.core.latex.fonts import fontawesome_support_block
19
+ from simple_resume.core.latex.formatting import format_date, linkify
20
+ from simple_resume.core.latex.sections import (
21
+ build_contact_lines,
22
+ prepare_sections,
23
+ prepare_skill_sections,
24
+ )
25
+ from simple_resume.core.latex.types import (
26
+ Block,
27
+ LatexEntry,
28
+ LatexRenderResult,
29
+ LatexSection,
30
+ ListBlock,
31
+ ParagraphBlock,
32
+ )
33
+
34
+ __all__ = [
35
+ # Types
36
+ "Block",
37
+ "LatexEntry",
38
+ "LatexRenderResult",
39
+ "LatexSection",
40
+ "ListBlock",
41
+ "ParagraphBlock",
42
+ # Escaping
43
+ "escape_latex",
44
+ "escape_url",
45
+ # Conversion
46
+ "collect_blocks",
47
+ "convert_inline",
48
+ "normalize_iterable",
49
+ # Formatting
50
+ "format_date",
51
+ "linkify",
52
+ # Sections
53
+ "build_contact_lines",
54
+ "prepare_sections",
55
+ "prepare_skill_sections",
56
+ # Context
57
+ "build_latex_context_pure",
58
+ # Fonts
59
+ "fontawesome_support_block",
60
+ ]
@@ -0,0 +1,56 @@
1
+ """LaTeX context building functions (pure version without I/O)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from simple_resume.core.latex.conversion import collect_blocks, convert_inline
8
+ from simple_resume.core.latex.sections import (
9
+ build_contact_lines,
10
+ prepare_sections,
11
+ prepare_skill_sections,
12
+ )
13
+
14
+
15
+ def build_latex_context_pure(data: dict[str, Any]) -> dict[str, Any]:
16
+ """Prepare the LaTeX template context from raw resume data (pure version).
17
+
18
+ This is the pure, core version of context building that does NOT perform
19
+ any file system operations. The fontawesome_block is set to None and must
20
+ be added by the shell layer which has access to the file system.
21
+
22
+ Args:
23
+ data: Raw resume data dictionary.
24
+
25
+ Returns:
26
+ Dictionary of context variables for LaTeX template rendering.
27
+ Note: fontawesome_block will be None and must be set by shell layer.
28
+
29
+ Examples:
30
+ >>> data = {"full_name": "John Doe", "job_title": "Engineer"}
31
+ >>> context = build_latex_context_pure(data)
32
+ >>> context["full_name"]
33
+ 'John Doe'
34
+ >>> context["headline"]
35
+ 'Engineer'
36
+
37
+ """
38
+ full_name = convert_inline(str(data.get("full_name", "")))
39
+ headline = data.get("job_title")
40
+ rendered_headline = convert_inline(str(headline)) if headline else None
41
+ summary_blocks = collect_blocks(data.get("description"))
42
+
43
+ return {
44
+ "full_name": full_name,
45
+ "headline": rendered_headline,
46
+ "contact_lines": build_contact_lines(data),
47
+ "summary_blocks": summary_blocks,
48
+ "skill_sections": prepare_skill_sections(data),
49
+ "sections": prepare_sections(data),
50
+ "fontawesome_block": None, # Must be set by shell layer
51
+ }
52
+
53
+
54
+ __all__ = [
55
+ "build_latex_context_pure",
56
+ ]