reader: create models package inside reader

This commit is contained in:
Sina Atalay
2024-07-03 02:05:04 +03:00
parent dd1042ab4c
commit ff2ed59ee1
22 changed files with 636 additions and 818 deletions

2
.gitmodules vendored
View File

@@ -1,3 +1,3 @@
[submodule "rendercv/tinytex-release"]
path = rendercv/tinytex-release
path = rendercv/renderer/tinytex-release
url = https://github.com/sinaatalay/tinytex-release.git

View File

@@ -12,44 +12,29 @@ The validators and data format of RenderCV are written using
"""
from .models import (
BulletEntry,
CurriculumVitae,
SocialNetwork,
available_theme_options,
BulletEntry,
EducationEntry,
ExperienceEntry,
LocaleCatalog,
NormalEntry,
OneLineEntry,
PublicationEntry,
LocaleCatalog,
RenderCVDataModel,
SocialNetwork,
locale_catalog,
read_input_file,
)
from .generators import (
generate_json_schema_file,
generate_json_schema,
create_a_sample_yaml_input_file,
get_a_sample_data_model,
)
from .field_types import (
available_social_networks,
)
from .model_types import (
available_entry_type_names,
available_themes,
Entry,
SectionInput,
)
from .computed_fields import (
format_date,
)
from .utilities import set_or_update_a_value, dictionary_to_yaml
from .reader import (
create_a_sample_data_model,
create_a_sample_yaml_input_file,
generate_json_schema_file,
generate_json_schema,
set_or_update_a_value,
read_input_file,
)
__all__ = [
"OneLineEntry",
@@ -62,15 +47,13 @@ __all__ = [
"CurriculumVitae",
"LocaleCatalog",
"RenderCVDataModel",
"locale_catalog",
"available_theme_options",
"available_themes",
"create_a_sample_data_model",
"create_a_sample_yaml_input_file",
"generate_json_schema_file",
"generate_json_schema",
"create_a_sample_yaml_input_file",
"get_a_sample_data_model",
"available_entry_type_names",
"available_themes",
"available_social_networks",
"read_input_file",
"set_or_update_a_value",
"dictionary_to_yaml",
"read_input_file",
"format_date",
]

View File

@@ -1,98 +0,0 @@
"""
The `rendercv.data_models.models` module contains all the Pydantic data models used in
RenderCV. These data models define the data format and the usage of computed fields and
the validators.
"""
import functools
from typing import Annotated, Optional
import pathlib
import annotated_types as at
import pydantic
import pydantic_extra_types.phone_numbers as pydantic_phone_numbers
from ..themes.classic import ClassicThemeOptions
from . import computed_fields as cf
from . import field_types
from . import utilities as util
from . import entry_validators
from . import entry_types
# Disable Pydantic warnings:
# warnings.filterwarnings("ignore")
from . import cv_model
from . import design_model
from . import locale_catalog_model
class RenderCVDataModel(entry_types.RenderCVBaseModel):
"""This class binds both the CV and the design information together."""
cv: cv_model.CurriculumVitae = pydantic.Field(
title="Curriculum Vitae",
description="The data of the CV.",
)
design: design_model.RenderCVDesign = pydantic.Field(
default=ClassicThemeOptions(theme="classic"),
title="Design",
description=(
"The design information of the CV. The default is the classic theme."
),
)
locale_catalog: Optional[locale_catalog_model.LocaleCatalog] = pydantic.Field(
default=None,
title="Locale Catalog",
description=(
"The locale catalog of the CV to allow the support of multiple languages."
),
)
def read_input_file(
file_path_or_contents: pathlib.Path | str,
) -> RenderCVDataModel:
"""Read the input file (YAML or JSON) and return them as an instance of
`RenderCVDataModel`, which is a Pydantic data model of RenderCV's data format.
Args:
file_path_or_contents (str): The path to the input file or the contents of the
input file as a string.
Returns:
RenderCVDataModel: The data models with $\\LaTeX$ and Markdown strings.
"""
if isinstance(file_path_or_contents, pathlib.Path):
# Check if the file exists:
if not file_path_or_contents.exists():
raise FileNotFoundError(
f"The input file [magenta]{file_path_or_contents}[/magenta] doesn't"
" exist!"
)
# Check the file extension:
accepted_extensions = [".yaml", ".yml", ".json", ".json5"]
if file_path_or_contents.suffix not in accepted_extensions:
user_friendly_accepted_extensions = [
f"[green]{ext}[/green]" for ext in accepted_extensions
]
user_friendly_accepted_extensions = ", ".join(
user_friendly_accepted_extensions
)
raise ValueError(
"The input file should have one of the following extensions:"
f" {user_friendly_accepted_extensions}. The input file is"
f" [magenta]{file_path_or_contents}[/magenta]."
)
file_content = file_path_or_contents.read_text(encoding="utf-8")
else:
file_content = file_path_or_contents
input_as_dictionary: dict[str, Any] = ruamel.yaml.YAML().load(file_content) # type: ignore
# Validate the parsed dictionary by creating an instance of RenderCVDataModel:
rendercv_data_model = RenderCVDataModel(**input_as_dictionary)
return rendercv_data_model

View File

@@ -1,16 +0,0 @@
"""
The `rendercv.data_models.validators` module contains all the functions used to validate
the data models of RenderCV, in addition to Pydantic inner validation.
"""
import re
from datetime import date as Date
from typing import Optional
import pydantic
from . import utilities as util
from .entry_types import StartDate, EndDate, ArbitraryDate

View File

View File

@@ -1,392 +0,0 @@
"""
The `rendercv.data_models.generators` module contains the functions that are used to
generate a sample YAML input file and the JSON schema of RenderCV based on the data
models defined in `rendercv.data_models.models`.
"""
import json
import pathlib
from typing import Any, Optional
import pydantic
from ..themes.classic import ClassicThemeOptions
from ..themes.sb2nov import Sb2novThemeOptions
from ..themes.moderncv import ModerncvThemeOptions
from ..themes.engineeringresumes import EngineeringresumesThemeOptions
from . import models
from . import utilities as utils
from . import entry_validators as field_val
def get_a_sample_data_model(
name: str = "John Doe", theme: str = "classic"
) -> models.RenderCVDataModel:
"""Return a sample data model for new users to start with.
Args:
name (str, optional): The name of the person. Defaults to "John Doe".
Returns:
RenderCVDataModel: A sample data model.
"""
# Check if the theme is valid:
if theme not in models.available_themes:
available_themes_string = ", ".join(models.available_themes)
raise ValueError(
f"The theme should be one of the following: {available_themes_string}!"
f' The provided theme is "{theme}".'
)
name = name.encode().decode("unicode-escape")
sections = {
"welcome_to_RenderCV!": [
(
"[RenderCV](https://github.com/sinaatalay/rendercv) is a LaTeX-based"
" CV/resume framework. It allows you to create a high-quality CV or"
" resume as a PDF file from a YAML file, with **Markdown syntax"
" support** and **complete control over the LaTeX code**."
),
(
"The boilerplate content is taken from"
" [here](https://github.com/dnl-blkv/mcdowell-cv), where a"
" *clean and tidy CV* pattern is proposed by"
" **[Gayle Laakmann McDowell](https://www.gayle.com/)**."
),
],
"quick_guide": [
models.BulletEntry(
bullet=(
"Each section title is arbitrary, and each section contains a list"
" of entries."
),
),
models.BulletEntry(
bullet=(
"There are 7 unique entry types: *models.BulletEntry*, *TextEntry*,"
" *models.EducationEntry*, *models.ExperienceEntry*,"
" *models.NormalEntry*, *models.PublicationEntry*, and"
" *models.OneLineEntry*."
),
),
models.BulletEntry(
bullet=(
"Select a section title, pick an entry type, and start writing your"
" section!"
)
),
models.BulletEntry(
bullet=(
"[Here](https://docs.rendercv.com/user_guide/), you can find a"
" comprehensive user guide for RenderCV."
)
),
],
"education": [
models.EducationEntry(
institution="University of Pennsylvania",
area="Computer Science",
degree="BS",
start_date="2000-09",
end_date="2005-05",
highlights=[
"GPA: 3.9/4.0 ([Transcript](https://example.com))",
(
"**Coursework:** Computer Architecture, Artificial"
" Intelligence, Comparison of Learning Algorithms,"
" Computational Theory"
),
],
),
],
"experience": [
models.ExperienceEntry(
company="Apple",
position="Software Engineer",
start_date="2005-06",
end_date="2007-08",
location="Cupertino, CA",
highlights=[
(
"Reduced time to render the user's buddy list by 75% by"
" implementing a prediction algorithm"
),
(
"Implemented iChat integration with OS X Spotlight Search by"
" creating a tool to extract metadata from saved chat"
" transcripts and provide metadata to a system-wide search"
" database"
),
(
"Redesigned chat file format and implemented backward"
" compatibility for search"
),
],
),
models.ExperienceEntry(
company="Microsoft",
position="Lead Student Ambassador",
start_date="2003-09",
end_date="2005-04",
location="Redmond, WA",
highlights=[
(
"Promoted to Lead Student Ambassador in the Fall of 2004,"
" supervised 10-15 Student Ambassadors"
),
(
"Created and taught a computer science course, CSE 099:"
" Software Design and Development"
),
],
),
models.ExperienceEntry(
company="University of Pennsylvania",
position="Head Teaching Assistant",
start_date="2001-10",
end_date="2003-05",
location="Philadelphia, PA",
highlights=[
(
"Implemented a user interface for the VS open file switcher"
" (ctrl-tab) and extended it to tool windows"
),
(
"Created a service to provide gradient across VS and VS"
" add-ins, optimized its performance via caching"
),
"Programmer Productivity Research Center (Summers 2001, 2002)",
(
"Built an app to compute the similarity of all methods in a"
" code base, reducing the time from $\\mathcal{O}(n^2)$ to"
" $\\mathcal{O}(n \\log n)$"
),
(
"Created a test case generation tool that creates random XML"
" docs from XML Schema"
),
],
),
models.ExperienceEntry(
company="Microsoft",
position="Software Engineer, Intern",
start_date="2003-06",
end_date="2003-08",
location="Redmond, WA",
highlights=[
(
"Automated the extraction and processing of large datasets from"
" legacy systems using SQL and Perl scripts"
),
],
),
],
"publications": [
models.PublicationEntry(
title=(
"Magneto-Thermal Thin Shell Approximation for 3D Finite Element"
" Analysis of No-Insulation Coils"
),
authors=[
"Albert Smith",
f"***{name}***",
"Jane Derry",
"Harry Tom",
"Frodo Baggins",
],
date="2004-01",
doi="10.1109/TASC.2023.3340648",
)
],
"projects": [
models.NormalEntry(
name="Multi-User Drawing Tool",
date="[github.com/name/repo](https://github.com/sinaatalay/rendercv)",
highlights=[
(
"Developed an electronic classroom where multiple users can"
' view and simultaneously draw on a "chalkboard" with each'
" person's edits synchronized"
),
"Tools Used: C++, MFC",
],
),
models.NormalEntry(
name="Synchronized Calendar",
date="[github.com/name/repo](https://github.com/sinaatalay/rendercv)",
highlights=[
(
"Developed a desktop calendar with globally shared and"
" synchronized calendars, allowing users to schedule meetings"
" with other users"
),
"Tools Used: C#, .NET, SQL, XML",
],
),
models.NormalEntry(
name="Operating System",
date="2002",
highlights=[
(
"Developed a UNIX-style OS with a scheduler, file system, text"
" editor, and calculator"
),
"Tools Used: C",
],
),
],
"additional_experience_and_awards": [
models.OneLineEntry(
label="Instructor (2003-2005)",
details="Taught 2 full-credit computer science courses",
),
models.OneLineEntry(
label="Third Prize, Senior Design Project",
details=(
"Awarded 3rd prize for a synchronized calendar project out of 100"
" entries"
),
),
],
"technologies": [
models.OneLineEntry(
label="Languages",
details="C++, C, Java, Objective-C, C#, SQL, JavaScript",
),
models.OneLineEntry(
label="Software",
details=".NET, Microsoft SQL Server, XCode, Interface Builder",
),
],
}
cv = models.CurriculumVitae(
name=name,
location="Your Location",
email="youremail@yourdomain.com",
phone="+905419999999", # type: ignore
website="https://yourwebsite.com", # type: ignore
social_networks=[
models.SocialNetwork(network="LinkedIn", username="yourusername"),
models.SocialNetwork(network="GitHub", username="yourusername"),
],
sections=sections, # type: ignore
)
themes = {
"classic": ClassicThemeOptions,
"moderncv": ModerncvThemeOptions,
"sb2nov": Sb2novThemeOptions,
"engineeringresumes": EngineeringresumesThemeOptions,
}
design = themes[theme](theme=theme)
return models.RenderCVDataModel(cv=cv, design=design)
def create_a_sample_yaml_input_file(
input_file_path: Optional[pathlib.Path] = None,
name: str = "John Doe",
theme: str = "classic",
) -> str:
"""Create a sample YAML input file and return it as a string. If the input file path
is provided, then also save the contents to the file.
Args:
input_file_path (pathlib.Path, optional): The path to save the input file.
Defaults to None.
name (str, optional): The name of the person. Defaults to "John Doe".
theme (str, optional): The theme of the CV. Defaults to "classic".
Returns:
str: The sample YAML input file as a string.
"""
data_model = get_a_sample_data_model(name=name, theme=theme)
# Instead of getting the dictionary with data_model.model_dump() directly, we
# convert it to JSON and then to a dictionary. Because the YAML library we are
# using sometimes has problems with the dictionary returned by model_dump().
# We exclude "cv.sections" because the data model automatically generates them.
# The user's "cv.sections" input is actually "cv.sections_input" in the data
# model. It is shown as "cv.sections" in the YAML file because an alias is being
# used. If"cv.sections" were not excluded, the automatically generated
# "cv.sections" would overwrite the "cv.sections_input". "cv.sections" are
# automatically generated from "cv.sections_input" to make the templating
# process easier. "cv.sections_input" exists for the convenience of the user.
data_model_as_json = data_model.model_dump_json(
exclude_none=True, by_alias=True, exclude={"cv": {"sections"}}
)
data_model_as_dictionary = json.loads(data_model_as_json)
yaml_string = utils.dictionary_to_yaml(data_model_as_dictionary)
if input_file_path is not None:
input_file_path.write_text(yaml_string, encoding="utf-8")
return yaml_string
def generate_json_schema() -> dict[str, Any]:
"""Generate the JSON schema of RenderCV.
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/).
Returns:
dict: The JSON schema of RenderCV.
"""
class RenderCVSchemaGenerator(pydantic.json_schema.GenerateJsonSchema):
def generate(self, schema, mode="validation"): # type: ignore
json_schema = super().generate(schema, mode=mode)
# Basic information about the schema:
json_schema["title"] = "RenderCV"
json_schema["description"] = "RenderCV data model."
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
for object_name, value in json_schema["$defs"].items():
# 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 = {
"type": "null",
}
for field_name, field in value["properties"].items():
if "anyOf" in field:
if null_type_dict in field["anyOf"]:
field["anyOf"].remove(null_type_dict)
field["oneOf"] = field["anyOf"]
del field["anyOf"]
return json_schema
schema = models.RenderCVDataModel.model_json_schema(
schema_generator=RenderCVSchemaGenerator
)
return schema
def generate_json_schema_file(json_schema_path: pathlib.Path):
"""Generate the JSON schema of RenderCV and save it to a file.
Args:
json_schema_path (pathlib.Path): The path to save the JSON schema.
"""
schema = generate_json_schema()
schema_json = json.dumps(schema, indent=2, ensure_ascii=False)
json_schema_path.write_text(schema_json, encoding="utf-8")

View File

View File

@@ -1,19 +0,0 @@
from typing import Optional, Any, Type, Literal
import pathlib
import pydantic
import importlib
import importlib.util
# from .types import (
# available_entry_types,
# available_theme_options,
# available_themes,
# available_entry_type_names,
# # RenderCVBuiltinDesign,
# )
from . import utilities as util
from . import field_types
from .models import RenderCVBaseModel

View File

View File

@@ -0,0 +1,29 @@
from .curriculum_vitae import CurriculumVitae, SocialNetwork
from .design import available_theme_options
from .entry_types import (
BulletEntry,
EducationEntry,
ExperienceEntry,
NormalEntry,
OneLineEntry,
PublicationEntry,
)
from .locale_catalog import LocaleCatalog
from .rendercv_data_model import RenderCVDataModel
from .computers import format_date
__all__ = [
"OneLineEntry",
"BulletEntry",
"EducationEntry",
"ExperienceEntry",
"PublicationEntry",
"NormalEntry",
"SocialNetwork",
"CurriculumVitae",
"LocaleCatalog",
"RenderCVDataModel",
"available_theme_options",
"format_date",
]

View File

@@ -7,12 +7,9 @@ etc.
from datetime import date as Date
from typing import Optional
import re
# from .models import locale_catalog, CurriculumVitae
from . import utilities as util
# from . import validators as val
from .locale_catalog import locale_catalog
def format_date(date: Date, use_full_name: bool = False) -> str:
@@ -91,8 +88,8 @@ def compute_time_span_string(
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.
start_year = util.get_date_object(start_date).year # type: ignore
end_year = util.get_date_object(end_date).year # type: ignore
start_year = get_date_object(start_date).year # type: ignore
end_year = get_date_object(end_date).year # type: ignore
time_span_in_years = end_year - start_year
@@ -106,8 +103,8 @@ def compute_time_span_string(
else:
# Then it means both start_date and end_date are in YYYY-MM-DD or YYYY-MM
# format.
end_date = util.get_date_object(end_date) # type: ignore
start_date = util.get_date_object(start_date) # type: ignore
end_date = get_date_object(end_date) # type: ignore
start_date = get_date_object(start_date) # type: ignore
# Calculate the number of days between start_date and end_date:
timespan_in_days = (end_date - start_date).days # type: ignore
@@ -176,7 +173,7 @@ def compute_date_string(
date_string = str(date)
else:
try:
date_object = util.get_date_object(date)
date_object = get_date_object(date)
if show_only_years:
date_string = str(date_object.year)
else:
@@ -190,7 +187,7 @@ def compute_date_string(
start_date = str(start_date)
else:
# Then it means start_date is either in YYYY-MM-DD or YYYY-MM format
date_object = util.get_date_object(start_date)
date_object = get_date_object(start_date)
if show_only_years:
start_date = date_object.year
else:
@@ -203,7 +200,7 @@ def compute_date_string(
end_date = str(end_date)
else:
# Then it means end_date is either in YYYY-MM-DD or YYYY-MM format
date_object = util.get_date_object(end_date)
date_object = get_date_object(end_date)
if show_only_years:
end_date = date_object.year
else:
@@ -296,7 +293,7 @@ def compute_connections(cv) -> list[dict[str, str]]:
)
if cv.website is not None:
website_placeholder = util.make_a_url_clean(cv.website)
website_placeholder = make_a_url_clean(cv.website)
connections.append(
{
"latex_icon": "\\faLink",
@@ -320,7 +317,7 @@ def compute_connections(cv) -> list[dict[str, str]]:
"Google Scholar": "\\faGraduationCap",
}
for social_network in cv.social_networks:
clean_url = util.make_a_url_clean(social_network.url)
clean_url = make_a_url_clean(social_network.url)
connection = {
"latex_icon": icon_dictionary[social_network.network],
"url": social_network.url,
@@ -339,36 +336,83 @@ def compute_connections(cv) -> list[dict[str, str]]:
return connections
# def compute_sections(
# sections_input: Optional[dict[str, models.SectionInput]],
# ) -> list[models.SectionBase]:
# """Compute the sections of the CV based on the input sections.
def make_a_url_clean(url: str) -> str:
"""Make a URL clean by removing the protocol, www, and trailing slashes.
# The original `sections` input is a dictionary where the keys are the section titles
# and the values are the list of entries in that section. This function converts the
# input sections to a list of `SectionBase` objects. This makes it easier to work with
# the sections in the rest of the code.
Example:
```python
make_a_url_clean("https://www.example.com/")
```
returns
`#!python "example.com"`
# Args:
# sections_input (Optional[dict[str, SectionInput]]): The input sections.
# Returns:
# list[SectionBase]: The computed sections.
# """
# sections: list[models.SectionBase] = []
Args:
url (str): The URL to make clean.
Returns:
str: The clean URL.
"""
url = url.replace("https://", "").replace("http://", "").replace("www.", "")
if url.endswith("/"):
url = url[:-1]
# if sections_input is not None:
# for title, section_or_entries in sections_input.items():
# title = util.dictionary_key_to_proper_section_title(title)
return url
# entry_type_name = val.validate_an_entry_type_and_get_entry_type_name(
# section_or_entries[0]
# )
# section = models.SectionBase(
# title=title,
# entry_type=entry_type_name,
# entries=section_or_entries,
# )
# sections.append(section)
def get_date_object(date: str | int) -> Date:
"""Parse a date string in YYYY-MM-DD, YYYY-MM, or YYYY format and return a
`datetime.date` object. This function is used throughout the validation process of
the data models.
# return sections
Args:
date (str | int): The date string to parse.
Returns:
Date: The parsed date.
"""
if isinstance(date, int):
date_object = Date.fromisoformat(f"{date}-01-01")
elif re.fullmatch(r"\d{4}-\d{2}-\d{2}", date):
# Then it is in YYYY-MM-DD format
date_object = Date.fromisoformat(date)
elif re.fullmatch(r"\d{4}-\d{2}", date):
# Then it is in YYYY-MM format
date_object = Date.fromisoformat(f"{date}-01")
elif re.fullmatch(r"\d{4}", date):
# Then it is in YYYY format
date_object = Date.fromisoformat(f"{date}-01-01")
elif date == "present":
date_object = Date.today()
else:
raise ValueError(
"This is not a valid date! Please use either YYYY-MM-DD, YYYY-MM, or"
" YYYY format."
)
return date_object
def dictionary_key_to_proper_section_title(key: str) -> str:
"""Convert a dictionary key to a proper section title.
Example:
```python
dictionary_key_to_proper_section_title("section_title")
```
returns
`#!python "Section Title"`
Args:
key (str): The key to convert to a proper section title.
Returns:
str: The proper section title.
"""
title = key.replace("_", " ")
words = title.split(" ")
# loop through the words and if the word doesn't contain any uppercase letters,
# capitalize the first letter of the word. If the word contains uppercase letters,
# don't change the word.
proper_title = " ".join(
word.capitalize() if word.islower() else word for word in words
)
return proper_title

View File

@@ -4,7 +4,7 @@ import pydantic
import functools
from . import entry_types
from . import computed_fields as cf
from . import computers as cf
from typing import Type, Literal
import re

View File

@@ -1,32 +1,20 @@
from typing import Annotated, Any
from typing import Annotated, Any, Type
import pydantic
from ..themes.classic import ClassicThemeOptions
from ..themes.engineeringresumes import EngineeringresumesThemeOptions
from ..themes.moderncv import ModerncvThemeOptions
from ..themes.sb2nov import Sb2novThemeOptions
from ...themes.classic import ClassicThemeOptions
from ...themes.engineeringresumes import EngineeringresumesThemeOptions
from ...themes.moderncv import ModerncvThemeOptions
from ...themes.sb2nov import Sb2novThemeOptions
from typing import Optional, Any, Type, Literal
import pathlib
import pydantic
import importlib
import importlib.util
# from .types import (
# available_entry_types,
# available_theme_options,
# available_themes,
# available_entry_type_names,
# # RenderCVBuiltinDesign,
# )
from . import utilities as util
from . import field_types
from . import entry_types
# ======================================================================================
# Create validator functions: ==========================================================
# ======================================================================================
@@ -139,7 +127,7 @@ def validate_design_options(
else:
# Then it means there is no __init__.py file in the custom theme folder.
# Create a dummy data model and use that instead.
class ThemeOptionsAreNotProvided(RenderCVBaseModel):
class ThemeOptionsAreNotProvided(entry_types.RenderCVBaseModel):
theme: str = theme_name
theme_data_model = ThemeOptionsAreNotProvided(theme=theme_name)
@@ -185,5 +173,3 @@ available_theme_options = {
"sb2nov": Sb2novThemeOptions,
"engineeringresumes": EngineeringresumesThemeOptions,
}
available_themes = list(available_theme_options.keys())

View File

@@ -3,8 +3,8 @@ import pydantic
import functools
import re
from datetime import date as Date
from . import utilities as util
from . import computed_fields as cf
from .. import utilities as util
from . import computers as cf
# ======================================================================================

View File

@@ -0,0 +1,42 @@
"""
The `rendercv.data_models.models` module contains all the Pydantic data models used in
RenderCV. These data models define the data format and the usage of computed fields and
the validators.
"""
from typing import Optional
import pydantic
from . import entry_types
from ...themes.classic import ClassicThemeOptions
from .design import RenderCVDesign
from .curriculum_vitae import CurriculumVitae
from .locale_catalog import LocaleCatalog
# Disable Pydantic warnings:
# warnings.filterwarnings("ignore")
class RenderCVDataModel(entry_types.RenderCVBaseModel):
"""This class binds both the CV and the design information together."""
cv: CurriculumVitae = pydantic.Field(
title="Curriculum Vitae",
description="The data of the CV.",
)
design: RenderCVDesign = pydantic.Field(
default=ClassicThemeOptions(theme="classic"),
title="Design",
description=(
"The design information of the CV. The default is the classic theme."
),
)
locale_catalog: Optional[LocaleCatalog] = pydantic.Field(
default=None,
title="Locale Catalog",
description=(
"The locale catalog of the CV to allow the support of multiple languages."
),
)

309
rendercv/reader/reader.py Normal file
View File

@@ -0,0 +1,309 @@
"""
The `rendercv.data_models.generators` module contains the functions that are used to
generate a sample YAML input file and the JSON schema of RenderCV based on the data
models defined in `rendercv.data_models.models`.
"""
import json
import pathlib
from typing import Any, Optional
import io
import pydantic
import ruamel.yaml
from . import models
def dictionary_to_yaml(dictionary: dict[str, Any]):
"""Converts a dictionary to a YAML string.
Args:
dictionary (dict[str, Any]): The dictionary to be converted to YAML.
Returns:
str: The YAML string.
"""
yaml_object = ruamel.yaml.YAML()
yaml_object.encoding = "utf-8"
yaml_object.width = 60
yaml_object.indent(mapping=2, sequence=4, offset=2)
with io.StringIO() as string_stream:
yaml_object.dump(dictionary, string_stream)
yaml_string = string_stream.getvalue()
return yaml_string
def read_a_yaml_file(file_path_or_contents: pathlib.Path) -> dict[str, Any]:
"""Read a YAML file and return its content as a dictionary. The YAML file can be
given as a path to the file or as the contents of the file as a string.
Args:
file_path_or_contents (pathlib.Path): The path to the YAML file or the contents
of the YAML file as a string.
Returns:
dict: The content of the YAML file as a dictionary.
"""
if isinstance(file_path_or_contents, pathlib.Path):
# Check if the file exists:
if not file_path_or_contents.exists():
raise FileNotFoundError(
f"The input file [magenta]{file_path_or_contents}[/magenta] doesn't"
" exist!"
)
# Check the file extension:
accepted_extensions = [".yaml", ".yml", ".json", ".json5"]
if file_path_or_contents.suffix not in accepted_extensions:
user_friendly_accepted_extensions = [
f"[green]{ext}[/green]" for ext in accepted_extensions
]
user_friendly_accepted_extensions = ", ".join(
user_friendly_accepted_extensions
)
raise ValueError(
"The input file should have one of the following extensions:"
f" {user_friendly_accepted_extensions}. The input file is"
f" [magenta]{file_path_or_contents}[/magenta]."
)
file_content = file_path_or_contents.read_text(encoding="utf-8")
else:
file_content = file_path_or_contents
yaml_as_a_dictionary: dict[str, Any] = ruamel.yaml.YAML().load(file_content)
return yaml_as_a_dictionary
def create_a_sample_data_model(
name: str = "John Doe", theme: str = "classic"
) -> models.RenderCVDataModel:
"""Return a sample data model for new users to start with.
Args:
name (str, optional): The name of the person. Defaults to "John Doe".
Returns:
RenderCVDataModel: A sample data model.
"""
# Check if the theme is valid:
if theme not in models.available_theme_options:
available_themes_string = ", ".join(models.available_theme_options.keys())
raise ValueError(
f"The theme should be one of the following: {available_themes_string}!"
f' The provided theme is "{theme}".'
)
# read the sample_content.yaml file
sample_content = pathlib.Path(__file__).parent / "sample_content.yaml"
sample_content_dictionary = read_a_yaml_file(sample_content)
cv = models.CurriculumVitae(**sample_content_dictionary)
# Update the name:
name = name.encode().decode("unicode-escape")
cv.name = name
design = models.available_theme_options[theme](theme=theme)
return models.RenderCVDataModel(cv=cv, design=design)
def create_a_sample_yaml_input_file(
input_file_path: Optional[pathlib.Path] = None,
name: str = "John Doe",
theme: str = "classic",
) -> str:
"""Create a sample YAML input file and return it as a string. If the input file path
is provided, then also save the contents to the file.
Args:
input_file_path (pathlib.Path, optional): The path to save the input file.
Defaults to None.
name (str, optional): The name of the person. Defaults to "John Doe".
theme (str, optional): The theme of the CV. Defaults to "classic".
Returns:
str: The sample YAML input file as a string.
"""
data_model = create_a_sample_data_model(name=name, theme=theme)
# Instead of getting the dictionary with data_model.model_dump() directly, we
# convert it to JSON and then to a dictionary. Because the YAML library we are
# using sometimes has problems with the dictionary returned by model_dump().
# We exclude "cv.sections" because the data model automatically generates them.
# The user's "cv.sections" input is actually "cv.sections_input" in the data
# model. It is shown as "cv.sections" in the YAML file because an alias is being
# used. If"cv.sections" were not excluded, the automatically generated
# "cv.sections" would overwrite the "cv.sections_input". "cv.sections" are
# automatically generated from "cv.sections_input" to make the templating
# process easier. "cv.sections_input" exists for the convenience of the user.
data_model_as_json = data_model.model_dump_json(
exclude_none=True, by_alias=True, exclude={"cv": {"sections"}}
)
data_model_as_dictionary = json.loads(data_model_as_json)
yaml_string = dictionary_to_yaml(data_model_as_dictionary)
if input_file_path is not None:
input_file_path.write_text(yaml_string, encoding="utf-8")
return yaml_string
def generate_json_schema() -> dict[str, Any]:
"""Generate the JSON schema of RenderCV.
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/).
Returns:
dict: The JSON schema of RenderCV.
"""
class RenderCVSchemaGenerator(pydantic.json_schema.GenerateJsonSchema):
def generate(self, schema, mode="validation"): # type: ignore
json_schema = super().generate(schema, mode=mode)
# Basic information about the schema:
json_schema["title"] = "RenderCV"
json_schema["description"] = "RenderCV data model."
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
for object_name, value in json_schema["$defs"].items():
# 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 = {
"type": "null",
}
for field_name, field in value["properties"].items():
if "anyOf" in field:
if null_type_dict in field["anyOf"]:
field["anyOf"].remove(null_type_dict)
field["oneOf"] = field["anyOf"]
del field["anyOf"]
return json_schema
schema = models.RenderCVDataModel.model_json_schema(
schema_generator=RenderCVSchemaGenerator
)
return schema
def generate_json_schema_file(json_schema_path: pathlib.Path):
"""Generate the JSON schema of RenderCV and save it to a file.
Args:
json_schema_path (pathlib.Path): The path to save the JSON schema.
"""
schema = generate_json_schema()
schema_json = json.dumps(schema, indent=2, ensure_ascii=False)
json_schema_path.write_text(schema_json, encoding="utf-8")
def set_or_update_a_value(
data_model: pydantic.BaseModel | dict | list,
key: str,
value: str,
sub_model: pydantic.BaseModel | dict | list = None,
):
"""Set or update a value in a data model for a specific key. For example, a key can
be `cv.sections.education.3.institution` and the value can be "Bogazici University".
Args:
data_model (pydantic.BaseModel | dict | list): The data model to set or update
the value.
key (str): The key to set or update the value.
value (Any): The value to set or update.
sub_model (pydantic.BaseModel | dict | list, optional): The sub model to set or
update the value. This is used for recursive calls. When the value is set
to a sub model, the original data model is validated. Defaults to None.
"""
# Recursively call this function until the last key is reached:
# Rename `sections` with `sections_input` since the key is `sections` is an alias:
key = key.replace("sections.", "sections_input.")
keys = key.split(".")
if sub_model is not None:
model = sub_model
else:
model = data_model
if len(keys) == 1:
# Set the value:
if value.startswith("{") and value.endswith("}"):
# Allow users to assign dictionaries:
value = eval(value)
elif value.startswith("[") and value.endswith("]"):
# Allow users to assign lists:
value = eval(value)
if isinstance(model, pydantic.BaseModel):
setattr(model, key, value)
elif isinstance(model, dict):
model[key] = value
elif isinstance(model, list):
model[int(key)] = value
else:
raise ValueError(
"The data model should be either a Pydantic data model, dictionary, or"
" list.",
)
data_model = type(data_model).model_validate(
(data_model.model_dump(by_alias=True))
)
return data_model
else:
# get the first key and call the function with remaining keys:
first_key = keys[0]
key = ".".join(keys[1:])
if isinstance(model, pydantic.BaseModel):
sub_model = getattr(model, first_key)
elif isinstance(model, dict):
sub_model = model[first_key]
elif isinstance(model, list):
sub_model = model[int(first_key)]
else:
raise ValueError(
"The data model should be either a Pydantic data model, dictionary, or"
" list.",
)
set_or_update_a_value(data_model, key, value, sub_model)
def read_input_file(
file_path_or_contents: pathlib.Path | str,
) -> models.RenderCVDataModel:
"""Read the input file (YAML or JSON) and return them as an instance of
`RenderCVDataModel`, which is a Pydantic data model of RenderCV's data format.
Args:
file_path_or_contents (str): The path to the input file or the contents of the
input file as a string.
Returns:
RenderCVDataModel: The data models with $\\LaTeX$ and Markdown strings.
"""
input_as_dictionary = read_a_yaml_file(file_path_or_contents)
# Validate the parsed dictionary by creating an instance of RenderCVDataModel:
rendercv_data_model = models.RenderCVDataModel(**input_as_dictionary)
return rendercv_data_model

View File

@@ -0,0 +1,135 @@
name: John Doe
location: Your Location
email: youremail@yourdomain.com
phone: tel:+90-541-999-99-99
website: https://yourwebsite.com/
social_networks:
- network: LinkedIn
username: yourusername
- network: GitHub
username: yourusername
sections:
welcome_to_RenderCV!:
- '[RenderCV](https://github.com/sinaatalay/rendercv) is
a LaTeX-based CV/resume framework. It allows you to create
a high-quality CV or resume as a PDF file from a YAML
file, with **full Markdown syntax support** and **complete
control over the LaTeX code**.'
- The boilerplate content is taken from [here](https://github.com/dnl-blkv/mcdowell-cv),
where a *clean and tidy CV* pattern is proposed by **[Gayle
Laakmann McDowell](https://www.gayle.com/)**.
quick_guide:
- bullet: Each section title is arbitrary, and each section
contains a list of entries.
- bullet: 'There are 7 unique entry types: *BulletEntry*,
*TextEntry*, *EducationEntry*, *ExperienceEntry*, *NormalEntry*,
*PublicationEntry*, and *OneLineEntry*.'
- bullet: Select a section title, pick an entry type, and
start writing your section!
- bullet: '[Here](https://docs.rendercv.com/user_guide/),
you can find a comprehensive user guide for RenderCV.'
education:
- institution: University of Pennsylvania
area: Computer Science
degree: BS
start_date: 2000-09
end_date: 2005-05
highlights:
- 'GPA: 3.9/4.0 ([Transcript](https://example.com))'
- '**Coursework:** Computer Architecture, Artificial
Intelligence, Comparison of Learning Algorithms, Computational
Theory'
experience:
- company: Apple
position: Software Engineer
location: Cupertino, CA
start_date: 2005-06
end_date: 2007-08
highlights:
- Reduced time to render the user's buddy list by 75%
by implementing a prediction algorithm
- Implemented iChat integration with OS X Spotlight
Search by creating a tool to extract metadata from
saved chat transcripts and provide metadata to a system-wide
search database
- Redesigned chat file format and implemented backward
compatibility for search
- company: Microsoft
position: Lead Student Ambassador
location: Redmond, WA
start_date: 2003-09
end_date: 2005-04
highlights:
- Promoted to Lead Student Ambassador in the Fall of
2004, supervised 10-15 Student Ambassadors
- 'Created and taught a computer science course, CSE
099: Software Design and Development'
- company: University of Pennsylvania
position: Head Teaching Assistant
location: Philadelphia, PA
start_date: 2001-10
end_date: 2003-05
highlights:
- Implemented a user interface for the VS open file
switcher (ctrl-tab) and extended it to tool windows
- Created a service to provide gradient across VS and
VS add-ins, optimized its performance via caching
- Programmer Productivity Research Center (Summers 2001,
2002)
- Built an app to compute the similarity of all methods
in a code base, reducing the time from $\mathcal{O}(n^2)$
to $\mathcal{O}(n \log n)$
- Created a test case generation tool that creates random
XML docs from XML Schema
- company: Microsoft
position: Software Engineer, Intern
location: Redmond, WA
start_date: 2003-06
end_date: 2003-08
highlights:
- Automated the extraction and processing of large datasets
from legacy systems using SQL and Perl scripts
publications:
- title: Magneto-Thermal Thin Shell Approximation for 3D
Finite Element Analysis of No-Insulation Coils
authors:
- Albert Smith
- '***John Doe***'
- Jane Derry
- Harry Tom
- Frodo Baggins
doi: 10.1109/TASC.2023.3340648
date: 2004-01
projects:
- name: Multi-User Drawing Tool
date: '[github.com/name/repo](https://github.com/sinaatalay/rendercv)'
highlights:
- Developed an electronic classroom where multiple users
can view and simultaneously draw on a "chalkboard"
with each person's edits synchronized
- 'Tools Used: C++, MFC'
- name: Synchronized Calendar
date: '[github.com/name/repo](https://github.com/sinaatalay/rendercv)'
highlights:
- Developed a desktop calendar with globally shared
and synchronized calendars, allowing users to schedule
meetings with other users
- 'Tools Used: C#, .NET, SQL, XML'
- name: Operating System
date: 2002
highlights:
- Developed a UNIX-style OS with a scheduler, file system,
text editor, and calculator
- 'Tools Used: C'
additional_experience_and_awards:
- label: Instructor (2003-2005)
details: Taught 2 full-credit computer science courses
- label: Third Prize, Senior Design Project
details: Awarded 3rd prize for a synchronized calendar
project out of 100 entries
technologies:
- label: Languages
details: C++, C, Java, Objective-C, C#, SQL, JavaScript
- label: Software
details: .NET, Microsoft SQL Server, XCode, Interface
Builder

View File

@@ -1,185 +0,0 @@
"""
The `rendercv.data_models.utilities` module contains utility functions that are required
by data models.
"""
import io
import re
from datetime import date as Date
from typing import Any
import pydantic
import ruamel.yaml
def dictionary_to_yaml(dictionary: dict[str, Any]):
"""Converts a dictionary to a YAML string.
Args:
dictionary (dict[str, Any]): The dictionary to be converted to YAML.
Returns:
str: The YAML string.
"""
yaml_object = ruamel.yaml.YAML()
yaml_object.encoding = "utf-8"
yaml_object.width = 60
yaml_object.indent(mapping=2, sequence=4, offset=2)
with io.StringIO() as string_stream:
yaml_object.dump(dictionary, string_stream)
yaml_string = string_stream.getvalue()
return yaml_string
def get_date_object(date: str | int) -> Date:
"""Parse a date string in YYYY-MM-DD, YYYY-MM, or YYYY format and return a
`datetime.date` object. This function is used throughout the validation process of
the data models.
Args:
date (str | int): The date string to parse.
Returns:
Date: The parsed date.
"""
if isinstance(date, int):
date_object = Date.fromisoformat(f"{date}-01-01")
elif re.fullmatch(r"\d{4}-\d{2}-\d{2}", date):
# Then it is in YYYY-MM-DD format
date_object = Date.fromisoformat(date)
elif re.fullmatch(r"\d{4}-\d{2}", date):
# Then it is in YYYY-MM format
date_object = Date.fromisoformat(f"{date}-01")
elif re.fullmatch(r"\d{4}", date):
# Then it is in YYYY format
date_object = Date.fromisoformat(f"{date}-01-01")
elif date == "present":
date_object = Date.today()
else:
raise ValueError(
"This is not a valid date! Please use either YYYY-MM-DD, YYYY-MM, or"
" YYYY format."
)
return date_object
def dictionary_key_to_proper_section_title(key: str) -> str:
"""Convert a dictionary key to a proper section title.
Example:
```python
dictionary_key_to_proper_section_title("section_title")
```
returns
`#!python "Section Title"`
Args:
key (str): The key to convert to a proper section title.
Returns:
str: The proper section title.
"""
title = key.replace("_", " ")
words = title.split(" ")
# loop through the words and if the word doesn't contain any uppercase letters,
# capitalize the first letter of the word. If the word contains uppercase letters,
# don't change the word.
proper_title = " ".join(
word.capitalize() if word.islower() else word for word in words
)
return proper_title
def set_or_update_a_value(
data_model: pydantic.BaseModel | dict | list,
key: str,
value: str,
sub_model: pydantic.BaseModel | dict | list = None,
):
"""Set or update a value in a data model for a specific key. For example, a key can
be `cv.sections.education.3.institution` and the value can be "Bogazici University".
Args:
data_model (pydantic.BaseModel | dict | list): The data model to set or update
the value.
key (str): The key to set or update the value.
value (Any): The value to set or update.
sub_model (pydantic.BaseModel | dict | list, optional): The sub model to set or
update the value. This is used for recursive calls. When the value is set
to a sub model, the original data model is validated. Defaults to None.
"""
# Recursively call this function until the last key is reached:
# Rename `sections` with `sections_input` since the key is `sections` is an alias:
key = key.replace("sections.", "sections_input.")
keys = key.split(".")
if sub_model is not None:
model = sub_model
else:
model = data_model
if len(keys) == 1:
# Set the value:
if value.startswith("{") and value.endswith("}"):
# Allow users to assign dictionaries:
value = eval(value)
elif value.startswith("[") and value.endswith("]"):
# Allow users to assign lists:
value = eval(value)
if isinstance(model, pydantic.BaseModel):
setattr(model, key, value)
elif isinstance(model, dict):
model[key] = value
elif isinstance(model, list):
model[int(key)] = value
else:
raise ValueError(
"The data model should be either a Pydantic data model, dictionary, or"
" list.",
)
data_model = type(data_model).model_validate(
(data_model.model_dump(by_alias=True))
)
return data_model
else:
# get the first key and call the function with remaining keys:
first_key = keys[0]
key = ".".join(keys[1:])
if isinstance(model, pydantic.BaseModel):
sub_model = getattr(model, first_key)
elif isinstance(model, dict):
sub_model = model[first_key]
elif isinstance(model, list):
sub_model = model[int(first_key)]
else:
raise ValueError(
"The data model should be either a Pydantic data model, dictionary, or"
" list.",
)
set_or_update_a_value(data_model, key, value, sub_model)
def make_a_url_clean(url: str) -> str:
"""Make a URL clean by removing the protocol, www, and trailing slashes.
Example:
```python
make_a_url_clean("https://www.example.com/")
```
returns
`#!python "example.com"`
Args:
url (str): The URL to make clean.
Returns:
str: The clean URL.
"""
url = url.replace("https://", "").replace("http://", "").replace("www.", "")
if url.endswith("/"):
url = url[:-1]
return url

View File

@@ -135,7 +135,7 @@ def text_entry() -> str:
@pytest.fixture
def rendercv_data_model() -> dm.RenderCVDataModel:
"""Return a sample RenderCV data model."""
return dm.get_a_sample_data_model()
return dm.create_a_sample_data_model()
@pytest.fixture

View File

@@ -181,13 +181,13 @@ def test_read_input_file_that_doesnt_exist(tmp_path):
dm.available_themes,
)
def test_get_a_sample_data_model(theme):
data_model = dm.get_a_sample_data_model("John Doe", theme)
data_model = dm.create_a_sample_data_model("John Doe", theme)
assert isinstance(data_model, dm.RenderCVDataModel)
def test_get_a_sample_data_model_invalid_theme():
with pytest.raises(ValueError):
dm.get_a_sample_data_model("John Doe", "invalid")
dm.create_a_sample_data_model("John Doe", "invalid")
def test_generate_json_schema():
@@ -677,7 +677,7 @@ def test_custom_theme_with_broken_init_file(tmp_path, testdata_directory_path):
def test_locale_catalog():
data_model = dm.get_a_sample_data_model("John Doe")
data_model = dm.create_a_sample_data_model("John Doe")
data_model.locale_catalog = dm.LocaleCatalog(
month="a",
months="b",
@@ -719,7 +719,7 @@ def test_locale_catalog():
def test_if_local_catalog_resets():
data_model = dm.get_a_sample_data_model("John Doe")
data_model = dm.create_a_sample_data_model("John Doe")
data_model.locale_catalog = dm.LocaleCatalog(
month="a",
@@ -727,7 +727,7 @@ def test_if_local_catalog_resets():
assert dm.locale_catalog["month"] == "a"
data_model = dm.get_a_sample_data_model("John Doe")
data_model = dm.create_a_sample_data_model("John Doe")
assert dm.locale_catalog["month"] == "month"