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,75 @@
1
+ #!/usr/bin/env python3
2
+ """Command line helpers for palette discovery."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+
10
+ from simple_resume.shell.palettes.loader import (
11
+ build_palettable_snapshot,
12
+ get_palette_registry,
13
+ )
14
+
15
+
16
+ def cmd_snapshot(args: argparse.Namespace) -> int:
17
+ """Export current palettable registry snapshot to JSON."""
18
+ snapshot = build_palettable_snapshot()
19
+ output = json.dumps(snapshot, indent=2)
20
+ if args.output:
21
+ args.output.write(output)
22
+ args.output.write("\n")
23
+ else:
24
+ print(output)
25
+ return 0
26
+
27
+
28
+ def cmd_list(_: argparse.Namespace) -> int:
29
+ """List all available palette names with preview colors."""
30
+ registry = get_palette_registry()
31
+ for palette in registry.list():
32
+ print(f"{palette.name}: {', '.join(palette.swatches[:6])}")
33
+ return 0
34
+
35
+
36
+ def build_parser() -> argparse.ArgumentParser:
37
+ """Build argument parser for palette CLI."""
38
+ parser = argparse.ArgumentParser(description="Palette utilities")
39
+ subparsers = parser.add_subparsers(dest="command", required=True)
40
+
41
+ snap = subparsers.add_parser("snapshot", help="Build palettable snapshot")
42
+ snap.add_argument(
43
+ "-o",
44
+ "--output",
45
+ type=argparse.FileType("w", encoding="utf-8"),
46
+ help="Output file (defaults to stdout)",
47
+ )
48
+ snap.set_defaults(func=cmd_snapshot)
49
+
50
+ list_cmd = subparsers.add_parser("list", help="List available palettes")
51
+ list_cmd.set_defaults(func=cmd_list)
52
+
53
+ return parser
54
+
55
+
56
+ def main(argv: list[str] | None = None) -> int:
57
+ """Run the palette CLI with given arguments."""
58
+ parser = build_parser()
59
+ args = parser.parse_args(argv)
60
+ result = args.func(args)
61
+ return int(result) if result is not None else 0
62
+
63
+
64
+ def snapshot() -> None:
65
+ """Entry point for palette-snapshot command."""
66
+ sys.exit(main(["snapshot"]))
67
+
68
+
69
+ def palette_list() -> None:
70
+ """Entry point for palette-list command."""
71
+ sys.exit(main(["list"]))
72
+
73
+
74
+ if __name__ == "__main__":
75
+ sys.exit(main())
@@ -0,0 +1,407 @@
1
+ """Provide a CLI entry point for generating randomized palette demo YAML files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import random
7
+ import secrets
8
+ import string
9
+ from pathlib import Path
10
+
11
+ from oyaml import safe_dump, safe_load
12
+
13
+ from simple_resume.core.markdown import derive_bold_color
14
+ from simple_resume.core.palettes.generators import generate_hcl_palette
15
+ from simple_resume.shell.palettes.loader import get_palette_registry
16
+
17
+ DEFAULT_OUTPUT = Path("sample/input/sample_palette_demo_random.yaml")
18
+ DEFAULT_TEMPLATE = Path("sample/input/sample_palette_demo.yaml")
19
+
20
+ # Realistic word banks for generating resume content
21
+ FIRST_NAMES = [
22
+ "Alex",
23
+ "Jordan",
24
+ "Morgan",
25
+ "Taylor",
26
+ "Casey",
27
+ "Riley",
28
+ "Quinn",
29
+ "Avery",
30
+ "Cameron",
31
+ "Dakota",
32
+ "Emerson",
33
+ "Harper",
34
+ "Hayden",
35
+ "Jamie",
36
+ "Kendall",
37
+ "Logan",
38
+ "Parker",
39
+ "Reese",
40
+ "Sage",
41
+ "Skylar",
42
+ ]
43
+
44
+ LAST_NAMES = [
45
+ "Anderson",
46
+ "Bennett",
47
+ "Carter",
48
+ "Davis",
49
+ "Ellis",
50
+ "Foster",
51
+ "Garcia",
52
+ "Hayes",
53
+ "Jackson",
54
+ "Kim",
55
+ "Lee",
56
+ "Martinez",
57
+ "Nelson",
58
+ "Parker",
59
+ "Rivera",
60
+ "Santos",
61
+ "Taylor",
62
+ "Williams",
63
+ "Zhang",
64
+ "Chen",
65
+ ]
66
+
67
+ COMPANIES = [
68
+ "TechCorp",
69
+ "DataSystems",
70
+ "CloudWorks",
71
+ "InnovateLabs",
72
+ "DevStack",
73
+ "CodeCraft",
74
+ "StreamFlow",
75
+ "PixelPerfect",
76
+ "ByteBridge",
77
+ "Quantum Dynamics",
78
+ "NexGen Solutions",
79
+ "Vertex Technologies",
80
+ "Horizon Digital",
81
+ "Pulse Networks",
82
+ ]
83
+
84
+ TECH_SKILLS = [
85
+ "API design",
86
+ "microservices architecture",
87
+ "cloud infrastructure",
88
+ "CI/CD pipelines",
89
+ "performance optimization",
90
+ "system monitoring",
91
+ "database design",
92
+ "caching strategies",
93
+ "load balancing",
94
+ "automated testing",
95
+ "containerization",
96
+ "orchestration",
97
+ ]
98
+
99
+ ACCOMPLISHMENTS = [
100
+ "Reduced latency by {percent}% through optimization",
101
+ "Increased test coverage from {low}% to {high}%",
102
+ "Led migration of {count} services to cloud infrastructure",
103
+ "Improved deployment frequency by {mult}x using automation",
104
+ "Architected scalable solution handling {volume} requests/day",
105
+ "Mentored team of {size} engineers on best practices",
106
+ "Reduced operational costs by {percent}% through efficient resource allocation",
107
+ "Implemented monitoring system tracking {metric_count}+ metrics",
108
+ "Streamlined development workflow reducing build time by {percent}%",
109
+ ]
110
+
111
+ PROJECT_DESCRIPTIONS = [
112
+ "Designed and implemented {feature} using {tech_stack}",
113
+ "Built {system_type} to handle {capability}",
114
+ "Created automated {process} reducing manual effort by {percent}%",
115
+ "Developed {feature} integrating with {platform}",
116
+ "Optimized {component} achieving {improvement}",
117
+ ]
118
+
119
+ FEATURES = [
120
+ "real-time analytics dashboard",
121
+ "payment processing system",
122
+ "user authentication service",
123
+ "data pipeline",
124
+ "API gateway",
125
+ "notification system",
126
+ "search engine",
127
+ "content delivery network",
128
+ ]
129
+
130
+ TECH_STACKS = [
131
+ "Python and FastAPI",
132
+ "Node.js and Express",
133
+ "Go and gRPC",
134
+ "Java and Spring Boot",
135
+ "React and TypeScript",
136
+ "Vue.js and Nuxt",
137
+ ]
138
+
139
+ IMPROVEMENTS = [
140
+ "99.9% uptime",
141
+ "sub-100ms response times",
142
+ "zero-downtime deployments",
143
+ "10x throughput increase",
144
+ "50% cost reduction",
145
+ ]
146
+
147
+
148
+ def _random_words(count: int, *, word_len: int = 5) -> list[str]:
149
+ """Generate a list of deterministic-length lowercase words."""
150
+ if count <= 0:
151
+ return []
152
+ alphabet = string.ascii_lowercase
153
+ return [
154
+ "".join(secrets.choice(alphabet) for _ in range(max(1, word_len)))
155
+ for _ in range(count)
156
+ ]
157
+
158
+
159
+ def _random_name() -> str:
160
+ """Generate a realistic random name."""
161
+ first = secrets.choice(FIRST_NAMES)
162
+ last = secrets.choice(LAST_NAMES)
163
+ return f"{first} {last}"
164
+
165
+
166
+ def _random_sentence(context: str = "general") -> str:
167
+ """Generate a realistic sentence based on context."""
168
+ if context == "accomplishment":
169
+ template = secrets.choice(ACCOMPLISHMENTS)
170
+ return template.format(
171
+ percent=secrets.randbelow(50) + 30,
172
+ low=secrets.randbelow(30) + 40,
173
+ high=secrets.randbelow(20) + 80,
174
+ count=secrets.randbelow(15) + 5,
175
+ mult=secrets.randbelow(5) + 2,
176
+ volume=f"{secrets.randbelow(900) + 100}K",
177
+ size=secrets.randbelow(8) + 3,
178
+ metric_count=secrets.randbelow(50) + 50,
179
+ )
180
+ elif context == "project":
181
+ template = secrets.choice(PROJECT_DESCRIPTIONS)
182
+ return template.format(
183
+ feature=secrets.choice(FEATURES),
184
+ tech_stack=secrets.choice(TECH_STACKS),
185
+ system_type=secrets.choice(
186
+ ["distributed system", "service", "platform", "framework"]
187
+ ),
188
+ capability=secrets.choice(
189
+ [
190
+ "high-volume traffic",
191
+ "real-time processing",
192
+ "multi-tenant operations",
193
+ ]
194
+ ),
195
+ percent=secrets.randbelow(50) + 30,
196
+ process=secrets.choice(
197
+ ["deployment pipeline", "testing suite", "code review process"]
198
+ ),
199
+ platform=secrets.choice(["AWS", "GCP", "Azure", "Kubernetes"]),
200
+ component=secrets.choice(
201
+ [
202
+ "database queries",
203
+ "API endpoints",
204
+ "frontend rendering",
205
+ ]
206
+ ),
207
+ improvement=secrets.choice(IMPROVEMENTS),
208
+ )
209
+ else:
210
+ # General technical description
211
+ skill = secrets.choice(TECH_SKILLS)
212
+ return f"Experienced with {skill} and modern development practices."
213
+
214
+
215
+ def _random_description(paragraphs: int = 2) -> str:
216
+ """Generate a realistic multi-paragraph description."""
217
+ paras = []
218
+ for _ in range(paragraphs):
219
+ sentences = []
220
+ for _ in range(2):
221
+ sentences.append(_random_sentence("general"))
222
+ paras.append(" ".join(sentences))
223
+ return "\n\n".join(paras)
224
+
225
+
226
+ def _random_email(name: str) -> str:
227
+ """Generate a random email address for the given name."""
228
+ handle = name.lower().replace(" ", ".")
229
+ suffix = "".join(
230
+ secrets.choice(string.ascii_lowercase + string.digits) for _ in range(8)
231
+ )
232
+ return f"{handle}.{suffix}@example.com"
233
+
234
+
235
+ def _random_linkedin(name: str) -> str:
236
+ """Generate a random LinkedIn profile URL for the given name."""
237
+ handle = name.lower().replace(" ", "-")
238
+ suffix = "".join(
239
+ secrets.choice(string.ascii_lowercase + string.digits) for _ in range(4)
240
+ )
241
+ return f"in/{handle}-{suffix}"
242
+
243
+
244
+ def _random_palette(seed: int | None = None, size: int = 6) -> dict[str, str]:
245
+ """Generate a random HCL-inspired color palette.
246
+
247
+ Args:
248
+ seed: Optional deterministic seed for reproducibility.
249
+ size: Number of colors in the palette.
250
+
251
+ Returns:
252
+ A dictionary of color key-value pairs.
253
+
254
+ """
255
+ colors = generate_hcl_palette(size, seed=seed or secrets.randbelow(9999) + 1)
256
+ keys = [
257
+ "theme_color",
258
+ "sidebar_color",
259
+ "sidebar_text_color",
260
+ "bar_background_color",
261
+ "date2_color",
262
+ "frame_color",
263
+ ]
264
+ mapping = {}
265
+ for key, color in zip(keys, colors):
266
+ mapping[key] = color
267
+ if "frame_color" in mapping and "bold_color" not in mapping:
268
+ mapping["bold_color"] = derive_bold_color(mapping["frame_color"])
269
+ return mapping
270
+
271
+
272
+ def _random_registry_palette() -> dict[str, str] | None:
273
+ """Select a random palette from the registered palettes.
274
+
275
+ Returns:
276
+ A dictionary of color key-value pairs, or None if no palettes are registered.
277
+
278
+ """
279
+ registry = get_palette_registry()
280
+ palettes = registry.list()
281
+ if not palettes:
282
+ return None
283
+ palette = secrets.choice(palettes)
284
+ keys = [
285
+ "theme_color",
286
+ "sidebar_color",
287
+ "sidebar_text_color",
288
+ "bar_background_color",
289
+ "date2_color",
290
+ "frame_color",
291
+ ]
292
+ mapping = {}
293
+ for key, color in zip(keys, palette.swatches):
294
+ mapping[key] = color
295
+ mapping["color_scheme"] = palette.name
296
+
297
+ # If palette doesn't have enough swatches, generate missing colors
298
+ if len(palette.swatches) < len(keys):
299
+ missing_keys = keys[len(palette.swatches) :]
300
+ additional_palette = generate_hcl_palette(
301
+ len(missing_keys), seed=secrets.randbelow(9999) + 1
302
+ )
303
+ for key, color in zip(missing_keys, additional_palette):
304
+ mapping[key] = color
305
+
306
+ if "frame_color" in mapping and "bold_color" not in mapping:
307
+ mapping["bold_color"] = derive_bold_color(mapping["frame_color"])
308
+ return mapping
309
+
310
+
311
+ def generate_random_yaml(
312
+ *,
313
+ output_path: Path,
314
+ template_path: Path,
315
+ seed: int | None = None,
316
+ ) -> None:
317
+ """Generate random resume YAML with palette variations and realistic content."""
318
+ if seed is not None:
319
+ random.seed(seed)
320
+ secrets.SystemRandom(seed)
321
+
322
+ base = safe_load(template_path.read_text(encoding="utf-8"))
323
+
324
+ # Generate realistic personal info
325
+ name = _random_name()
326
+ base["full_name"] = name
327
+ base["job_title"] = secrets.choice(
328
+ [
329
+ "Senior Software Engineer",
330
+ "Principal Engineer",
331
+ "Staff Engineer",
332
+ "Engineering Manager",
333
+ "Technical Lead",
334
+ "Solutions Architect",
335
+ "DevOps Engineer",
336
+ "Full Stack Developer",
337
+ ]
338
+ )
339
+ base["phone"] = f"(512) 555-{secrets.randbelow(9000) + 1000}"
340
+ base["email"] = _random_email(name)
341
+ base["linkedin"] = _random_linkedin(name)
342
+ github_handle = name.lower().replace(" ", "")
343
+ base["github"] = github_handle
344
+ base["web"] = f"https://{github_handle}.dev"
345
+
346
+ # Generate realistic summary description
347
+ base["description"] = _random_description()
348
+
349
+ # Generate realistic work experience and project descriptions
350
+ for section_name, section in base["body"].items():
351
+ for entry in section:
352
+ # Determine context based on section name
353
+ if "experience" in section_name.lower() or "work" in section_name.lower():
354
+ context = "accomplishment"
355
+ elif "project" in section_name.lower():
356
+ context = "project"
357
+ else:
358
+ context = "general"
359
+
360
+ # Generate 2-4 bullet points with realistic content
361
+ num_bullets = secrets.randbelow(3) + 2
362
+ bullets = [_random_sentence(context) for _ in range(num_bullets)]
363
+ entry["description"] = "- " + "\n- ".join(bullets)
364
+
365
+ # Add realistic company names for work experience
366
+ if "experience" in section_name.lower() and "company" in entry:
367
+ entry["company"] = secrets.choice(COMPANIES)
368
+
369
+ config = base.setdefault("config", {})
370
+ config["output_mode"] = "markdown"
371
+ config["sidebar_width"] = 60
372
+ config["sidebar_padding_top"] = 6
373
+ config["h3_padding_top"] = 5
374
+
375
+ # Use palette from registry or generate one
376
+ palette = _random_registry_palette()
377
+ if palette is None:
378
+ palette = _random_palette(size=6)
379
+ palette["color_scheme"] = f"generator_{secrets.randbelow(9000) + 1000}"
380
+ config.update(palette)
381
+ if "bold_color" not in config and "frame_color" in config:
382
+ config["bold_color"] = derive_bold_color(config["frame_color"])
383
+
384
+ output_path.write_text(safe_dump(base, sort_keys=False), encoding="utf-8")
385
+
386
+
387
+ def main() -> None:
388
+ """Run the random palette demo generator CLI."""
389
+ parser = argparse.ArgumentParser(
390
+ description="Generate random resume content + palette demo YAML"
391
+ )
392
+ parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
393
+ parser.add_argument("--template", type=Path, default=DEFAULT_TEMPLATE)
394
+ parser.add_argument(
395
+ "--seed", type=int, help="Deterministic seed for reproducibility"
396
+ )
397
+ args = parser.parse_args()
398
+
399
+ generate_random_yaml(
400
+ output_path=args.output,
401
+ template_path=args.template,
402
+ seed=args.seed,
403
+ )
404
+ print(f"Wrote {args.output}")
405
+
406
+
407
+ __all__ = ["main"]
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env python3
2
+ """Provide configuration helpers and shared constants.
3
+
4
+ See `wiki/Path-Handling-Guide.md` for path handling conventions.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import atexit
10
+ import os
11
+ import sys
12
+ from contextlib import ExitStack
13
+ from importlib import resources
14
+ from pathlib import Path
15
+ from typing import Union
16
+
17
+ from simple_resume.core.paths import Paths
18
+
19
+ # Keep an open handle to package resources so they're available even when the
20
+ # distribution is zipped (e.g., installed from a wheel).
21
+ _asset_stack = ExitStack()
22
+ PACKAGE_ROOT = _asset_stack.enter_context(
23
+ resources.as_file(resources.files("simple_resume"))
24
+ )
25
+ ASSETS_ROOT = PACKAGE_ROOT / "shell" / "assets"
26
+ atexit.register(_asset_stack.close)
27
+
28
+ PATH_CONTENT = ASSETS_ROOT
29
+ TEMPLATE_LOC = ASSETS_ROOT / "templates"
30
+ STATIC_LOC = ASSETS_ROOT / "static"
31
+
32
+ # Legacy string-based paths preserved for configuration.
33
+ PATH_DATA = "resume_private"
34
+ PATH_INPUT = f"{PATH_DATA}/input"
35
+ PATH_OUTPUT = f"{PATH_DATA}/output"
36
+
37
+
38
+ if sys.version_info >= (3, 10):
39
+ PathLike = str | os.PathLike[str]
40
+ else:
41
+ PathLike = Union[str, os.PathLike[str]]
42
+
43
+
44
+ def resolve_paths(
45
+ data_dir: PathLike | None = None,
46
+ *,
47
+ content_dir: PathLike | None = None,
48
+ templates_dir: PathLike | None = None,
49
+ static_dir: PathLike | None = None,
50
+ ) -> Paths:
51
+ """Return the active data, input, and output paths.
52
+
53
+ Args:
54
+ data_dir: Optional directory containing `input/` and `output/` folders.
55
+ If omitted, the `RESUME_DATA_DIR` environment variable is used.
56
+ If neither is provided, the default is `./resume_private`.
57
+ content_dir: Optional override for the package content directory.
58
+ templates_dir: Optional override for the templates directory.
59
+ static_dir: Optional override for the static assets directory.
60
+
61
+ Returns:
62
+ A `Paths` dataclass with resolved data, template, and static paths.
63
+
64
+ """
65
+ base = data_dir or os.environ.get("RESUME_DATA_DIR") or PATH_DATA
66
+ base_path = Path(base)
67
+
68
+ if data_dir is None:
69
+ data_path = Path(PATH_DATA)
70
+ input_path = Path(PATH_INPUT)
71
+ output_path = Path(PATH_OUTPUT)
72
+ else:
73
+ data_path = base_path
74
+ input_path = base_path / "input"
75
+ output_path = base_path / "output"
76
+
77
+ content_path = Path(content_dir) if content_dir is not None else PATH_CONTENT
78
+ templates_path = (
79
+ Path(templates_dir) if templates_dir is not None else content_path / "templates"
80
+ )
81
+ static_path = (
82
+ Path(static_dir) if static_dir is not None else content_path / "static"
83
+ )
84
+
85
+ return Paths(
86
+ data=data_path,
87
+ input=input_path,
88
+ output=output_path,
89
+ content=content_path,
90
+ templates=templates_path,
91
+ static=static_path,
92
+ )
93
+
94
+
95
+ # Files
96
+ FILE_DEFAULT = "sample_1"