2024-01-18 17:24:30 +00:00
|
|
|
"""
|
2024-01-28 18:15:26 +00:00
|
|
|
This module contains all the necessary classes to store CV data. The YAML input file is
|
|
|
|
transformed into instances of these classes (i.e., the input file is read) in the
|
|
|
|
[input_reader](https://sinaatalay.github.io/rendercv/code_documentation/input_reader/)
|
|
|
|
module. RenderCV utilizes these instances to generate a CV. These classes are called
|
|
|
|
data models.
|
|
|
|
|
|
|
|
The data models are initialized with data validation to prevent unexpected bugs. During
|
|
|
|
the initialization, we ensure that everything is in the correct place and that the user
|
|
|
|
has provided a valid RenderCV input. This is achieved through the use of
|
|
|
|
[Pydantic](https://pypi.org/project/pydantic/).
|
2024-01-18 17:24:30 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
from datetime import date as Date
|
|
|
|
from typing import Literal
|
2024-01-18 22:37:40 +00:00
|
|
|
from typing_extensions import Annotated, Optional
|
2024-01-18 17:24:30 +00:00
|
|
|
from functools import cached_property
|
2024-01-26 18:47:42 +00:00
|
|
|
from urllib.request import urlopen, HTTPError
|
2024-01-18 17:24:30 +00:00
|
|
|
import os
|
|
|
|
import json
|
|
|
|
|
2024-01-26 17:09:28 +00:00
|
|
|
import pydantic
|
|
|
|
import pydantic_extra_types.phone_numbers as pydantic_phone_numbers
|
2024-01-26 18:47:42 +00:00
|
|
|
import pydantic.functional_validators as pydantic_functional_validators
|
2024-01-18 17:24:30 +00:00
|
|
|
|
2024-01-26 18:47:42 +00:00
|
|
|
from . import utilities
|
|
|
|
from .terminal_reporter import warning
|
2024-01-28 18:15:26 +00:00
|
|
|
from .templates.classic import ClassicThemeOptions
|
2024-01-18 17:24:30 +00:00
|
|
|
|
|
|
|
|
2024-01-28 18:15:26 +00:00
|
|
|
# Create a custom type called PastDate that accepts a string in YYYY-MM-DD format and
|
|
|
|
# returns a Date object. It also checks if the date is in the past.
|
|
|
|
# See https://docs.pydantic.dev/2.5/concepts/types/#custom-types for more information
|
|
|
|
# about custom types.
|
2024-01-18 17:24:30 +00:00
|
|
|
PastDate = Annotated[
|
|
|
|
str,
|
2024-01-26 17:09:28 +00:00
|
|
|
pydantic.Field(pattern=r"\d{4}-?(\d{2})?-?(\d{2})?"),
|
2024-01-26 18:47:42 +00:00
|
|
|
pydantic_functional_validators.AfterValidator(utilities.parse_date_string),
|
2024-01-18 17:24:30 +00:00
|
|
|
]
|
|
|
|
|
2024-01-28 18:15:26 +00:00
|
|
|
|
|
|
|
class RenderCVBaseModel(pydantic.BaseModel):
|
|
|
|
"""
|
|
|
|
This class is the parent class of all the data models in RenderCV. It has only one
|
|
|
|
difference from the default `pydantic.BaseModel`: It raises an error if an unknown
|
|
|
|
key is provided in the input file.
|
|
|
|
"""
|
|
|
|
|
|
|
|
model_config = pydantic.ConfigDict(extra="forbid")
|
|
|
|
|
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-01-26 17:09:28 +00:00
|
|
|
start_date: Optional[PastDate] = pydantic.Field(
|
2024-01-18 17:24:30 +00:00
|
|
|
default=None,
|
|
|
|
title="Start Date",
|
|
|
|
description="The start date of the event in YYYY-MM-DD format.",
|
|
|
|
examples=["2020-09-24"],
|
|
|
|
)
|
2024-01-26 17:09:28 +00:00
|
|
|
end_date: Optional[Literal["present"] | PastDate] = pydantic.Field(
|
2024-01-18 17:24:30 +00:00
|
|
|
default=None,
|
|
|
|
title="End Date",
|
|
|
|
description=(
|
|
|
|
"The end date of the event in YYYY-MM-DD format. If the event is still"
|
|
|
|
' ongoing, then the value should be "present".'
|
|
|
|
),
|
|
|
|
examples=["2020-09-24", "present"],
|
|
|
|
)
|
2024-01-28 18:15:26 +00:00
|
|
|
date: Optional[PastDate | 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-01-28 18:15:26 +00:00
|
|
|
highlights: Optional[list[str]] = pydantic.Field(
|
2024-01-18 17:24:30 +00:00
|
|
|
default=[],
|
|
|
|
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-01-18 17:24:30 +00:00
|
|
|
default=None,
|
|
|
|
title="Location",
|
2024-01-28 18:15:26 +00:00
|
|
|
description="The location of the event.",
|
2024-01-18 17:24:30 +00:00
|
|
|
examples=["Istanbul, Turkey"],
|
|
|
|
)
|
2024-01-26 17:09:28 +00:00
|
|
|
url: Optional[pydantic.HttpUrl] = None
|
2024-01-28 18:15:26 +00:00
|
|
|
url_text_input: Optional[str] = pydantic.Field(default=None, alias="url_text")
|
2024-01-18 17:24:30 +00:00
|
|
|
|
2024-01-26 17:09:28 +00:00
|
|
|
@pydantic.model_validator(mode="after")
|
2024-01-18 17:24:30 +00:00
|
|
|
@classmethod
|
|
|
|
def check_dates(cls, model):
|
2024-01-28 18:15:26 +00:00
|
|
|
"""
|
|
|
|
Check if the dates are provided correctly and convert them to `Date` objects if
|
|
|
|
they are provided in YYYY-MM-DD format.
|
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
|
|
|
|
|
|
|
|
if date_is_provided and start_date_is_provided and end_date_is_provided:
|
2024-01-26 18:47:42 +00:00
|
|
|
warning(
|
2024-01-18 17:24:30 +00:00
|
|
|
'"start_date", "end_date" and "date" are all provided in of the'
|
2024-01-28 20:13:23 +00:00
|
|
|
" entries. start_date and end_date will be ignored."
|
2024-01-18 17:24:30 +00:00
|
|
|
)
|
2024-01-28 20:13:23 +00:00
|
|
|
model.start_date = None
|
|
|
|
model.end_date = None
|
2024-01-18 17:24:30 +00:00
|
|
|
|
|
|
|
elif date_is_provided and start_date_is_provided and not end_date_is_provided:
|
2024-01-26 18:47:42 +00:00
|
|
|
warning(
|
2024-01-18 17:24:30 +00:00
|
|
|
'Both "date" and "start_date" is provided in of the entries.'
|
|
|
|
' "start_date" will be ignored.'
|
|
|
|
)
|
|
|
|
model.start_date = None
|
|
|
|
model.end_date = None
|
|
|
|
|
|
|
|
elif date_is_provided and end_date_is_provided and not start_date_is_provided:
|
2024-01-26 18:47:42 +00:00
|
|
|
warning(
|
2024-01-18 17:24:30 +00:00
|
|
|
'Both "date" and "end_date" is provided in of the entries. "end_date"'
|
|
|
|
" will be ignored."
|
|
|
|
)
|
|
|
|
model.start_date = None
|
|
|
|
model.end_date = None
|
|
|
|
|
|
|
|
elif start_date_is_provided and not end_date_is_provided:
|
2024-01-26 18:47:42 +00:00
|
|
|
warning(
|
2024-01-18 17:24:30 +00:00
|
|
|
'"start_date" is provided in of the entries, but "end_date" is not.'
|
|
|
|
' "end_date" will be set to "present".'
|
|
|
|
)
|
|
|
|
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.'
|
|
|
|
' "start_date" is required.'
|
|
|
|
)
|
|
|
|
|
2024-01-18 17:24:30 +00:00
|
|
|
if model.start_date is not None and model.end_date is not None:
|
|
|
|
if model.end_date == "present":
|
|
|
|
end_date = Date.today()
|
|
|
|
elif isinstance(model.end_date, int):
|
|
|
|
# Then it means user only provided the year, so convert it to a Date
|
|
|
|
# object with the first day of the year (just for the date comparison)
|
|
|
|
end_date = Date(model.end_date, 1, 1)
|
|
|
|
elif isinstance(model.end_date, Date):
|
|
|
|
# Then it means user provided either YYYY-MM-DD or YYYY-MM
|
|
|
|
end_date = model.end_date
|
|
|
|
else:
|
|
|
|
raise RuntimeError("end_date is neither an integer nor a Date object.")
|
|
|
|
|
|
|
|
if isinstance(model.start_date, int):
|
|
|
|
# Then it means user only provided the year, so convert it to a Date
|
|
|
|
# object with the first day of the year (just for the date comparison)
|
|
|
|
start_date = Date(model.start_date, 1, 1)
|
|
|
|
elif isinstance(model.start_date, Date):
|
|
|
|
# Then it means user provided either YYYY-MM-DD or YYYY-MM
|
|
|
|
start_date = model.start_date
|
|
|
|
else:
|
|
|
|
raise RuntimeError(
|
|
|
|
"start_date is neither an integer nor a Date object."
|
|
|
|
)
|
|
|
|
|
|
|
|
if start_date > end_date:
|
|
|
|
raise ValueError(
|
|
|
|
'"start_date" can not be after "end_date". Please check the dates.'
|
|
|
|
)
|
|
|
|
|
|
|
|
return model
|
|
|
|
|
2024-01-26 17:09:28 +00:00
|
|
|
@pydantic.computed_field
|
2024-01-18 17:24:30 +00:00
|
|
|
@cached_property
|
2024-01-28 18:15:26 +00:00
|
|
|
def date_string(self) -> Optional[str]:
|
|
|
|
"""
|
|
|
|
Return a date string based on the `date`, `start_date`, and `end_date` fields.
|
|
|
|
|
|
|
|
Example:
|
|
|
|
```python
|
|
|
|
entry = dm.EntryBase(start_date=2020-10-11, end_date=2021-04-04)
|
|
|
|
entry.date_string
|
|
|
|
```
|
|
|
|
will return:
|
|
|
|
`#!python "2020-10-11 to 2021-04-04"`
|
|
|
|
"""
|
2024-01-18 17:24:30 +00:00
|
|
|
if self.date is not None:
|
|
|
|
if isinstance(self.date, str):
|
|
|
|
date_string = self.date
|
|
|
|
elif isinstance(self.date, Date):
|
2024-01-26 18:47:42 +00:00
|
|
|
date_string = utilities.format_date(self.date)
|
2024-01-18 17:24:30 +00:00
|
|
|
else:
|
|
|
|
raise RuntimeError("Date is neither a string nor a Date object.")
|
|
|
|
|
|
|
|
elif self.start_date is not None and self.end_date is not None:
|
2024-01-26 18:47:42 +00:00
|
|
|
if isinstance(self.start_date, (int, Date)):
|
|
|
|
start_date = utilities.format_date(self.start_date)
|
|
|
|
else:
|
|
|
|
raise RuntimeError(
|
|
|
|
"This error shouldn't have been raised. Please open an issue on"
|
|
|
|
" GitHub."
|
|
|
|
)
|
2024-01-18 17:24:30 +00:00
|
|
|
if self.end_date == "present":
|
|
|
|
end_date = "present"
|
2024-01-26 18:47:42 +00:00
|
|
|
elif isinstance(self.end_date, (int, Date)):
|
|
|
|
end_date = utilities.format_date(self.end_date)
|
2024-01-18 17:24:30 +00:00
|
|
|
else:
|
2024-01-26 18:47:42 +00:00
|
|
|
raise RuntimeError(
|
|
|
|
"This error shouldn't have been raised. Please open an issue on"
|
|
|
|
" GitHub."
|
|
|
|
)
|
2024-01-18 17:24:30 +00:00
|
|
|
|
|
|
|
date_string = f"{start_date} to {end_date}"
|
|
|
|
|
|
|
|
else:
|
|
|
|
date_string = None
|
|
|
|
|
|
|
|
return date_string
|
|
|
|
|
2024-01-26 17:09:28 +00:00
|
|
|
@pydantic.computed_field
|
2024-01-18 17:24:30 +00:00
|
|
|
@cached_property
|
2024-01-28 18:15:26 +00:00
|
|
|
def time_span(self) -> Optional[str]:
|
|
|
|
"""
|
|
|
|
Return a time span string based on the `date`, `start_date`, and `end_date`
|
|
|
|
fields.
|
|
|
|
|
|
|
|
Example:
|
|
|
|
```python
|
|
|
|
entry = dm.EntryBase(start_date=2020-01-01, end_date=2020-04-20)
|
|
|
|
entry.time_span
|
|
|
|
```
|
|
|
|
will return:
|
|
|
|
`#!python "4 months"`
|
|
|
|
"""
|
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):
|
|
|
|
return None
|
|
|
|
|
|
|
|
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.
|
|
|
|
if isinstance(start_date, int):
|
|
|
|
start_year = start_date
|
|
|
|
else:
|
|
|
|
start_year = start_date.year # type: ignore
|
|
|
|
|
|
|
|
if isinstance(end_date, int):
|
|
|
|
end_year = end_date
|
|
|
|
elif end_date == "present":
|
|
|
|
end_year = Date.today().year
|
|
|
|
else:
|
|
|
|
end_year = end_date.year # type: ignore
|
|
|
|
|
|
|
|
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:
|
|
|
|
if end_date == "present":
|
|
|
|
end_date = Date.today()
|
|
|
|
|
|
|
|
# calculate the number of days between start_date and end_date:
|
|
|
|
timespan_in_days = (end_date - start_date).days # type: ignore
|
|
|
|
|
|
|
|
if timespan_in_days < 0:
|
2024-01-26 18:47:42 +00:00
|
|
|
raise RuntimeError(
|
|
|
|
"This error shouldn't have been raised. Please open an issue on"
|
|
|
|
" GitHub."
|
|
|
|
)
|
2024-01-18 17:24:30 +00:00
|
|
|
|
2024-01-28 20:13:23 +00:00
|
|
|
# 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-26 17:09:28 +00:00
|
|
|
@pydantic.computed_field
|
2024-01-18 17:24:30 +00:00
|
|
|
@cached_property
|
2024-01-28 18:15:26 +00:00
|
|
|
def url_text(self) -> Optional[str]:
|
|
|
|
"""
|
|
|
|
Return a URL text based on the `url_text_input` and `url` fields.
|
|
|
|
"""
|
|
|
|
url_text = None
|
|
|
|
if self.url_text_input is not None:
|
|
|
|
url_text = self.url_text_input
|
|
|
|
elif self.url is not None:
|
|
|
|
url_text_dictionary = {
|
2024-01-28 20:13:23 +00:00
|
|
|
"github": "view on GitHub",
|
2024-01-28 18:15:26 +00:00
|
|
|
"linkedin": "view on LinkedIn",
|
|
|
|
"instagram": "view on Instagram",
|
|
|
|
"youtube": "view on YouTube",
|
|
|
|
}
|
|
|
|
url_text = "view on my website"
|
|
|
|
for key in url_text_dictionary:
|
|
|
|
if key in str(self.url):
|
|
|
|
url_text = url_text_dictionary[key]
|
|
|
|
break
|
|
|
|
|
|
|
|
return url_text
|
|
|
|
|
|
|
|
|
|
|
|
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.",
|
|
|
|
examples=["Bogazici University"],
|
|
|
|
)
|
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-01-28 18:15:26 +00:00
|
|
|
study_type: Optional[str] = pydantic.Field(
|
2024-01-18 17:24:30 +00:00
|
|
|
default=None,
|
|
|
|
title="Study Type",
|
|
|
|
description="The type of the degree.",
|
|
|
|
examples=["BS", "BA", "PhD", "MS"],
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-01-28 18:15:26 +00:00
|
|
|
class PublicationEntry(RenderCVBaseModel):
|
|
|
|
"""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-01-28 18:15:26 +00:00
|
|
|
date: str = pydantic.Field(
|
2024-01-18 17:24:30 +00:00
|
|
|
title="Publication Date",
|
|
|
|
description="The date of the publication.",
|
|
|
|
examples=["2021-10-31"],
|
|
|
|
)
|
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-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."""
|
|
|
|
doi_url = f"https://doi.org/{doi}"
|
|
|
|
|
|
|
|
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:
|
|
|
|
raise ValueError(f"{doi} cannot be found in the DOI System.")
|
|
|
|
|
|
|
|
return doi
|
|
|
|
|
2024-01-26 17:09:28 +00:00
|
|
|
@pydantic.computed_field
|
2024-01-18 17:24:30 +00:00
|
|
|
@cached_property
|
|
|
|
def doi_url(self) -> str:
|
|
|
|
return f"https://doi.org/{self.doi}"
|
|
|
|
|
|
|
|
|
2024-01-26 18:47:42 +00:00
|
|
|
# ======================================================================================
|
|
|
|
# Section models: ======================================================================
|
|
|
|
# ======================================================================================
|
|
|
|
|
|
|
|
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-01-28 18:15:26 +00:00
|
|
|
# title is excluded from the JSON schema because this will be written by RenderCV
|
|
|
|
# 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-01-28 18:15:26 +00:00
|
|
|
# A custom type Section. It is a union of all the section types and the correct section
|
|
|
|
# type is determined by the entry_type field.
|
|
|
|
# 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-01-26 18:47:42 +00:00
|
|
|
# ======================================================================================
|
|
|
|
# Full RenderCV data models: ===========================================================
|
|
|
|
# ======================================================================================
|
2024-01-18 17:24:30 +00:00
|
|
|
|
2024-01-28 18:15:26 +00:00
|
|
|
# RenderCV requires users to specify the entry type for each section in their CV in
|
|
|
|
# order to render the correct thing in the CV. However, for certain sections, specifying
|
|
|
|
# the entry type can be redundant. To simplify this process for users, default entry
|
|
|
|
# types are stored in a dictionary for certain section titles so that users do not have
|
|
|
|
# to specify them.
|
2024-01-18 22:37:40 +00:00
|
|
|
default_entry_types_for_a_given_title: dict[
|
|
|
|
str,
|
|
|
|
tuple[type[EducationEntry], type[SectionWithEducationEntries]]
|
|
|
|
| tuple[type[ExperienceEntry], type[SectionWithExperienceEntries]]
|
|
|
|
| tuple[type[PublicationEntry], type[SectionWithPublicationEntries]]
|
|
|
|
| tuple[type[NormalEntry], type[SectionWithNormalEntries]]
|
|
|
|
| tuple[type[OneLineEntry], type[SectionWithOneLineEntries]]
|
2024-01-28 18:15:26 +00:00
|
|
|
| tuple[type[str], type[SectionWithTextEntries]],
|
2024-01-18 22:37:40 +00:00
|
|
|
] = {
|
|
|
|
"Education": (EducationEntry, SectionWithEducationEntries),
|
|
|
|
"Experience": (ExperienceEntry, SectionWithExperienceEntries),
|
|
|
|
"Work Experience": (ExperienceEntry, SectionWithExperienceEntries),
|
|
|
|
"Research Experience": (ExperienceEntry, SectionWithExperienceEntries),
|
|
|
|
"Publications": (PublicationEntry, SectionWithPublicationEntries),
|
|
|
|
"Papers": (PublicationEntry, SectionWithPublicationEntries),
|
|
|
|
"Projects": (NormalEntry, SectionWithNormalEntries),
|
|
|
|
"Academic Projects": (NormalEntry, SectionWithNormalEntries),
|
|
|
|
"University Projects": (NormalEntry, SectionWithNormalEntries),
|
|
|
|
"Personal Projects": (NormalEntry, SectionWithNormalEntries),
|
|
|
|
"Certificates": (NormalEntry, SectionWithNormalEntries),
|
|
|
|
"Extracurricular Activities": (ExperienceEntry, SectionWithExperienceEntries),
|
|
|
|
"Test Scores": (OneLineEntry, SectionWithOneLineEntries),
|
|
|
|
"Skills": (OneLineEntry, SectionWithOneLineEntries),
|
2024-01-26 18:47:42 +00:00
|
|
|
"Programming Skills": (NormalEntry, SectionWithNormalEntries),
|
2024-01-18 22:37:40 +00:00
|
|
|
"Other Skills": (OneLineEntry, SectionWithOneLineEntries),
|
|
|
|
"Awards": (OneLineEntry, SectionWithOneLineEntries),
|
|
|
|
"Interests": (OneLineEntry, SectionWithOneLineEntries),
|
2024-01-28 18:15:26 +00:00
|
|
|
"Summary": (str, SectionWithTextEntries),
|
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-01-28 20:37:00 +00:00
|
|
|
@pydantic.model_validator(mode="after")
|
|
|
|
@classmethod
|
|
|
|
def check_networks(cls, model):
|
|
|
|
if model.network == "Mastodon":
|
|
|
|
if not model.username.startswith("@"):
|
|
|
|
raise ValueError(
|
|
|
|
"Mastodon username should start with '@'. The username is"
|
|
|
|
f" {model.username}."
|
|
|
|
)
|
|
|
|
if model.username.count("@") > 2:
|
|
|
|
raise ValueError(
|
|
|
|
"Mastodon username should contain only two '@'. The username is"
|
|
|
|
f" {model.username}."
|
|
|
|
)
|
|
|
|
|
|
|
|
return model
|
|
|
|
|
2024-01-26 18:47:42 +00:00
|
|
|
@pydantic.computed_field
|
|
|
|
@cached_property
|
2024-01-28 18:15:26 +00:00
|
|
|
def url(self) -> pydantic.HttpUrl:
|
|
|
|
"""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
|
|
|
|
|
|
|
|
HttpUrlAdapter = pydantic.TypeAdapter(pydantic.HttpUrl)
|
|
|
|
url = HttpUrlAdapter.validate_python(url)
|
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-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 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",
|
|
|
|
description="The email of the person. It will be rendered in the heading.",
|
|
|
|
)
|
2024-01-26 17:09:28 +00:00
|
|
|
phone: Optional[pydantic_phone_numbers.PhoneNumber] = None
|
|
|
|
website: Optional[pydantic.HttpUrl] = None
|
|
|
|
social_networks: Optional[list[SocialNetwork]] = pydantic.Field(
|
2024-01-18 17:24:30 +00:00
|
|
|
default=None,
|
|
|
|
title="Social Networks",
|
|
|
|
description=(
|
|
|
|
"The social networks of the person. They will be rendered in the heading."
|
|
|
|
),
|
|
|
|
)
|
2024-01-26 17:09:28 +00:00
|
|
|
section_order: Optional[list[str]] = pydantic.Field(
|
2024-01-18 17:24:30 +00:00
|
|
|
default=None,
|
|
|
|
title="Section Order",
|
|
|
|
description=(
|
|
|
|
"The order of sections in the CV. The section title should be used."
|
|
|
|
),
|
|
|
|
)
|
2024-01-18 22:37:40 +00:00
|
|
|
sections_input: dict[
|
|
|
|
str,
|
|
|
|
Section
|
|
|
|
| list[EducationEntry]
|
|
|
|
| list[NormalEntry]
|
|
|
|
| list[OneLineEntry]
|
|
|
|
| list[PublicationEntry]
|
|
|
|
| list[ExperienceEntry]
|
2024-01-28 18:15:26 +00:00
|
|
|
| list[str],
|
2024-01-26 17:09:28 +00:00
|
|
|
] = pydantic.Field(
|
2024-01-18 17:24:30 +00:00
|
|
|
default=None,
|
|
|
|
title="Sections",
|
|
|
|
description="The sections of the CV.",
|
|
|
|
alias="sections",
|
|
|
|
)
|
|
|
|
|
2024-01-26 17:09:28 +00:00
|
|
|
@pydantic.field_validator("sections_input", mode="before")
|
2024-01-18 17:24:30 +00:00
|
|
|
@classmethod
|
2024-01-18 22:37:40 +00:00
|
|
|
def parse_and_validate_sections(
|
|
|
|
cls,
|
|
|
|
sections_input: dict[
|
|
|
|
str,
|
|
|
|
Section
|
|
|
|
| list[EducationEntry]
|
|
|
|
| list[NormalEntry]
|
|
|
|
| list[OneLineEntry]
|
|
|
|
| list[PublicationEntry]
|
|
|
|
| list[ExperienceEntry]
|
2024-01-28 18:15:26 +00:00
|
|
|
| list[str],
|
2024-01-18 22:37:40 +00:00
|
|
|
],
|
|
|
|
) -> dict[
|
|
|
|
str,
|
|
|
|
Section
|
|
|
|
| list[EducationEntry]
|
|
|
|
| list[NormalEntry]
|
|
|
|
| list[OneLineEntry]
|
|
|
|
| list[PublicationEntry]
|
|
|
|
| list[ExperienceEntry]
|
2024-01-28 18:15:26 +00:00
|
|
|
| list[str],
|
2024-01-18 22:37:40 +00:00
|
|
|
]:
|
2024-01-28 18:15:26 +00:00
|
|
|
"""
|
|
|
|
Parse and validate the sections of the CV.
|
|
|
|
"""
|
2024-01-18 17:24:30 +00:00
|
|
|
|
|
|
|
if sections_input is not None:
|
|
|
|
# check if the section names are unique, get the keys of the sections:
|
|
|
|
keys = list(sections_input.keys())
|
|
|
|
unique_keys = list(set(keys))
|
|
|
|
if len(keys) != len(unique_keys):
|
2024-01-18 22:37:40 +00:00
|
|
|
duplicate_keys = list(set([key for key in keys if keys.count(key) > 1]))
|
2024-01-18 17:24:30 +00:00
|
|
|
raise ValueError(
|
|
|
|
"The section names should be unique. The following section names"
|
|
|
|
f" are duplicated: {duplicate_keys}"
|
|
|
|
)
|
|
|
|
|
2024-01-18 22:37:40 +00:00
|
|
|
for title, section_or_entries in sections_input.items():
|
2024-01-28 18:15:26 +00:00
|
|
|
title = title.replace("_", " ").title()
|
2024-01-18 22:37:40 +00:00
|
|
|
if isinstance(section_or_entries, list):
|
|
|
|
# Then it means the user provided entries directly. Then it means
|
|
|
|
# the section title should have a default entry type.
|
|
|
|
if title in default_entry_types_for_a_given_title:
|
|
|
|
(
|
|
|
|
entry_type,
|
|
|
|
section_type,
|
|
|
|
) = default_entry_types_for_a_given_title[title]
|
|
|
|
|
|
|
|
# try if the entries are of the correct type by casting them
|
|
|
|
# to the entry type one by one
|
|
|
|
for entry in section_or_entries:
|
2024-01-28 18:15:26 +00:00
|
|
|
if entry_type is str:
|
2024-01-26 18:47:42 +00:00
|
|
|
if not isinstance(entry, str):
|
|
|
|
raise pydantic.ValidationError(
|
|
|
|
f'"{entry}" is not a valid string.'
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
entry = entry_type(**entry) # type: ignore
|
|
|
|
except pydantic.ValidationError as err:
|
|
|
|
raise pydantic.ValidationError(
|
|
|
|
f'"{entry}" is not a valid'
|
|
|
|
f" {entry_type.__name__}."
|
|
|
|
) from err
|
2024-01-18 22:37:40 +00:00
|
|
|
|
|
|
|
else:
|
2024-01-18 17:24:30 +00:00
|
|
|
raise ValueError(
|
2024-01-28 18:15:26 +00:00
|
|
|
f'"{title}" doesn\'t have a default entry type. Please'
|
|
|
|
" provide the entry type."
|
2024-01-18 17:24:30 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
return sections_input
|
|
|
|
|
2024-01-26 17:09:28 +00:00
|
|
|
@pydantic.computed_field
|
2024-01-18 17:24:30 +00:00
|
|
|
@cached_property
|
|
|
|
def sections(self) -> list[Section]:
|
2024-01-28 18:15:26 +00:00
|
|
|
"""Return all the sections of the CV with their titles."""
|
2024-01-18 17:24:30 +00:00
|
|
|
sections = []
|
|
|
|
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-01-18 22:37:40 +00:00
|
|
|
if isinstance(
|
|
|
|
section_or_entries,
|
|
|
|
(
|
|
|
|
SectionWithEducationEntries,
|
|
|
|
SectionWithExperienceEntries,
|
|
|
|
SectionWithNormalEntries,
|
|
|
|
SectionWithOneLineEntries,
|
|
|
|
SectionWithPublicationEntries,
|
|
|
|
SectionWithTextEntries,
|
|
|
|
),
|
|
|
|
):
|
|
|
|
if section_or_entries.title is None:
|
|
|
|
section_or_entries.title = title
|
2024-01-28 18:15:26 +00:00
|
|
|
|
2024-01-18 22:37:40 +00:00
|
|
|
sections.append(section_or_entries)
|
2024-01-28 18:15:26 +00:00
|
|
|
|
2024-01-18 22:37:40 +00:00
|
|
|
elif isinstance(section_or_entries, list):
|
|
|
|
if title in default_entry_types_for_a_given_title:
|
|
|
|
(
|
|
|
|
entry_type,
|
|
|
|
section_type,
|
|
|
|
) = default_entry_types_for_a_given_title[title]
|
2024-01-28 18:15:26 +00:00
|
|
|
|
|
|
|
if entry_type is str:
|
|
|
|
entry_type = "TextEntry"
|
|
|
|
else:
|
|
|
|
entry_type = entry_type.__name__
|
|
|
|
|
2024-01-18 22:37:40 +00:00
|
|
|
section = section_type(
|
|
|
|
title=title,
|
2024-01-28 18:15:26 +00:00
|
|
|
entry_type=entry_type, # type: ignore
|
2024-01-18 22:37:40 +00:00
|
|
|
entries=section_or_entries, # type: ignore
|
2024-01-18 17:24:30 +00:00
|
|
|
)
|
|
|
|
sections.append(section)
|
|
|
|
else:
|
|
|
|
raise RuntimeError(
|
|
|
|
"This error shouldn't have been raised. Please open an"
|
|
|
|
" issue on GitHub."
|
|
|
|
)
|
2024-01-28 18:15:26 +00:00
|
|
|
|
2024-01-18 17:24:30 +00:00
|
|
|
else:
|
|
|
|
raise RuntimeError(
|
|
|
|
"This error shouldn't have been raised. Please open an"
|
|
|
|
" issue on GitHub."
|
|
|
|
)
|
|
|
|
|
|
|
|
return sections
|
|
|
|
|
|
|
|
|
|
|
|
# ======================================================================================
|
|
|
|
# ======================================================================================
|
|
|
|
# ======================================================================================
|
|
|
|
|
|
|
|
|
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-01-28 18:15:26 +00:00
|
|
|
design: ClassicThemeOptions = pydantic.Field(
|
|
|
|
title="Design",
|
|
|
|
description="The design information.",
|
|
|
|
discriminator="theme",
|
|
|
|
)
|
2024-01-18 17:24:30 +00:00
|
|
|
|
|
|
|
|
|
|
|
def generate_json_schema(output_directory: str) -> str:
|
|
|
|
"""Generate the JSON schema of the data model and save it to a file.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
output_directory (str): The output directory to save the schema.
|
|
|
|
"""
|
|
|
|
|
2024-01-26 17:09:28 +00:00
|
|
|
class RenderCVSchemaGenerator(pydantic.json_schema.GenerateJsonSchema):
|
2024-01-18 17:24:30 +00:00
|
|
|
def generate(self, schema, mode="validation"):
|
|
|
|
json_schema = super().generate(schema, mode=mode)
|
|
|
|
json_schema["title"] = "RenderCV Input"
|
|
|
|
|
|
|
|
# remove the description of the class (RenderCVDataModel)
|
|
|
|
del json_schema["description"]
|
|
|
|
|
|
|
|
# add $id
|
|
|
|
json_schema[
|
|
|
|
"$id"
|
|
|
|
] = "https://raw.githubusercontent.com/sinaatalay/rendercv/main/schema.json"
|
|
|
|
|
|
|
|
# add $schema
|
|
|
|
json_schema["$schema"] = "http://json-schema.org/draft-07/schema#"
|
|
|
|
|
|
|
|
# Loop through $defs and remove docstring descriptions and fix optional
|
|
|
|
# fields
|
|
|
|
for key, value in json_schema["$defs"].items():
|
|
|
|
# Don't allow additional properties
|
|
|
|
value["additionalProperties"] = False
|
|
|
|
|
|
|
|
# I don't want the docstrings in the schema, so remove them:
|
|
|
|
if "This class" in value["description"]:
|
|
|
|
del value["description"]
|
|
|
|
|
|
|
|
# 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"]
|
|
|
|
|
|
|
|
# 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"]
|
|
|
|
and "anyOf" in value["properties"]["date"]
|
|
|
|
):
|
|
|
|
del value["properties"]["date"]["anyOf"][0]
|
|
|
|
|
|
|
|
return json_schema
|
|
|
|
|
|
|
|
schema = RenderCVDataModel.model_json_schema(
|
|
|
|
schema_generator=RenderCVSchemaGenerator
|
|
|
|
)
|
|
|
|
schema = json.dumps(schema, indent=2)
|
|
|
|
|
|
|
|
# Change all anyOf to oneOf
|
|
|
|
schema = schema.replace('"anyOf"', '"oneOf"')
|
|
|
|
|
|
|
|
path_to_schema = os.path.join(output_directory, "schema.json")
|
|
|
|
with open(path_to_schema, "w") as f:
|
|
|
|
f.write(schema)
|
|
|
|
|
|
|
|
return path_to_schema
|