Coverage for rendercv/data/generator.py: 100%

63 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-10-07 17:51 +0000

1""" 

2The `rendercv.data.generator` module contains all the functions for generating the JSON 

3Schema of the input data format and a sample YAML input file. 

4""" 

5 

6import io 

7import json 

8import pathlib 

9from typing import Optional 

10 

11import pydantic 

12import ruamel.yaml 

13 

14from . import models, reader 

15 

16 

17def dictionary_to_yaml(dictionary: dict) -> str: 

18 """Converts a dictionary to a YAML string. 

19 

20 Args: 

21 dictionary (dict): The dictionary to be converted to YAML. 

22 

23 Returns: 

24 str: The YAML string. 

25 """ 

26 yaml_object = ruamel.yaml.YAML() 

27 yaml_object.encoding = "utf-8" 

28 yaml_object.width = 60 

29 yaml_object.indent(mapping=2, sequence=4, offset=2) 

30 with io.StringIO() as string_stream: 

31 yaml_object.dump(dictionary, string_stream) 

32 yaml_string = string_stream.getvalue() 

33 

34 return yaml_string 

35 

36 

37def create_a_sample_data_model( 

38 name: str = "John Doe", theme: str = "classic" 

39) -> models.RenderCVDataModel: 

40 """Return a sample data model for new users to start with. 

41 

42 Args: 

43 name (str, optional): The name of the person. Defaults to "John Doe". 

44 

45 Returns: 

46 RenderCVDataModel: A sample data model. 

47 """ 

48 # Check if the theme is valid: 

49 if theme not in models.available_theme_options: 

50 available_themes_string = ", ".join(models.available_theme_options.keys()) 

51 raise ValueError( 

52 f"The theme should be one of the following: {available_themes_string}!" 

53 f' The provided theme is "{theme}".' 

54 ) 

55 

56 # read the sample_content.yaml file 

57 sample_content = pathlib.Path(__file__).parent / "sample_content.yaml" 

58 sample_content_dictionary = reader.read_a_yaml_file(sample_content) 

59 cv = models.CurriculumVitae(**sample_content_dictionary) 

60 

61 # Update the name: 

62 name = name.encode().decode("unicode-escape") 

63 cv.name = name 

64 

65 design = models.available_theme_options[theme](theme=theme) 

66 

67 return models.RenderCVDataModel(cv=cv, design=design) 

68 

69 

70def create_a_sample_yaml_input_file( 

71 input_file_path: Optional[pathlib.Path] = None, 

72 name: str = "John Doe", 

73 theme: str = "classic", 

74) -> str: 

75 """Create a sample YAML input file and return it as a string. If the input file path 

76 is provided, then also save the contents to the file. 

77 

78 Args: 

79 input_file_path (pathlib.Path, optional): The path to save the input file. 

80 Defaults to None. 

81 name (str, optional): The name of the person. Defaults to "John Doe". 

82 theme (str, optional): The theme of the CV. Defaults to "classic". 

83 

84 Returns: 

85 str: The sample YAML input file as a string. 

86 """ 

87 data_model = create_a_sample_data_model(name=name, theme=theme) 

88 

89 # Instead of getting the dictionary with data_model.model_dump() directly, we 

90 # convert it to JSON and then to a dictionary. Because the YAML library we are 

91 # using sometimes has problems with the dictionary returned by model_dump(). 

92 

93 # We exclude "cv.sections" because the data model automatically generates them. 

94 # The user's "cv.sections" input is actually "cv.sections_input" in the data 

95 # model. It is shown as "cv.sections" in the YAML file because an alias is being 

96 # used. If"cv.sections" were not excluded, the automatically generated 

97 # "cv.sections" would overwrite the "cv.sections_input". "cv.sections" are 

98 # automatically generated from "cv.sections_input" to make the templating 

99 # process easier. "cv.sections_input" exists for the convenience of the user. 

100 data_model_as_json = data_model.model_dump_json( 

101 exclude_none=True, by_alias=True, exclude={"cv": {"sections"}} 

102 ) 

103 data_model_as_dictionary = json.loads(data_model_as_json) 

104 

105 yaml_string = dictionary_to_yaml(data_model_as_dictionary) 

106 

107 if input_file_path is not None: 

108 input_file_path.write_text(yaml_string, encoding="utf-8") 

109 

110 return yaml_string 

111 

112 

113def generate_json_schema() -> dict: 

114 """Generate the JSON schema of RenderCV. 

115 

116 JSON schema is generated for the users to make it easier for them to write the input 

117 file. The JSON Schema of RenderCV is saved in the root directory of the repository 

118 and distributed to the users with the 

119 [JSON Schema Store](https://www.schemastore.org/). 

120 

121 Returns: 

122 dict: The JSON schema of RenderCV. 

123 """ 

124 

125 class RenderCVSchemaGenerator(pydantic.json_schema.GenerateJsonSchema): 

126 def generate(self, schema, mode="validation"): # type: ignore 

127 json_schema = super().generate(schema, mode=mode) 

128 

129 # Basic information about the schema: 

130 json_schema["title"] = "RenderCV" 

131 json_schema["description"] = "RenderCV data model." 

132 json_schema["$id"] = ( 

133 "https://raw.githubusercontent.com/sinaatalay/rendercv/main/schema.json" 

134 ) 

135 json_schema["$schema"] = "http://json-schema.org/draft-07/schema#" 

136 

137 # Loop through $defs and remove docstring descriptions and fix optional 

138 # fields 

139 for object_name, value in json_schema["$defs"].items(): 

140 # Don't allow additional properties 

141 value["additionalProperties"] = False 

142 

143 # If a type is optional, then Pydantic sets the type to a list of two 

144 # types, one of which is null. The null type can be removed since we 

145 # already have the required field. Moreover, we would like to warn 

146 # users if they provide null values. They can remove the fields if they 

147 # don't want to provide them. 

148 null_type_dict = { 

149 "type": "null", 

150 } 

151 for field_name, field in value["properties"].items(): 

152 if "anyOf" in field: 

153 if null_type_dict in field["anyOf"]: 

154 field["anyOf"].remove(null_type_dict) 

155 

156 field["oneOf"] = field["anyOf"] 

157 del field["anyOf"] 

158 

159 # Currently, YAML extension in VS Code doesn't work properly with the 

160 # `ListOfEntries` objects. For the best user experience, we will update 

161 # the JSON Schema. If YAML extension in VS Code starts to work properly, 

162 # then we should remove the following code for the correct JSON Schema. 

163 ListOfEntriesForJsonSchema = list[models.Entry] 

164 list_of_entries_json_schema = pydantic.TypeAdapter( 

165 ListOfEntriesForJsonSchema 

166 ).json_schema() 

167 del list_of_entries_json_schema["$defs"] 

168 

169 # Update the JSON Schema: 

170 json_schema["$defs"]["CurriculumVitae"]["properties"]["sections"]["oneOf"][ 

171 0 

172 ]["additionalProperties"] = list_of_entries_json_schema 

173 

174 return json_schema 

175 

176 schema = models.RenderCVDataModel.model_json_schema( 

177 schema_generator=RenderCVSchemaGenerator 

178 ) 

179 

180 return schema 

181 

182 

183def generate_json_schema_file(json_schema_path: pathlib.Path): 

184 """Generate the JSON schema of RenderCV and save it to a file. 

185 

186 Args: 

187 json_schema_path (pathlib.Path): The path to save the JSON schema. 

188 """ 

189 schema = generate_json_schema() 

190 schema_json = json.dumps(schema, indent=2, ensure_ascii=False) 

191 json_schema_path.write_text(schema_json, encoding="utf-8")