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

132 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-12-25 23:06 +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: The phone number to format. 

32 

33 Returns: 

34 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 return phonenumbers.format_number( 

41 parsed_number, getattr(phonenumbers.PhoneNumberFormat, format) 

42 ) 

43 

44 

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

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

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

48 `rendercv.data_models.models` module. 

49 

50 Example: 

51 ```python 

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

53 ``` 

54 will return 

55 

56 `"May 2024"` 

57 

58 Args: 

59 date: The date to format. 

60 date_style: The style of the date string. If not provided, the default date 

61 style from the `locale_catalog` dictionary will be used. 

62 

63 Returns: 

64 The formatted date. 

65 """ 

66 full_month_names = LOCALE_CATALOG["full_names_of_months"] 

67 short_month_names = LOCALE_CATALOG["abbreviations_for_months"] 

68 

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

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

71 

72 placeholders = { 

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

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

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

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

77 "MONTH": str(month), 

78 "YEAR": str(year), 

79 } 

80 if date_style is None: 

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

82 

83 assert isinstance(date_style, str) 

84 

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

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

87 

88 return date_style 

89 

90 

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

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

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

94 full_month_names = LOCALE_CATALOG["full_names_of_months"] 

95 short_month_names = LOCALE_CATALOG["abbreviations_for_months"] 

96 

97 month = Date.today().month 

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

99 

100 placeholders = { 

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

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

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

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

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

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

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

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

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

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

111 "NAME": name, 

112 "YEAR": str(year), 

113 "MONTH": str(month), 

114 } 

115 

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

117 value = value.replace(placeholder, placeholder_value) 

118 

119 return value 

120 

121 

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

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

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

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

126 """ 

127 value = replace_placeholders(value) 

128 

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

130 

131 

132def compute_time_span_string( 

133 start_date: Optional[str | int], 

134 end_date: Optional[str | int], 

135 date: Optional[str | int], 

136) -> str: 

137 """ 

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

139 

140 Example: 

141 ```python 

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

143 ``` 

144 

145 returns 

146 

147 `"4 months"` 

148 

149 Args: 

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

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

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

153 provided, start_date and end_date will be ignored. 

154 

155 Returns: 

156 The computed time span string. 

157 """ 

158 date_is_provided = date is not None 

159 start_date_is_provided = start_date is not None 

160 end_date_is_provided = end_date is not None 

161 

162 if date_is_provided: 

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

164 # empty string. 

165 return "" 

166 

167 if not start_date_is_provided and not end_date_is_provided: 

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

169 return "" 

170 

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

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

173 # specific than years. 

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

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

176 

177 time_span_in_years = end_year - start_year 

178 

179 if time_span_in_years < 2: 

180 time_span_string = "1 year" 

181 else: 

182 time_span_string = f"{time_span_in_years} years" 

183 

184 return time_span_string 

185 

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

187 # format. 

188 end_date = get_date_object(end_date) # type: ignore 

189 start_date = get_date_object(start_date) # type: ignore 

190 

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

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

193 

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

195 how_many_years = timespan_in_days // 365 

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

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

198 how_many_years += how_many_months // 12 

199 how_many_months %= 12 

200 

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

202 if how_many_years == 0: 

203 how_many_years_string = None 

204 elif how_many_years == 1: 

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

206 else: 

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

208 

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

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

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

212 elif how_many_months == 0: 

213 how_many_months_string = None 

214 else: 

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

216 

217 # Combine howManyYearsString and howManyMonthsString: 

218 if how_many_years_string is None and how_many_months_string is not None: 

219 time_span_string = how_many_months_string 

220 elif how_many_months_string is None and how_many_years_string is not None: 

221 time_span_string = how_many_years_string 

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

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

224 else: 

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

226 raise ValueError(message) 

227 

228 return time_span_string.strip() 

229 

230 

231def compute_date_string( 

232 start_date: Optional[str | int], 

233 end_date: Optional[str | int], 

234 date: Optional[str | int], 

235 show_only_years: bool = False, 

236) -> str: 

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

238 

239 Example: 

240 ```python 

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

242 ``` 

243 returns 

244 ``` 

245 "Jan 2020 to Jan 2021" 

246 ``` 

247 

248 Args: 

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

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

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

252 provided, start_date and end_date will be ignored. 

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

254 

255 Returns: 

256 The computed date string. 

257 """ 

258 date_is_provided = date is not None 

259 start_date_is_provided = start_date is not None 

260 end_date_is_provided = end_date is not None 

261 

262 if date_is_provided: 

263 if isinstance(date, int): 

264 # Then it means only the year is provided 

265 date_string = str(date) 

266 else: 

267 try: 

268 date_object = get_date_object(date) 

269 if show_only_years: 

270 date_string = str(date_object.year) 

271 else: 

272 date_string = format_date(date_object) 

273 except ValueError: 

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

275 date_string = str(date) 

276 elif start_date_is_provided and end_date_is_provided: 

277 if isinstance(start_date, int): 

278 # Then it means only the year is provided 

279 start_date = str(start_date) 

280 else: 

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

282 date_object = get_date_object(start_date) 

283 if show_only_years: 

284 start_date = date_object.year 

285 else: 

286 start_date = format_date(date_object) 

287 

288 if end_date == "present": 

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

290 elif isinstance(end_date, int): 

291 # Then it means only the year is provided 

292 end_date = str(end_date) 

293 else: 

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

295 date_object = get_date_object(end_date) 

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

297 

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

299 

300 else: 

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

302 # string: 

303 date_string = "" 

304 

305 return date_string 

306 

307 

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

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

310 

311 Example: 

312 ```python 

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

314 ``` 

315 returns 

316 `"example.com"` 

317 

318 Args: 

319 url: The URL to make clean. 

320 

321 Returns: 

322 The clean URL. 

323 """ 

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

325 if url.endswith("/"): 

326 url = url[:-1] 

327 

328 return url 

329 

330 

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

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

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

334 the data models. 

335 

336 Args: 

337 date: The date string to parse. 

338 

339 Returns: 

340 The parsed date. 

341 """ 

342 if isinstance(date, int): 

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

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

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

346 date_object = Date.fromisoformat(date) 

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

348 # Then it is in YYYY-MM format 

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

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

351 # Then it is in YYYY format 

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

353 elif date == "present": 

354 date_object = Date.today() 

355 else: 

356 message = ( 

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

358 " YYYY format." 

359 ) 

360 raise ValueError(message) 

361 

362 return date_object 

363 

364 

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

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

367 

368 Example: 

369 ```python 

370 dictionary_key_to_proper_section_title("section_title") 

371 ``` 

372 returns 

373 `"Section Title"` 

374 

375 Args: 

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

377 

378 Returns: 

379 The proper section title. 

380 """ 

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

382 words = title.split(" ") 

383 

384 words_not_capitalized_in_a_title = [ 

385 "a", 

386 "and", 

387 "as", 

388 "at", 

389 "but", 

390 "by", 

391 "for", 

392 "from", 

393 "if", 

394 "in", 

395 "into", 

396 "like", 

397 "near", 

398 "nor", 

399 "of", 

400 "off", 

401 "on", 

402 "onto", 

403 "or", 

404 "over", 

405 "so", 

406 "than", 

407 "that", 

408 "to", 

409 "upon", 

410 "when", 

411 "with", 

412 "yet", 

413 ] 

414 

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

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

417 # don't change the word. 

418 return " ".join( 

419 ( 

420 word.capitalize() 

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

422 else word 

423 ) 

424 for word in words 

425 )