mirror of https://github.com/eyhc1/rendercv.git
generate JSON schema
This commit is contained in:
parent
504ccd13f4
commit
0b95eb68da
|
@ -15,6 +15,7 @@ from functools import cached_property
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import os
|
import os
|
||||||
from importlib.resources import files
|
from importlib.resources import files
|
||||||
|
import json
|
||||||
|
|
||||||
from pydantic import (
|
from pydantic import (
|
||||||
BaseModel,
|
BaseModel,
|
||||||
|
@ -26,6 +27,7 @@ from pydantic import (
|
||||||
EmailStr,
|
EmailStr,
|
||||||
PastDate,
|
PastDate,
|
||||||
)
|
)
|
||||||
|
from pydantic.json_schema import GenerateJsonSchema
|
||||||
from pydantic.functional_validators import AfterValidator
|
from pydantic.functional_validators import AfterValidator
|
||||||
from pydantic_extra_types.phone_numbers import PhoneNumber
|
from pydantic_extra_types.phone_numbers import PhoneNumber
|
||||||
from pydantic_extra_types.color import Color
|
from pydantic_extra_types.color import Color
|
||||||
|
@ -57,7 +59,7 @@ dictionary = [
|
||||||
"dc",
|
"dc",
|
||||||
"grammarly",
|
"grammarly",
|
||||||
"css",
|
"css",
|
||||||
"html"
|
"html",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -216,6 +218,63 @@ def format_date(date: Date) -> str:
|
||||||
return date_string
|
return date_string
|
||||||
|
|
||||||
|
|
||||||
|
def generate_json_schema(output_directory: str) -> str:
|
||||||
|
"""Generate the JSON schema of the data model and save it to a file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
output_directory (str): The output directory to save the schema.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class RenderCVSchemaGenerator(GenerateJsonSchema):
|
||||||
|
def generate(self, schema, mode="validation"):
|
||||||
|
json_schema = super().generate(schema, mode=mode)
|
||||||
|
json_schema["title"] = "RenderCV Input"
|
||||||
|
|
||||||
|
# remove the description of the class (RenderCVDataModel)
|
||||||
|
del json_schema["description"]
|
||||||
|
|
||||||
|
# add $id
|
||||||
|
json_schema[
|
||||||
|
"$id"
|
||||||
|
] = "https://raw.githubusercontent.com/sinaatalay/rendercv/main/schema.json"
|
||||||
|
|
||||||
|
# add $schema
|
||||||
|
json_schema["$schema"] = "http://json-schema.org/draft-07/schema#"
|
||||||
|
|
||||||
|
# Loop through $defs and remove docstring descriptions and fix optional
|
||||||
|
# fields
|
||||||
|
for key, value in json_schema["$defs"].items():
|
||||||
|
if "This class" in value["description"]:
|
||||||
|
del value["description"]
|
||||||
|
|
||||||
|
null_type_dict = {}
|
||||||
|
null_type_dict["type"] = "null"
|
||||||
|
for field in value["properties"].values():
|
||||||
|
if "anyOf" in field:
|
||||||
|
if (
|
||||||
|
len(field["anyOf"]) == 2
|
||||||
|
and null_type_dict in field["anyOf"]
|
||||||
|
):
|
||||||
|
field["allOf"] = [field["anyOf"][0]]
|
||||||
|
del field["anyOf"]
|
||||||
|
|
||||||
|
return json_schema
|
||||||
|
|
||||||
|
schema = RenderCVDataModel.model_json_schema(
|
||||||
|
schema_generator=RenderCVSchemaGenerator
|
||||||
|
)
|
||||||
|
schema = json.dumps(schema, indent=2)
|
||||||
|
|
||||||
|
# Change all anyOf to oneOf
|
||||||
|
schema = schema.replace('"anyOf"', '"oneOf"')
|
||||||
|
|
||||||
|
path_to_schema = os.path.join(output_directory, "schema.json")
|
||||||
|
with open(path_to_schema, "w") as f:
|
||||||
|
f.write(schema)
|
||||||
|
|
||||||
|
return path_to_schema
|
||||||
|
|
||||||
|
|
||||||
# ======================================================================================
|
# ======================================================================================
|
||||||
# ======================================================================================
|
# ======================================================================================
|
||||||
# ======================================================================================
|
# ======================================================================================
|
||||||
|
@ -230,7 +289,6 @@ LaTeXDimension = Annotated[
|
||||||
str,
|
str,
|
||||||
Field(
|
Field(
|
||||||
pattern=r"\d+\.?\d* *(cm|in|pt|mm|ex|em)",
|
pattern=r"\d+\.?\d* *(cm|in|pt|mm|ex|em)",
|
||||||
examples=["1.35 cm", "1 in", "12 pt", "14 mm", "2 ex", "3 em"],
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
SpellCheckedString = Annotated[str, AfterValidator(check_spelling)]
|
SpellCheckedString = Annotated[str, AfterValidator(check_spelling)]
|
||||||
|
@ -250,22 +308,22 @@ class ClassicThemePageMargins(BaseModel):
|
||||||
top: LaTeXDimension = Field(
|
top: LaTeXDimension = Field(
|
||||||
default="1.35 cm",
|
default="1.35 cm",
|
||||||
title="Top Margin",
|
title="Top Margin",
|
||||||
description="The top margin of the page.",
|
description="The top margin of the page with units.",
|
||||||
)
|
)
|
||||||
bottom: LaTeXDimension = Field(
|
bottom: LaTeXDimension = Field(
|
||||||
default="1.35 cm",
|
default="1.35 cm",
|
||||||
title="Bottom Margin",
|
title="Bottom Margin",
|
||||||
description="The bottom margin of the page.",
|
description="The bottom margin of the page with units.",
|
||||||
)
|
)
|
||||||
left: LaTeXDimension = Field(
|
left: LaTeXDimension = Field(
|
||||||
default="1.35 cm",
|
default="1.35 cm",
|
||||||
title="Left Margin",
|
title="Left Margin",
|
||||||
description="The left margin of the page.",
|
description="The left margin of the page with units.",
|
||||||
)
|
)
|
||||||
right: LaTeXDimension = Field(
|
right: LaTeXDimension = Field(
|
||||||
default="1.35 cm",
|
default="1.35 cm",
|
||||||
title="Right Margin",
|
title="Right Margin",
|
||||||
description="The right margin of the page.",
|
description="The right margin of the page with units.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -336,18 +394,22 @@ class ClassicThemeMargins(BaseModel):
|
||||||
page: ClassicThemePageMargins = Field(
|
page: ClassicThemePageMargins = Field(
|
||||||
default=ClassicThemePageMargins(),
|
default=ClassicThemePageMargins(),
|
||||||
title="Page Margins",
|
title="Page Margins",
|
||||||
|
description="Page margins for the classic theme.",
|
||||||
)
|
)
|
||||||
section_title: ClassicThemeSectionTitleMargins = Field(
|
section_title: ClassicThemeSectionTitleMargins = Field(
|
||||||
default=ClassicThemeSectionTitleMargins(),
|
default=ClassicThemeSectionTitleMargins(),
|
||||||
title="Section Title Margins",
|
title="Section Title Margins",
|
||||||
|
description="Section title margins for the classic theme.",
|
||||||
)
|
)
|
||||||
entry_area: ClassicThemeEntryAreaMargins = Field(
|
entry_area: ClassicThemeEntryAreaMargins = Field(
|
||||||
default=ClassicThemeEntryAreaMargins(),
|
default=ClassicThemeEntryAreaMargins(),
|
||||||
title="Entry Area Margins",
|
title="Entry Area Margins",
|
||||||
|
description="Entry area margins for the classic theme.",
|
||||||
)
|
)
|
||||||
highlights_area: ClassicThemeHighlightsAreaMargins = Field(
|
highlights_area: ClassicThemeHighlightsAreaMargins = Field(
|
||||||
default=ClassicThemeHighlightsAreaMargins(),
|
default=ClassicThemeHighlightsAreaMargins(),
|
||||||
title="Highlights Area Margins",
|
title="Highlights Area Margins",
|
||||||
|
description="Highlights area margins for the classic theme.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -375,7 +437,6 @@ class ClassicThemeOptions(BaseModel):
|
||||||
default="3.6 cm",
|
default="3.6 cm",
|
||||||
title="Date and Location Column Width",
|
title="Date and Location Column Width",
|
||||||
description="The width of the date and location column.",
|
description="The width of the date and location column.",
|
||||||
examples=["1.35 cm", "1 in", "12 pt", "14 mm", "2 ex", "3 em"],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
show_timespan_in: list[str] = Field(
|
show_timespan_in: list[str] = Field(
|
||||||
|
@ -385,7 +446,6 @@ class ClassicThemeOptions(BaseModel):
|
||||||
"The time span will be shown in the date and location column in these"
|
"The time span will be shown in the date and location column in these"
|
||||||
" sections. The input should be a list of strings."
|
" sections. The input should be a list of strings."
|
||||||
),
|
),
|
||||||
examples=[["Education", "Experience"]],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
show_last_updated_date: bool = Field(
|
show_last_updated_date: bool = Field(
|
||||||
|
@ -416,19 +476,16 @@ class Design(BaseModel):
|
||||||
default="SourceSans3",
|
default="SourceSans3",
|
||||||
title="Font",
|
title="Font",
|
||||||
description="The font of the CV.",
|
description="The font of the CV.",
|
||||||
examples=["SourceSans3", "Roboto", "EBGaramond"],
|
|
||||||
)
|
)
|
||||||
font_size: Literal["10pt", "11pt", "12pt"] = Field(
|
font_size: Literal["10pt", "11pt", "12pt"] = Field(
|
||||||
default="10pt",
|
default="10pt",
|
||||||
title="Font Size",
|
title="Font Size",
|
||||||
description="The font size of the CV. It can be 10pt, 11pt, or 12pt.",
|
description="The font size of the CV. It can be 10pt, 11pt, or 12pt.",
|
||||||
examples=["10pt", "11pt", "12pt"],
|
|
||||||
)
|
)
|
||||||
page_size: Literal["a4paper", "letterpaper"] = Field(
|
page_size: Literal["a4paper", "letterpaper"] = Field(
|
||||||
default="a4paper",
|
default="a4paper",
|
||||||
title="Page Size",
|
title="Page Size",
|
||||||
description="The page size of the CV. It can be a4paper or letterpaper.",
|
description="The page size of the CV. It can be a4paper or letterpaper.",
|
||||||
examples=["a4paper", "letterpaper"],
|
|
||||||
)
|
)
|
||||||
options: Optional[ClassicThemeOptions] = Field(
|
options: Optional[ClassicThemeOptions] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
|
@ -534,7 +591,7 @@ class Event(BaseModel):
|
||||||
" and end date should be provided instead. All of them can't be provided at"
|
" and end date should be provided instead. All of them can't be provided at"
|
||||||
" the same time."
|
" the same time."
|
||||||
),
|
),
|
||||||
examples=["2020-09-24"],
|
examples=["2020-09-24", "My Custom Date"],
|
||||||
)
|
)
|
||||||
highlights: Optional[list[SpellCheckedString]] = Field(
|
highlights: Optional[list[SpellCheckedString]] = Field(
|
||||||
default=[],
|
default=[],
|
||||||
|
@ -760,12 +817,10 @@ class ExperienceEntry(Event):
|
||||||
company: str = Field(
|
company: str = Field(
|
||||||
title="Company",
|
title="Company",
|
||||||
description="The company name. It will be shown as bold text.",
|
description="The company name. It will be shown as bold text.",
|
||||||
examples=["CERN", "Apple"],
|
|
||||||
)
|
)
|
||||||
position: str = Field(
|
position: str = Field(
|
||||||
title="Position",
|
title="Position",
|
||||||
description="The position. It will be shown as normal text.",
|
description="The position. It will be shown as normal text.",
|
||||||
examples=["Software Engineer", "Mechanical Engineer"],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -775,12 +830,11 @@ class EducationEntry(Event):
|
||||||
institution: str = Field(
|
institution: str = Field(
|
||||||
title="Institution",
|
title="Institution",
|
||||||
description="The institution name. It will be shown as bold text.",
|
description="The institution name. It will be shown as bold text.",
|
||||||
examples=["Massachusetts Institute of Technology", "Bogazici University"],
|
examples=["Bogazici University"],
|
||||||
)
|
)
|
||||||
area: str = Field(
|
area: str = Field(
|
||||||
title="Area",
|
title="Area",
|
||||||
description="The area of study. It will be shown as normal text.",
|
description="The area of study. It will be shown as normal text.",
|
||||||
examples=["Mechanical Engineering", "Computer Science"],
|
|
||||||
)
|
)
|
||||||
study_type: Optional[str] = Field(
|
study_type: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
|
@ -792,7 +846,6 @@ class EducationEntry(Event):
|
||||||
default=None,
|
default=None,
|
||||||
title="GPA",
|
title="GPA",
|
||||||
description="The GPA of the degree.",
|
description="The GPA of the degree.",
|
||||||
examples=["4.00/4.00", "3.80/4.00"],
|
|
||||||
)
|
)
|
||||||
transcript_url: Optional[HttpUrl] = Field(
|
transcript_url: Optional[HttpUrl] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
|
@ -826,37 +879,30 @@ class PublicationEntry(Event):
|
||||||
title: str = Field(
|
title: str = Field(
|
||||||
title="Title of the Publication",
|
title="Title of the Publication",
|
||||||
description="The title of the publication. It will be shown as bold text.",
|
description="The title of the publication. It will be shown as bold text.",
|
||||||
examples=["My Awesome Paper", "My Awesome Book"],
|
|
||||||
)
|
)
|
||||||
authors: list[str] = Field(
|
authors: list[str] = Field(
|
||||||
title="Authors",
|
title="Authors",
|
||||||
description="The authors of the publication in order as a list of strings.",
|
description="The authors of the publication in order as a list of strings.",
|
||||||
examples=["John Doe", "Jane Doe"],
|
|
||||||
)
|
)
|
||||||
doi: str = Field(
|
doi: str = Field(
|
||||||
title="DOI",
|
title="DOI",
|
||||||
description="The DOI of the publication.",
|
description="The DOI of the publication.",
|
||||||
examples=["10.1103/PhysRevB.76.054309"],
|
examples=["10.48550/arXiv.2310.03138"],
|
||||||
)
|
)
|
||||||
date: str = Field(
|
date: str = Field(
|
||||||
title="Publication Date",
|
title="Publication Date",
|
||||||
description="The date of the publication.",
|
description="The date of the publication.",
|
||||||
examples=[2021, 2022],
|
examples=["2021-10-31"],
|
||||||
)
|
)
|
||||||
cited_by: Optional[int] = Field(
|
cited_by: Optional[int] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Cited By",
|
title="Cited By",
|
||||||
description="The number of citations of the publication.",
|
description="The number of citations of the publication.",
|
||||||
examples=[10, 100],
|
|
||||||
)
|
)
|
||||||
journal: Optional[str] = Field(
|
journal: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Journal",
|
title="Journal",
|
||||||
description="The journal or the conference name.",
|
description="The journal or the conference name.",
|
||||||
examples=[
|
|
||||||
"Physical Review B",
|
|
||||||
"ASME International Mechanical Engineering Congress and Exposition",
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@field_validator("doi")
|
@field_validator("doi")
|
||||||
|
@ -886,12 +932,10 @@ class SocialNetwork(BaseModel):
|
||||||
network: Literal["LinkedIn", "GitHub", "Instagram"] = Field(
|
network: Literal["LinkedIn", "GitHub", "Instagram"] = Field(
|
||||||
title="Social Network",
|
title="Social Network",
|
||||||
description="The social network name.",
|
description="The social network name.",
|
||||||
examples=["LinkedIn", "GitHub", "Instagram"],
|
|
||||||
)
|
)
|
||||||
username: str = Field(
|
username: str = Field(
|
||||||
title="Username",
|
title="Username",
|
||||||
description="The username of the social network. The link will be generated.",
|
description="The username of the social network. The link will be generated.",
|
||||||
examples=["johndoe", "johndoe123"],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -947,7 +991,7 @@ class Section(BaseModel):
|
||||||
title: str = Field(
|
title: str = Field(
|
||||||
title="Section Title",
|
title="Section Title",
|
||||||
description="The title of the section.",
|
description="The title of the section.",
|
||||||
examples=["Awards", "My Custom Section", "Languages"],
|
examples=["My Custom Section"],
|
||||||
)
|
)
|
||||||
entry_type: Literal[
|
entry_type: Literal[
|
||||||
"OneLineEntry",
|
"OneLineEntry",
|
||||||
|
@ -988,19 +1032,16 @@ class CurriculumVitae(BaseModel):
|
||||||
name: str = Field(
|
name: str = Field(
|
||||||
title="Name",
|
title="Name",
|
||||||
description="The name of the person.",
|
description="The name of the person.",
|
||||||
examples=["John Doe", "Jane Doe"],
|
|
||||||
)
|
)
|
||||||
label: Optional[str] = Field(
|
label: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Label",
|
title="Label",
|
||||||
description="The label of the person.",
|
description="The label of the person.",
|
||||||
examples=["Software Engineer", "Mechanical Engineer"],
|
|
||||||
)
|
)
|
||||||
location: Optional[str] = Field(
|
location: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Location",
|
title="Location",
|
||||||
description="The location of the person. This is not rendered currently.",
|
description="The location of the person. This is not rendered currently.",
|
||||||
examples=["Istanbul, Turkey", "Boston, MA, USA"],
|
|
||||||
)
|
)
|
||||||
email: Optional[EmailStr] = Field(
|
email: Optional[EmailStr] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
|
@ -1027,7 +1068,6 @@ class CurriculumVitae(BaseModel):
|
||||||
description=(
|
description=(
|
||||||
"The order of sections in the CV. The section title should be used."
|
"The order of sections in the CV. The section title should be used."
|
||||||
),
|
),
|
||||||
examples=[["Education", "Work Experience", "Skills"]],
|
|
||||||
)
|
)
|
||||||
education: Optional[list[EducationEntry]] = Field(
|
education: Optional[list[EducationEntry]] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
|
@ -1250,7 +1290,11 @@ class RenderCVDataModel(BaseModel):
|
||||||
title="Design",
|
title="Design",
|
||||||
description="The design of the CV.",
|
description="The design of the CV.",
|
||||||
)
|
)
|
||||||
cv: CurriculumVitae
|
cv: CurriculumVitae = Field(
|
||||||
|
default=CurriculumVitae(name="John Doe"),
|
||||||
|
title="Curriculum Vitae",
|
||||||
|
description="The data of the CV.",
|
||||||
|
)
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
import rendercv.__main__ as rendercv
|
import os
|
||||||
|
from rendercv.__main__ import main as rendercv_main
|
||||||
|
from rendercv.data_model import generate_json_schema
|
||||||
|
|
||||||
# input_file_path = "personal.yaml"
|
input_file_path = "personal.yaml"
|
||||||
# rendercv.main(input_file_path)
|
rendercv_main(input_file_path)
|
||||||
|
|
||||||
# This script is equivalent to running the following command in the terminal:
|
# This script is equivalent to running the following command in the terminal:
|
||||||
# python -m rendercv personal.yaml
|
# python -m rendercv personal.yaml
|
||||||
# or
|
# or
|
||||||
# rendercv personal.yaml
|
# rendercv personal.yaml
|
||||||
|
|
||||||
from rendercv.data_model import RenderCVDataModel
|
# Generate schema.json
|
||||||
|
# generate_json_schema(os.path.join(os.path.dirname(__file__)))
|
||||||
jsoan = RenderCVDataModel.model_json_schema()
|
|
||||||
import json
|
|
||||||
# write json to file
|
|
||||||
with open("json_schema.json", "w") as f:
|
|
||||||
f.write(json.dumps(jsoan))
|
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,6 @@
|
||||||
import unittest
|
import unittest
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
from rendercv import data_model
|
from rendercv import data_model
|
||||||
|
|
||||||
|
@ -781,3 +783,25 @@ class TestDataModel(unittest.TestCase):
|
||||||
with self.subTest(msg="custom sections with duplicate titles"):
|
with self.subTest(msg="custom sections with duplicate titles"):
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
data_model.CurriculumVitae(**input)
|
data_model.CurriculumVitae(**input)
|
||||||
|
|
||||||
|
def test_if_json_schema_is_the_latest(self):
|
||||||
|
tests_directory = os.path.dirname(__file__)
|
||||||
|
path_to_generated_schema = data_model.generate_json_schema(tests_directory)
|
||||||
|
|
||||||
|
# Read the generated JSON schema:
|
||||||
|
with open(path_to_generated_schema, "r") as f:
|
||||||
|
generated_json_schema = json.load(f)
|
||||||
|
|
||||||
|
# Remove the generated JSON schema:
|
||||||
|
os.remove(path_to_generated_schema)
|
||||||
|
|
||||||
|
# Read the repository's current JSON schema:
|
||||||
|
path_to_schema = os.path.join(
|
||||||
|
os.path.dirname(tests_directory),
|
||||||
|
"schema.json"
|
||||||
|
)
|
||||||
|
with open(path_to_schema, "r") as f:
|
||||||
|
current_json_schema = json.load(f)
|
||||||
|
|
||||||
|
# Compare the two JSON schemas:
|
||||||
|
self.assertEqual(generated_json_schema, current_json_schema)
|
Loading…
Reference in New Issue