Coverage for rendercv/data/models/computers.py: 100%

123 statements  

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

1""" 

2The `rendercv.data.models.computers` module contains functions that compute some 

3properties based on the input data. For example, it includes functions that calculate 

4the time span between two dates, the date string, the URL of a social network, etc. 

5""" 

6 

7import pathlib 

8import re 

9from datetime import date as Date 

10from typing import Optional 

11 

12import phonenumbers 

13 

14from .curriculum_vitae import curriculum_vitae 

15from .locale_catalog import LOCALE_CATALOG 

16 

17 

18def format_phone_number(phone_number: str) -> str: 

19 """Format a phone number to the format specified in the `locale_catalog` dictionary. 

20 

21 Example: 

22 ```python 

23 format_phone_number("+17034800500") 

24 ``` 

25 returns 

26 ```python 

27 "(703) 480-0500" 

28 ``` 

29 

30 Args: 

31 phone_number (str): The phone number to format. 

32 

33 Returns: 

34 str: The formatted phone number. 

35 """ 

36 

37 format = LOCALE_CATALOG["phone_number_format"].upper() # type: ignore 

38 

39 parsed_number = phonenumbers.parse(phone_number, None) 

40 formatted_number = phonenumbers.format_number( 

41 parsed_number, getattr(phonenumbers.PhoneNumberFormat, format) 

42 ) 

43 return formatted_number 

44 

45 

46def format_date(date: Date, date_style: Optional[str] = None) -> str: 

47 """Formats a `Date` object to a string in the following format: "Jan 2021". The 

48 month names are taken from the `locale_catalog` dictionary from the 

49 `rendercv.data_models.models` module. 

50 

51 Example: 

52 ```python 

53 format_date(Date(2024, 5, 1)) 

54 ``` 

55 will return 

56 

57 `#!python "May 2024"` 

58 

59 Args: 

60 date (Date): The date to format. 

61 date_style (Optional[str]): The style of the date string. If not provided, the 

62 default date style from the `locale_catalog` dictionary will be used. 

63 

64 Returns: 

65 str: The formatted date. 

66 """ 

67 full_month_names = LOCALE_CATALOG["full_names_of_months"] 

68 short_month_names = LOCALE_CATALOG["abbreviations_for_months"] 

69 

70 month = int(date.strftime("%m")) 

71 year = date.strftime(format="%Y") 

72 

73 placeholders = { 

74 "FULL_MONTH_NAME": full_month_names[month - 1], 

75 "MONTH_ABBREVIATION": short_month_names[month - 1], 

76 "MONTH_IN_TWO_DIGITS": f"{month:02d}", 

77 "YEAR_IN_TWO_DIGITS": str(year[-2:]), 

78 "MONTH": str(month), 

79 "YEAR": str(year), 

80 } 

81 if date_style is None: 

82 date_style = LOCALE_CATALOG["date_style"] # type: ignore 

83 

84 for placeholder, value in placeholders.items(): 

85 date_style = date_style.replace(placeholder, value) # type: ignore 

86 

87 date_string = date_style 

88 

89 return date_string # type: ignore 

90 

91 

92def convert_string_to_path(value: str) -> pathlib.Path: 

93 """Converts a string to a `pathlib.Path` object by replacing the placeholders 

94 with the corresponding values. If the path is not an absolute path, it is 

95 converted to an absolute path by prepending the current working directory. 

96 """ 

97 name = curriculum_vitae["name"] # Curriculum Vitae owner's name 

98 full_month_names = LOCALE_CATALOG["full_names_of_months"] 

99 short_month_names = LOCALE_CATALOG["abbreviations_for_months"] 

100 

101 month = Date.today().month 

102 year = str(Date.today().year) 

103 

104 placeholders = { 

105 "NAME_IN_SNAKE_CASE": name.replace(" ", "_"), 

106 "NAME_IN_LOWER_SNAKE_CASE": name.replace(" ", "_").lower(), 

107 "NAME_IN_UPPER_SNAKE_CASE": name.replace(" ", "_").upper(), 

108 "NAME_IN_KEBAB_CASE": name.replace(" ", "-"), 

109 "NAME_IN_LOWER_KEBAB_CASE": name.replace(" ", "-").lower(), 

110 "NAME_IN_UPPER_KEBAB_CASE": name.replace(" ", "-").upper(), 

111 "FULL_MONTH_NAME": full_month_names[month - 1], 

112 "MONTH_ABBREVIATION": short_month_names[month - 1], 

113 "MONTH_IN_TWO_DIGITS": f"{month:02d}", 

114 "YEAR_IN_TWO_DIGITS": str(year[-2:]), 

115 "NAME": name, 

116 "YEAR": str(year), 

117 "MONTH": str(month), 

118 } 

119 

120 for placeholder, placeholder_value in placeholders.items(): 

121 value = value.replace(placeholder, placeholder_value) 

