Coverage for src / rendercv / schema / models / design / classic_theme.py: 100%

157 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-25 13:49 +0000

1from typing import Literal 

2 

3import pydantic 

4 

5from rendercv.schema.models.base import BaseModelWithoutExtraKeys 

6from rendercv.schema.models.design.color import Color 

7from rendercv.schema.models.design.font_family import FontFamily as FontFamilyType 

8from rendercv.schema.models.design.typst_dimension import ( 

9 TypstDimension, 

10 length_common_description, 

11) 

12 

13# Page 

14 

15type PageSize = Literal["a4", "a5", "us-letter", "us-executive"] 

16 

17 

18class Page(BaseModelWithoutExtraKeys): 

19 size: PageSize = pydantic.Field( 

20 default="us-letter", 

21 description=( 

22 "The page size. Use 'a4' (international standard) or 'us-letter' (US" 

23 " standard). The default value is `us-letter`." 

24 ), 

25 ) 

26 top_margin: TypstDimension = pydantic.Field( 

27 default="0.7in", 

28 description=length_common_description + " The default value is `0.7in`.", 

29 ) 

30 bottom_margin: TypstDimension = pydantic.Field( 

31 default="0.7in", 

32 description=length_common_description + " The default value is `0.7in`.", 

33 ) 

34 left_margin: TypstDimension = pydantic.Field( 

35 default="0.7in", 

36 description=length_common_description + " The default value is `0.7in`.", 

37 ) 

38 right_margin: TypstDimension = pydantic.Field( 

39 default="0.7in", 

40 description=length_common_description + " The default value is `0.7in`.", 

41 ) 

42 show_footer: bool = pydantic.Field( 

43 default=True, 

44 description=( 

45 "Show the footer at the bottom of pages. The default value is `true`." 

46 ), 

47 ) 

48 show_top_note: bool = pydantic.Field( 

49 default=True, 

50 description=( 

51 "Show the top note at the top of the first page. The default value is" 

52 " `true`." 

53 ), 

54 ) 

55 

56 

57# Colors 

58 

59color_common_description = ( 

60 "The color can be specified either with their name" 

61 " (https://www.w3.org/TR/SVG11/types.html#ColorKeywords), hexadecimal value, RGB" 

62 " value, or HSL value." 

63) 

64color_common_examples = ["Black", "7fffd4", "rgb(0,79,144)", "hsl(270, 60%, 70%)"] 

65 

66 

67class Colors(BaseModelWithoutExtraKeys): 

68 body: Color = pydantic.Field( 

69 default=Color("rgb(0, 0, 0)"), 

70 description=( 

71 color_common_description + " The default value is `rgb(0, 0, 0)`." 

72 ), 

73 examples=color_common_examples, 

74 ) 

75 name: Color = pydantic.Field( 

76 default=Color("rgb(0, 79, 144)"), 

77 description=color_common_description 

78 + " The default value is `rgb(0, 79, 144)`.", 

79 examples=color_common_examples, 

80 ) 

81 headline: Color = pydantic.Field( 

82 default=Color("rgb(0, 79, 144)"), 

83 description=color_common_description 

84 + " The default value is `rgb(0, 79, 144)`.", 

85 examples=color_common_examples, 

86 ) 

87 connections: Color = pydantic.Field( 

88 default=Color("rgb(0, 79, 144)"), 

89 description=color_common_description 

90 + " The default value is `rgb(0, 79, 144)`.", 

91 examples=color_common_examples, 

92 ) 

93 section_titles: Color = pydantic.Field( 

94 default=Color("rgb(0, 79, 144)"), 

95 description=color_common_description 

96 + " The default value is `rgb(0, 79, 144)`.", 

97 examples=color_common_examples, 

98 ) 

99 links: Color = pydantic.Field( 

100 default=Color("rgb(0, 79, 144)"), 

101 description=color_common_description 

102 + " The default value is `rgb(0, 79, 144)`.", 

103 examples=color_common_examples, 

104 ) 

105 footer: Color = pydantic.Field( 

106 default=Color("rgb(128, 128, 128)"), 

107 description=color_common_description 

108 + " The default value is `rgb(128, 128, 128)`.", 

109 examples=color_common_examples, 

110 ) 

111 top_note: Color = pydantic.Field( 

112 default=Color("rgb(128, 128, 128)"), 

113 description=color_common_description 

114 + " The default value is `rgb(128, 128, 128)`.", 

115 examples=color_common_examples, 

116 ) 

117 

118 

119# Typography 

120 

121type BodyAlignment = Literal["left", "justified", "justified-with-no-hyphenation"] 

122type Alignment = Literal["left", "center", "right"] 

123 

124 

125class FontFamily(BaseModelWithoutExtraKeys): 

126 body: FontFamilyType = pydantic.Field( 

127 default="Source Sans 3", 

128 description=( 

129 "The font family for body text. The default value is `Source Sans 3`." 

130 ), 

131 ) 

132 name: FontFamilyType = pydantic.Field( 

133 default="Source Sans 3", 

134 description=( 

135 "The font family for the name. The default value is `Source Sans 3`." 

136 ), 

137 ) 

138 headline: FontFamilyType = pydantic.Field( 

139 default="Source Sans 3", 

140 description=( 

141 "The font family for the headline. The default value is `Source Sans 3`." 

142 ), 

143 ) 

144 connections: FontFamilyType = pydantic.Field( 

145 default="Source Sans 3", 

146 description=( 

147 "The font family for connections. The default value is `Source Sans 3`." 

148 ), 

149 ) 

150 section_titles: FontFamilyType = pydantic.Field( 

151 default="Source Sans 3", 

152 description=( 

153 "The font family for section titles. The default value is `Source Sans 3`." 

154 ), 

155 ) 

156 

157 

158class FontSize(BaseModelWithoutExtraKeys): 

159 body: TypstDimension = pydantic.Field( 

160 default="10pt", 

161 description="The font size for body text. The default value is `10pt`.", 

162 ) 

163 name: TypstDimension = pydantic.Field( 

164 default="30pt", 

165 description="The font size for the name. The default value is `30pt`.", 

166 ) 

167 headline: TypstDimension = pydantic.Field( 

168 default="10pt", 

169 description="The font size for the headline. The default value is `10pt`.", 

170 ) 

171 connections: TypstDimension = pydantic.Field( 

172 default="10pt", 

173 description="The font size for connections. The default value is `10pt`.", 

174 ) 

175 section_titles: TypstDimension = pydantic.Field( 

176 default="1.4em", 

177 description="The font size for section titles. The default value is `1.4em`.", 

178 ) 

179 

180 

181class SmallCaps(BaseModelWithoutExtraKeys): 

182 name: bool = pydantic.Field( 

183 default=False, 

184 description=( 

185 "Whether to use small caps for the name. The default value is `false`." 

186 ), 

187 ) 

188 headline: bool = pydantic.Field( 

189 default=False, 

190 description=( 

191 "Whether to use small caps for the headline. The default value is `false`." 

192 ), 

193 ) 

194 connections: bool = pydantic.Field( 

195 default=False, 

196 description=( 

197 "Whether to use small caps for connections. The default value is `false`." 

198 ), 

199 ) 

200 section_titles: bool = pydantic.Field( 

201 default=False, 

202 description=( 

203 "Whether to use small caps for section titles. The default value is" 

204 " `false`." 

205 ), 

206 ) 

207 

208 

209class Bold(BaseModelWithoutExtraKeys): 

210 name: bool = pydantic.Field( 

211 default=True, 

212 description="Whether to make the name bold. The default value is `true`.", 

213 ) 

214 headline: bool = pydantic.Field( 

215 default=False, 

216 description="Whether to make the headline bold. The default value is `false`.", 

217 ) 

218 connections: bool = pydantic.Field( 

219 default=False, 

220 description="Whether to make connections bold. The default value is `false`.", 

221 ) 

222 section_titles: bool = pydantic.Field( 

223 default=True, 

224 description="Whether to make section titles bold. The default value is `true`.", 

225 ) 

226 

227 

228class Typography(BaseModelWithoutExtraKeys): 

229 line_spacing: TypstDimension = pydantic.Field( 

230 default="0.6em", 

231 description=( 

232 "Space between lines of text. Larger values create more vertical space. The" 

233 " default value is `0.6em`." 

234 ), 

235 ) 

236 alignment: Literal["left", "justified", "justified-with-no-hyphenation"] = ( 

237 pydantic.Field( 

238 default="justified", 

239 description=( 

240 "Text alignment. Options: 'left', 'justified' (spreads text across full" 

241 " width), 'justified-with-no-hyphenation' (justified without word" 

242 " breaks). The default value is `justified`." 

243 ), 

244 ) 

245 ) 

246 date_and_location_column_alignment: Alignment = pydantic.Field( 

247 default="right", 

248 description=( 

249 "Alignment for dates and locations in entries. Options: 'left', 'center'," 

250 " 'right'. The default value is `right`." 

251 ), 

252 ) 

