sync-cv-formatter 1.2.0__tar.gz

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 (31) hide show
  1. sync_cv_formatter-1.2.0/PKG-INFO +90 -0
  2. sync_cv_formatter-1.2.0/README.md +69 -0
  3. sync_cv_formatter-1.2.0/pyproject.toml +47 -0
  4. sync_cv_formatter-1.2.0/setup.cfg +4 -0
  5. sync_cv_formatter-1.2.0/src/sync_cv_formatter/__init__.py +13 -0
  6. sync_cv_formatter-1.2.0/src/sync_cv_formatter/renderers/__init__.py +4 -0
  7. sync_cv_formatter-1.2.0/src/sync_cv_formatter/renderers/html_renderer.py +211 -0
  8. sync_cv_formatter-1.2.0/src/sync_cv_formatter/renderers/pdf_renderer.py +42 -0
  9. sync_cv_formatter-1.2.0/src/sync_cv_formatter/schemas/__init__.py +27 -0
  10. sync_cv_formatter-1.2.0/src/sync_cv_formatter/schemas/resume_document.py +100 -0
  11. sync_cv_formatter-1.2.0/src/sync_cv_formatter/templates/html/_base_head.html.j2 +10 -0
  12. sync_cv_formatter-1.2.0/src/sync_cv_formatter/templates/html/_macros.html.j2 +43 -0
  13. sync_cv_formatter-1.2.0/src/sync_cv_formatter/templates/html/_sections.html.j2 +57 -0
  14. sync_cv_formatter-1.2.0/src/sync_cv_formatter/templates/html/_sections_experience_first.html.j2 +57 -0
  15. sync_cv_formatter-1.2.0/src/sync_cv_formatter/templates/html/_sections_tail.html.j2 +95 -0
  16. sync_cv_formatter-1.2.0/src/sync_cv_formatter/templates/html/classic/styles.css +217 -0
  17. sync_cv_formatter-1.2.0/src/sync_cv_formatter/templates/html/classic/template.html.j2 +16 -0
  18. sync_cv_formatter-1.2.0/src/sync_cv_formatter/templates/html/compact/styles.css +245 -0
  19. sync_cv_formatter-1.2.0/src/sync_cv_formatter/templates/html/compact/template.html.j2 +21 -0
  20. sync_cv_formatter-1.2.0/src/sync_cv_formatter/templates/html/executive/styles.css +229 -0
  21. sync_cv_formatter-1.2.0/src/sync_cv_formatter/templates/html/executive/template.html.j2 +19 -0
  22. sync_cv_formatter-1.2.0/src/sync_cv_formatter/templates/html/modern/styles.css +253 -0
  23. sync_cv_formatter-1.2.0/src/sync_cv_formatter/templates/html/modern/template.html.j2 +22 -0
  24. sync_cv_formatter-1.2.0/src/sync_cv_formatter/templates/html/professional/styles.css +258 -0
  25. sync_cv_formatter-1.2.0/src/sync_cv_formatter/templates/html/professional/template.html.j2 +19 -0
  26. sync_cv_formatter-1.2.0/src/sync_cv_formatter.egg-info/PKG-INFO +90 -0
  27. sync_cv_formatter-1.2.0/src/sync_cv_formatter.egg-info/SOURCES.txt +29 -0
  28. sync_cv_formatter-1.2.0/src/sync_cv_formatter.egg-info/dependency_links.txt +1 -0
  29. sync_cv_formatter-1.2.0/src/sync_cv_formatter.egg-info/requires.txt +2 -0
  30. sync_cv_formatter-1.2.0/src/sync_cv_formatter.egg-info/top_level.txt +1 -0
  31. sync_cv_formatter-1.2.0/tests/test_html_renderer.py +78 -0
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.4
2
+ Name: sync_cv_formatter
3
+ Version: 1.2.0
4
+ Summary: CV/resume HTML and PDF rendering with hiring-manager-focused templates
5
+ Author: SyncQues
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/SyncQues/CV-Formatter
8
+ Project-URL: Repository, https://github.com/SyncQues/CV-Formatter
9
+ Project-URL: Issues, https://github.com/SyncQues/CV-Formatter/issues
10
+ Keywords: resume,cv,pdf,html,jinja2
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Text Processing :: Markup :: HTML
17
+ Requires-Python: >=3.12
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: jinja2>=3.1.6
20
+ Requires-Dist: pydantic>=2.12.3
21
+
22
+ # CV Formatter
23
+
24
+ Public Python package for resume/CV HTML and PDF rendering — single source of truth for SyncQues resume templates, schemas, and rendering.
25
+
26
+ **Repository:** https://github.com/SyncQues/CV-Formatter
27
+ **PyPI:** https://pypi.org/project/sync_cv_formatter/
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install sync_cv_formatter
33
+ # or
34
+ uv add sync_cv_formatter
35
+ ```
36
+
37
+ ## Consumers
38
+
39
+ - `SyncQues-Backend` — recompile and live preview
40
+ - `SyncQues-Resume` — initial resume generation
41
+
42
+ ## Usage
43
+
44
+ ```python
45
+ from sync_cv_formatter import ResumeDocument, populate_html_template, render_html_to_pdf
46
+
47
+ document = ResumeDocument.model_validate(content_json)
48
+ html = populate_html_template(document, template_id="modern")
49
+ pdf_bytes = render_html_to_pdf(html)
50
+ ```
51
+
52
+ ## Templates
53
+
54
+ | ID | Style |
55
+ |----|-------|
56
+ | `professional` | Centered serif, traditional |
57
+ | `executive` | Experience-first, navy serif/sans |
58
+ | `modern` | Inter sans-serif, teal accent |
59
+ | `classic` | Times New Roman, ATS-maximum |
60
+ | `compact` | Dense one-page, IBM Plex |
61
+
62
+ Legacy `creative` maps to `modern`.
63
+
64
+ ## Local development
65
+
66
+ ```bash
67
+ uv sync --dev
68
+ uv run pytest
69
+ ```
70
+
71
+ Editable install from a checkout:
72
+
73
+ ```bash
74
+ uv add --editable ../CV-Formatter
75
+ ```
76
+
77
+ ## Release process
78
+
79
+ 1. Change templates/code in this repo
80
+ 2. Bump `version` in `pyproject.toml`
81
+ 3. Run `uv run pytest`
82
+ 4. Commit, tag (`git tag v1.2.1`), push tag
83
+ 5. GitHub Actions publishes to PyPI (or `uv build && uv publish`)
84
+ 6. Bump pin in Backend + Resume `pyproject.toml` and `uv lock`
85
+
86
+ ## Versioning
87
+
88
+ - **MAJOR** — breaking `content_json` schema changes
89
+ - **MINOR** — template/CSS changes or new optional fields
90
+ - **PATCH** — bug fixes
@@ -0,0 +1,69 @@
1
+ # CV Formatter
2
+
3
+ Public Python package for resume/CV HTML and PDF rendering — single source of truth for SyncQues resume templates, schemas, and rendering.
4
+
5
+ **Repository:** https://github.com/SyncQues/CV-Formatter
6
+ **PyPI:** https://pypi.org/project/sync_cv_formatter/
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install sync_cv_formatter
12
+ # or
13
+ uv add sync_cv_formatter
14
+ ```
15
+
16
+ ## Consumers
17
+
18
+ - `SyncQues-Backend` — recompile and live preview
19
+ - `SyncQues-Resume` — initial resume generation
20
+
21
+ ## Usage
22
+
23
+ ```python
24
+ from sync_cv_formatter import ResumeDocument, populate_html_template, render_html_to_pdf
25
+
26
+ document = ResumeDocument.model_validate(content_json)
27
+ html = populate_html_template(document, template_id="modern")
28
+ pdf_bytes = render_html_to_pdf(html)
29
+ ```
30
+
31
+ ## Templates
32
+
33
+ | ID | Style |
34
+ |----|-------|
35
+ | `professional` | Centered serif, traditional |
36
+ | `executive` | Experience-first, navy serif/sans |
37
+ | `modern` | Inter sans-serif, teal accent |
38
+ | `classic` | Times New Roman, ATS-maximum |
39
+ | `compact` | Dense one-page, IBM Plex |
40
+
41
+ Legacy `creative` maps to `modern`.
42
+
43
+ ## Local development
44
+
45
+ ```bash
46
+ uv sync --dev
47
+ uv run pytest
48
+ ```
49
+
50
+ Editable install from a checkout:
51
+
52
+ ```bash
53
+ uv add --editable ../CV-Formatter
54
+ ```
55
+
56
+ ## Release process
57
+
58
+ 1. Change templates/code in this repo
59
+ 2. Bump `version` in `pyproject.toml`
60
+ 3. Run `uv run pytest`
61
+ 4. Commit, tag (`git tag v1.2.1`), push tag
62
+ 5. GitHub Actions publishes to PyPI (or `uv build && uv publish`)
63
+ 6. Bump pin in Backend + Resume `pyproject.toml` and `uv lock`
64
+
65
+ ## Versioning
66
+
67
+ - **MAJOR** — breaking `content_json` schema changes
68
+ - **MINOR** — template/CSS changes or new optional fields
69
+ - **PATCH** — bug fixes
@@ -0,0 +1,47 @@
1
+ [project]
2
+ name = "sync_cv_formatter"
3
+ version = "1.2.0"
4
+ description = "CV/resume HTML and PDF rendering with hiring-manager-focused templates"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ authors = [{ name = "SyncQues" }]
9
+ keywords = ["resume", "cv", "pdf", "html", "jinja2"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Intended Audience :: Developers",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python :: 3.13",
16
+ "Topic :: Text Processing :: Markup :: HTML",
17
+ ]
18
+ dependencies = [
19
+ "jinja2>=3.1.6",
20
+ "pydantic>=2.12.3",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/SyncQues/CV-Formatter"
25
+ Repository = "https://github.com/SyncQues/CV-Formatter"
26
+ Issues = "https://github.com/SyncQues/CV-Formatter/issues"
27
+
28
+ [dependency-groups]
29
+ dev = [
30
+ "build>=1.2",
31
+ "pytest>=8.3",
32
+ "twine>=6.1",
33
+ ]
34
+
35
+ [build-system]
36
+ requires = ["setuptools>=61"]
37
+ build-backend = "setuptools.build_meta"
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["src"]
41
+
42
+ [tool.setuptools.package-data]
43
+ sync_cv_formatter = ["templates/html/**/*"]
44
+
45
+ [tool.pytest.ini_options]
46
+ testpaths = ["tests"]
47
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,13 @@
1
+ from sync_cv_formatter.renderers.html_renderer import (
2
+ populate_html_template,
3
+ save_html_file,
4
+ )
5
+ from sync_cv_formatter.renderers.pdf_renderer import render_html_to_pdf
6
+ from sync_cv_formatter.schemas.resume_document import ResumeDocument
7
+
8
+ __all__ = [
9
+ "ResumeDocument",
10
+ "populate_html_template",
11
+ "render_html_to_pdf",
12
+ "save_html_file",
13
+ ]
@@ -0,0 +1,4 @@
1
+ from sync_cv_formatter.renderers.html_renderer import populate_html_template, save_html_file
2
+ from sync_cv_formatter.renderers.pdf_renderer import render_html_to_pdf
3
+
4
+ __all__ = ["populate_html_template", "render_html_to_pdf", "save_html_file"]
@@ -0,0 +1,211 @@
1
+ from datetime import datetime
2
+ from pathlib import Path
3
+ from urllib.parse import urlparse
4
+
5
+ from jinja2 import Environment, FileSystemLoader, select_autoescape
6
+
7
+ from sync_cv_formatter.schemas.resume_document import ResumeDocument, ResumeTemplateId
8
+
9
+ _PKG_DIR = Path(__file__).resolve().parent.parent
10
+ HTML_TEMPLATES_DIR = _PKG_DIR / "templates" / "html"
11
+
12
+ _MONTHS = (
13
+ "Jan",
14
+ "Feb",
15
+ "Mar",
16
+ "Apr",
17
+ "May",
18
+ "Jun",
19
+ "Jul",
20
+ "Aug",
21
+ "Sep",
22
+ "Oct",
23
+ "Nov",
24
+ "Dec",
25
+ )
26
+
27
+
28
+ def _format_resume_date(value: str | None) -> str:
29
+ if not value:
30
+ return "Present"
31
+
32
+ normalized = str(value).strip()
33
+ if normalized.lower() == "present":
34
+ return "Present"
35
+
36
+ try:
37
+ parsed = datetime.fromisoformat(normalized.replace("Z", "+00:00"))
38
+ return f"{_MONTHS[parsed.month - 1]} {parsed.year}"
39
+ except ValueError:
40
+ pass
41
+
42
+ if len(normalized) >= 7 and normalized[4] == "-":
43
+ try:
44
+ year = int(normalized[:4])
45
+ month = int(normalized[5:7])
46
+ if 1 <= month <= 12:
47
+ return f"{_MONTHS[month - 1]} {year}"
48
+ except ValueError:
49
+ pass
50
+
51
+ return normalized
52
+
53
+
54
+ def _format_external_url(url: str | None) -> str:
55
+ if not url:
56
+ return ""
57
+ normalized = str(url).strip()
58
+ return normalized if normalized.startswith("http") else f"https://{normalized}"
59
+
60
+
61
+ def _resume_link_domain(url: str | None) -> str | None:
62
+ href = _format_external_url(url)
63
+ if not href:
64
+ return None
65
+ try:
66
+ hostname = urlparse(href).hostname or ""
67
+ return hostname.removeprefix("www.") or None
68
+ except ValueError:
69
+ return None
70
+
71
+
72
+ def _resume_favicon_url(url: str | None) -> str:
73
+ domain = _resume_link_domain(url)
74
+ if not domain:
75
+ return ""
76
+ return f"https://www.google.com/s2/favicons?domain={domain}&sz=32"
77
+
78
+
79
+ _PLATFORM_LABELS = {
80
+ "linkedin": "LinkedIn",
81
+ "github": "GitHub",
82
+ "gitlab": "GitLab",
83
+ "twitter": "Twitter",
84
+ "x": "X",
85
+ "youtube": "YouTube",
86
+ "instagram": "Instagram",
87
+ "facebook": "Facebook",
88
+ "portfolio": "Portfolio",
89
+ "website": "Website",
90
+ "syncques": "SyncQues",
91
+ "leetcode": "LeetCode",
92
+ "stackoverflow": "Stack Overflow",
93
+ "medium": "Medium",
94
+ "behance": "Behance",
95
+ "dribbble": "Dribbble",
96
+ "credential": "View Credential",
97
+ "live": "Live",
98
+ }
99
+
100
+ _DOMAIN_LABEL_PATTERNS: list[tuple[str, str]] = [
101
+ ("linkedin.com", "LinkedIn"),
102
+ ("github.com", "GitHub"),
103
+ ("gitlab.com", "GitLab"),
104
+ ("twitter.com", "X"),
105
+ ("x.com", "X"),
106
+ ("youtube.com", "YouTube"),
107
+ ("youtu.be", "YouTube"),
108
+ ("instagram.com", "Instagram"),
109
+ ("facebook.com", "Facebook"),
110
+ ("fb.com", "Facebook"),
111
+ ("syncques.com", "SyncQues"),
112
+ ("leetcode.com", "LeetCode"),
113
+ ("stackoverflow.com", "Stack Overflow"),
114
+ ("medium.com", "Medium"),
115
+ ("behance.net", "Behance"),
116
+ ("dribbble.com", "Dribbble"),
117
+ ("credly.com", "Credly"),
118
+ ]
119
+
120
+
121
+ def _resume_link_label(
122
+ platform_or_kind: str, url: str | None = None, fallback: str | None = None
123
+ ) -> str:
124
+ platform_key = platform_or_kind.lower().replace(" ", "_")
125
+ if platform_key in _PLATFORM_LABELS:
126
+ return _PLATFORM_LABELS[platform_key]
127
+
128
+ href = _format_external_url(url)
129
+ if href:
130
+ lower_href = href.lower()
131
+ for domain, label in _DOMAIN_LABEL_PATTERNS:
132
+ if domain in lower_href:
133
+ return label
134
+
135
+ domain = _resume_link_domain(href)
136
+ if domain:
137
+ base = domain.split(".")[0]
138
+ return base[:1].upper() + base[1:]
139
+
140
+ if fallback:
141
+ return fallback
142
+
143
+ return platform_or_kind.replace("_", " ").title()
144
+
145
+
146
+ TEMPLATE_DIRS: dict[ResumeTemplateId, str] = {
147
+ "professional": "professional",
148
+ "executive": "executive",
149
+ "modern": "modern",
150
+ "classic": "classic",
151
+ "compact": "compact",
152
+ }
153
+
154
+ _LEGACY_TEMPLATE_ALIASES: dict[str, ResumeTemplateId] = {
155
+ "creative": "modern",
156
+ }
157
+
158
+
159
+ def _get_template_env(template_id: ResumeTemplateId) -> Environment:
160
+ template_dir = HTML_TEMPLATES_DIR / TEMPLATE_DIRS[template_id]
161
+ env = Environment(
162
+ loader=FileSystemLoader(
163
+ [str(template_dir), str(HTML_TEMPLATES_DIR)],
164
+ ),
165
+ autoescape=select_autoescape(["html", "xml"]),
166
+ )
167
+ env.filters["format_resume_date"] = _format_resume_date
168
+ env.filters["format_external_url"] = _format_external_url
169
+ env.filters["resume_favicon_url"] = _resume_favicon_url
170
+ env.globals["resume_link_label"] = _resume_link_label
171
+ return env
172
+
173
+
174
+ def _load_css(template_id: ResumeTemplateId) -> str:
175
+ css_path = HTML_TEMPLATES_DIR / TEMPLATE_DIRS[template_id] / "styles.css"
176
+ return css_path.read_text(encoding="utf-8")
177
+
178
+
179
+ def _resolve_template_id(
180
+ template_id: str | ResumeTemplateId | None,
181
+ *,
182
+ fallback: ResumeTemplateId = "professional",
183
+ ) -> ResumeTemplateId:
184
+ normalized = str(template_id or fallback).strip().lower()
185
+ if normalized in _LEGACY_TEMPLATE_ALIASES:
186
+ return _LEGACY_TEMPLATE_ALIASES[normalized]
187
+ if normalized in TEMPLATE_DIRS:
188
+ return normalized # type: ignore[return-value]
189
+ return fallback
190
+
191
+
192
+ def populate_html_template(
193
+ document: ResumeDocument,
194
+ template_id: ResumeTemplateId | None = None,
195
+ *,
196
+ interactive: bool = False,
197
+ ) -> str:
198
+ selected_template = _resolve_template_id(template_id or document.template_id)
199
+ env = _get_template_env(selected_template)
200
+ template = env.get_template("template.html.j2")
201
+ return template.render(
202
+ document=document,
203
+ css_content=_load_css(selected_template),
204
+ interactive=interactive,
205
+ )
206
+
207
+
208
+ def save_html_file(content: str, output_path: str) -> str:
209
+ html_path = output_path.replace(".pdf", ".html")
210
+ Path(html_path).write_text(content, encoding="utf-8")
211
+ return html_path
@@ -0,0 +1,42 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def render_html_to_pdf(html_file: str, output_dir: str | None = None) -> tuple[bool, str]:
5
+ html_path = Path(html_file)
6
+
7
+ if not html_path.exists():
8
+ return False, f"HTML file not found: {html_file}"
9
+
10
+ if output_dir is None:
11
+ output_dir = html_path.parent
12
+
13
+ output_dir = Path(output_dir)
14
+ output_dir.mkdir(parents=True, exist_ok=True)
15
+ pdf_path = output_dir / html_path.with_suffix(".pdf").name
16
+
17
+ try:
18
+ from playwright.sync_api import sync_playwright
19
+ except ImportError:
20
+ return False, "Playwright is not installed. Run: playwright install chromium"
21
+
22
+ try:
23
+ file_url = html_path.resolve().as_uri()
24
+ with sync_playwright() as playwright:
25
+ browser = playwright.chromium.launch(headless=True)
26
+ page = browser.new_page()
27
+ page.goto(file_url, wait_until="networkidle")
28
+ page.pdf(
29
+ path=str(pdf_path),
30
+ format="A4",
31
+ print_background=True,
32
+ prefer_css_page_size=True,
33
+ margin={"top": "0", "right": "0", "bottom": "0", "left": "0"},
34
+ )
35
+ browser.close()
36
+
37
+ if pdf_path.exists():
38
+ return True, f"PDF created successfully: {pdf_path}"
39
+
40
+ return False, "PDF file was not created by Playwright"
41
+ except Exception as exc:
42
+ return False, f"HTML to PDF rendering failed: {exc}"
@@ -0,0 +1,27 @@
1
+ from sync_cv_formatter.schemas.resume_document import (
2
+ AchievementItem,
3
+ BasicsSection,
4
+ EducationItem,
5
+ ExperienceItem,
6
+ ProjectItem,
7
+ ResumeDocument,
8
+ ResumeDocumentMetadata,
9
+ ResumeSections,
10
+ ResumeTemplateId,
11
+ ResumeTypeValue,
12
+ SkillsSection,
13
+ )
14
+
15
+ __all__ = [
16
+ "AchievementItem",
17
+ "BasicsSection",
18
+ "EducationItem",
19
+ "ExperienceItem",
20
+ "ProjectItem",
21
+ "ResumeDocument",
22
+ "ResumeDocumentMetadata",
23
+ "ResumeSections",
24
+ "ResumeTemplateId",
25
+ "ResumeTypeValue",
26
+ "SkillsSection",
27
+ ]
@@ -0,0 +1,100 @@
1
+ from datetime import datetime, timezone
2
+ from typing import Literal
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+ ResumeTemplateId = Literal["professional", "executive", "modern", "classic", "compact"]
7
+ ResumeTypeValue = Literal["standard", "ats_optimized"]
8
+
9
+
10
+ class BasicsSection(BaseModel):
11
+ full_name: str = ""
12
+ email: str = ""
13
+ phone: str | None = None
14
+ location: str | None = None
15
+ summary: str | None = None
16
+ syncques_url: str | None = None
17
+ social_links: dict[str, str] = Field(default_factory=dict)
18
+
19
+
20
+ class SkillsSection(BaseModel):
21
+ flat: list[str] = Field(default_factory=list)
22
+ categorized: dict[str, list[str]] = Field(default_factory=dict)
23
+
24
+
25
+ class ExperienceItem(BaseModel):
26
+ title: str = ""
27
+ company: str = ""
28
+ location: str | None = None
29
+ start_date: str | None = None
30
+ end_date: str | None = "Present"
31
+ description: str = ""
32
+
33
+
34
+ class EducationItem(BaseModel):
35
+ institution: str = ""
36
+ degree: str = ""
37
+ field_of_study: str | None = None
38
+ location: str | None = None
39
+ start_date: str | None = None
40
+ end_date: str | None = "Present"
41
+ gpa: str | None = None
42
+ description: str | None = None
43
+
44
+
45
+ class ProjectItem(BaseModel):
46
+ title: str = ""
47
+ description: str = ""
48
+ technologies: list[str] = Field(default_factory=list)
49
+ project_url: str | None = None
50
+ repository_url: str | None = None
51
+ start_date: str | None = None
52
+ end_date: str | None = None
53
+
54
+
55
+ class AchievementItem(BaseModel):
56
+ title: str = ""
57
+ organization: str | None = None
58
+ description: str | None = None
59
+ date_earned: str | None = None
60
+ credential_url: str | None = None
61
+
62
+
63
+ class ResumeSections(BaseModel):
64
+ skills: SkillsSection = Field(default_factory=SkillsSection)
65
+ experience: list[ExperienceItem] = Field(default_factory=list)
66
+ education: list[EducationItem] = Field(default_factory=list)
67
+ projects: list[ProjectItem] = Field(default_factory=list)
68
+ achievements: list[AchievementItem] = Field(default_factory=list)
69
+
70
+
71
+ class ResumeDocumentMetadata(BaseModel):
72
+ generated_at: str
73
+ resume_type: ResumeTypeValue = "standard"
74
+ job_title: str | None = None
75
+ job_description: str | None = None
76
+ last_edited_at: str | None = None
77
+ content_version: int = 1
78
+
79
+
80
+ class ResumeDocument(BaseModel):
81
+ schema_version: str = "1.0"
82
+ template_id: ResumeTemplateId = "professional"
83
+ basics: BasicsSection = Field(default_factory=BasicsSection)
84
+ sections: ResumeSections = Field(default_factory=ResumeSections)
85
+ metadata: ResumeDocumentMetadata
86
+
87
+ @classmethod
88
+ def default_metadata(
89
+ cls,
90
+ resume_type: ResumeTypeValue = "standard",
91
+ job_title: str | None = None,
92
+ job_description: str | None = None,
93
+ ) -> ResumeDocumentMetadata:
94
+ return ResumeDocumentMetadata(
95
+ generated_at=datetime.now(timezone.utc).isoformat(),
96
+ resume_type=resume_type,
97
+ job_title=job_title,
98
+ job_description=job_description,
99
+ content_version=1,
100
+ )
@@ -0,0 +1,10 @@
1
+ <meta charset="UTF-8" />
2
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
3
+ <title>{{ document.basics.full_name }} — Resume</title>
4
+ <style>{{ css_content }}</style>
5
+ {% if interactive %}
6
+ <style>
7
+ [data-resume-section] { cursor: pointer; border-radius: 2px; }
8
+ [data-resume-section]:hover { background: rgba(0, 51, 153, 0.05); }
9
+ </style>
10
+ {% endif %}
@@ -0,0 +1,43 @@
1
+ {% macro resume_contact(document) %}
2
+ {% set ns = namespace(first=true) %}
3
+ {% if document.basics.email %}
4
+ {% if not ns.first %}<span class="contact-sep">·</span>{% endif %}
5
+ <a href="mailto:{{ document.basics.email }}">{{ document.basics.email }}</a>
6
+ {% set ns.first = false %}
7
+ {% endif %}
8
+ {% if document.basics.phone %}
9
+ {% if not ns.first %}<span class="contact-sep">·</span>{% endif %}
10
+ <a href="tel:{{ document.basics.phone }}">{{ document.basics.phone }}</a>
11
+ {% set ns.first = false %}
12
+ {% endif %}
13
+ {% if document.basics.location %}
14
+ {% if not ns.first %}<span class="contact-sep">·</span>{% endif %}
15
+ <span>{{ document.basics.location }}</span>
16
+ {% set ns.first = false %}
17
+ {% endif %}
18
+ {% if document.basics.syncques_url %}
19
+ {% if not ns.first %}<span class="contact-sep">·</span>{% endif %}
20
+ {{ external_link('syncques', document.basics.syncques_url) }}
21
+ {% set ns.first = false %}
22
+ {% endif %}
23
+ {% for platform, url in document.basics.social_links.items() %}
24
+ {% if url %}
25
+ {% if not ns.first %}<span class="contact-sep">·</span>{% endif %}
26
+ {{ external_link(platform, url) }}
27
+ {% set ns.first = false %}
28
+ {% endif %}
29
+ {% endfor %}
30
+ {% endmacro %}
31
+
32
+ {% macro external_link(platform, url, fallback='') %}
33
+ {% set href = url | format_external_url %}
34
+ {% if href %}
35
+ <a href="{{ href }}" class="resume-external-link">
36
+ {% set favicon = url | resume_favicon_url %}
37
+ {% if favicon %}
38
+ <img src="{{ favicon }}" alt="" class="resume-link-icon" width="12" height="12" />
39
+ {% endif %}
40
+ <span>{{ resume_link_label(platform, url, fallback) }}</span>
41
+ </a>
42
+ {% endif %}
43
+ {% endmacro %}