122 

123 return pathlib.Path(value).absolute() 

124 

125 

126def compute_time_span_string( 

127 start_date: Optional[str | int], 

128 end_date: Optional[str | int], 

129 date: Optional[str | int], 

130) -> str: 

131 """ 

132 Return a time span string based on the provided dates. 

133 

134 Example: 

135 ```python 

136 get_time_span_string("2020-01-01", "2020-05-01", None) 

137 ``` 

138 

139 returns 

140 

141 `#!python "4 months"` 

142 

143 Args: 

144 start_date (Optional[str]): A start date in YYYY-MM-DD, YYYY-MM, or YYYY format. 

145 end_date (Optional[str]): An end date in YYYY-MM-DD, YYYY-MM, or YYYY format or 

146 "present". 

147 date (Optional[str]): A date in YYYY-MM-DD, YYYY-MM, or YYYY format or a custom 

148 string. If provided, start_date and end_date will be ignored. 

149 

150 Returns: 

151 str: The computed time span string. 

152 """ 

153 date_is_provided = date is not None 

154 start_date_is_provided = start_date is not None 

155 end_date_is_provided = end_date is not None 

156 

157 if date_is_provided: 

158 # If only the date is provided, the time span is irrelevant. So, return an 

159 # empty string. 

160 return "" 

161 

162 elif not start_date_is_provided and not end_date_is_provided: 

163 # If neither start_date nor end_date is provided, return an empty string. 

164 return "" 

165 

166 elif isinstance(start_date, int) or isinstance(end_date, int): 

167 # Then it means one of the dates is year, so time span cannot be more 

168 # specific than years. 

169 start_year = get_date_object(start_date).year # type: ignore 

170 end_year = get_date_object(end_date).year # type: ignore 

171 

172 time_span_in_years = end_year - start_year 

173 

174 if time_span_in_years < 2: 

175 time_span_string = "1 year" 

176 else: 

177 time_span_string = f"{time_span_in_years} years" 

178 

179 return time_span_string 

180 

181 else: 

182 # Then it means both start_date and end_date are in YYYY-MM-DD or YYYY-MM 

183 # format. 

184 end_date = get_date_object(end_date) # type: ignore 

185 start_date = get_date_object(start_date) # type: ignore 

186 

187 # Calculate the number of days between start_date and end_date: 

188 timespan_in_days = (end_date - start_date).days # type: ignore 

189 

190 # Calculate the number of years between start_date and end_date: 

191 how_many_years = timespan_in_days // 365 

192 if how_many_years == 0: 

193 how_many_years_string = None 

194 elif how_many_years == 1: 

195 how_many_years_string = f"1 {LOCALE_CATALOG['year']}" 

196 else: 

197 how_many_years_string = f"{how_many_years} {LOCALE_CATALOG['years']}" 

198 

199 # Calculate the number of months between start_date and end_date: 

200 how_many_months = round((timespan_in_days % 365) / 30) 

201 if how_many_months <= 1: 

202 how_many_months_string = f"1 {LOCALE_CATALOG['month']}" 

203 else: 

204 how_many_months_string = f"{how_many_months} {LOCALE_CATALOG['months']}" 

205 

206 # Combine howManyYearsString and howManyMonthsString: 

207 if how_many_years_string is None: 

208 time_span_string = how_many_months_string 

209 else: 

210 time_span_string = f"{how_many_years_string} {how_many_months_string}" 

211 

212 return time_span_string 

213 

214 

215def compute_date_string( 

216 start_date: Optional[str | int], 

217 end_date: Optional[str | int], 

218 date: Optional[str | int], 

219 show_only_years: bool = False, 

220) -> str: 

221 """Return a date string based on the provided dates. 

222 

223 Example: 

224 ```python 

225 get_date_string("2020-01-01", "2021-01-01", None) 

226 ``` 

227 returns 

228 ``` 

229 "Jan 2020 to Jan 2021" 

230 ``` 

231 

232 Args: 

233 start_date (Optional[str]): A start date in YYYY-MM-DD, YYYY-MM, or YYYY 

234 format. 

235 end_date (Optional[str]): An end date in YYYY-MM-DD, YYYY-MM, or YYYY format 

236 or "present". 

237 date (Optional[str]): A date in YYYY-MM-DD, YYYY-MM, or YYYY format or 

238 a custom string. If provided, start_date and end_date will be ignored. 

239 show_only_years (bool): If True, only the years will be shown in the date 

240 string. 

241 

242 Returns: 

243 str: The computed date string. 

244 """ 

245 date_is_provided = date is not None 

246 start_date_is_provided = start_date is not None 

247 end_date_is_provided = end_date is not None 

248 

249 if date_is_provided: 

250 if isinstance(date, int): 

251 # Then it means only the year is provided 

252 date_string = str(date) 

253 else: 

254 try: 

255 date_object = get_date_object(date) 

