Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/cron_descriptor/ExpressionDescriptor.py: 43%

253 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:35 +0000

1# The MIT License (MIT) 

2# 

3# Copyright (c) 2016 Adam Schubert 

4# 

5# Permission is hereby granted, free of charge, to any person obtaining a copy 

6# of this software and associated documentation files (the "Software"), to deal 

7# in the Software without restriction, including without limitation the rights 

8# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 

9# copies of the Software, and to permit persons to whom the Software is 

10# furnished to do so, subject to the following conditions: 

11# 

12# The above copyright notice and this permission notice shall be included in all 

13# copies or substantial portions of the Software. 

14# 

15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 

16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 

18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 

19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 

20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 

21# SOFTWARE. 

22 

23import re 

24import datetime 

25import calendar 

26 

27from .GetText import GetText 

28from .CasingTypeEnum import CasingTypeEnum 

29from .DescriptionTypeEnum import DescriptionTypeEnum 

30from .ExpressionParser import ExpressionParser 

31from .Options import Options 

32from .StringBuilder import StringBuilder 

33from .Exception import FormatException, WrongArgumentException 

34 

35 

36class ExpressionDescriptor: 

37 

38 """ 

39 Converts a Cron Expression into a human readable string 

40 """ 

41 

42 _special_characters = ['/', '-', ',', '*'] 

43 

44 _expression = '' 

45 _options = None 

46 _expression_parts = [] 

47 

48 def __init__(self, expression, options=None, **kwargs): 

49 """Initializes a new instance of the ExpressionDescriptor 

50 

51 Args: 

52 expression: The cron expression string 

53 options: Options to control the output description 

54 Raises: 

55 WrongArgumentException: if kwarg is unknown 

56 

57 """ 

58 if options is None: 

59 options = Options() 

60 self._expression = expression 

61 self._options = options 

62 self._expression_parts = [] 

63 

64 # if kwargs in _options, overwrite it, if not raise exception 

65 for kwarg in kwargs: 

66 if hasattr(self._options, kwarg): 

67 setattr(self._options, kwarg, kwargs[kwarg]) 

68 else: 

69 raise WrongArgumentException("Unknown {} configuration argument".format(kwarg)) 

70 

71 # Initializes localization 

72 self.get_text = GetText(options.locale_code, options.locale_location) 

73 

74 # Parse expression 

75 parser = ExpressionParser(self._expression, self._options) 

76 self._expression_parts = parser.parse() 

77 

78 def _(self, message): 

79 return self.get_text.trans.gettext(message) 

80 

81 def get_description(self, description_type=DescriptionTypeEnum.FULL): 

82 """Generates a humanreadable string for the Cron Expression 

83 

84 Args: 

85 description_type: Which part(s) of the expression to describe 

86 Returns: 

87 The cron expression description 

88 Raises: 

89 Exception: 

90 

91 """ 

92 choices = { 

93 DescriptionTypeEnum.FULL: self.get_full_description, 

94 DescriptionTypeEnum.TIMEOFDAY: self.get_time_of_day_description, 

95 DescriptionTypeEnum.HOURS: self.get_hours_description, 

96 DescriptionTypeEnum.MINUTES: self.get_minutes_description, 

97 DescriptionTypeEnum.SECONDS: self.get_seconds_description, 

98 DescriptionTypeEnum.DAYOFMONTH: self.get_day_of_month_description, 

99 DescriptionTypeEnum.MONTH: self.get_month_description, 

100 DescriptionTypeEnum.DAYOFWEEK: self.get_day_of_week_description, 

101 DescriptionTypeEnum.YEAR: self.get_year_description, 

102 } 

103 

104 return choices.get(description_type, self.get_seconds_description)() 

105 

106 def get_full_description(self): 

107 """Generates the FULL description 

108 

109 Returns: 

110 The FULL description 

111 Raises: 

112 FormatException: if formatting fails 

113 

114 """ 

115 

116 try: 

117 time_segment = self.get_time_of_day_description() 

118 day_of_month_desc = self.get_day_of_month_description() 

119 month_desc = self.get_month_description() 

120 day_of_week_desc = self.get_day_of_week_description() 

121 year_desc = self.get_year_description() 

122 

123 description = "{0}{1}{2}{3}{4}".format( 

124 time_segment, 

125 day_of_month_desc, 

126 day_of_week_desc, 

127 month_desc, 

128 year_desc) 

129 

130 description = self.transform_verbosity(description, self._options.verbose) 

