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
|
2024-02-04 20:49:40 +00:00
|
|
|
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(
|
2024-02-04 20:49:40 +00:00
|
|
|
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
|
2024-01-31 19:00:22 +00:00
|
|
|
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:
|
2024-01-31 19:00:22 +00:00
|
|
|
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
|
2024-01-31 19:00:22 +00:00
|
|
|
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.
|
2024-01-31 19:00:22 +00:00
|
|
|
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.",
|
|
|
|
)
|
2024-02-04 20:49:40 +00:00
|
|
|
degree: Optional[str] = pydantic.Field(
|
2024-01-18 17:24:30 +00:00
|
|
|
default=None,
|
2024-02-04 20:49:40 +00:00
|
|
|
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:
|
2024-02-04 20:49:40 +00:00
|
|
|
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-01-31 19:00:22 +00:00
|
|
|
|
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
|
|
|
|
|
|
|
|
2024-02-24 21:23:35 +00:00
|
|
|
def get_a_sample_data_model(
|
|
|
|
name: str = "John Doe", theme: str = "classic"
|
|
|
|
) -> RenderCVDataModel:
|
2024-02-14 19:08:39 +00:00
|
|
|
"""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.
|
|
|
|
"""
|
2024-02-04 20:49:40 +00:00
|
|
|
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**."
|
|
|
|
),
|
|
|
|
],
|
2024-02-04 20:49:40 +00:00
|
|
|
"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",
|
2024-02-04 20:49:40 +00:00
|
|
|
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."
|
|
|
|
),
|
2024-02-04 20:49:40 +00:00
|
|
|
],
|
|
|
|
),
|
|
|
|
],
|
2024-02-24 21:23:35 +00:00
|
|
|
"experience": [
|
2024-02-04 20:49:40 +00:00
|
|
|
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",
|
2024-02-04 20:49:40 +00:00
|
|
|
highlights=[
|
2024-02-24 19:07:47 +00:00
|
|
|
(
|
|
|
|
"Reduced time to render the user's buddy list by 75% by"
|
2024-02-24 21:23:35 +00:00
|
|
|
" implementing a prediction algorithm."
|
2024-02-24 19:07:47 +00:00
|
|
|
),
|
|
|
|
(
|
|
|
|
"Implemented iChat integration with OS X Spotlight Search by"
|
2024-02-24 21:23:35 +00:00
|
|
|
" 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."
|
|
|
|
),
|
|
|
|
(
|
2024-02-24 21:23:35 +00:00
|
|
|
"Redesigned chat file format and implemented backward"
|
2024-02-24 19:07:47 +00:00
|
|
|
" compatibility for search."
|
|
|
|
),
|
2024-02-04 20:49:40 +00:00
|
|
|
],
|
|
|
|
),
|
|
|
|
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=[
|
|
|
|
(
|
2024-02-24 21:23:35 +00:00
|
|
|
"Promoted to Lead Student Ambassador in the Fall of 2004, supervised"
|
2024-02-24 19:07:47 +00:00
|
|
|
" 10 - 15 Student Ambassadors."
|
|
|
|
),
|
|
|
|
(
|
2024-02-24 21:23:35 +00:00
|
|
|
"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."
|
|
|
|
),
|
|
|
|
(
|
2024-02-24 21:23:35 +00:00
|
|
|
"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)",
|
|
|
|
(
|
2024-02-24 21:23:35 +00:00
|
|
|
"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)$. "
|
|
|
|
),
|
|
|
|
(
|
2024-02-24 21:23:35 +00:00
|
|
|
"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=[
|
|
|
|
(
|
2024-02-24 21:23:35 +00:00
|
|
|
"Promoted to Lead Student Ambassador in the Fall of 2004, supervised"
|
2024-02-24 19:07:47 +00:00
|
|
|
" 10 - 15 Student Ambassadors."
|
|
|
|
),
|
|
|
|
],
|
2024-02-04 20:49:40 +00:00
|
|
|
),
|
|
|
|
],
|
|
|
|
"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",
|
2024-02-04 20:49:40 +00:00
|
|
|
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",
|
2024-02-04 20:49:40 +00:00
|
|
|
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."
|
|
|
|
),
|
2024-02-24 21:23:35 +00:00
|
|
|
"Used C#.NET, SQL, and XML.",
|
2024-02-04 20:49:40 +00:00
|
|
|
],
|
|
|
|
),
|
|
|
|
NormalEntry(
|
2024-02-24 19:07:47 +00:00
|
|
|
name="Operating System",
|
|
|
|
date="2002",
|
2024-02-04 20:49:40 +00:00
|
|
|
highlights=[
|
2024-02-24 19:07:47 +00:00
|
|
|
(
|
2024-02-24 21:23:35 +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-04 20:49:40 +00:00
|
|
|
],
|
|
|
|
),
|
|
|
|
],
|
2024-02-24 19:07:47 +00:00
|
|
|
"additional_experience_and_awards": [
|
2024-02-04 20:49:40 +00:00
|
|
|
OneLineEntry(
|
2024-02-24 19:07:47 +00:00
|
|
|
name="Instructor (2003 - 2005)",
|
|
|
|
details="Taught two full-credit Computer Science courses.",
|
2024-02-04 20:49:40 +00:00
|
|
|
),
|
|
|
|
OneLineEntry(
|
2024-02-24 19:07:47 +00:00
|
|
|
name="Third Prize, Senior Design Projects",
|
2024-02-04 20:49:40 +00:00
|
|
|
details=(
|
2024-02-24 21:23:35 +00:00
|
|
|
"Awarded 3rd prize for a synchronized calendar project out of 100"
|
2024-02-24 19:07:47 +00:00
|
|
|
" projects."
|
2024-02-04 20:49:40 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
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
|
|
|
),
|
|
|
|
],
|
2024-02-04 20:49:40 +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
|
2024-02-04 20:49:40 +00:00
|
|
|
)
|
|
|
|
|
2024-02-24 21:23:35 +00:00
|
|
|
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-04 20:49:40 +00:00
|
|
|
|
2024-02-14 19:46:22 +00:00
|
|
|
return RenderCVDataModel(cv=cv, design=design)
|
2024-02-04 20:49:40 +00:00
|
|
|
|
|
|
|
|
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):
|
2024-02-14 19:08:39 +00:00
|
|
|
"""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)
|