document and enhance data_models.py

This commit is contained in:
Sina Atalay 2024-01-28 19:15:26 +01:00
parent 1d1d45f39d
commit b642cb4b19
1 changed files with 181 additions and 246 deletions

View File

@ -1,5 +1,14 @@
""" """
in the end: document the whole code! This module contains all the necessary classes to store CV data. The YAML input file is
transformed into instances of these classes (i.e., the input file is read) in the
[input_reader](https://sinaatalay.github.io/rendercv/code_documentation/input_reader/)
module. RenderCV utilizes these instances to generate a CV. These classes are called
data models.
The data models are initialized with data validation to prevent unexpected bugs. During
the initialization, we ensure that everything is in the correct place and that the user
has provided a valid RenderCV input. This is achieved through the use of
[Pydantic](https://pypi.org/project/pydantic/).
""" """
from datetime import date as Date from datetime import date as Date
@ -16,40 +25,39 @@ import pydantic.functional_validators as pydantic_functional_validators
from . import utilities from . import utilities
from .terminal_reporter import warning from .terminal_reporter import warning
from .templates.classic import ClassicThemeOptions
# To understand how to create custom data types, see:
# https://docs.pydantic.dev/latest/usage/types/custom/ # use links with pydantic version tags!
# LaTeXDimension = Annotated[ # Create a custom type called PastDate that accepts a string in YYYY-MM-DD format and
# str, # returns a Date object. It also checks if the date is in the past.
# pydantic.Field( # See https://docs.pydantic.dev/2.5/concepts/types/#custom-types for more information
# pattern=r"\d+\.?\d* *(cm|in|pt|mm|ex|em)", # about custom types.
# ),
# ]
LaTeXString = Annotated[
str,
pydantic_functional_validators.AfterValidator(utilities.escape_latex_characters),
]
PastDate = Annotated[ PastDate = Annotated[
str, str,
pydantic.Field(pattern=r"\d{4}-?(\d{2})?-?(\d{2})?"), pydantic.Field(pattern=r"\d{4}-?(\d{2})?-?(\d{2})?"),
pydantic_functional_validators.AfterValidator(utilities.parse_date_string), pydantic_functional_validators.AfterValidator(utilities.parse_date_string),
] ]
PastDateAdapter = pydantic.TypeAdapter(PastDate)
class RenderCVBaseModel(pydantic.BaseModel):
"""
This class is the parent class of all the data models in RenderCV. It has only one
difference from the default `pydantic.BaseModel`: It raises an error if an unknown
key is provided in the input file.
"""
model_config = pydantic.ConfigDict(extra="forbid")
# ====================================================================================== # ======================================================================================
# Entry models: ======================================================================== # Entry models: ========================================================================
# ====================================================================================== # ======================================================================================
class EntryBase(pydantic.BaseModel): class EntryBase(RenderCVBaseModel):
"""This class is the parent class for classes like `#!python EducationEntry`, """This class is the parent class of some of the entry types. It is being used
`#!python ExperienceEntry`, `#!python NormalEntry`, and `#!python OneLineEntry`. because some of the entry types have common fields like dates, highlights, location,
etc.
It stores the common fields between these classes like dates, location, highlights,
and URL.
""" """
start_date: Optional[PastDate] = pydantic.Field( start_date: Optional[PastDate] = pydantic.Field(
@ -67,7 +75,7 @@ class EntryBase(pydantic.BaseModel):
), ),
examples=["2020-09-24", "present"], examples=["2020-09-24", "present"],
) )
date: Optional[PastDate | int | LaTeXString] = pydantic.Field( date: Optional[PastDate | int | str] = pydantic.Field(
default=None, default=None,
title="Date", title="Date",
description=( description=(
@ -78,45 +86,27 @@ class EntryBase(pydantic.BaseModel):
), ),
examples=["2020-09-24", "My Custom Date"], examples=["2020-09-24", "My Custom Date"],
) )
highlights: Optional[list[LaTeXString]] = pydantic.Field( highlights: Optional[list[str]] = pydantic.Field(
default=[], default=[],
title="Highlights", title="Highlights",
description=( description="The highlights of the event as a list of strings.",
"The highlights of the event. It will be rendered as bullet points."
),
examples=["Did this.", "Did that."], examples=["Did this.", "Did that."],
) )
location: Optional[LaTeXString] = pydantic.Field( location: Optional[str] = pydantic.Field(
default=None, default=None,
title="Location", title="Location",
description=( description="The location of the event.",
"The location of the event. It will be shown with the date in the"
" same column."
),
examples=["Istanbul, Turkey"], examples=["Istanbul, Turkey"],
) )
url: Optional[pydantic.HttpUrl] = None url: Optional[pydantic.HttpUrl] = None
url_text_input: Optional[str] = pydantic.Field(default=None, alias="url_text")
@pydantic.field_validator("date")
@classmethod
def check_date(
cls, date: PastDate | LaTeXString
) -> Optional[PastDate | int | LaTeXString]:
"""Check if the date is a string or a Date object and return accordingly."""
if date is None:
new_date = None
elif isinstance(date, Date):
new_date = date
else:
raise TypeError(f"{date} is an invalid date.")
return new_date
@pydantic.model_validator(mode="after") @pydantic.model_validator(mode="after")
@classmethod @classmethod
def check_dates(cls, model): def check_dates(cls, model):
"""Make sure that either `#!python start_date` and `#!python end_date` or only """
`#!python date` is provided. Check if the dates are provided correctly and convert them to `Date` objects if
they are provided in YYYY-MM-DD format.
""" """
date_is_provided = False date_is_provided = False
start_date_is_provided = False start_date_is_provided = False
@ -192,7 +182,18 @@ class EntryBase(pydantic.BaseModel):
@pydantic.computed_field @pydantic.computed_field
@cached_property @cached_property
def date_string(self) -> Optional[LaTeXString]: def date_string(self) -> Optional[str]:
"""
Return a date string based on the `date`, `start_date`, and `end_date` fields.
Example:
```python
entry = dm.EntryBase(start_date=2020-10-11, end_date=2021-04-04)
entry.date_string
```
will return:
`#!python "2020-10-11 to 2021-04-04"`
"""
if self.date is not None: if self.date is not None:
if isinstance(self.date, str): if isinstance(self.date, str):
date_string = self.date date_string = self.date
@ -228,7 +229,19 @@ class EntryBase(pydantic.BaseModel):
@pydantic.computed_field @pydantic.computed_field
@cached_property @cached_property
def time_span(self) -> Optional[LaTeXString]: def time_span(self) -> Optional[str]:
"""
Return a time span string based on the `date`, `start_date`, and `end_date`
fields.
Example:
```python
entry = dm.EntryBase(start_date=2020-01-01, end_date=2020-04-20)
entry.time_span
```
will return:
`#!python "4 months"`
"""
if self.date is not None: if self.date is not None:
time_span = "" time_span = ""
elif self.start_date is not None and self.end_date is not None: elif self.start_date is not None and self.end_date is not None:
@ -254,96 +267,77 @@ class EntryBase(pydantic.BaseModel):
@pydantic.computed_field @pydantic.computed_field
@cached_property @cached_property
def markdown_url(self) -> Optional[str]: def url_text(self) -> Optional[str]:
if self.url is None: """
return None Return a URL text based on the `url_text_input` and `url` fields.
else: """
url = str(self.url) url_text = None
if self.url_text_input is not None:
url_text = self.url_text_input
elif self.url is not None:
url_text_dictionary = {
"gitub": "view on GitHub",
"linkedin": "view on LinkedIn",
"instagram": "view on Instagram",
"youtube": "view on YouTube",
}
url_text = "view on my website"
for key in url_text_dictionary:
if key in str(self.url):
url_text = url_text_dictionary[key]
break
if "github" in url: return url_text
link_text = "view on GitHub"
elif "linkedin" in url:
link_text = "view on LinkedIn"
elif "instagram" in url:
link_text = "view on Instagram"
elif "youtube" in url:
link_text = "view on YouTube"
else:
link_text = "view on my website"
markdown_url = f"[{link_text}]({url})"
return markdown_url
@pydantic.computed_field
@cached_property
def month_and_year(self) -> Optional[LaTeXString]:
if self.date is not None:
# Then it means start_date and end_date are not provided.
try:
# If this runs, it means the date is an ISO format string, and it can be
# parsed
month_and_year = utilities.format_date(self.date) # type: ignore
except TypeError:
month_and_year = str(self.date)
else:
# Then it means start_date and end_date are provided and month_and_year
# doesn't make sense.
month_and_year = None
return month_and_year
class OneLineEntry(pydantic.BaseModel): class OneLineEntry(RenderCVBaseModel):
"""This class stores [OneLineEntry](../user_guide.md#onelineentry) information.""" """This class is the data model of `OneLineEntry`."""
name: LaTeXString = pydantic.Field( name: str = pydantic.Field(
title="Name", title="Name",
description="The name of the entry. It will be shown as bold text.", description="The name of the entry. It will be shown as bold text.",
) )
details: LaTeXString = pydantic.Field( details: str = pydantic.Field(
title="Details", title="Details",
description="The details of the entry. It will be shown as normal text.", description="The details of the entry. It will be shown as normal text.",
) )
class NormalEntry(EntryBase): class NormalEntry(EntryBase):
"""This class stores [NormalEntry](../user_guide.md#normalentry) information.""" """This class is the data model of `NormalEntry`."""
name: LaTeXString = pydantic.Field( name: str = pydantic.Field(
title="Name", title="Name",
description="The name of the entry. It will be shown as bold text.", description="The name of the entry. It will be shown as bold text.",
) )
class ExperienceEntry(EntryBase): class ExperienceEntry(EntryBase):
"""This class stores [ExperienceEntry](../user_guide.md#experienceentry) """This class is the data model of `ExperienceEntry`."""
information.
"""
company: LaTeXString = pydantic.Field( company: str = pydantic.Field(
title="Company", title="Company",
description="The company name. It will be shown as bold text.", description="The company name. It will be shown as bold text.",
) )
position: LaTeXString = pydantic.Field( position: str = pydantic.Field(
title="Position", title="Position",
description="The position. It will be shown as normal text.", description="The position. It will be shown as normal text.",
) )
class EducationEntry(EntryBase): class EducationEntry(EntryBase):
"""This class stores [EducationEntry](../user_guide.md#educationentry) information.""" """This class is the data model of `EducationEntry`."""
institution: LaTeXString = pydantic.Field( institution: str = pydantic.Field(
title="Institution", title="Institution",
description="The institution name. It will be shown as bold text.", description="The institution name. It will be shown as bold text.",
examples=["Bogazici University"], examples=["Bogazici University"],
) )
area: LaTeXString = pydantic.Field( area: str = pydantic.Field(
title="Area", title="Area",
description="The area of study. It will be shown as normal text.", description="The area of study. It will be shown as normal text.",
) )
study_type: Optional[LaTeXString] = pydantic.Field( study_type: Optional[str] = pydantic.Field(
default=None, default=None,
title="Study Type", title="Study Type",
description="The type of the degree.", description="The type of the degree.",
@ -351,16 +345,14 @@ class EducationEntry(EntryBase):
) )
class PublicationEntry(pydantic.BaseModel): class PublicationEntry(RenderCVBaseModel):
"""This class stores [PublicationEntry](../user_guide.md#publicationentry) """THis class is the data model of `PublicationEntry`."""
information.
"""
title: LaTeXString = pydantic.Field( title: str = pydantic.Field(
title="Title of the Publication", title="Title of the Publication",
description="The title of the publication. It will be shown as bold text.", description="The title of the publication. It will be shown as bold text.",
) )
authors: list[LaTeXString] = pydantic.Field( authors: list[str] = pydantic.Field(
title="Authors", title="Authors",
description="The authors of the publication in order as a list of strings.", description="The authors of the publication in order as a list of strings.",
) )
@ -369,12 +361,12 @@ class PublicationEntry(pydantic.BaseModel):
description="The DOI of the publication.", description="The DOI of the publication.",
examples=["10.48550/arXiv.2310.03138"], examples=["10.48550/arXiv.2310.03138"],
) )
date: LaTeXString = pydantic.Field( date: str = pydantic.Field(
title="Publication Date", title="Publication Date",
description="The date of the publication.", description="The date of the publication.",
examples=["2021-10-31"], examples=["2021-10-31"],
) )
journal: Optional[LaTeXString] = pydantic.Field( journal: Optional[str] = pydantic.Field(
default=None, default=None,
title="Journal", title="Journal",
description="The journal or the conference name.", description="The journal or the conference name.",
@ -414,99 +406,62 @@ entries_field_of_section_model = pydantic.Field(
) )
class SectionBase(pydantic.BaseModel): class SectionBase(RenderCVBaseModel):
"""This class stores a section information. """This class is the parent class of all the section types. It is being used
because all of the section types have a common field called `title`.
It is the parent class of all the section classes like
`#!python SectionWithEducationEntries`, `#!python SectionWithExperienceEntries`,
`#!python SectionWithNormalEntries`, `#!python SectionWithOneLineEntries`, and
`#!python SectionWithPublicationEntries`.
""" """
title: Optional[LaTeXString] = pydantic.Field(default=None) # title is excluded from the JSON schema because this will be written by RenderCV
link_text: Optional[LaTeXString] = pydantic.Field( # depending on the key in the input file.
default=None, title: Optional[str] = pydantic.Field(default=None, exclude=True)
title="Link Text",
description=(
"If the section has a link, then what should be the text of the link? If"
" this field is not provided, then the link text will be generated"
" automatically based on the URL."
),
examples=["view on GitHub", "view on LinkedIn"],
)
class SectionWithEducationEntries(SectionBase): class SectionWithEducationEntries(SectionBase):
"""This class stores a section with """This class is the data model of the section with `EducationEntry`s."""
[EducationEntry](../user_guide.md#educationentry)s.
"""
entry_type: Literal["EducationEntry"] = entry_type_field_of_section_model entry_type: Literal["EducationEntry"] = entry_type_field_of_section_model
entries: list[EducationEntry] = entries_field_of_section_model entries: list[EducationEntry] = entries_field_of_section_model
class SectionWithExperienceEntries(SectionBase): class SectionWithExperienceEntries(SectionBase):
"""This class stores a section with """This class is the data model of the section with `ExperienceEntry`s."""
[ExperienceEntry](../user_guide.md#experienceentry)s.
"""
entry_type: Literal["ExperienceEntry"] = entry_type_field_of_section_model entry_type: Literal["ExperienceEntry"] = entry_type_field_of_section_model
entries: list[ExperienceEntry] = entries_field_of_section_model entries: list[ExperienceEntry] = entries_field_of_section_model
class SectionWithNormalEntries(SectionBase): class SectionWithNormalEntries(SectionBase):
"""This class stores a section with """This class is the data model of the section with `NormalEntry`s."""
[NormalEntry](../user_guide.md#normalentry)s.
"""
entry_type: Literal["NormalEntry"] = entry_type_field_of_section_model entry_type: Literal["NormalEntry"] = entry_type_field_of_section_model
entries: list[NormalEntry] = entries_field_of_section_model entries: list[NormalEntry] = entries_field_of_section_model
class SectionWithOneLineEntries(SectionBase): class SectionWithOneLineEntries(SectionBase):
"""This class stores a section with """This class is the data model of the section with `OneLineEntry`s."""
[OneLineEntry](../user_guide.md#onelineentry)s.
"""
entry_type: Literal["OneLineEntry"] = entry_type_field_of_section_model entry_type: Literal["OneLineEntry"] = entry_type_field_of_section_model
entries: list[OneLineEntry] = entries_field_of_section_model entries: list[OneLineEntry] = entries_field_of_section_model
class SectionWithPublicationEntries(SectionBase): class SectionWithPublicationEntries(SectionBase):
"""This class stores a section with """This class is the data model of the section with `PublicationEntry`s."""
[PublicationEntry](../user_guide.md#publicationentry)s.
"""
entry_type: Literal["PublicationEntry"] = entry_type_field_of_section_model entry_type: Literal["PublicationEntry"] = entry_type_field_of_section_model
entries: list[PublicationEntry] = entries_field_of_section_model entries: list[PublicationEntry] = entries_field_of_section_model
class SectionWithTextEntries(SectionBase): class SectionWithTextEntries(SectionBase):
"""This class stores a section with """This class is the data model of the section with `TextEntry`s."""
[TextEntry](../user_guide.md#textentry)s.
"""
entry_type: Literal["TextEntry"] = entry_type_field_of_section_model entry_type: Literal["TextEntry"] = entry_type_field_of_section_model
entries: list[LaTeXString] = entries_field_of_section_model entries: list[str] = entries_field_of_section_model
class SocialNetwork(pydantic.BaseModel): # A custom type Section. It is a union of all the section types and the correct section
"""This class stores a social network information. # type is determined by the entry_type field.
# See https://docs.pydantic.dev/2.5/concepts/fields/#discriminator for more information
Currently, only LinkedIn, Github, and Instagram are supported. # about discriminators.
"""
network: Literal["LinkedIn", "GitHub", "Instagram", "Orcid"] = pydantic.Field(
title="Social Network",
description="The social network name.",
)
username: str = pydantic.Field(
title="Username",
description="The username of the social network. The link will be generated.",
)
# Section type
Section = Annotated[ Section = Annotated[
SectionWithEducationEntries SectionWithEducationEntries
| SectionWithExperienceEntries | SectionWithExperienceEntries
@ -523,7 +478,11 @@ Section = Annotated[
# Full RenderCV data models: =========================================================== # Full RenderCV data models: ===========================================================
# ====================================================================================== # ======================================================================================
# Default entry types for a given section title # RenderCV requires users to specify the entry type for each section in their CV in
# order to render the correct thing in the CV. However, for certain sections, specifying
# the entry type can be redundant. To simplify this process for users, default entry
# types are stored in a dictionary for certain section titles so that users do not have
# to specify them.
default_entry_types_for_a_given_title: dict[ default_entry_types_for_a_given_title: dict[
str, str,
tuple[type[EducationEntry], type[SectionWithEducationEntries]] tuple[type[EducationEntry], type[SectionWithEducationEntries]]
@ -531,7 +490,7 @@ default_entry_types_for_a_given_title: dict[
| tuple[type[PublicationEntry], type[SectionWithPublicationEntries]] | tuple[type[PublicationEntry], type[SectionWithPublicationEntries]]
| tuple[type[NormalEntry], type[SectionWithNormalEntries]] | tuple[type[NormalEntry], type[SectionWithNormalEntries]]
| tuple[type[OneLineEntry], type[SectionWithOneLineEntries]] | tuple[type[OneLineEntry], type[SectionWithOneLineEntries]]
| tuple[type[LaTeXString], type[SectionWithTextEntries]], | tuple[type[str], type[SectionWithTextEntries]],
] = { ] = {
"Education": (EducationEntry, SectionWithEducationEntries), "Education": (EducationEntry, SectionWithEducationEntries),
"Experience": (ExperienceEntry, SectionWithExperienceEntries), "Experience": (ExperienceEntry, SectionWithExperienceEntries),
@ -551,68 +510,53 @@ default_entry_types_for_a_given_title: dict[
"Other Skills": (OneLineEntry, SectionWithOneLineEntries), "Other Skills": (OneLineEntry, SectionWithOneLineEntries),
"Awards": (OneLineEntry, SectionWithOneLineEntries), "Awards": (OneLineEntry, SectionWithOneLineEntries),
"Interests": (OneLineEntry, SectionWithOneLineEntries), "Interests": (OneLineEntry, SectionWithOneLineEntries),
"Summary": (LaTeXString, SectionWithTextEntries), "Summary": (str, SectionWithTextEntries),
} }
class Connection(pydantic.BaseModel): class SocialNetwork(RenderCVBaseModel):
"""This class stores a connection/communication information. """This class is the data model of a social network."""
Warning: network: Literal["LinkedIn", "GitHub", "Instagram", "Orcid"] = pydantic.Field(
This class isn't designed for users to use, but it is used by RenderCV to make title="Social Network",
the $\\LaTeX$ templating easier. description="The social network name.",
""" )
username: str = pydantic.Field(
name: Literal[ title="Username",
"LinkedIn", description="The username of the social network. The link will be generated.",
"GitHub", )
"Instagram",
"Orcid",
"phone",
"email",
"website",
"location",
]
value: str
@pydantic.computed_field @pydantic.computed_field
@cached_property @cached_property
def url(self) -> Optional[pydantic.HttpUrl | str]: def url(self) -> pydantic.HttpUrl:
if self.name == "LinkedIn": """Return the URL of the social network."""
url = f"https://www.linkedin.com/in/{self.value}" url_dictionary = {
elif self.name == "GitHub": "LinkedIn": "https://linkedin.com/in/",
url = f"https://www.github.com/{self.value}" "GitHub": "https://github.com/",
elif self.name == "Instagram": "Instagram": "https://instagram.com/",
url = f"https://www.instagram.com/{self.value}" "Orcid": "https://orcid.org/",
elif self.name == "Orcid": }
url = f"https://orcid.org/{self.value}" url = url_dictionary[self.network] + self.username
elif self.name == "email":
url = f"mailto:{self.value}" HttpUrlAdapter = pydantic.TypeAdapter(pydantic.HttpUrl)
elif self.name == "website": url = HttpUrlAdapter.validate_python(url)
url = self.value
elif self.name == "phone":
url = self.value
elif self.name == "location":
url = None
else:
raise RuntimeError(f'"{self.name}" is not a valid connection.')
return url return url
class CurriculumVitae(pydantic.BaseModel): class CurriculumVitae(RenderCVBaseModel):
"""This class binds all the information of a CV together.""" """This class is the data model of the CV."""
name: LaTeXString = pydantic.Field( name: str = pydantic.Field(
title="Name", title="Name",
description="The name of the person.", description="The name of the person.",
) )
label: Optional[LaTeXString] = pydantic.Field( label: Optional[str] = pydantic.Field(
default=None, default=None,
title="Label", title="Label",
description="The label of the person.", description="The label of the person.",
) )
location: Optional[LaTeXString] = pydantic.Field( location: Optional[str] = pydantic.Field(
default=None, default=None,
title="Location", title="Location",
description="The location of the person. This is not rendered currently.", description="The location of the person. This is not rendered currently.",
@ -646,7 +590,7 @@ class CurriculumVitae(pydantic.BaseModel):
| list[OneLineEntry] | list[OneLineEntry]
| list[PublicationEntry] | list[PublicationEntry]
| list[ExperienceEntry] | list[ExperienceEntry]
| list[LaTeXString], | list[str],
] = pydantic.Field( ] = pydantic.Field(
default=None, default=None,
title="Sections", title="Sections",
@ -666,7 +610,7 @@ class CurriculumVitae(pydantic.BaseModel):
| list[OneLineEntry] | list[OneLineEntry]
| list[PublicationEntry] | list[PublicationEntry]
| list[ExperienceEntry] | list[ExperienceEntry]
| list[LaTeXString], | list[str],
], ],
) -> dict[ ) -> dict[
str, str,
@ -676,9 +620,11 @@ class CurriculumVitae(pydantic.BaseModel):
| list[OneLineEntry] | list[OneLineEntry]
| list[PublicationEntry] | list[PublicationEntry]
| list[ExperienceEntry] | list[ExperienceEntry]
| list[LaTeXString], | list[str],
]: ]:
"""""" """
Parse and validate the sections of the CV.
"""
if sections_input is not None: if sections_input is not None:
# check if the section names are unique, get the keys of the sections: # check if the section names are unique, get the keys of the sections:
@ -692,6 +638,7 @@ class CurriculumVitae(pydantic.BaseModel):
) )
for title, section_or_entries in sections_input.items(): for title, section_or_entries in sections_input.items():
title = title.replace("_", " ").title()
if isinstance(section_or_entries, list): if isinstance(section_or_entries, list):
# Then it means the user provided entries directly. Then it means # Then it means the user provided entries directly. Then it means
# the section title should have a default entry type. # the section title should have a default entry type.
@ -704,7 +651,7 @@ class CurriculumVitae(pydantic.BaseModel):
# try if the entries are of the correct type by casting them # try if the entries are of the correct type by casting them
# to the entry type one by one # to the entry type one by one
for entry in section_or_entries: for entry in section_or_entries:
if entry_type is LaTeXString: if entry_type is str:
if not isinstance(entry, str): if not isinstance(entry, str):
raise pydantic.ValidationError( raise pydantic.ValidationError(
f'"{entry}" is not a valid string.' f'"{entry}" is not a valid string.'
@ -720,8 +667,8 @@ class CurriculumVitae(pydantic.BaseModel):
else: else:
raise ValueError( raise ValueError(
f'"{title}" is a custom section and it doesn\'t have' f'"{title}" doesn\'t have a default entry type. Please'
" a default entry type. Please provide the entry type." " provide the entry type."
) )
return sections_input return sections_input
@ -729,14 +676,11 @@ class CurriculumVitae(pydantic.BaseModel):
@pydantic.computed_field @pydantic.computed_field
@cached_property @cached_property
def sections(self) -> list[Section]: def sections(self) -> list[Section]:
"""Compute the sections of the CV. """Return all the sections of the CV with their titles."""
Returns:
list[Section]: The sections of the CV.
"""
sections = [] sections = []
if self.sections_input is not None: if self.sections_input is not None:
for title, section_or_entries in self.sections_input.items(): for title, section_or_entries in self.sections_input.items():
title = title.replace("_", " ").title()
if isinstance( if isinstance(
section_or_entries, section_or_entries,
( (
@ -750,16 +694,24 @@ class CurriculumVitae(pydantic.BaseModel):
): ):
if section_or_entries.title is None: if section_or_entries.title is None:
section_or_entries.title = title section_or_entries.title = title
sections.append(section_or_entries) sections.append(section_or_entries)
elif isinstance(section_or_entries, list): elif isinstance(section_or_entries, list):
if title in default_entry_types_for_a_given_title: if title in default_entry_types_for_a_given_title:
( (
entry_type, entry_type,
section_type, section_type,
) = default_entry_types_for_a_given_title[title] ) = default_entry_types_for_a_given_title[title]
if entry_type is str:
entry_type = "TextEntry"
else:
entry_type = entry_type.__name__
section = section_type( section = section_type(
title=title, title=title,
entry_type=entry_type.__name__, # type: ignore entry_type=entry_type, # type: ignore
entries=section_or_entries, # type: ignore entries=section_or_entries, # type: ignore
) )
sections.append(section) sections.append(section)
@ -768,6 +720,7 @@ class CurriculumVitae(pydantic.BaseModel):
"This error shouldn't have been raised. Please open an" "This error shouldn't have been raised. Please open an"
" issue on GitHub." " issue on GitHub."
) )
else: else:
raise RuntimeError( raise RuntimeError(
"This error shouldn't have been raised. Please open an" "This error shouldn't have been raised. Please open an"
@ -776,42 +729,24 @@ class CurriculumVitae(pydantic.BaseModel):
return sections return sections
@pydantic.computed_field
@cached_property
def connections(self) -> list[Connection]:
connections = []
if self.location is not None:
connections.append(Connection(name="location", value=self.location))
if self.phone is not None:
connections.append(Connection(name="phone", value=self.phone))
if self.email is not None:
connections.append(Connection(name="email", value=self.email))
if self.website is not None:
connections.append(Connection(name="website", value=str(self.website)))
if self.social_networks is not None:
for social_network in self.social_networks:
connections.append(
Connection(
name=social_network.network, value=social_network.username
)
)
return connections
# ====================================================================================== # ======================================================================================
# ====================================================================================== # ======================================================================================
# ====================================================================================== # ======================================================================================
class RenderCVDataModel(pydantic.BaseModel): class RenderCVDataModel(RenderCVBaseModel):
"""This class binds both the CV and the design information together.""" """This class binds both the CV and the design information together."""
cv: CurriculumVitae = pydantic.Field( cv: CurriculumVitae = pydantic.Field(
default=CurriculumVitae(name="John Doe"),
title="Curriculum Vitae", title="Curriculum Vitae",
description="The data of the CV.", description="The data of the CV.",
) )
design: ClassicThemeOptions = pydantic.Field(
title="Design",
description="The design information.",
discriminator="theme",
)
def generate_json_schema(output_directory: str) -> str: def generate_json_schema(output_directory: str) -> str: