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

135 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-01-26 00:25 +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 import locale 

16 

17 

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

19 """Format a phone number to the format specified in the `locale` 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: The phone number to format. 

32 

33 Returns: 

34 The formatted phone number. 

35 """ 

36 

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

38 

39 parsed_number = phonenumbers.parse(phone_number, None) 

40 return phonenumbers.format_number( 

41 parsed_number, getattr(phonenumbers.PhoneNumberFormat, format) 

42 ) 

43 

44 

45def get_date_input() -> Date: 

46 """Return the date input. 

47 

48 Returns: 

49 The date input. 

50 """ 

51 from .rendercv_settings import DATE_INPUT 

52 

53 return DATE_INPUT 

54 

55 

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

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

58 month names are taken from the `locale` dictionary from the 

59 `rendercv.data_models.models` module. 

60 

61 Example: 

62 ```python 

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

64 ``` 

65 will return 

66 

67 `"May 2024"` 

68 

69 Args: 

70 date: The date to format. 

71 date_template: The template of the date string. If not provided, the default date 

72 style from the `locale` dictionary will be used. 

73 

74 Returns: 

75 The formatted date. 

76 """ 

77 full_month_names = locale["full_names_of_months"] 

78 short_month_names = locale["abbreviations_for_months"] 

79 

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

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

82 

83 placeholders = { 

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

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

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

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

88 "MONTH": str(month), 

89 "YEAR": str(year), 

90 } 

91 if date_template is None: 

92 date_template = locale["date_template"] # type: ignore 

93 

94 assert isinstance(date_template, str) 

95 

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

97 date_template = date_template.replace(placeholder, value) # type: ignore 

98 

99 return date_template 

100 

101 

102def replace_placeholders(value: str) -> str: 

103 """Replaces the placeholders in a string with the corresponding values.""" 

104 name = curriculum_vitae.get("name", "None") 

105 full_month_names = locale["full_names_of_months"] 

106 short_month_names = locale["abbreviations_for_months"] 

107 

108 month = get_date_input().month 

109 year = str(get_date_input().year) 

110 

111 placeholders = { 

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

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

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

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

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

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

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

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

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

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

122 "NAME": name, 

123 "YEAR": str(year), 

124 "MONTH": str(month), 

125 } 

126 

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

128 value = value.replace(placeholder, placeholder_value) 

129 

130 return value 

131 

132 

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

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

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

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

137 """ 

138 value = replace_placeholders(value) 

139 

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

141 

142 

143def compute_time_span_string( 

144 start_date: Optional[str | int], 

145 end_date: Optional[str | int], 

146 date: Optional[str | int], 

147) -> str: 

148 """ 

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

150 

151 Example: 

152 ```python 

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

154 ``` 

155 

156 returns 

157 

158 `"4 months"` 

159 

160 Args: 

161 start_date: A start date in YYYY-MM-DD, YYYY-MM, or YYYY format. 

162 end_date: An end date in YYYY-MM-DD, YYYY-MM, or YYYY format or "present". 

163 date: A date in YYYY-MM-DD, YYYY-MM, or YYYY format or a custom string. If 

164 provided, start_date and end_date will be ignored. 

165 

166 Returns: 

167 The computed time span string. 

168 """ 

169 date_is_provided = date is not None 

170 start_date_is_provided = start_date is not None 

171 end_date_is_provided = end_date is not None 

172 

173 if date_is_provided: 

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

175 # empty string. 

176 return "" 

177 

178 if not start_date_is_provided and not end_date_is_provided: 

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

180 return "" 

181 

182 if isinstance(start_date, int) or isinstance(end_date, int): 

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

184 # specific than years. 

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

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

187 

188 time_span_in_years = end_year - start_year 

189 

190 if time_span_in_years < 2: 

191 time_span_string = "1 year" 

192 else: 

193 time_span_string = f"{time_span_in_years} years" 

194 

195 return time_span_string 

196 

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

198 # format. 

199 end_date = get_date_object(end_date) # type: ignore 

200 start_date = get_date_object(start_date) # type: ignore 

201 

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

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

204 

205 # Calculate the number of years and months between start_date and end_date: 

206 how_many_years = timespan_in_days // 365 

207 how_many_months = (timespan_in_days % 365) // 30 + 1 

208 # Deal with overflow (prevent rounding to 1 year 12 months, etc.) 

209 how_many_years += how_many_months // 12 

210 how_many_months %= 12 

211 

212 # Format the number of years and months between start_date and end_date: 

213 if how_many_years == 0: 

214 how_many_years_string = None 

215 elif how_many_years == 1: 

216 how_many_years_string = f"1 {locale['year']}" 

217 else: 

218 how_many_years_string = f"{how_many_years} {locale['years']}" 

219 

220 # Format the number of months between start_date and end_date: 

221 if how_many_months == 1 or (how_many_years_string is None and how_many_months == 0): 

222 how_many_months_string = f"1 {locale['month']}" 

223 elif how_many_months == 0: 