131 description = ExpressionDescriptor.transform_case(description, self._options.casing_type) 

132 except Exception: 

133 description = self._( 

134 "An error occurred when generating the expression description. Check the cron expression syntax." 

135 ) 

136 raise FormatException(description) 

137 

138 return description 

139 

140 def get_time_of_day_description(self): 

141 """Generates a description for only the TIMEOFDAY portion of the expression 

142 

143 Returns: 

144 The TIMEOFDAY description 

145 

146 """ 

147 seconds_expression = self._expression_parts[0] 

148 minute_expression = self._expression_parts[1] 

149 hour_expression = self._expression_parts[2] 

150 

151 description = StringBuilder() 

152 

153 # handle special cases first 

154 if any(exp in minute_expression for exp in self._special_characters) is False and \ 

155 any(exp in hour_expression for exp in self._special_characters) is False and \ 

156 any(exp in seconds_expression for exp in self._special_characters) is False: 

157 # specific time of day (i.e. 10 14) 

158 description.append(self._("At ")) 

159 description.append( 

160 self.format_time( 

161 hour_expression, 

162 minute_expression, 

163 seconds_expression)) 

164 elif seconds_expression == "" and "-" in minute_expression and \ 

165 "," not in minute_expression and \ 

166 any(exp in hour_expression for exp in self._special_characters) is False: 

167 # minute range in single hour (i.e. 0-10 11) 

168 minute_parts = minute_expression.split('-') 

169 description.append(self._("Every minute between {0} and {1}").format( 

170 self.format_time(hour_expression, minute_parts[0]), self.format_time(hour_expression, minute_parts[1]))) 

171 elif seconds_expression == "" and "," in hour_expression and "-" not in hour_expression and \ 

172 any(exp in minute_expression for exp in self._special_characters) is False: 

173 # hours list with single minute (o.e. 30 6,14,16) 

174 hour_parts = hour_expression.split(',') 

175 description.append(self._("At")) 

176 for i, hour_part in enumerate(hour_parts): 

177 description.append(" ") 

178 description.append(self.format_time(hour_part, minute_expression)) 

179 

180 if i < (len(hour_parts) - 2): 

181 description.append(",") 

182 

183 if i == len(hour_parts) - 2: 

184 description.append(self._(" and")) 

185 else: 

186 # default time description 

187 seconds_description = self.get_seconds_description() 

188 minutes_description = self.get_minutes_description() 

189 hours_description = self.get_hours_description() 

190 

191 description.append(seconds_description) 

192 

193 if description and minutes_description: 

194 description.append(", ") 

195 

196 description.append(minutes_description) 

197 

198 if description and hours_description: 

199 description.append(", ") 

200 

201 description.append(hours_description) 

202 return str(description) 

203 

204 def get_seconds_description(self): 

205 """Generates a description for only the SECONDS portion of the expression 

206 

207 Returns: 

208 The SECONDS description 

209 

210 """ 

211 

212 def get_description_format(s): 

213 if s == "0": 

214 return "" 

215 

216 try: 

217 if int(s) < 20: 

218 return self._("at {0} seconds past the minute") 

219 else: 

220 return self._("at {0} seconds past the minute [grThen20]") or self._("at {0} seconds past the minute") 

221 except ValueError: 

222 return self._("at {0} seconds past the minute") 

223 

224 return self.get_segment_description( 

225 self._expression_parts[0], 

226 self._("every second"), 

227 lambda s: s, 

228 lambda s: self._("every {0} seconds").format(s), 

229 lambda s: self._("seconds {0} through {1} past the minute"), 

230 get_description_format, 

231 lambda s: self._(", second {0} through second {1}") or self._(", {0} through {1}") 

232 ) 

233 

234 def get_minutes_description(self): 

235 """Generates a description for only the MINUTE portion of the expression 

236 

237 Returns: 

238 The MINUTE description 

239 

240 """ 

241 seconds_expression = self._expression_parts[0] 

242 

243 def get_description_format(s): 

244 if s == "0" and seconds_expression == "": 

245 return "" 

246 

247 try: 

248 if int(s) < 20: 

249 return self._("at {0} minutes past the hour") 

250 else: 

251 return self._("at {0} minutes past the hour [grThen20]") or self._("at {0} minutes past the hour") 

252 except ValueError: 

253 return self._("at {0} minutes past the hour") 

254 

255 return self.get_segment_description( 

256 self._expression_parts[1], 

257 self._("every minute"), 

258 lambda s: s, 

259 lambda s: self._("every {0} minutes").format(s), 

260 lambda s: self._("minutes {0} through {1} past the hour"), 

261 get_description_format, 

262 lambda s: self._(", minute {0} through minute {1}") or self._(", {0} through {1}") 

263 ) 

264 

265 def get_hours_description(self): 

266 """Generates a description for only the HOUR portion of the expression 

267 

268 Returns: 

269 The HOUR description 

270 

271 """ 

272 expression = self._expression_parts[2] 

273 return self.get_segment_description( 

274 expression, 

275 self._("every hour"), 

276 lambda s: self.format_time(s, "0"), 

277 lambda s: self._("every {0} hours").format(s), 

278 lambda s: self._("between {0} and {1}"), 

279 lambda s: self._("at {0}"), 

280 lambda s: self._(", hour {0} through hour {1}") or self._(", {0} through {1}") 

281 ) 

282 

283 def get_day_of_week_description(self): 

284 """Generates a description for only the DAYOFWEEK portion of the expression 

285 

286 Returns: 

287 The DAYOFWEEK description 

288 

289 """ 

290 

291 if self._expression_parts[5] == "*": 

292 # DOW is specified as * so we will not generate a description and defer to DOM part. 

293 # Otherwise, we could get a contradiction like "on day 1 of the month, every day" 

294 # or a dupe description like "every day, every day". 

295 return "" 

296 

297 def get_day_name(s): 

298 exp = s 

299 if "#" in s: 

300 exp, _ = s.split("#", 2) 

301 elif "L" in s: 

302 exp = exp.replace("L", '') 

303 return ExpressionDescriptor.number_to_day(int(exp)) 

304 

305 def get_format(s): 

306 if "#" in s: 

307 day_of_week_of_month = s[s.find("#") + 1:] 

308 

309 try: 

310 day_of_week_of_month_number = int(day_of_week_of_month) 

311 choices = { 

312 1: self._("first"), 

313 2: self._("second"), 

314 3: self._("third"), 

315 4: self._("forth"), 

316 5: self._("fifth"), 

317 } 

318 day_of_week_of_month_description = choices.get(day_of_week_of_month_number, '') 

319 except ValueError: 

320 day_of_week_of_month_description = '' 

321 

322 formatted = "{}{}{}".format(self._(", on the "), day_of_week_of_month_description, self._(" {0} of the month")) 

323 elif "L" in s: 

324 formatted = self._(", on the last {0} of the month") 

325 else: 

326 formatted = self._(", only on {0}") 

327 

328 return formatted 

329 

330 return self.get_segment_description( 

331 self._expression_parts[5], 

332 self._(", every day"), 

333 lambda s: get_day_name(s), 

334 lambda s: self._(", every {0} days of the week").format(s), 

335 lambda s: self._(", {0} through {1}"), 

336 lambda s: get_format(s), 

337 lambda s: self._(", {0} through {1}") 

338 ) 

339 

340 def get_month_description(self): 

341 """Generates a description for only the MONTH portion of the expression 

342 

343 Returns: 

344 The MONTH description 

345 

346 """ 

347 return self.get_segment_description( 

348 self._expression_parts[4], 

349 '', 

350 lambda s: datetime.date(datetime.date.today().year, int(s), 1).strftime("%B"), 

351 lambda s: self._(", every {0} months").format(s), 

352 lambda s: self._(", month {0} through month {1}") or self._(", {0} through {1}"), 

353 lambda s: self._(", only in {0}"), 

354 lambda s: self._(", month {0} through month {1}") or self._(", {0} through {1}") 

355 ) 

356 

357 def get_day_of_month_description(self): 

358 """Generates a description for only the DAYOFMONTH portion of the expression 

359 

360 Returns: 

361 The DAYOFMONTH description 

362 

363 """ 

364 expression = self._expression_parts[3] 

365 

366 if expression == "L": 

367 description = self._(", on the last day of the month") 

368 elif expression == "LW" or expression == "WL": 

369 description = self._(", on the last weekday of the month") 

370 else: 

371 regex = re.compile(r"(\d{1,2}W)|(W\d{1,2})") 

372 m = regex.match(expression) 

373 if m: # if matches 

374 day_number = int(m.group().replace("W", "")) 

375 

376 day_string = self._("first weekday") if day_number == 1 else self._("weekday nearest day {0}").format(day_number) 

377 description = self._(", on the {0} of the month").format(day_string) 

378 else: 

379 # Handle "last day offset"(i.e.L - 5: "5 days before the last day of the month") 

380 regex = re.compile(r"L-(\d{1,2})") 

381 m = regex.match(expression) 

382 if m: # if matches 

383 off_set_days = m.group(1) 

384 description = self._(", {0} days before the last day of the month").format(off_set_days) 

385 else: 

386 description = self.get_segment_description( 

387 expression, 

388 self._(", every day"), 

389 lambda s: s, 

390 lambda s: self._(", every day") if s == "1" else self._(", every {0} days"), 

391 lambda s: self._(", between day {0} and {1} of the month"), 

392 lambda s: self._(", on day {0} of the month"), 

393 lambda s: self._(", {0} through {1}") 

394 ) 

395 

396 return description 

397 

398 def get_year_description(self): 

399 """Generates a description for only the YEAR portion of the expression 

400 

401 Returns: 

402 The YEAR description 

403 

404 """ 

405 

406 def format_year(s): 

407 regex = re.compile(r"^\d+$") 

408 if regex.match(s): 

409 year_int = int(s) 

410 if year_int < 1900: 

411 return year_int 

412 return datetime.date(year_int, 1, 1).strftime("%Y") 

413 else: 

414 return s 

415 

416 return self.get_segment_description( 

417 self._expression_parts[6], 

418 '', 

419 lambda s: format_year(s), 

420 lambda s: self._(", every {0} years").format(s), 

421 lambda s: self._(", year {0} through year {1}") or self._(", {0} through {1}"), 

422 lambda s: self._(", only in {0}"), 

423 lambda s: self._(", year {0} through year {1}") or self._(", {0} through {1}") 

424 ) 

425 

426 def get_segment_description( 

427 self, 

428 expression, 

429 all_description, 

430 get_single_item_description, 

431 get_interval_description_format, 

432 get_between_description_format, 

433 get_description_format, 

434 get_range_format 

435 ): 

436 """Returns segment description 

437 Args: 

438 expression: Segment to descript 

439 all_description: * 

440 get_single_item_description: 1 

441 get_interval_description_format: 1/2 

442 get_between_description_format: 1-2 

443 get_description_format: format get_single_item_description 

444 get_range_format: function that formats range expressions depending on cron parts 

445 Returns: 

446 segment description 

447 

448 """ 

449 description = None 

450 if expression is None or expression == '': 

451 description = '' 

452 elif expression == "*": 

453 description = all_description 

454 elif any(ext in expression for ext in ['/', '-', ',']) is False: 

455 description = get_description_format(expression).format(get_single_item_description(expression)) 

456 elif "/" in expression: 

457 segments = expression.split('/') 

458 description = get_interval_description_format(segments[1]).format(get_single_item_description(segments[1])) 

459 

460 # interval contains 'between' piece (i.e. 2-59/3 ) 

461 if "-" in segments[0]: 

462 between_segment_description = self.generate_between_segment_description( 

463 segments[0], 

464 get_between_description_format, 

465 get_single_item_description 

466 ) 

467 if not between_segment_description.startswith(", "): 

468 description += ", " 

469 

470 description += between_segment_description 

471 elif any(ext in segments[0] for ext in ['*', ',']) is False: 

472 range_item_description = get_description_format(segments[0]).format( 

473 get_single_item_description(segments[0]) 

474 ) 

475 range_item_description = range_item_description.replace(", ", "") 

476 

477 description += self._(", starting {0}").format(range_item_description) 

478 elif "," in expression: 

479 segments = expression.split(',') 

480 

481 description_content = '' 

482 for i, segment in enumerate(segments): 

483 if i > 0 and len(segments) > 2: 

484 description_content += "," 

485 

486 if i < len(segments) - 1: 

487 description_content += " " 

488 

489 if i > 0 and len(segments) > 1 and (i == len(segments) - 1 or len(segments) == 2): 

490 description_content += self._(" and ") 

491 

492 if "-" in segment: 

493 between_segment_description = self.generate_between_segment_description( 

494 segment, 

495 get_range_format, 

496 get_single_item_description 

497 ) 

498 

499 between_segment_description = between_segment_description.replace(", ", "") 

500 

501 description_content += between_segment_description 

502 else: 

503 description_content += get_single_item_description(segment) 

504 

505 description = get_description_format(expression).format(description_content) 

506 elif "-" in expression: 

507 description = self.generate_between_segment_description( 

508 expression, 

509 get_between_description_format, 

510 get_single_item_description 

511 ) 

512 

513 return description 

514 

515 def generate_between_segment_description( 

516 self, 

517 between_expression, 

518 get_between_description_format, 

519 get_single_item_description 

520 ): 

521 """ 

522 Generates the between segment description 

523 :param between_expression: 

524 :param get_between_description_format: 

525 :param get_single_item_description: 

526 :return: The between segment description 

527 """ 

528 description = "" 

529 between_segments = between_expression.split('-') 

530 between_segment_1_description = get_single_item_description(between_segments[0]) 

531 between_segment_2_description = get_single_item_description(between_segments[1]) 

532 between_segment_2_description = between_segment_2_description.replace(":00", ":59") 

533 

534 between_description_format = get_between_description_format(between_expression) 

535 description += between_description_format.format(between_segment_1_description, between_segment_2_description) 

536 

537 return description 

538 

539 def format_time( 

540 self, 

541 hour_expression, 

542 minute_expression, 

543 second_expression='' 

544 ): 

545 """Given time parts, will construct a formatted time description 

546 Args: 

547 hour_expression: Hours part 

548 minute_expression: Minutes part 

549 second_expression: Seconds part 

550 Returns: 

551 Formatted time description 

552 

553 """ 

554 hour = int(hour_expression) 

555 

556 period = '' 

557 if self._options.use_24hour_time_format is False: 

558 period = self._("PM") if (hour >= 12) else self._("AM") 

559 if period: 

560 # add preceding space 

561 period = " " + period 

562 

563 if hour > 12: 

564 hour -= 12 

565 

566 if hour == 0: 

567 hour = 12 

568 

569 minute = str(int(minute_expression)) # Removes leading zero if any 

570 second = '' 

571 if second_expression is not None and second_expression: 

572 second = "{}{}".format(":", str(int(second_expression)).zfill(2)) 

573 

574 return "{0}:{1}{2}{3}".format(str(hour).zfill(2), minute.zfill(2), second, period) 

575 

576 def transform_verbosity(self, description, use_verbose_format): 

577 """Transforms the verbosity of the expression description by stripping verbosity from original description 

578 Args: 

579 description: The description to transform 

580 use_verbose_format: If True, will leave description as it, if False, will strip verbose parts 

581 Returns: 

582 The transformed description with proper verbosity 

583 

584 """ 

585 if use_verbose_format is False: 

586 description = description.replace(self._(", every minute"), '') 

587 description = description.replace(self._(", every hour"), '') 

588 description = description.replace(self._(", every day"), '') 

589 description = re.sub(r', ?$', '', description) 

590 return description 

591 

592 @staticmethod 

593 def transform_case(description, case_type): 

594 """Transforms the case of the expression description, based on options 

595 Args: 

596 description: The description to transform 

597 case_type: The casing type that controls the output casing 

598 Returns: 

599 The transformed description with proper casing 

600 """ 

601 if case_type == CasingTypeEnum.Sentence: 

602 description = "{}{}".format( 

603 description[0].upper(), 

604 description[1:]) 

605 elif case_type == CasingTypeEnum.Title: 

606 description = description.title() 

607 else: 

608 description = description.lower() 

609 return description 

610 

611 @staticmethod 

612 def number_to_day(day_number): 

613 """Returns localized day name by its CRON number 

614 

615 Args: 

616 day_number: Number of a day 

617 Returns: 

618 Day corresponding to day_number 

619 Raises: 

620 IndexError: When day_number is not found 

621 """ 

622 try: 

623 return [ 

624 calendar.day_name[6], 

625 calendar.day_name[0], 

626 calendar.day_name[1], 

627 calendar.day_name[2], 

628 calendar.day_name[3], 

629 calendar.day_name[4], 

630 calendar.day_name[5] 

631 ][day_number] 

632 except IndexError: 

633 raise IndexError("Day {} is out of range!".format(day_number)) 

634 

635 def __str__(self): 

636 return self.get_description() 

637 

638 def __repr__(self): 

639 return self.get_description() 

640 

641 

642def get_description(expression, options=None): 

643 """Generates a human readable string for the Cron Expression 

644 Args: 

645 expression: The cron expression string 

646 options: Options to control the output description 

647 Returns: 

648 The cron expression description 

649 

650 """ 

651 descripter = ExpressionDescriptor(expression, options) 

652 return descripter.get_description(DescriptionTypeEnum.FULL)