253 font_family: FontFamily | FontFamilyType = pydantic.Field( 

254 default_factory=FontFamily, 

255 description=( 

256 "The font family. You can provide a single font name as a string (applies" 

257 " to all elements), or a dictionary with keys 'body', 'name', 'headline'," 

258 " 'connections', and 'section_titles' to customize each element. Any system" 

259 " font can be used." 

260 ), 

261 ) 

262 font_size: FontSize = pydantic.Field( 

263 default_factory=FontSize, 

264 description="Font sizes for different elements.", 

265 ) 

266 small_caps: SmallCaps = pydantic.Field( 

267 default_factory=SmallCaps, 

268 description="Small caps styling for different elements.", 

269 ) 

270 bold: Bold = pydantic.Field( 

271 default_factory=Bold, 

272 description="Bold styling for different elements.", 

273 ) 

274 

275 @pydantic.field_validator( 

276 "font_family", mode="plain", json_schema_input_type=FontFamily | FontFamilyType 

277 ) 

278 @classmethod 

279 def validate_font_family( 

280 cls, font_family: FontFamily | FontFamilyType 

281 ) -> FontFamily: 

282 """Convert string font to FontFamily object with uniform styling. 

283 

284 Why: 

285 Users can provide simple string "Latin Modern Roman" for all text, 

286 or specify per-element fonts via FontFamily dict. Validator accepts 

287 both, expanding strings to full FontFamily objects. 

288 

289 Args: 

290 font_family: String font name or FontFamily object. 

291 

292 Returns: 

293 FontFamily object with all fields populated. 

294 """ 

295 if isinstance(font_family, str): 

296 return FontFamily( 

297 body=font_family, 

298 name=font_family, 

299 headline=font_family, 

300 connections=font_family, 

301 section_titles=font_family, 

302 ) 

303 

304 return FontFamily.model_validate(font_family) 

305 

306 

307# Links 

308 

309 

310class Links(BaseModelWithoutExtraKeys): 

311 underline: bool = pydantic.Field( 

312 default=False, 

313 description="Underline hyperlinks. The default value is `false`.", 

314 ) 

315 show_external_link_icon: bool = pydantic.Field( 

316 default=False, 

317 description=( 

318 "Show an external link icon next to URLs. The default value is `false`." 

319 ), 

320 ) 

321 

322 

323# Header 

324 

325type PhoneNumberFormatType = Literal["national", "international", "E164"] 

326 

327 

328class Connections(BaseModelWithoutExtraKeys): 

329 phone_number_format: PhoneNumberFormatType = pydantic.Field( 

330 default="national", 

331 description="Phone number format. The default value is `national`.", 

332 ) 

333 hyperlink: bool = pydantic.Field( 

334 default=True, 

335 description=( 

336 "Make contact information clickable in the PDF. The default value is" 

337 " `true`." 

338 ), 

339 ) 

340 show_icons: bool = pydantic.Field( 

341 default=True, 

342 description=( 

343 "Show icons next to contact information. The default value is `true`." 

344 ), 

345 ) 

346 display_urls_instead_of_usernames: bool = pydantic.Field( 

347 default=False, 

348 description=( 

349 "Display full URLs instead of labels. The default value is `false`." 

350 ), 

351 ) 

352 separator: str = pydantic.Field( 

353 default="", 

354 description=( 

355 "Character(s) to separate contact items (e.g., '|' or '•'). Leave empty for" 

356 " no separator. The default value is `''`." 

357 ), 

358 ) 

359 space_between_connections: TypstDimension = pydantic.Field( 

360 default="0.5cm", 

361 description=( 

362 "Horizontal space between contact items. " 

363 + length_common_description 

364 + " The default value is `0.5cm`." 

365 ), 

366 ) 

367 

368 

369class Header(BaseModelWithoutExtraKeys): 

370 alignment: Alignment = pydantic.Field( 

371 default="center", 

372 description=( 

373 "Header alignment. Options: 'left', 'center', 'right'. The default value is" 

374 " `center`." 

375 ), 

376 ) 

377 photo_width: TypstDimension = pydantic.Field( 

378 default="3.5cm", 

379 description="Photo width. " 

380 + length_common_description 

381 + " The default value is `3.5cm`.", 

382 ) 

383 photo_position: Literal["left", "right"] = pydantic.Field( 

384 default="left", 

385 description="Photo position (left or right). The default value is `left`.", 

386 ) 

387 photo_space_left: TypstDimension = pydantic.Field( 

388 default="0.4cm", 

389 description=( 

390 "Space to the left of the photo. " 

391 + length_common_description 

392 + " The default value is `0.4cm`." 

393 ), 

394 ) 

