mirror of
https://github.com/rendercv/rendercv.git
synced 2025-12-23 21:47:55 -05:00
reader: create models package inside reader
This commit is contained in:
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
29
rendercv/reader/models/__init__.py
Normal file
29
rendercv/reader/models/__init__.py
Normal 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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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
|
||||
|
||||
|
||||
# ======================================================================================
|
||||
42
rendercv/reader/models/rendercv_data_model.py
Normal file
42
rendercv/reader/models/rendercv_data_model.py
Normal 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
309
rendercv/reader/reader.py
Normal 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
|
||||
135
rendercv/reader/sample_content.yaml
Normal file
135
rendercv/reader/sample_content.yaml
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user