224 how_many_months_string = None 

225 else: 

226 how_many_months_string = f"{how_many_months} {locale['months']}" 

227 

228 # Combine howManyYearsString and howManyMonthsString: 

229 if how_many_years_string is None and how_many_months_string is not None: 

230 time_span_string = how_many_months_string 

231 elif how_many_months_string is None and how_many_years_string is not None: 

232 time_span_string = how_many_years_string 

233 elif how_many_years_string is not None and how_many_months_string is not None: 

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

235 else: 

236 message = "The time span is not valid!" 

237 raise ValueError(message) 

238 

239 return time_span_string.strip() 

240 

241 

242def compute_date_string( 

243 start_date: Optional[str | int], 

244 end_date: Optional[str | int], 

245 date: Optional[str | int], 

246 show_only_years: bool = False, 

247) -> str: 

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

249 

250 Example: 

251 ```python 

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

253 ``` 

254 returns 

255 ``` 

256 "Jan 2020 to Jan 2021" 

257 ``` 

258 

259 Args: 

260 start_date: A start date in YYYY-MM-DD, YYYY-MM, or YYYY format. 

261 end_date: An end date in YYYY-MM-DD, YYYY-MM, or YYYY format or "present". 

262 date: A date in YYYY-MM-DD, YYYY-MM, or YYYY format or a custom string. If 

263 provided, start_date and end_date will be ignored. 

264 show_only_years: If True, only the years will be shown in the date string. 

265 

266 Returns: 

267 The computed date string. 

268 """ 

269 date_is_provided = date is not None 

270 start_date_is_provided = start_date is not None 

271 end_date_is_provided = end_date is not None 

272 

273 if date_is_provided: 

274 if isinstance(date, int): 

275 # Then it means only the year is provided 

276 date_string = str(date) 

277 else: 

278 try: 

279 date_object = get_date_object(date) 

280 if show_only_years: 

281 date_string = str(date_object.year) 

282 else: 

283 date_string = format_date(date_object) 

284 except ValueError: 

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

286 date_string = str(date) 

287 elif start_date_is_provided and end_date_is_provided: 

288 if isinstance(start_date, int): 

289 # Then it means only the year is provided 

290 start_date = str(start_date) 

291 else: 

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

293 date_object = get_date_object(start_date) 

294 if show_only_years: 

295 start_date = date_object.year 

296 else: 

297 start_date = format_date(date_object) 

298 

299 if end_date == "present": 

300 end_date = locale["present"] # type: ignore 

301 elif isinstance(end_date, int): 

302 # Then it means only the year is provided 

303 end_date = str(end_date) 

304 else: 

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

306 date_object = get_date_object(end_date) 

307 end_date = date_object.year if show_only_years else format_date(date_object) 

308 

309 date_string = f"{start_date} {locale['to']} {end_date}" 

310 

311 else: 

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

313 # string: 

314 date_string = "" 

315 

316 return date_string 

317 

318 

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

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

321 

322 Example: 

323 ```python 

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

325 ``` 

326 returns 

327 `"example.com"` 

328 

329 Args: 

330 url: The URL to make clean. 

331 

332 Returns: 

333 The clean URL. 

334 """ 

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

336 if url.endswith("/"): 

337 url = url[:-1] 

338 

339 return url 

340 

341 

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

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

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

345 the data models. 

346 

347 Args: 

348 date: The date string to parse. 

349 

350 Returns: 

351 The parsed date. 

352 """ 

353 if isinstance(date, int): 

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

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

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

357 date_object = Date.fromisoformat(date) 

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

359 # Then it is in YYYY-MM format 

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

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

362 # Then it is in YYYY format 

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

364 elif date == "present": 

365 date_object = get_date_input() 

366 else: 

367 message = ( 

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

369 " YYYY format." 

370 ) 

371 raise ValueError(message) 

372 

373 return date_object 

374 

375 

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

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

378 

379 Example: 

380 ```python 

381 dictionary_key_to_proper_section_title("section_title") 

382 ``` 

383 returns 

384 `"Section Title"` 

385 

386 Args: 

387 key: The key to convert to a proper section title. 

388 

389 Returns: 

390 The proper section title. 

391 """ 

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

393 words = title.split(" ") 

394 

395 words_not_capitalized_in_a_title = [ 

396 "a", 

397 "and", 

398 "as", 

399 "at", 

400 "but", 

401 "by", 

402 "for", 

403 "from", 

404 "if", 

405 "in", 

406 "into", 

407 "like", 

408 "near", 

409 "nor", 

410 "of", 

411 "off", 

412 "on", 

413 "onto", 

414 "or", 

415 "over", 

416 "so", 

417 "than", 

418 "that", 

419 "to", 

420 "upon", 

421 "when", 

422 "with", 

423 "yet", 

424 ] 

425 

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

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

428 # don't change the word. 

429 return " ".join( 

430 ( 

431 word.capitalize() 

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

433 else word 

434 ) 

435 for word in words 

436 )