395 photo_space_right: TypstDimension = pydantic.Field( 

396 default="0.4cm", 

397 description=( 

398 "Space to the right of the photo. " 

399 + length_common_description 

400 + " The default value is `0.4cm`." 

401 ), 

402 ) 

403 space_below_name: TypstDimension = pydantic.Field( 

404 default="0.7cm", 

405 description="Space below your name. " 

406 + length_common_description 

407 + " The default value is `0.7cm`.", 

408 ) 

409 space_below_headline: TypstDimension = pydantic.Field( 

410 default="0.7cm", 

411 description="Space below the headline. " 

412 + length_common_description 

413 + " The default value is `0.7cm`.", 

414 ) 

415 space_below_connections: TypstDimension = pydantic.Field( 

416 default="0.7cm", 

417 description="Space below contact information. " 

418 + length_common_description 

419 + " The default value is `0.7cm`.", 

420 ) 

421 connections: Connections = pydantic.Field( 

422 default_factory=Connections, 

423 description="Contact information settings.", 

424 ) 

425 

426 

427# Section Titles 

428 

429type SectionTitleType = Literal[ 

430 "with_partial_line", 

431 "with_full_line", 

432 "without_line", 

433 "moderncv", 

434 "centered_without_line", 

435 "centered_with_partial_line", 

436 "centered_with_centered_partial_line", 

437 "centered_with_full_line", 

438] 

439 

440 

441class SectionTitles(BaseModelWithoutExtraKeys): 

442 type: SectionTitleType = pydantic.Field( 

443 default="with_partial_line", 

444 description=( 

445 "Section title visual style. Use 'with_partial_line' for a line next to the" 

446 " title, 'with_full_line' for a line across the page, 'without_line' for no" 

447 " line, 'moderncv' for the ModernCV style, 'centered_without_line' for a" 

448 " centered title with no line, 'centered_with_partial_line' for a centered" 

449 " title with baseline partial lines on both sides," 

450 " 'centered_with_centered_partial_line' for a centered title with" 

451 " middle-aligned lines on both sides, or 'centered_with_full_line' for a" 

452 " full-width line underneath. The default value is `with_partial_line`." 

453 ), 

454 ) 

455 line_thickness: TypstDimension = pydantic.Field( 

456 default="0.5pt", 

457 description=length_common_description + " The default value is `0.5pt`.", 

458 ) 

459 space_above: TypstDimension = pydantic.Field( 

460 default="0.5cm", 

461 description=length_common_description + " The default value is `0.5cm`.", 

462 ) 

463 space_below: TypstDimension = pydantic.Field( 

464 default="0.3cm", 

465 description=length_common_description + " The default value is `0.3cm`.", 

466 ) 

467 

468 

469# Sections 

470 

471 

472class Sections(BaseModelWithoutExtraKeys): 

473 allow_page_break: bool = pydantic.Field( 

474 default=True, 

475 description=( 

476 "Allow page breaks within sections. If false, sections that don't fit will" 

477 " start on a new page. The default value is `true`." 

478 ), 

479 ) 

480 space_between_regular_entries: TypstDimension = pydantic.Field( 

481 default="1.2em", 

482 description=( 

483 "Vertical space between entries. " 

484 + length_common_description 

485 + " The default value is `1.2em`." 

486 ), 

487 ) 

488 space_between_text_based_entries: TypstDimension = pydantic.Field( 

489 default="0.3em", 

490 description=( 

491 "Vertical space between text-based entries. " 

492 + length_common_description 

493 + " The default value is `0.3em`." 

494 ), 

495 ) 

496 show_time_spans_in: list[str] = pydantic.Field( 

497 default=["experience"], 

498 description=( 

499 "Section titles where time spans (e.g., '2 years 3 months') should be" 

500 " displayed. The default value is `['experience']`." 

501 ), 

502 examples=[["Experience"], ["Experience", "Education"]], 

503 ) 

504 

505 @pydantic.field_validator("show_time_spans_in", mode="after") 

506 @classmethod 

507 def convert_section_titles_to_snake_case(cls, value: list[str]) -> list[str]: 

508 return [section_title.lower().replace(" ", "_") for section_title in value] 

509 

510 

511# Entries 

512 

513type Bullet = Literal["●", "•", "◦", "-", "◆", "★", "■", "—", "○"] 

514 

515 

516class Summary(BaseModelWithoutExtraKeys): 

517 space_above: TypstDimension = pydantic.Field( 

518 default="0cm", 

519 description=( 

520 "Space above summary text. " 

521 + length_common_description 

522 + " The default value is `0cm`." 

523 ), 

524 ) 