256 if show_only_years: 

257 date_string = str(date_object.year) 

258 else: 

259 date_string = format_date(date_object) 

260 except ValueError: 

261 # Then it is a custom date string (e.g., "My Custom Date") 

262 date_string = str(date) 

263 elif start_date_is_provided and end_date_is_provided: 

264 if isinstance(start_date, int): 

265 # Then it means only the year is provided 

266 start_date = str(start_date) 

267 else: 

268 # Then it means start_date is either in YYYY-MM-DD or YYYY-MM format 

269 date_object = get_date_object(start_date) 

270 if show_only_years: 

271 start_date = date_object.year 

272 else: 

273 start_date = format_date(date_object) 

274 

275 if end_date == "present": 

276 end_date = LOCALE_CATALOG["present"] # type: ignore 

277 elif isinstance(end_date, int): 

278 # Then it means only the year is provided 

279 end_date = str(end_date) 

280 else: 

281 # Then it means end_date is either in YYYY-MM-DD or YYYY-MM format 

282 date_object = get_date_object(end_date) 

283 if show_only_years: 

284 end_date = date_object.year 

285 else: 

286 end_date = format_date(date_object) 

287 

288 date_string = f"{start_date} {LOCALE_CATALOG['to']} {end_date}" 

289 

290 else: 

291 # Neither date, start_date, nor end_date are provided, so return an empty 

292 # string: 

293 date_string = "" 

294 

295 return date_string 

296 

297 

298def make_a_url_clean(url: str) -> str: 

299 """Make a URL clean by removing the protocol, www, and trailing slashes. 

300 

301 Example: 

302 ```python 

303 make_a_url_clean("https://www.example.com/") 

304 ``` 

305 returns 

306 `#!python "example.com"` 

307 

308 Args: 

309 url (str): The URL to make clean. 

310 

311 Returns: 

312 str: The clean URL. 

313 """ 

314 url = url.replace("https://", "").replace("http://", "") 

315 if url.endswith("/"): 

316 url = url[:-1] 

317 

318 return url 

319 

320 

321def get_date_object(date: str | int) -> Date: 

322 """Parse a date string in YYYY-MM-DD, YYYY-MM, or YYYY format and return a 

323 `datetime.date` object. This function is used throughout the validation process of 

324 the data models. 

325 

326 Args: 

327 date (str | int): The date string to parse. 

328 

329 Returns: 

330 Date: The parsed date. 

331 """ 

332 if isinstance(date, int): 

333 date_object = Date.fromisoformat(f"{date}-01-01") 

334 elif re.fullmatch(r"\d{4}-\d{2}-\d{2}", date): 

335 # Then it is in YYYY-MM-DD format 

336 date_object = Date.fromisoformat(date) 

337 elif re.fullmatch(r"\d{4}-\d{2}", date): 

338 # Then it is in YYYY-MM format 

339 date_object = Date.fromisoformat(f"{date}-01") 

340 elif re.fullmatch(r"\d{4}", date): 

341 # Then it is in YYYY format 

342 date_object = Date.fromisoformat(f"{date}-01-01") 

343 elif date == "present": 

344 date_object = Date.today() 

345 else: 

346 raise ValueError( 

347 "This is not a valid date! Please use either YYYY-MM-DD, YYYY-MM, or" 

348 " YYYY format." 

349 ) 

350 

351 return date_object 

352 

353 

354def dictionary_key_to_proper_section_title(key: str) -> str: 

355 """Convert a dictionary key to a proper section title. 

356 

357 Example: 

358 ```python 

359 dictionary_key_to_proper_section_title("section_title") 

360 ``` 

361 returns 

362 `#!python "Section Title"` 

363 

364 Args: 

365 key (str): The key to convert to a proper section title. 

366 

367 Returns: 

368 str: The proper section title. 

369 """ 

370 title = key.replace("_", " ") 

371 words = title.split(" ") 

372 

373 words_not_capitalized_in_a_title = [ 

374 "a", 

375 "and", 

376 "as", 

377 "at", 

378 "but", 

379 "by", 

380 "for", 

381 "from", 

382 "if", 

383 "in", 

384 "into", 

385 "like", 

386 "near", 

387 "nor", 

388 "of", 

389 "off", 

390 "on", 

391 "onto", 

392 "or", 

393 "over", 

394 "so", 

395 "than", 

396 "that", 

397 "to", 

398 "upon", 

399 "when", 

400 "with", 

401 "yet", 

402 ] 

403 

404 # loop through the words and if the word doesn't contain any uppercase letters, 

405 # capitalize the first letter of the word. If the word contains uppercase letters, 

406 # don't change the word. 

407 proper_title = " ".join( 

408 ( 

409 word.capitalize() 

410 if (word.islower() and word not in words_not_capitalized_in_a_title) 

411 else word 

412 ) 

413 for word in words 

414 ) 

415 

416 return proper_title