-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsections.py
More file actions
256 lines (223 loc) · 10.1 KB
/
Copy pathsections.py
File metadata and controls
256 lines (223 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
"""Section registry — the single source of truth for what sections exist.
# 🤖 ADD-A-SECTION-HERE
#
# To add a new section to the CV (e.g. "Publications"), append a
# SectionDef to ``DEFAULT_SECTIONS`` below. That's it — every other file
# (templates, validation, form, importers, AI extract prompt) reads from
# this registry. Pick one of the existing ``shape`` values, or add a new
# shape via ``engine/render/templates.py:RENDERERS`` and the matching
# ``tools/editor/static/form.js:SHAPE_RENDERERS``.
The registry is consumed by:
* ``engine/render/templates.py`` — picks a renderer per shape
* ``engine/render/content.py`` — validates required fields per section
* ``engine/render/importers.py`` — maps rendercv keys + plain-text headers
* ``engine/render/ai_extract.py`` — builds the Claude system prompt
* ``tools/editor/server.py:/api/schema`` — exposes the registry to the
frontend
* ``tools/editor/static/form.js`` — builds form sections from the
fetched schema
Order matters — list order = render order on the page = form order in
the editor = outline order in the sidebar.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
# Available render shapes. To add a new one, register it in
# ``engine/render/templates.py:RENDERERS`` (Python) AND in
# ``tools/editor/static/form.js:SHAPE_RENDERERS`` (JavaScript) so both
# the printed CV and the editor form know how to render it.
SHAPES = ("experience", "education", "skills", "compact", "publication")
@dataclass(frozen=True)
class FieldDef:
"""One item field exposed to the editor form."""
name: str
label: str
type: str = "text"
required: bool = False
placeholder: str = ""
rows: int = 1
max_rows: int = 7
hint: str = ""
@dataclass(frozen=True)
class SectionDef:
"""One section's metadata, frozen so it can be cached freely."""
# YAML key + URL slug (lowercase, identifier-safe).
key: str
# Display title (rendered in the section heading on the PDF and form).
label: str
# Eyebrow text shown above the section heading in the editor form.
# Keep this short and descriptive — e.g. "Where you've worked".
eyebrow: str
# Singular form for the "Add <singular>" button in the editor form.
# E.g. "role" → "Add role" (for experience), "paper" → "Add paper".
singular: str
# Visual shape — pick from SHAPES.
shape: str
# Field names every item in this section must have to validate.
# Items missing any of these raise a clear error at build time.
required_fields: tuple[str, ...] = ()
# Aliases the rendercv importer accepts (their schema has a few
# synonyms — we accept all of them and normalise to ``key``).
rendercv_aliases: tuple[str, ...] = ()
# Regex used by the plain-text importer to detect this section's
# heading line (case-insensitive, whole line). Empty = not detected
# in plain-text mode.
text_header_pattern: str = ""
# Optional metadata for the AI extract system prompt. If set,
# overrides the default "auto" description Claude sees for this
# section's shape.
ai_hint: str = ""
# Editor field metadata for one repeated item.
fields: tuple[FieldDef, ...] = ()
# ──────────────────────────────────────────────────────────────────────
# DEFAULT_SECTIONS — the shipped registry.
#
# Add / remove / reorder entries here. The frontend will pick up
# changes automatically the next time it fetches /api/schema (Cmd+R
# in the browser is enough — no rebuild required).
# ──────────────────────────────────────────────────────────────────────
DEFAULT_SECTIONS: tuple[SectionDef, ...] = (
SectionDef(
key="experience",
label="Experience",
eyebrow="Where you've worked",
singular="role",
shape="experience",
required_fields=("role", "company", "start", "end"),
rendercv_aliases=("experience", "work_experience", "professional_experience"),
text_header_pattern=r"^\s*(work\s+)?(experience|employment|professional\s+experience)\s*$",
fields=(
FieldDef("role", "Role", required=True, placeholder="Senior Data Engineer"),
FieldDef("company", "Company", required=True, placeholder="Acme GmbH"),
FieldDef("location", "Location", placeholder="Berlin, Germany"),
FieldDef("start", "Start", required=True, placeholder="2022"),
FieldDef("end", "End", required=True, placeholder="Present"),
FieldDef("bullets", "Bullets", type="list", placeholder="One impact per bullet"),
FieldDef("stack", "Stack", type="textarea", rows=2, placeholder="Python, Spark, Airflow"),
),
),
SectionDef(
key="education",
label="Education",
eyebrow="Where you studied",
singular="degree",
shape="education",
required_fields=("degree", "school", "start", "end"),
rendercv_aliases=("education",),
text_header_pattern=r"^\s*education\s*$",
fields=(
FieldDef("degree", "Degree", required=True, placeholder="MSc Computer Science"),
FieldDef("school", "School", required=True, placeholder="ETH Zurich"),
FieldDef("location", "Location", placeholder="Zurich, Switzerland"),
FieldDef("start", "Start", required=True, placeholder="2018"),
FieldDef("end", "End", required=True, placeholder="2020"),
FieldDef("note", "Note", type="textarea", rows=2, placeholder="Relevant thesis, honors, coursework."),
),
),
SectionDef(
key="skills",
label="Skills",
eyebrow="The toolkit",
singular="category",
shape="skills",
required_fields=("label",),
rendercv_aliases=("skills", "technical_skills"),
text_header_pattern=r"^\s*(technical\s+)?(skills|expertise)\s*$",
fields=(
FieldDef("label", "Category", required=True, placeholder="Data"),
FieldDef("items", "Skills", type="chips", placeholder="Python, SQL, Spark"),
),
),
SectionDef(
key="projects",
label="Projects",
eyebrow="Things you built",
singular="project",
shape="compact",
required_fields=("title",),
rendercv_aliases=("projects", "personal_projects", "open_source"),
text_header_pattern=r"^\s*(personal\s+)?projects\s*$",
fields=(
FieldDef("title", "Title", required=True, placeholder="OSS contribution"),
FieldDef("date", "Date", placeholder="08/2023"),
FieldDef("desc", "Description", type="textarea", rows=3, placeholder="Short description with concrete scope and impact."),
),
),
SectionDef(
key="leadership",
label="Leadership",
eyebrow="How you led",
singular="entry",
shape="compact",
required_fields=("title",),
rendercv_aliases=("leadership", "service", "mentoring", "volunteer"),
text_header_pattern=r"^\s*(leadership|service|volunteering|community)\s*$",
fields=(
FieldDef("title", "Title", required=True, placeholder="Mentoring lead"),
FieldDef("date", "Date", placeholder="2024"),
FieldDef("desc", "Description", type="textarea", rows=3, placeholder="Scope, audience, and measurable result."),
),
),
SectionDef(
key="others",
label="Other",
eyebrow="Awards & extras",
singular="entry",
shape="compact",
required_fields=("title",),
rendercv_aliases=(
"other", "others", "awards", "honors",
"publications", "certifications", "volunteer",
),
text_header_pattern=r"^\s*(awards|honors|publications|certifications|extras?|other|others|additional)\s*$",
fields=(
FieldDef("title", "Title", required=True, placeholder="Certification or award"),
FieldDef("date", "Date", placeholder="2024"),
FieldDef("desc", "Description", type="textarea", rows=3, placeholder="Issuer, venue, or context."),
),
),
)
# ──────────────────────────────────────────────────────────────────────
# HELPERS
# ──────────────────────────────────────────────────────────────────────
def all_sections() -> tuple[SectionDef, ...]:
"""Return the section list. (A function so callers can mock it in tests.)"""
return DEFAULT_SECTIONS
def by_key(key: str) -> SectionDef | None:
"""Look up a section by its YAML key; returns None if absent."""
for s in DEFAULT_SECTIONS:
if s.key == key:
return s
return None
def section_keys() -> tuple[str, ...]:
"""All registered section keys, in render order."""
return tuple(s.key for s in DEFAULT_SECTIONS)
def to_json_dict() -> list[dict[str, Any]]:
"""Serialisable shape for the /api/schema endpoint.
The frontend uses this to build the form. Field names are camelCased
to match JS conventions; on the Python side we keep snake_case.
"""
return [
{
"key": s.key,
"label": s.label,
"eyebrow": s.eyebrow,
"singular": s.singular,
"shape": s.shape,
"requiredFields": list(s.required_fields),
"fields": [
{
"name": f.name,
"label": f.label,
"type": f.type,
"required": f.required,
"placeholder": f.placeholder,
"rows": f.rows,
"maxRows": f.max_rows,
"hint": f.hint,
}
for f in s.fields
],
}
for s in DEFAULT_SECTIONS
]