525 space_left: TypstDimension = pydantic.Field( 

526 default="0cm", 

527 description=( 

528 "Left margin for summary text. " 

529 + length_common_description 

530 + " The default value is `0cm`." 

531 ), 

532 ) 

533 

534 

535class Highlights(BaseModelWithoutExtraKeys): 

536 bullet: Bullet = pydantic.Field( 

537 default="•", 

538 description="Bullet character for highlights. The default value is `•`.", 

539 ) 

540 nested_bullet: Bullet = pydantic.Field( 

541 default="•", 

542 description="Bullet character for nested highlights. The default value is `•`.", 

543 ) 

544 space_left: TypstDimension = pydantic.Field( 

545 default="0.15cm", 

546 description=( 

547 "Left indentation. " 

548 + length_common_description 

549 + " The default value is `0.15cm`." 

550 ), 

551 ) 

552 space_above: TypstDimension = pydantic.Field( 

553 default="0cm", 

554 description=( 

555 "Space above highlights. " 

556 + length_common_description 

557 + " The default value is `0cm`." 

558 ), 

559 ) 

560 space_between_items: TypstDimension = pydantic.Field( 

561 default="0cm", 

562 description=( 

563 "Space between highlight items. " 

564 + length_common_description 

565 + " The default value is `0cm`." 

566 ), 

567 ) 

568 space_between_bullet_and_text: TypstDimension = pydantic.Field( 

569 default="0.5em", 

570 description=( 

571 "Space between bullet and text. " 

572 + length_common_description 

573 + " The default value is `0.5em`." 

574 ), 

575 ) 

576 

577 

578class Entries(BaseModelWithoutExtraKeys): 

579 date_and_location_width: TypstDimension = pydantic.Field( 

580 default="4.15cm", 

581 description=( 

582 "Width of the date/location column. " 

583 + length_common_description 

584 + " The default value is `4.15cm`." 

585 ), 

586 ) 

587 side_space: TypstDimension = pydantic.Field( 

588 default="0.2cm", 

589 description=( 

590 "Left and right margins. " 

591 + length_common_description 

592 + " The default value is `0.2cm`." 

593 ), 

594 ) 

595 space_between_columns: TypstDimension = pydantic.Field( 

596 default="0.1cm", 

597 description=( 

598 "Space between main content and date/location columns. " 

599 + length_common_description 

600 + " The default value is `0.1cm`." 

601 ), 

602 ) 

603 allow_page_break: bool = pydantic.Field( 

604 default=False, 

605 description=( 

606 "Allow page breaks within entries. If false, entries that don't fit will" 

607 " move to a new page. The default value is `false`." 

608 ), 

609 ) 

610 short_second_row: bool = pydantic.Field( 

611 default=True, 

612 description=( 

613 "Shorten the second row to align with the date/location column. The default" 

614 " value is `true`." 

615 ), 

616 ) 

617 degree_width: TypstDimension = pydantic.Field( 

618 default="1cm", 

619 description=( 

620 "Width of the degree column. " 

621 + length_common_description 

622 + " The default value is `1cm`." 

623 ), 

624 ) 

625 summary: Summary = pydantic.Field( 

626 default_factory=Summary, 

627 description="Summary text settings.", 

628 ) 

629 highlights: Highlights = pydantic.Field( 

630 default_factory=Highlights, 

631 description="Highlights settings.", 

632 ) 

633 

634 

635# Templates 

636 

637 

638class OneLineEntryTemplate(BaseModelWithoutExtraKeys): 

639 main_column: str = pydantic.Field( 

640 default="**LABEL:** DETAILS", 

641 description=( 

642 "Template for one-line entries. Available placeholders:\n- `LABEL`: The" 

643 ' label text (e.g., "Languages", "Citizenship")\n- `DETAILS`: The details' 

644 ' text (e.g., "English (native), Spanish (fluent)")\n\nYou can also add' 

645 " arbitrary keys to entries and use them as UPPERCASE placeholders.\n\nThe" 

646 " default value is `**LABEL:** DETAILS`." 

647 ), 

648 ) 

649 

650 

651class EducationEntryTemplate(BaseModelWithoutExtraKeys): 

