rendercv/rendercv/data_models.py

1411 lines
52 KiB
Python
Raw Normal View History

2024-01-18 17:24:30 +00:00
"""
2024-02-02 18:31:07 +00:00
This module contains all the necessary classes to store CV data. These classes are called
data models. The YAML input file is transformed into instances of these classes (i.e.,
the input file is read) with the [`read_input_file`](#read_input_file) function.
2024-02-24 15:20:13 +00:00
RenderCV utilizes these instances to generate a $\\LaTeX$ file which is then rendered into a
2024-02-02 18:31:07 +00:00
PDF file.
2024-01-28 18:15:26 +00:00
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/).
2024-01-18 17:24:30 +00:00
"""
from datetime import date as Date
2024-02-18 16:45:25 +00:00
from typing import Literal, Any, Type, Annotated, Optional, get_args
2024-02-10 20:30:29 +00:00
import importlib
2024-02-11 18:35:55 +00:00
import importlib.util
import importlib.machinery
2024-02-10 19:31:54 +00:00
import functools
2024-01-26 18:47:42 +00:00
from urllib.request import urlopen, HTTPError
2024-01-18 17:24:30 +00:00
import json
2024-01-30 18:49:05 +00:00
import re
import ssl
2024-02-02 18:31:07 +00:00
import pathlib
2024-01-18 17:24:30 +00:00
2024-01-26 17:09:28 +00:00
import pydantic
import pydantic_extra_types.phone_numbers as pydantic_phone_numbers
import ruamel.yaml
2024-01-18 17:24:30 +00:00
2024-01-29 15:13:09 +00:00
from .themes.classic import ClassicThemeOptions
2024-02-10 19:31:54 +00:00
from .themes.moderncv import ModerncvThemeOptions
2024-02-15 18:30:57 +00:00
from .themes.sb2nov import Sb2novThemeOptions
2024-01-18 17:24:30 +00:00
2024-02-02 18:31:07 +00:00
# Create a custom type called RenderCVDate that accepts only strings in YYYY-MM-DD or
# YYYY-MM format:
# This type is used to validate the date fields in the data.
2024-01-28 18:15:26 +00:00
# See https://docs.pydantic.dev/2.5/concepts/types/#custom-types for more information
# about custom types.
2024-02-17 17:46:58 +00:00
date_pattern_for_json_schema = r"\d{4}(-\d{2})?(-\d{2})?"
date_pattern_for_validation = r"\d{4}-\d{2}(-\d{2})?"
2024-02-02 17:02:03 +00:00
RenderCVDate = Annotated[
2024-01-18 17:24:30 +00:00
str,
2024-02-17 17:46:58 +00:00
pydantic.Field(
pattern=date_pattern_for_validation,
json_schema_extra={"pattern": date_pattern_for_json_schema},
),
2024-01-18 17:24:30 +00:00
]
2024-01-30 18:49:05 +00:00
def get_date_object(date: str | int) -> Date:
"""Parse a date string in YYYY-MM-DD, YYYY-MM, or YYYY format and return a
2024-02-02 18:31:07 +00:00
datetime.date object. This function is used throughout the validation process of the
data models.
2024-01-30 18:49:05 +00:00
Args:
2024-02-18 18:13:29 +00:00
date (str): The date string to parse.
2024-01-30 18:49:05 +00:00
Returns:
datetime.date: The parsed date.
"""
if isinstance(date, int):
date_object = Date.fromisoformat(f"{date}-01-01")
2024-02-08 19:12:17 +00:00
elif re.fullmatch(r"\d{4}-\d{2}-\d{2}", date):
2024-01-30 18:49:05 +00:00
# Then it is in YYYY-MM-DD format
date_object = Date.fromisoformat(date)
2024-02-08 19:12:17 +00:00
elif re.fullmatch(r"\d{4}-\d{2}", date):
2024-01-30 18:49:05 +00:00
# Then it is in YYYY-MM format
date_object = Date.fromisoformat(f"{date}-01")
2024-02-08 19:12:17 +00:00
elif re.fullmatch(r"\d{4}", date):
2024-02-03 12:58:20 +00:00
# Then it is in YYYY format
date_object = Date.fromisoformat(f"{date}-01-01")
2024-01-30 18:49:05 +00:00
elif date == "present":
date_object = Date.today()
else:
raise ValueError(
2024-02-08 19:12:17 +00:00
"This is not a valid date! Please use either YYYY-MM-DD, YYYY-MM, or"
" YYYY format."
2024-01-30 18:49:05 +00:00
)
return date_object
2024-01-18 17:24:30 +00:00
2024-01-28 18:15:26 +00:00
2024-02-03 11:58:14 +00:00
def format_date(date: Date) -> str:
"""Formats a `Date` object to a string in the following format: "Jan. 2021".
It uses month abbreviations, taken from
[Yale University Library](https://web.library.yale.edu/cataloging/months).
Example:
```python
format_date(Date(2024, 5, 1))
```
will return
`#!python "May 2024"`
Args:
date (Date): The date to format.
Returns:
str: The formatted date.
"""
# Month abbreviations,
# taken from: https://web.library.yale.edu/cataloging/months
abbreviations_of_months = [
"Jan.",
"Feb.",
"Mar.",
"Apr.",
"May",
"June",
"July",
"Aug.",
"Sept.",
"Oct.",
"Nov.",
"Dec.",
]
month = int(date.strftime("%m"))
month_abbreviation = abbreviations_of_months[month - 1]
year = date.strftime(format="%Y")
date_string = f"{month_abbreviation} {year}"
return date_string
2024-01-28 18:15:26 +00:00
class RenderCVBaseModel(pydantic.BaseModel):
2024-02-02 18:31:07 +00:00
"""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.
2024-01-28 18:15:26 +00:00
"""
2024-02-13 17:51:39 +00:00
model_config = pydantic.ConfigDict(extra="forbid", validation_error_cause=True)
2024-01-28 18:15:26 +00:00
2024-01-18 22:37:40 +00:00
2024-01-26 18:47:42 +00:00
# ======================================================================================
# Entry models: ========================================================================
# ======================================================================================
2024-01-18 17:24:30 +00:00
2024-01-28 18:15:26 +00:00
class EntryBase(RenderCVBaseModel):
"""This class is the parent class of some of the entry types. It is being used
because some of the entry types have common fields like dates, highlights, location,
etc.
2024-01-18 17:24:30 +00:00
"""
2024-02-02 17:02:03 +00:00
start_date: Optional[int | RenderCVDate] = pydantic.Field(
2024-01-18 17:24:30 +00:00
default=None,
title="Start Date",
2024-02-06 18:18:46 +00:00
description=(
"The start date of the event in YYYY-MM-DD, YYYY-MM, or YYYY format."
),
2024-01-18 17:24:30 +00:00
examples=["2020-09-24"],
2024-02-14 16:39:28 +00:00
json_schema_extra={"default": "2000-01-01"},
2024-01-18 17:24:30 +00:00
)
2024-02-02 17:02:03 +00:00
end_date: Optional[Literal["present"] | int | RenderCVDate] = pydantic.Field(
2024-01-18 17:24:30 +00:00
default=None,
title="End Date",
description=(
2024-02-06 18:18:46 +00:00
"The end date of the event in YYYY-MM-DD, YYYY-MM, or YYYY format. If the"
' event is still ongoing, then type "present" or provide only the start'
" date."
2024-01-18 17:24:30 +00:00
),
examples=["2020-09-24", "present"],
2024-02-14 16:39:28 +00:00
json_schema_extra={"default": "2020-01-01"},
2024-01-18 17:24:30 +00:00
)
2024-02-02 17:02:03 +00:00
date: Optional[RenderCVDate | int | str] = pydantic.Field(
2024-01-18 17:24:30 +00:00
default=None,
title="Date",
description=(
"If the event is a one-day event, then this field should be filled in"
" YYYY-MM-DD format. If the event is a multi-day event, then the start date"
" and end date should be provided instead. All of them can't be provided at"
" the same time."
),
examples=["2020-09-24", "My Custom Date"],
2024-02-14 16:39:28 +00:00
json_schema_extra={"default": "Custom Date or 2020-01-01"},
2024-01-18 17:24:30 +00:00
)
2024-01-28 18:15:26 +00:00
highlights: Optional[list[str]] = pydantic.Field(
default=None,
2024-01-18 17:24:30 +00:00
title="Highlights",
2024-01-28 18:15:26 +00:00
description="The highlights of the event as a list of strings.",
2024-01-18 17:24:30 +00:00
examples=["Did this.", "Did that."],
)
2024-01-28 18:15:26 +00:00
location: Optional[str] = pydantic.Field(
2024-02-11 15:15:09 +00:00
default=None,
2024-01-18 17:24:30 +00:00
title="Location",
2024-01-28 18:15:26 +00:00
description="The location of the event.",
2024-02-14 16:39:28 +00:00
examples=["Istanbul, Türkiye"],
2024-01-18 17:24:30 +00:00
)
2024-02-08 19:12:17 +00:00
@pydantic.model_validator(
mode="after",
) # type: ignore
2024-01-18 17:24:30 +00:00
@classmethod
2024-02-07 18:16:55 +00:00
def check_dates(cls, model: "EntryBase") -> "EntryBase":
2024-01-28 18:15:26 +00:00
"""
2024-02-02 18:31:07 +00:00
Check if the dates are provided correctly and do the necessary adjustments.
2024-01-18 17:24:30 +00:00
"""
date_is_provided = False
start_date_is_provided = False
end_date_is_provided = False
if model.date is not None:
date_is_provided = True
if model.start_date is not None:
start_date_is_provided = True
if model.end_date is not None:
end_date_is_provided = True
2024-02-09 19:14:17 +00:00
if date_is_provided:
try:
date_object = get_date_object(model.date) # type: ignore
except ValueError:
# Then it is a custom date string (e.g., "My Custom Date")
pass
else:
today_object = Date.today()
if date_object > today_object:
raise ValueError(
2024-02-11 21:43:27 +00:00
'"date" cannot be in the future!',
2024-02-09 19:14:17 +00:00
"date", # this is the location of the error
model.date, # this is value of the error
)
2024-01-18 17:24:30 +00:00
elif start_date_is_provided and not end_date_is_provided:
model.end_date = "present"
2024-01-28 20:13:23 +00:00
elif not start_date_is_provided and end_date_is_provided:
raise ValueError(
'"end_date" is provided in of the entries, but "start_date" is not.'
2024-02-08 19:12:17 +00:00
' Either provide both "start_date" and "end_date" or provide "date".',
"start_date", # this is the location of the error
"", # this supposed to be the value of the error
2024-01-28 20:13:23 +00:00
)
2024-01-18 17:24:30 +00:00
if model.start_date is not None and model.end_date is not None:
2024-02-08 19:12:17 +00:00
try:
end_date = get_date_object(model.end_date)
except ValueError as e:
2024-02-17 17:46:58 +00:00
raise ValueError(str(e), "end_date", str(model.end_date))
2024-02-08 19:12:17 +00:00
try:
start_date = get_date_object(model.start_date)
except ValueError as e:
2024-02-17 17:46:58 +00:00
raise ValueError(str(e), "start_date", str(model.start_date))
2024-01-18 17:24:30 +00:00
if start_date > end_date:
raise ValueError(
2024-02-11 21:43:27 +00:00
'"start_date" can not be after "end_date"!',
"start_date", # this is the location of the error
2024-02-17 17:46:58 +00:00
str(model.start_date), # this is value of the error
2024-01-30 18:49:05 +00:00
)
elif end_date > Date.today():
raise ValueError(
2024-02-11 21:43:27 +00:00
'"end_date" cannot be in the future!',
2024-02-08 19:12:17 +00:00
"end_date", # this is the location of the error
2024-02-17 17:46:58 +00:00
str(model.end_date), # this is value of the error
2024-01-30 18:49:05 +00:00
)
2024-01-18 17:24:30 +00:00
return model
2024-02-10 19:31:54 +00:00
@functools.cached_property
def date_string(self) -> str:
2024-01-28 18:15:26 +00:00
"""
Return a date string based on the `date`, `start_date`, and `end_date` fields.
Example:
```python
2024-02-02 18:31:07 +00:00
entry = dm.EntryBase(start_date=2020-10-11, end_date=2021-04-04).date_string
2024-01-28 18:15:26 +00:00
```
will return:
`#!python "2020-10-11 to 2021-04-04"`
"""
2024-01-18 17:24:30 +00:00
if self.date is not None:
2024-01-30 18:49:05 +00:00
try:
date_object = get_date_object(self.date)
2024-02-03 11:58:14 +00:00
date_string = format_date(date_object)
2024-01-30 18:49:05 +00:00
except ValueError:
2024-02-02 18:31:07 +00:00
# Then it is a custom date string (e.g., "My Custom Date")
2024-01-30 18:49:05 +00:00
date_string = str(self.date)
2024-01-18 17:24:30 +00:00
elif self.start_date is not None and self.end_date is not None:
2024-01-30 18:49:05 +00:00
if isinstance(self.start_date, int):
2024-02-02 18:31:07 +00:00
# Then it means only the year is provided
2024-01-30 18:49:05 +00:00
start_date = str(self.start_date)
2024-01-26 18:47:42 +00:00
else:
2024-02-02 18:31:07 +00:00
# Then it means start_date is either in YYYY-MM-DD or YYYY-MM format
2024-01-30 18:49:05 +00:00
date_object = get_date_object(self.start_date)
2024-02-03 11:58:14 +00:00
start_date = format_date(date_object)
2024-01-30 18:49:05 +00:00
2024-01-18 17:24:30 +00:00
if self.end_date == "present":
end_date = "present"
2024-01-30 18:49:05 +00:00
elif isinstance(self.end_date, int):
2024-02-02 18:31:07 +00:00
# Then it means only the year is provided
2024-01-30 18:49:05 +00:00
end_date = str(self.end_date)
2024-01-18 17:24:30 +00:00
else:
2024-02-02 18:31:07 +00:00
# Then it means end_date is either in YYYY-MM-DD or YYYY-MM format
2024-01-30 18:49:05 +00:00
date_object = get_date_object(self.end_date)
2024-02-03 11:58:14 +00:00
end_date = format_date(date_object)
2024-01-18 17:24:30 +00:00
date_string = f"{start_date} to {end_date}"
else:
2024-02-02 18:31:07 +00:00
# Neither date, start_date, nor end_date is provided, so return an empty
# string:
date_string = ""
2024-01-18 17:24:30 +00:00
return date_string
2024-02-10 19:31:54 +00:00
@functools.cached_property
def date_string_only_years(self) -> str:
"""
Return a date string that only contains years 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).date_string
```
will return:
`#!python "2020 to 2021"`
"""
if self.date is not None:
try:
date_object = get_date_object(self.date)
date_string = format_date(date_object)
except ValueError:
# Then it is a custom date string (e.g., "My Custom Date")
date_string = str(self.date)
elif self.start_date is not None and self.end_date is not None:
if isinstance(self.start_date, int):
# Then it means only the year is provided
start_date = str(self.start_date)
else:
# Then it means start_date is either in YYYY-MM-DD or YYYY-MM format
date_object = get_date_object(self.start_date)
start_date = date_object.year
if self.end_date == "present":
end_date = "present"
elif isinstance(self.end_date, int):
# Then it means only the year is provided
end_date = str(self.end_date)
else:
# Then it means end_date is either in YYYY-MM-DD or YYYY-MM format
date_object = get_date_object(self.end_date)
end_date = date_object.year
date_string = f"{start_date} to {end_date}"
else:
# Neither date, start_date, nor end_date is provided, so return an empty
# string:
date_string = ""
return date_string
@functools.cached_property
def time_span_string(self) -> str:
2024-01-28 18:15:26 +00:00
"""
Return a time span string based on the `date`, `start_date`, and `end_date`
fields.
Example:
```python
2024-02-02 18:31:07 +00:00
entry = dm.EntryBase(start_date=2020-01-01, end_date=2020-04-20).time_span
2024-01-28 18:15:26 +00:00
```
will return:
`#!python "4 months"`
"""
2024-01-28 20:13:23 +00:00
start_date = self.start_date
end_date = self.end_date
date = self.date
if date is not None or (start_date is None and end_date is None):
2024-02-02 18:31:07 +00:00
# If only the date is provided, the time span is irrelevant. So, return an
# empty string.
return ""
2024-01-28 20:13:23 +00:00
elif isinstance(start_date, int) or isinstance(end_date, int):
# Then it means one of the dates is year, so time span cannot be more
# specific than years.
2024-01-30 18:49:05 +00:00
start_year = get_date_object(start_date).year # type: ignore
end_year = get_date_object(end_date).year # type: ignore
2024-01-28 20:13:23 +00:00
time_span_in_years = end_year - start_year
if time_span_in_years < 2:
time_span_string = "1 year"
2024-01-26 18:47:42 +00:00
else:
2024-01-28 20:13:23 +00:00
time_span_string = f"{time_span_in_years} years"
return time_span_string
else:
2024-02-02 18:31:07 +00:00
# Then it means both start_date and end_date are in YYYY-MM-DD or YYYY-MM
# format.
2024-01-30 18:49:05 +00:00
end_date = get_date_object(end_date) # type: ignore
start_date = get_date_object(start_date) # type: ignore
2024-01-28 20:13:23 +00:00
# calculate the number of days between start_date and end_date:
timespan_in_days = (end_date - start_date).days # type: ignore
# calculate the number of years between start_date and end_date:
how_many_years = timespan_in_days // 365
if how_many_years == 0:
how_many_years_string = None
elif how_many_years == 1:
how_many_years_string = "1 year"
else:
how_many_years_string = f"{how_many_years} years"
# calculate the number of months between start_date and end_date:
how_many_months = round((timespan_in_days % 365) / 30)
if how_many_months <= 1:
how_many_months_string = "1 month"
else:
how_many_months_string = f"{how_many_months} months"
# combine howManyYearsString and howManyMonthsString:
if how_many_years_string is None:
time_span_string = how_many_months_string
else:
time_span_string = f"{how_many_years_string} {how_many_months_string}"
return time_span_string
2024-01-18 17:24:30 +00:00
2024-01-28 18:15:26 +00:00
class OneLineEntry(RenderCVBaseModel):
"""This class is the data model of `OneLineEntry`."""
name: str = pydantic.Field(
2024-01-18 17:24:30 +00:00
title="Name",
description="The name of the entry. It will be shown as bold text.",
)
2024-01-28 18:15:26 +00:00
details: str = pydantic.Field(
2024-01-18 17:24:30 +00:00
title="Details",
description="The details of the entry. It will be shown as normal text.",
)
2024-01-18 22:37:40 +00:00
class NormalEntry(EntryBase):
2024-01-28 18:15:26 +00:00
"""This class is the data model of `NormalEntry`."""
2024-01-18 17:24:30 +00:00
2024-01-28 18:15:26 +00:00
name: str = pydantic.Field(
2024-01-18 17:24:30 +00:00
title="Name",
description="The name of the entry. It will be shown as bold text.",
)
2024-01-18 22:37:40 +00:00
class ExperienceEntry(EntryBase):
2024-01-28 18:15:26 +00:00
"""This class is the data model of `ExperienceEntry`."""
2024-01-18 17:24:30 +00:00
2024-01-28 18:15:26 +00:00
company: str = pydantic.Field(
2024-01-18 17:24:30 +00:00
title="Company",
description="The company name. It will be shown as bold text.",
)
2024-01-28 18:15:26 +00:00
position: str = pydantic.Field(
2024-01-18 17:24:30 +00:00
title="Position",
description="The position. It will be shown as normal text.",
)
2024-01-18 22:37:40 +00:00
class EducationEntry(EntryBase):
2024-01-28 18:15:26 +00:00
"""This class is the data model of `EducationEntry`."""
2024-01-18 17:24:30 +00:00
2024-01-28 18:15:26 +00:00
institution: str = pydantic.Field(
2024-01-18 17:24:30 +00:00
title="Institution",
description="The institution name. It will be shown as bold text.",
)
2024-01-28 18:15:26 +00:00
area: str = pydantic.Field(
2024-01-18 17:24:30 +00:00
title="Area",
description="The area of study. It will be shown as normal text.",
)
degree: Optional[str] = pydantic.Field(
2024-01-18 17:24:30 +00:00
default=None,
title="Degree",
2024-01-18 17:24:30 +00:00
description="The type of the degree.",
examples=["BS", "BA", "PhD", "MS"],
2024-02-14 16:39:28 +00:00
json_schema_extra={"default": "PhD"},
2024-01-18 17:24:30 +00:00
)
2024-01-28 18:15:26 +00:00
class PublicationEntry(RenderCVBaseModel):
2024-02-02 18:31:07 +00:00
"""This class is the data model of `PublicationEntry`."""
2024-01-18 17:24:30 +00:00
2024-01-28 18:15:26 +00:00
title: str = pydantic.Field(
2024-01-18 17:24:30 +00:00
title="Title of the Publication",
description="The title of the publication. It will be shown as bold text.",
)
2024-01-28 18:15:26 +00:00
authors: list[str] = pydantic.Field(
2024-01-18 17:24:30 +00:00
title="Authors",
description="The authors of the publication in order as a list of strings.",
)
2024-01-26 17:09:28 +00:00
doi: str = pydantic.Field(
2024-01-18 17:24:30 +00:00
title="DOI",
description="The DOI of the publication.",
examples=["10.48550/arXiv.2310.03138"],
)
2024-02-02 17:02:03 +00:00
date: int | RenderCVDate = pydantic.Field(
2024-01-18 17:24:30 +00:00
title="Publication Date",
2024-02-06 18:18:46 +00:00
description=(
"The date of the publication in YYYY-MM-DD, YYYY-MM, or YYYY format."
),
2024-02-02 17:02:03 +00:00
examples=["2021-10-31", "2010"],
2024-02-14 16:39:28 +00:00
json_schema_extra={"default": "2020-01-01"},
2024-01-18 17:24:30 +00:00
)
2024-01-28 18:15:26 +00:00
journal: Optional[str] = pydantic.Field(
2024-01-18 17:24:30 +00:00
default=None,
title="Journal",
description="The journal or the conference name.",
)
2024-02-02 17:02:03 +00:00
@pydantic.field_validator("date")
@classmethod
def check_date(cls, date: int | RenderCVDate) -> int | RenderCVDate:
"""Check if the date is in the past."""
date_object = get_date_object(date)
if date_object > Date.today():
2024-02-08 19:12:17 +00:00
raise ValueError("The publication date cannot be in the future!")
2024-02-02 17:02:03 +00:00
return date
2024-01-26 17:09:28 +00:00
@pydantic.field_validator("doi")
2024-01-18 17:24:30 +00:00
@classmethod
def check_doi(cls, doi: str) -> str:
"""Check if the DOI exists in the DOI System."""
2024-02-02 18:31:07 +00:00
# see https://stackoverflow.com/a/60671292/18840665 for the explanation of the
# next line:
2024-02-18 18:13:29 +00:00
ssl._create_default_https_context = ssl._create_unverified_context # type: ignore
2024-01-30 18:49:05 +00:00
doi_url = f"http://doi.org/{doi}"
2024-01-18 17:24:30 +00:00
try:
2024-01-26 18:47:42 +00:00
urlopen(doi_url)
except HTTPError as err:
2024-01-18 17:24:30 +00:00
if err.code == 404:
2024-02-08 19:12:17 +00:00
raise ValueError("DOI cannot be found in the DOI System!")
2024-01-18 17:24:30 +00:00
return doi
2024-02-10 19:31:54 +00:00
@functools.cached_property
2024-01-18 17:24:30 +00:00
def doi_url(self) -> str:
2024-02-02 18:31:07 +00:00
"""Return the URL of the DOI."""
2024-01-18 17:24:30 +00:00
return f"https://doi.org/{self.doi}"
2024-02-10 19:31:54 +00:00
@functools.cached_property
2024-02-02 17:02:03 +00:00
def date_string(self) -> str:
2024-02-02 18:31:07 +00:00
"""Return the date string of the publication."""
2024-02-02 17:02:03 +00:00
if isinstance(self.date, int):
date_string = str(self.date)
2024-02-18 18:13:29 +00:00
else:
# Then it is a string
2024-02-02 17:02:03 +00:00
date_object = get_date_object(self.date)
2024-02-03 11:58:14 +00:00
date_string = format_date(date_object)
2024-02-02 17:02:03 +00:00
return date_string
2024-01-18 17:24:30 +00:00
2024-01-26 18:47:42 +00:00
# ======================================================================================
# Section models: ======================================================================
# ======================================================================================
2024-02-02 18:31:07 +00:00
# Each section data model has a field called `entry_type` and a field called `entries`.
# Since the same pydantic.Field object is used in all of the section models, it is
# defined as a separate variable and used in all of the section models:
2024-01-26 18:47:42 +00:00
entry_type_field_of_section_model = pydantic.Field(
title="Entry Type",
description="The type of the entries in the section.",
)
entries_field_of_section_model = pydantic.Field(
title="Entries",
description="The entries of the section. The format depends on the entry type.",
)
2024-01-28 18:15:26 +00:00
class SectionBase(RenderCVBaseModel):
"""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`.
2024-01-18 17:24:30 +00:00
"""
2024-02-02 18:31:07 +00:00
# Title is excluded from the JSON schema because this will be written by RenderCV
2024-01-28 18:15:26 +00:00
# depending on the key in the input file.
title: Optional[str] = pydantic.Field(default=None, exclude=True)
2024-01-18 17:24:30 +00:00
class SectionWithEducationEntries(SectionBase):
2024-01-28 18:15:26 +00:00
"""This class is the data model of the section with `EducationEntry`s."""
2024-01-18 17:24:30 +00:00
2024-01-18 22:37:40 +00:00
entry_type: Literal["EducationEntry"] = entry_type_field_of_section_model
entries: list[EducationEntry] = entries_field_of_section_model
2024-01-18 17:24:30 +00:00
class SectionWithExperienceEntries(SectionBase):
2024-01-28 18:15:26 +00:00
"""This class is the data model of the section with `ExperienceEntry`s."""
2024-01-18 17:24:30 +00:00
2024-01-18 22:37:40 +00:00
entry_type: Literal["ExperienceEntry"] = entry_type_field_of_section_model
entries: list[ExperienceEntry] = entries_field_of_section_model
2024-01-18 17:24:30 +00:00
class SectionWithNormalEntries(SectionBase):
2024-01-28 18:15:26 +00:00
"""This class is the data model of the section with `NormalEntry`s."""
2024-01-18 17:24:30 +00:00
2024-01-18 22:37:40 +00:00
entry_type: Literal["NormalEntry"] = entry_type_field_of_section_model
entries: list[NormalEntry] = entries_field_of_section_model
2024-01-18 17:24:30 +00:00
class SectionWithOneLineEntries(SectionBase):
2024-01-28 18:15:26 +00:00
"""This class is the data model of the section with `OneLineEntry`s."""
2024-01-18 17:24:30 +00:00
2024-01-18 22:37:40 +00:00
entry_type: Literal["OneLineEntry"] = entry_type_field_of_section_model
entries: list[OneLineEntry] = entries_field_of_section_model
2024-01-18 17:24:30 +00:00
class SectionWithPublicationEntries(SectionBase):
2024-01-28 18:15:26 +00:00
"""This class is the data model of the section with `PublicationEntry`s."""
2024-01-18 17:24:30 +00:00
2024-01-18 22:37:40 +00:00
entry_type: Literal["PublicationEntry"] = entry_type_field_of_section_model
entries: list[PublicationEntry] = entries_field_of_section_model
2024-01-18 17:24:30 +00:00
class SectionWithTextEntries(SectionBase):
2024-01-28 18:15:26 +00:00
"""This class is the data model of the section with `TextEntry`s."""
2024-01-18 17:24:30 +00:00
2024-01-18 22:37:40 +00:00
entry_type: Literal["TextEntry"] = entry_type_field_of_section_model
2024-01-28 18:15:26 +00:00
entries: list[str] = entries_field_of_section_model
2024-01-18 17:24:30 +00:00
2024-01-18 22:37:40 +00:00
2024-02-02 18:31:07 +00:00
# Create a custom type called Section:
# It is a union of all the section types and the correct section type is determined by
# the entry_type field, thanks Pydantic's discriminator feature.
2024-01-28 18:15:26 +00:00
# See https://docs.pydantic.dev/2.5/concepts/fields/#discriminator for more information
# about discriminators.
2024-01-18 17:24:30 +00:00
Section = Annotated[
2024-01-18 22:37:40 +00:00
SectionWithEducationEntries
| SectionWithExperienceEntries
| SectionWithNormalEntries
| SectionWithOneLineEntries
| SectionWithPublicationEntries
| SectionWithTextEntries,
2024-01-26 17:09:28 +00:00
pydantic.Field(
2024-01-18 17:24:30 +00:00
discriminator="entry_type",
),
]
2024-02-08 19:12:17 +00:00
def get_entry_and_section_type(
entry: (
dict[str, Any]
| EducationEntry
| ExperienceEntry
| PublicationEntry
| NormalEntry
| OneLineEntry
| str
),
) -> tuple[
str,
Type[
SectionWithTextEntries
| SectionWithOneLineEntries
| SectionWithExperienceEntries
| SectionWithEducationEntries
| SectionWithPublicationEntries
| SectionWithNormalEntries
],
]:
"""Determine the entry and section type based on the entry.
Args:
2024-02-18 18:13:29 +00:00
entry (dict[str, Any] | EducationEntry | ExperienceEntry | PublicationEntry | NormalEntry | OneLineEntry | str): The entry to determine the type.
2024-02-08 19:12:17 +00:00
Returns:
2024-02-18 18:13:29 +00:00
tuple[str, Type[SectionWithTextEntries | SectionWithOneLineEntries | SectionWithExperienceEntries | SectionWithEducationEntries | SectionWithPublicationEntries | SectionWithNormalEntries]]: The entry type and the section type.
2024-02-08 19:12:17 +00:00
"""
if isinstance(entry, dict):
2024-02-14 18:49:37 +00:00
if "details" in entry:
2024-02-08 19:12:17 +00:00
entry_type = "OneLineEntry"
section_type = SectionWithOneLineEntries
2024-02-14 18:49:37 +00:00
elif "company" in entry or "position" in entry:
2024-02-08 19:12:17 +00:00
entry_type = "ExperienceEntry"
section_type = SectionWithExperienceEntries
2024-02-14 18:49:37 +00:00
elif "institution" in entry or "area" in entry or "degree" in entry:
2024-02-08 19:12:17 +00:00
entry_type = "EducationEntry"
section_type = SectionWithEducationEntries
2024-02-14 18:49:37 +00:00
elif "title" in entry or "authors" in entry or "doi" in entry:
2024-02-08 19:12:17 +00:00
entry_type = "PublicationEntry"
section_type = SectionWithPublicationEntries
elif "name" in entry:
entry_type = "NormalEntry"
section_type = SectionWithNormalEntries
else:
raise ValueError("The entry is not provided correctly.")
else:
if isinstance(entry, str):
entry_type = "TextEntry"
section_type = SectionWithTextEntries
elif isinstance(entry, OneLineEntry):
entry_type = "OneLineEntry"
section_type = SectionWithOneLineEntries
elif isinstance(entry, ExperienceEntry):
entry_type = "ExperienceEntry"
section_type = SectionWithExperienceEntries
elif isinstance(entry, EducationEntry):
entry_type = "EducationEntry"
section_type = SectionWithEducationEntries
elif isinstance(entry, PublicationEntry):
entry_type = "PublicationEntry"
section_type = SectionWithPublicationEntries
2024-02-18 18:13:29 +00:00
elif isinstance(entry, NormalEntry): # type: ignore
2024-02-08 19:12:17 +00:00
entry_type = "NormalEntry"
section_type = SectionWithNormalEntries
else:
raise RuntimeError(
"This error shouldn't have been raised. Please open an issue on GitHub."
)
return entry_type, section_type
def validate_section_input(
sections_input: Section | list[Any],
) -> Section | list[Any]:
"""Validate a SectionInput object and raise an error if it is not valid.
Sections input is very complex. It is either a `Section` object or a list of
entries. Since there are multiple entry types, it is not possible to validate it
directly. This function looks at the entry list's first element and determines the
section's entry type based on the first element. Then, it validates the rest of the
list based on the determined entry type. If it is a `Section` object, then it
validates it directly.
Args:
sections_input (Section | list[Any]): The sections input to validate.
Returns:
Section | list[Any]: The validated sections input.
"""
if isinstance(sections_input, list):
2024-02-11 21:43:27 +00:00
# find the entry type based on the first identifiable entry:
entry_type = None
section_type = None
for entry in sections_input:
try:
entry_type, section_type = get_entry_and_section_type(entry)
break
except ValueError:
pass
2024-02-13 17:51:39 +00:00
2024-02-11 21:43:27 +00:00
if entry_type is None or section_type is None:
raise ValueError(
2024-02-14 18:49:37 +00:00
"RenderCV couldn't match this section with any entry type! Please check"
" the entries and make sure they are provided correctly.",
"", # this is the location of the error
"", # this is value of the error
2024-02-11 21:43:27 +00:00
)
2024-02-08 19:12:17 +00:00
test_section = {
"title": "Test Section",
"entry_type": entry_type,
"entries": sections_input,
}
2024-02-13 17:51:39 +00:00
try:
section_type.model_validate(
test_section,
context={"section": "test"},
)
except pydantic.ValidationError as e:
new_error = ValueError(
"There are problems with the entries. RenderCV detected the entry type"
f" of this section to be {entry_type}! The problems are shown below.",
"", # this is the location of the error
"", # this is value of the error
)
raise new_error from e
2024-02-08 19:12:17 +00:00
return sections_input
# Create a custom type called SectionInput so that it can be validated with
# `validate_section_input` function.
SectionInput = Annotated[
2024-02-14 18:49:37 +00:00
list[
2024-02-08 19:12:17 +00:00
EducationEntry
| ExperienceEntry
| PublicationEntry
| NormalEntry
| OneLineEntry
| str
],
pydantic.BeforeValidator(validate_section_input),
]
2024-01-26 18:47:42 +00:00
# ======================================================================================
# Full RenderCV data models: ===========================================================
# ======================================================================================
2024-01-18 17:24:30 +00:00
2024-02-10 19:31:54 +00:00
url_validator = pydantic.TypeAdapter(pydantic.HttpUrl) # type: ignore
2024-01-18 22:37:40 +00:00
2024-01-28 18:15:26 +00:00
class SocialNetwork(RenderCVBaseModel):
"""This class is the data model of a social network."""
2024-01-26 18:47:42 +00:00
2024-01-28 20:37:00 +00:00
network: Literal[
"LinkedIn", "GitHub", "Instagram", "Orcid", "Mastodon", "Twitter"
] = pydantic.Field(
2024-01-28 18:15:26 +00:00
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.",
)
2024-01-26 18:47:42 +00:00
2024-02-07 18:16:55 +00:00
@pydantic.model_validator(mode="after") # type: ignore
2024-01-28 20:37:00 +00:00
@classmethod
2024-02-07 18:16:55 +00:00
def check_networks(cls, model: "SocialNetwork") -> "SocialNetwork":
2024-02-02 18:31:07 +00:00
"""Check if the `SocialNetwork` is provided correctly."""
2024-01-28 20:37:00 +00:00
if model.network == "Mastodon":
if not model.username.startswith("@"):
2024-02-08 19:12:17 +00:00
raise ValueError("Mastodon username should start with '@'!", "username")
2024-01-28 20:37:00 +00:00
if model.username.count("@") > 2:
raise ValueError(
2024-02-08 19:12:17 +00:00
"Mastodon username should contain only two '@'!", "username"
2024-01-28 20:37:00 +00:00
)
return model
2024-02-10 19:31:54 +00:00
@pydantic.model_validator(mode="after") # type: ignore
@classmethod
def validate_urls(cls, model: "SocialNetwork") -> "SocialNetwork":
"""Validate the URLs of the social networks."""
url = model.url
url_validator.validate_strings(url)
return model
@functools.cached_property
def url(self) -> str:
2024-01-28 18:15:26 +00:00
"""Return the URL of the social network."""
url_dictionary = {
"LinkedIn": "https://linkedin.com/in/",
"GitHub": "https://github.com/",
"Instagram": "https://instagram.com/",
"Orcid": "https://orcid.org/",
2024-01-28 20:37:00 +00:00
"Mastodon": "https://mastodon.social/",
"Twitter": "https://twitter.com/",
2024-01-28 18:15:26 +00:00
}
url = url_dictionary[self.network] + self.username
2024-01-26 18:47:42 +00:00
return url
2024-01-28 18:15:26 +00:00
class CurriculumVitae(RenderCVBaseModel):
"""This class is the data model of the CV."""
2024-01-18 17:24:30 +00:00
2024-02-20 19:11:03 +00:00
name: Optional[str] = pydantic.Field(
default=None,
2024-01-18 17:24:30 +00:00
title="Name",
description="The name of the person.",
)
2024-01-28 18:15:26 +00:00
label: Optional[str] = pydantic.Field(
2024-01-18 17:24:30 +00:00
default=None,
title="Label",
description="The label of the person.",
)
2024-01-28 18:15:26 +00:00
location: Optional[str] = pydantic.Field(
2024-01-18 17:24:30 +00:00
default=None,
title="Location",
description="The location of the person. This is not rendered currently.",
)
2024-01-26 17:09:28 +00:00
email: Optional[pydantic.EmailStr] = pydantic.Field(
2024-01-18 17:24:30 +00:00
default=None,
title="Email",
2024-02-14 16:39:28 +00:00
description="The email of the person.",
)
phone: Optional[pydantic_phone_numbers.PhoneNumber] = pydantic.Field(
default=None,
title="Phone",
description="The phone number of the person.",
)
website: Optional[pydantic.HttpUrl] = pydantic.Field(
default=None,
title="Website",
description="The website of the person.",
2024-01-18 17:24:30 +00:00
)
2024-01-26 17:09:28 +00:00
social_networks: Optional[list[SocialNetwork]] = pydantic.Field(
2024-01-18 17:24:30 +00:00
default=None,
title="Social Networks",
2024-02-14 16:39:28 +00:00
description="The social networks of the person.",
2024-01-18 17:24:30 +00:00
)
2024-02-18 18:13:29 +00:00
sections_input: Optional[dict[str, SectionInput]] = pydantic.Field(
2024-01-18 17:24:30 +00:00
default=None,
title="Sections",
description="The sections of the CV.",
alias="sections",
)
2024-02-10 19:31:54 +00:00
@functools.cached_property
2024-01-18 17:24:30 +00:00
def sections(self) -> list[Section]:
2024-01-28 18:15:26 +00:00
"""Return all the sections of the CV with their titles."""
2024-02-18 18:13:29 +00:00
sections: list[Section] = []
2024-01-18 17:24:30 +00:00
if self.sections_input is not None:
2024-01-18 22:37:40 +00:00
for title, section_or_entries in self.sections_input.items():
2024-01-28 18:15:26 +00:00
title = title.replace("_", " ").title()
2024-02-08 19:12:17 +00:00
2024-02-18 18:13:29 +00:00
entry_type, section_type = get_entry_and_section_type(
section_or_entries[0]
)
2024-02-08 19:12:17 +00:00
2024-02-18 18:13:29 +00:00
section = section_type(
title=title,
entry_type=entry_type, # type: ignore
entries=section_or_entries, # type: ignore
)
sections.append(section)
2024-01-18 17:24:30 +00:00
return sections
# ======================================================================================
# ======================================================================================
# ======================================================================================
2024-02-02 18:31:07 +00:00
# Create a custom type called Design:
# It is a union of all the design options and the correct design option is determined by
# the theme field, thanks Pydantic's discriminator feature.
# See https://docs.pydantic.dev/2.5/concepts/fields/#discriminator for more information
# about discriminators.
2024-02-10 20:30:29 +00:00
RenderCVDesign = Annotated[
2024-02-15 18:30:57 +00:00
ClassicThemeOptions | ModerncvThemeOptions | Sb2novThemeOptions,
2024-02-10 21:57:40 +00:00
pydantic.Field(discriminator="theme"),
2024-02-10 20:30:29 +00:00
]
2024-02-15 17:09:48 +00:00
rendercv_design_validator = pydantic.TypeAdapter(RenderCVDesign)
2024-02-15 18:30:57 +00:00
available_themes = ["classic", "moderncv", "sb2nov"]
2024-01-29 16:31:24 +00:00
2024-01-18 17:24:30 +00:00
2024-01-28 18:15:26 +00:00
class RenderCVDataModel(RenderCVBaseModel):
2024-01-18 17:24:30 +00:00
"""This class binds both the CV and the design information together."""
2024-01-26 17:09:28 +00:00
cv: CurriculumVitae = pydantic.Field(
2024-01-18 17:24:30 +00:00
title="Curriculum Vitae",
description="The data of the CV.",
)
2024-02-17 17:46:58 +00:00
design: RenderCVDesign | pydantic.json_schema.SkipJsonSchema[Any] = pydantic.Field(
2024-02-09 19:14:17 +00:00
default=ClassicThemeOptions(theme="classic"),
2024-01-28 18:15:26 +00:00
title="Design",
2024-02-18 13:56:01 +00:00
description=(
"The design information of the CV. The default is the classic theme."
),
2024-01-28 18:15:26 +00:00
)
2024-01-18 17:24:30 +00:00
2024-02-15 17:09:48 +00:00
@pydantic.field_validator("design", mode="before")
2024-02-10 20:30:29 +00:00
@classmethod
def initialize_if_custom_theme_is_used(
cls, design: RenderCVDesign | Any
) -> RenderCVDesign | Any:
"""Initialize the custom theme if it is used and validate it. Otherwise, return
the built-in theme."""
2024-02-18 14:12:24 +00:00
# `get_args` for an Annotated object returns the arguments when Annotated is
# used. The first argument is actually the union of the types, so we need to
# access the first argument to use isinstance function.
theme_data_model_types = get_args(RenderCVDesign)[0]
if isinstance(design, theme_data_model_types):
# then it means RenderCVDataModel is already initialized with a design, so
# return it as is:
return design
elif design["theme"] in available_themes: # type: ignore
# then it means it's a built-in theme, but it is not initialized (validated)
# yet. So, validate and return it:
2024-02-15 17:09:48 +00:00
return rendercv_design_validator.validate_python(design)
2024-02-10 20:30:29 +00:00
else:
2024-02-13 17:51:39 +00:00
theme_name: str = design["theme"] # type: ignore
2024-02-18 18:13:29 +00:00
if not isinstance(theme_name, str):
raise RuntimeError(
"This error shouldn't have been raised. Please open an issue on"
" GitHub."
)
2024-02-10 20:30:29 +00:00
# check if the theme name is valid:
2024-02-13 17:51:39 +00:00
if not theme_name.isalpha():
2024-02-10 20:30:29 +00:00
raise ValueError(
"The custom theme name should contain only letters.",
"theme", # this is the location of the error
2024-02-13 17:51:39 +00:00
theme_name, # this is value of the error
2024-02-10 20:30:29 +00:00
)
# then it is a custom theme
2024-02-13 17:51:39 +00:00
custom_theme_folder = pathlib.Path(theme_name)
2024-02-10 20:30:29 +00:00
2024-02-11 18:35:55 +00:00
# check if the custom theme folder exists:
if not custom_theme_folder.exists():
raise ValueError(
f"The custom theme folder `{custom_theme_folder}` does not exist."
" It should be in the working directory as the input file.",
"", # this is the location of the error
2024-02-13 17:51:39 +00:00
theme_name, # this is value of the error
2024-02-11 18:35:55 +00:00
)
2024-02-10 20:30:29 +00:00
# check if all the necessary files are provided in the custom theme folder:
required_files = [
"EducationEntry.j2.tex", # education entry template
"ExperienceEntry.j2.tex", # experience entry template
"NormalEntry.j2.tex", # normal entry template
"OneLineEntry.j2.tex", # one line entry template
"PublicationEntry.j2.tex", # publication entry template
"TextEntry.j2.tex", # text entry template
2024-02-10 21:57:40 +00:00
"SectionBeginning.j2.tex", # section beginning template
"SectionEnding.j2.tex", # section ending template
2024-02-10 20:30:29 +00:00
"Preamble.j2.tex", # preamble template
"Header.j2.tex", # header template
]
for file in required_files:
file_path = custom_theme_folder / file
if not file_path.exists():
raise ValueError(
f"You provided a custom theme, but the file `{file}` is not"
f" found in the folder `{custom_theme_folder}`.",
"", # this is the location of the error
2024-02-13 17:51:39 +00:00
theme_name, # this is value of the error
2024-02-10 20:30:29 +00:00
)
2024-02-13 17:51:39 +00:00
# import __init__.py file from the custom theme folder if it exists:
path_to_init_file = pathlib.Path(f"{theme_name}/__init__.py")
if path_to_init_file.exists():
spec = importlib.util.spec_from_file_location(
"", # this is somehow not required
path_to_init_file,
2024-02-11 18:35:55 +00:00
)
2024-02-13 17:51:39 +00:00
if spec is None:
raise RuntimeError(
"This error shouldn't have been raised. Please open an issue on"
" GitHub."
)
2024-02-11 18:35:55 +00:00
theme_module = importlib.util.module_from_spec(spec)
2024-02-13 17:51:39 +00:00
spec.loader.exec_module(theme_module) # type: ignore
2024-02-11 18:35:55 +00:00
ThemeDataModel = getattr(
2024-02-13 17:51:39 +00:00
theme_module, f"{theme_name.title()}ThemeOptions" # type: ignore
2024-02-11 18:35:55 +00:00
)
2024-02-10 20:30:29 +00:00
2024-02-11 18:35:55 +00:00
# initialize and validate the custom theme data model:
theme_data_model = ThemeDataModel(**design)
2024-02-13 17:51:39 +00:00
else:
# Then it means there is no __init__.py file in the custom theme folder.
# So, create a dummy data model and use that instead.
class ThemeOptionsAreNotProvided(RenderCVBaseModel):
theme: str = theme_name
theme_data_model = ThemeOptionsAreNotProvided(theme=theme_name)
2024-02-10 20:30:29 +00:00
return theme_data_model
2024-01-18 17:24:30 +00:00
2024-02-13 17:51:39 +00:00
def read_input_file(
file_path: pathlib.Path,
2024-02-13 19:00:16 +00:00
) -> RenderCVDataModel:
2024-02-13 17:51:39 +00:00
"""Read the input file and return two instances of RenderCVDataModel. The first
2024-02-24 15:20:13 +00:00
instance is the data model with $\\LaTeX$ strings and the second instance is the data
2024-02-13 17:51:39 +00:00
model with markdown strings.
2024-02-02 18:31:07 +00:00
2024-01-30 18:49:05 +00:00
Args:
file_path (str): The path to the input file.
Returns:
2024-02-24 15:20:13 +00:00
tuple[RenderCVDataModel, RenderCVDataModel]: The data models with $\\LaTeX$ and
2024-02-13 17:51:39 +00:00
markdown strings.
2024-01-30 18:49:05 +00:00
"""
# check if the file exists:
if not file_path.exists():
2024-02-15 17:09:48 +00:00
raise FileNotFoundError(
f"The input file [magenta]{file_path}[/magenta] doesn't exist!"
)
2024-01-30 18:49:05 +00:00
# check the file extension:
accepted_extensions = [".yaml", ".yml", ".json", ".json5"]
2024-02-02 18:31:07 +00:00
if file_path.suffix not in accepted_extensions:
2024-02-15 17:09:48 +00:00
user_friendly_accepted_extensions = [
f"[green]{ext}[/green]" for ext in accepted_extensions
]
user_friendly_accepted_extensions = ", ".join(user_friendly_accepted_extensions)
2024-01-30 18:49:05 +00:00
raise ValueError(
2024-02-02 18:31:07 +00:00
"The input file should have one of the following extensions:"
2024-02-15 17:09:48 +00:00
f" {user_friendly_accepted_extensions}. The input file is"
f" [magenta]{file_path}[/magenta]."
2024-01-30 18:49:05 +00:00
)
2024-02-10 19:31:54 +00:00
file_content = file_path.read_text(encoding="utf-8")
2024-02-18 18:13:29 +00:00
input_as_dictionary: dict[str, Any] = ruamel.yaml.YAML().load(file_content) # type: ignore
2024-02-02 18:31:07 +00:00
# validate the parsed dictionary by creating an instance of RenderCVDataModel:
2024-02-13 19:00:16 +00:00
rendercv_data_model = RenderCVDataModel(**input_as_dictionary)
2024-01-30 18:49:05 +00:00
2024-02-13 19:00:16 +00:00
return rendercv_data_model
2024-02-02 18:31:07 +00:00
def get_a_sample_data_model(
name: str = "John Doe", theme: str = "classic"
) -> RenderCVDataModel:
"""Return a sample data model for new users to start with.
Args:
name (str, optional): The name of the person. Defaults to "John Doe".
Returns:
RenderCVDataModel: A sample data model.
"""
sections = {
2024-02-24 19:07:47 +00:00
"summary": [
(
"This is an example resume to showcase the capabilities of the"
" open-source LaTeX CV generator,"
" [RenderCV](https://github.com/sinaatalay/rendercv). A substantial"
" part of the content is taken from"
" [here](https://www.careercup.com/resume), where a *clean and tidy CV*"
" pattern is proposed by **Gayle L. McDowell**."
),
],
"education": [
EducationEntry(
2024-02-24 19:07:47 +00:00
institution="University of Pennsylvania",
area="Computer Science",
degree="BS",
start_date="2000-09",
end_date="2005-05",
highlights=[
2024-02-24 19:07:47 +00:00
"GPA: 3.9/4.0 ([Transcript](https://example.com))",
(
"**Coursework:** Software Foundations, Computer"
" Architecture, Algorithms, Artificial Intelligence, Comparison"
" of Learning Algorithms, Computational Theory."
),
],
),
],
"experience": [
ExperienceEntry(
2024-02-24 19:07:47 +00:00
company="Apple Computer",
position="Software Engineer, Intern",
start_date="2004-06",
end_date="2004-08",
location="CA, USA",
highlights=[
2024-02-24 19:07:47 +00:00
(
"Reduced time to render the user's buddy list by 75% by"
" implementing a prediction algorithm."
2024-02-24 19:07:47 +00:00
),
(
"Implemented iChat integration with OS X Spotlight Search by"
" creating a tool that extracts metadata from saved chat"
2024-02-24 19:07:47 +00:00
" transcripts and provides metadata to a system-wide search"
" database."
),
(
"Redesigned chat file format and implemented backward"
2024-02-24 19:07:47 +00:00
" compatibility for search."
),
],
),
ExperienceEntry(
2024-02-24 19:07:47 +00:00
company="Microsoft Corporation",
position="Lead Student Ambassador",
start_date="2003-09",
end_date="2005-04",
location="WA, USA",
highlights=[
(
"Promoted to Lead Student Ambassador in the Fall of 2004, supervised"
2024-02-24 19:07:47 +00:00
" 10 - 15 Student Ambassadors."
),
(
"Created and taught a computer science course, CSE 099: Software"
2024-02-24 19:07:47 +00:00
" Design and Development."
),
],
),
ExperienceEntry(
company="University of Pennsylvania",
position="Head Teaching Assistant",
start_date="2001-10",
end_date="2005-05",
location="PA, USA",
highlights=[
(
"Implemented a user interface for the VS open file switcher"
" (ctrl-tab) and extended it to tool windows."
),
(
"Created a service to provide gradient across VS and VS add-ins."
2024-02-24 19:07:47 +00:00
" Optimized service via caching."
),
"Programmer Productivity Research Center (Summers 2001, 2002)",
(
"Built app to compute the similarity of all methods in a code base,"
2024-02-24 19:07:47 +00:00
" reduced time from $\\mathcal{O}(n^2)$ to $\\mathcal{O}(n"
" \\log n)$. "
),
(
"Created a test case generation tool that creates random XML"
2024-02-24 19:07:47 +00:00
" docs from XML Schema."
),
],
),
ExperienceEntry(
company="Microsoft Corporation",
position="Software Design Engineer, Intern",
start_date="2003-06",
end_date="2003-08",
location="WA, USA",
highlights=[
(
"Promoted to Lead Student Ambassador in the Fall of 2004, supervised"
2024-02-24 19:07:47 +00:00
" 10 - 15 Student Ambassadors."
),
],
),
],
"publications": [
PublicationEntry(
2024-02-24 19:07:47 +00:00
title=(
"Magneto-Thermal Thin Shell Approximation for 3D Finite Element"
" Analysis of No-Insulation Coils"
),
authors=[
"Albert Smith",
name,
"Jane Derry",
"Harry Tom",
"Anotherfirstname Andsurname",
],
date="2004-01",
doi="10.1109/TASC.2023.3340648",
)
],
"projects": [
NormalEntry(
2024-02-24 19:07:47 +00:00
name="Multi-User Drawing Tool",
date="2004",
highlights=[
(
"Developed an electronic classroom where multiple users can"
' view and simultaneously draw on a "chalkboard" with each'
" person's edits synchronized."
),
"Used C++ and MFC.",
],
),
NormalEntry(
name="Synchronized Calendar",
start_date="2003",
end_date="2004",
highlights=[
2024-02-24 19:07:47 +00:00
(
"Developed a desktop calendar with globally shared and"
" synchronized calendars, allowing users to schedule meetings"
" with other users."
),
"Used C#.NET, SQL, and XML.",
],
),
NormalEntry(
2024-02-24 19:07:47 +00:00
name="Operating System",
date="2002",
highlights=[
2024-02-24 19:07:47 +00:00
(
"Developed a UNIX-style OS with a scheduler, file system, text"
" editor, and calculator."
2024-02-24 19:07:47 +00:00
),
"Used C.",
],
),
],
2024-02-24 19:07:47 +00:00
"additional_experience_and_awards": [
OneLineEntry(
2024-02-24 19:07:47 +00:00
name="Instructor (2003 - 2005)",
details="Taught two full-credit Computer Science courses.",
),
OneLineEntry(
2024-02-24 19:07:47 +00:00
name="Third Prize, Senior Design Projects",
details=(
"Awarded 3rd prize for a synchronized calendar project out of 100"
2024-02-24 19:07:47 +00:00
" projects."
),
),
],
2024-02-24 19:07:47 +00:00
"technologies": [
2024-02-14 18:49:37 +00:00
OneLineEntry(
2024-02-24 19:07:47 +00:00
name="Languages",
details="C++, C, Java, Objective-C, C#.NET, SQL, JavaScript",
2024-02-14 18:49:37 +00:00
),
OneLineEntry(
2024-02-24 19:07:47 +00:00
name="Software",
details=(
"Visual Studio, Microsoft SQL Server, Eclipse, XCode, Interface"
" Builder"
),
2024-02-14 18:49:37 +00:00
),
],
}
cv = CurriculumVitae(
name=name,
location="Your Location",
email="youremail@yourdomain.com",
phone="+905419999999", # type: ignore
website="https://yourwebsite.com", # type: ignore
social_networks=[
SocialNetwork(network="LinkedIn", username="yourusername"),
SocialNetwork(network="GitHub", username="yourusername"),
],
2024-02-18 18:13:29 +00:00
sections=sections, # type: ignore
)
if theme not in available_themes:
raise ValueError(
f"The theme should be one of the following: {available_themes}! The"
f" provided theme is {theme}."
)
if theme == "classic":
design = ClassicThemeOptions(theme="classic", show_timespan_in=["Experience"])
else:
design = rendercv_design_validator.validate_python({"theme": theme}) # type: ignore
2024-02-14 19:46:22 +00:00
return RenderCVDataModel(cv=cv, design=design)
2024-02-18 18:13:29 +00:00
def generate_json_schema() -> dict[str, Any]:
2024-02-06 18:18:46 +00:00
"""Generate the JSON schema of RenderCV.
2024-02-02 18:31:07 +00:00
JSON schema is generated for the users to make it easier for them to write the input
file. The JSON Schema of RenderCV is saved in the `docs` directory of the repository
and distributed to the users with the
[JSON Schema Store](https://www.schemastore.org/).
2024-02-06 18:18:46 +00:00
Returns:
dict: The JSON schema of RenderCV.
2024-02-02 18:31:07 +00:00
"""
class RenderCVSchemaGenerator(pydantic.json_schema.GenerateJsonSchema):
2024-02-18 18:13:29 +00:00
def generate(self, schema, mode="validation"): # type: ignore
2024-02-02 18:31:07 +00:00
json_schema = super().generate(schema, mode=mode)
2024-02-06 18:18:46 +00:00
# Basic information about the schema:
json_schema["title"] = "RenderCV"
json_schema["description"] = "RenderCV data model."
2024-02-02 18:31:07 +00:00
json_schema["$id"] = (
"https://raw.githubusercontent.com/sinaatalay/rendercv/main/schema.json"
)
json_schema["$schema"] = "http://json-schema.org/draft-07/schema#"
# Loop through $defs and remove docstring descriptions and fix optional
# fields
2024-02-18 18:13:29 +00:00
for _, value in json_schema["$defs"].items():
2024-02-02 18:31:07 +00:00
# Don't allow additional properties
value["additionalProperties"] = False
# If a type is optional, then Pydantic sets the type to a list of two
# types, one of which is null. The null type can be removed since we
# already have the required field. Moreover, we would like to warn
# users if they provide null values. They can remove the fields if they
# don't want to provide them.
null_type_dict = {}
null_type_dict["type"] = "null"
for field in value["properties"].values():
if "anyOf" in field:
if (
len(field["anyOf"]) == 2
and null_type_dict in field["anyOf"]
):
field["allOf"] = [field["anyOf"][0]]
del field["anyOf"]
2024-02-06 18:18:46 +00:00
else:
field["oneOf"] = field["anyOf"]
del field["anyOf"]
2024-02-02 18:31:07 +00:00
# In date field, we both accept normal strings and Date objects. They
# are both strings, therefore, if user provides a Date object, then
# JSON schema will complain that it matches two different types.
# Remember that all of the anyOfs are changed to oneOfs. Only one of
# the types can be matched. Therefore, we remove the first type, which
# is the string with the YYYY-MM-DD format.
if (
"date" in value["properties"]
2024-02-09 19:14:17 +00:00
and "oneOf" in value["properties"]["date"]
2024-02-02 18:31:07 +00:00
):
2024-02-09 19:14:17 +00:00
del value["properties"]["date"]["oneOf"][0]
2024-02-02 18:31:07 +00:00
return json_schema
schema = RenderCVDataModel.model_json_schema(
schema_generator=RenderCVSchemaGenerator
)
2024-02-06 18:18:46 +00:00
return schema
2024-02-02 18:31:07 +00:00
2024-02-06 18:18:46 +00:00
def generate_json_schema_file(json_schema_path: pathlib.Path):
"""Generate the JSON schema of RenderCV and save it to a file.
Args:
json_schema_path (pathlib.Path): The path to save the JSON schema.
"""
2024-02-06 18:18:46 +00:00
schema = generate_json_schema()
schema_json = json.dumps(schema, indent=2)
json_schema_path.write_text(schema_json)