652 main_column: str = pydantic.Field( 

653 default="**INSTITUTION**, AREA\nSUMMARY\nHIGHLIGHTS", 

654 description=( 

655 "Template for education entry main column. Available placeholders:\n-" 

656 " `INSTITUTION`: Institution name\n- `AREA`: Field of study/major\n-" 

657 " `DEGREE`: Degree type (e.g., BS, PhD)\n- `DEGREE_WITH_AREA`: Locale-aware" 

658 " phrase combining degree and area (e.g., 'BS in Computer Science')\n-" 

659 " `SUMMARY`: Summary text\n-" 

660 " `HIGHLIGHTS`: Bullet points list\n- `LOCATION`: Location text\n- `DATE`:" 

661 " Formatted date or date range\n\nYou can also add arbitrary keys to" 

662 " entries and use them as UPPERCASE placeholders.\n\nThe default value is" 

663 " `**INSTITUTION**, AREA\\nSUMMARY\\nHIGHLIGHTS`." 

664 ), 

665 ) 

666 degree_column: str | None = pydantic.Field( 

667 default="**DEGREE**", 

668 description=( 

669 "Optional degree column template. If provided, displays degree in separate" 

670 " column. If `null`, no degree column is shown. Available placeholders:\n-" 

671 " `INSTITUTION`: Institution name\n- `AREA`: Field of study/major\n-" 

672 " `DEGREE`: Degree type (e.g., BS, PhD)\n- `SUMMARY`: Summary text\n-" 

673 " `HIGHLIGHTS`: Bullet points list\n- `LOCATION`: Location text\n- `DATE`:" 

674 " Formatted date or date range\n\nYou can also add arbitrary keys to" 

675 " entries and use them as UPPERCASE placeholders.\n\nThe default value is" 

676 " `**DEGREE**`." 

677 ), 

678 ) 

679 date_and_location_column: str = pydantic.Field( 

680 default="LOCATION\nDATE", 

681 description=( 

682 "Template for education entry date/location column. Available" 

683 " placeholders:\n- `INSTITUTION`: Institution name\n- `AREA`: Field of" 

684 " study/major\n- `DEGREE`: Degree type (e.g., BS, PhD)\n- `SUMMARY`:" 

685 " Summary text\n- `HIGHLIGHTS`: Bullet points list\n- `LOCATION`: Location" 

686 " text\n- `DATE`: Formatted date or date range\n\nYou can also add" 

687 " arbitrary keys to entries and use them as UPPERCASE placeholders.\n\nThe" 

688 " default value is `LOCATION\\nDATE`." 

689 ), 

690 ) 

691 

692 

693class NormalEntryTemplate(BaseModelWithoutExtraKeys): 

694 main_column: str = pydantic.Field( 

695 default="**NAME**\nSUMMARY\nHIGHLIGHTS", 

696 description=( 

697 "Template for normal entry main column. Available placeholders:\n- `NAME`:" 

698 " Entry name/title\n- `SUMMARY`: Summary text\n- `HIGHLIGHTS`: Bullet" 

699 " points list\n- `LOCATION`: Location text\n- `DATE`: Formatted date or" 

700 " date range\n\nYou can also add arbitrary keys to entries and use them as" 

701 " UPPERCASE placeholders.\n\nThe default value is" 

702 " `**NAME**\\nSUMMARY\\nHIGHLIGHTS`." 

703 ), 

704 ) 

705 date_and_location_column: str = pydantic.Field( 

706 default="LOCATION\nDATE", 

707 description=( 

708 "Template for normal entry date/location column. Available placeholders:\n-" 

709 " `NAME`: Entry name/title\n- `SUMMARY`: Summary text\n- `HIGHLIGHTS`:" 

710 " Bullet points list\n- `LOCATION`: Location text\n- `DATE`: Formatted date" 

711 " or date range\n\nYou can also add arbitrary keys to entries and use them" 

712 " as UPPERCASE placeholders.\n\nThe default value is `LOCATION\\nDATE`." 

713 ), 

714 ) 

715 

716 

717class ExperienceEntryTemplate(BaseModelWithoutExtraKeys): 

718 main_column: str = pydantic.Field( 

719 default="**COMPANY**, POSITION\nSUMMARY\nHIGHLIGHTS", 

720 description=( 

721 "Template for experience entry main column. Available placeholders:\n-" 

722 " `COMPANY`: Company name\n- `POSITION`: Job title/position\n- `SUMMARY`:" 

723 " Summary text\n- `HIGHLIGHTS`: Bullet points list\n- `LOCATION`: Location" 

724 " text\n- `DATE`: Formatted date or date range\n\nYou can also add" 

725 " arbitrary keys to entries and use them as UPPERCASE placeholders.\n\nThe" 

726 " default value is `**COMPANY**, POSITION\\nSUMMARY\\nHIGHLIGHTS`." 

727 ), 

728 ) 

729 date_and_location_column: str = pydantic.Field( 

730 default="LOCATION\nDATE", 

731 description=( 

732 "Template for experience entry date/location column. Available" 

733 " placeholders:\n- `COMPANY`: Company name\n- `POSITION`: Job" 

734 " title/position\n- `SUMMARY`: Summary text\n- `HIGHLIGHTS`: Bullet points" 

735 " list\n- `LOCATION`: Location text\n- `DATE`: Formatted date or date" 

736 " range\n\nYou can also add arbitrary keys to entries and use them as" 

737 " UPPERCASE placeholders.\n\nThe default value is `LOCATION\\nDATE`." 

738 ), 

739 ) 

740 

741 

742class PublicationEntryTemplate(BaseModelWithoutExtraKeys): 

743 main_column: str = pydantic.Field( 

744 default="**TITLE**\nSUMMARY\nAUTHORS\nURL (JOURNAL)", 

745 description=( 

746 "Template for publication entry main column. Available placeholders:\n-" 

747 " `TITLE`: Publication title\n- `AUTHORS`: List of authors (formatted as" 

748 " comma-separated string)\n- `SUMMARY`: Summary/abstract text\n- `DOI`:" 

749 " Digital Object Identifier\n- `URL`: Publication URL (if DOI not" 

750 " provided)\n- `JOURNAL`: Journal/conference/venue name\n- `DATE`:" 

751 " Formatted date\n\nYou can also add arbitrary keys to entries and use them" 

752 " as UPPERCASE placeholders.\n\nThe default value is" 

753 " `**TITLE**\\nSUMMARY\\nAUTHORS\\nURL (JOURNAL)`." 

754 ), 

755 ) 

756 date_and_location_column: str = pydantic.Field( 

757 default="DATE", 

758 description=( 

759 "Template for publication entry date column. Available placeholders:\n-" 

760 " `TITLE`: Publication title\n- `AUTHORS`: List of authors (formatted as" 

761 " comma-separated string)\n- `SUMMARY`: Summary/abstract text\n- `DOI`:" 

762 " Digital Object Identifier\n- `URL`: Publication URL (if DOI not" 

763 " provided)\n- `JOURNAL`: Journal/conference/venue name\n- `DATE`:" 

764 " Formatted date\n\nYou can also add arbitrary keys to entries and use them" 

765 " as UPPERCASE placeholders.\n\nThe default value is `DATE`." 

766 ), 

767 ) 

768 

769 

770class Templates(BaseModelWithoutExtraKeys): 

771 footer: str = pydantic.Field( 

772 default="*NAME -- PAGE_NUMBER/TOTAL_PAGES*", 

773 description=( 

774 "Template for the footer. Available placeholders:\n" 

775 "- `NAME`: The CV owner's name from `cv.name`\n" 

776 "- `PAGE_NUMBER`: Current page number\n" 

777 "- `TOTAL_PAGES`: Total number of pages\n" 

778 "- `CURRENT_DATE`: Formatted date based on `design.templates.single_date`\n" 

779 "- `MONTH_NAME`: Full month name (e.g., January)\n" 

780 "- `MONTH_ABBREVIATION`: Abbreviated month name (e.g., Jan)\n" 

781 "- `MONTH`: Month number (e.g., 1)\n" 

782 "- `MONTH_IN_TWO_DIGITS`: Zero-padded month (e.g., 01)\n" 

783 "- `DAY`: Day of the month (e.g., 5)\n" 

784 "- `DAY_IN_TWO_DIGITS`: Zero-padded day (e.g., 05)\n" 

785 "- `YEAR`: Full year (e.g., 2025)\n" 

786 "- `YEAR_IN_TWO_DIGITS`: Two-digit year (e.g., 25)\n\n" 

787 "The default value is `*NAME -- PAGE_NUMBER/TOTAL_PAGES*`." 

788 ), 

789 ) 

790 top_note: str = pydantic.Field( 

791 default="*LAST_UPDATED CURRENT_DATE*", 

792 description=( 

793 "Template for the top note. Available placeholders:\n- `LAST_UPDATED`:" 

794 ' Localized "last updated" text from `locale.last_updated`\n-' 

795 " `CURRENT_DATE`: Formatted date based on `design.templates.single_date`\n-" 

796 " `NAME`: The CV owner's name from `cv.name`\n- `MONTH_NAME`: Full month" 

797 " name (e.g., January)\n- `MONTH_ABBREVIATION`: Abbreviated month name" 

798 " (e.g., Jan)\n- `MONTH`: Month number (e.g., 1)\n- `MONTH_IN_TWO_DIGITS`:" 

799 " Zero-padded month (e.g., 01)\n- `DAY`: Day of the month (e.g., 5)\n-" 

800 " `DAY_IN_TWO_DIGITS`: Zero-padded day (e.g., 05)\n- `YEAR`: Full year" 

801 " (e.g., 2025)\n- `YEAR_IN_TWO_DIGITS`: Two-digit year (e.g., 25)\n\n" 

802 "The default value is `*LAST_UPDATED CURRENT_DATE*`." 

803 ), 

804 ) 

805 single_date: str = pydantic.Field( 

806 default="MONTH_ABBREVIATION YEAR", 

807 description=( 

808 "Template for single dates. Available placeholders:\n" 

809 "- `MONTH_NAME`: Full month name (e.g., January)\n" 

810 "- `MONTH_ABBREVIATION`: Abbreviated month name (e.g., Jan)\n" 

811 "- `MONTH`: Month number (e.g., 1)\n" 

812 "- `MONTH_IN_TWO_DIGITS`: Zero-padded month (e.g., 01)\n" 

813 "- `DAY`: Day of the month (e.g., 5)\n" 

814 "- `DAY_IN_TWO_DIGITS`: Zero-padded day (e.g., 05)\n" 

815 "- `YEAR`: Full year (e.g., 2025)\n" 

816 "- `YEAR_IN_TWO_DIGITS`: Two-digit year (e.g., 25)\n\n" 

817 "The default value is `MONTH_ABBREVIATION YEAR`." 

818 ), 

819 ) 

820 date_range: str = pydantic.Field( 

821 default="START_DATE – END_DATE", 

822 description=( 

823 "Template for date ranges. Available placeholders:\n- `START_DATE`:" 

824 " Formatted start date based on `design.templates.single_date`\n-" 

825 " `END_DATE`: Formatted end date based on `design.templates.single_date`" 

826 ' (or "present"/"ongoing" for current positions)\n\nThe default value is' 

827 " `START_DATE – END_DATE`." 

828 ), 

829 ) 

830 time_span: str = pydantic.Field( 

831 default="HOW_MANY_YEARS YEARS HOW_MANY_MONTHS MONTHS", 

832 description=( 

833 "Template for time spans (duration calculations). Available" 

834 " placeholders:\n- `HOW_MANY_YEARS`: Number of years (e.g., 2)\n- `YEARS`:" 

835 ' Localized word for "years" from `locale.years` (or singular "year")\n-' 

836 " `HOW_MANY_MONTHS`: Number of months (e.g., 3)\n- `MONTHS`: Localized word" 

837 ' for "months" from `locale.months` (or singular "month")\n\nThe default' 

838 " value is `HOW_MANY_YEARS YEARS HOW_MANY_MONTHS MONTHS`." 

839 ), 

840 ) 

841 one_line_entry: OneLineEntryTemplate = pydantic.Field( 

842 default_factory=OneLineEntryTemplate, 

843 description="Template for one-line entries.", 

844 ) 

845 education_entry: EducationEntryTemplate = pydantic.Field( 

846 default_factory=EducationEntryTemplate, 

847 description="Template for education entries.", 

848 ) 

849 normal_entry: NormalEntryTemplate = pydantic.Field( 

850 default_factory=NormalEntryTemplate, 

851 description="Template for normal entries.", 

852 ) 

853 experience_entry: ExperienceEntryTemplate = pydantic.Field( 

854 default_factory=ExperienceEntryTemplate, 

855 description="Template for experience entries.", 

856 ) 

857 publication_entry: PublicationEntryTemplate = pydantic.Field( 

858 default_factory=PublicationEntryTemplate, 

859 description="Template for publication entries.", 

860 ) 

861 

862 

863# ClassicTheme 

864 

865 

866class ClassicTheme(BaseModelWithoutExtraKeys): 

867 theme: Literal["classic"] = "classic" 

868 page: Page = pydantic.Field(default_factory=Page) 

869 colors: Colors = pydantic.Field(default_factory=Colors) 

870 typography: Typography = pydantic.Field(default_factory=Typography) 

871 links: Links = pydantic.Field(default_factory=Links) 

872 header: Header = pydantic.Field(default_factory=Header) 

873 section_titles: SectionTitles = pydantic.Field(default_factory=SectionTitles) 

874 sections: Sections = pydantic.Field(default_factory=Sections) 

875 entries: Entries = pydantic.Field(default_factory=Entries) 

876 templates: Templates = pydantic.Field(default_factory=Templates)