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

264 statements  

« prev     ^ index     » next       coverage.py v7.0.1, created at 2022-12-25 06:11 +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 _parsed = False 

48 

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

50 """Initializes a new instance of the ExpressionDescriptor 

51 

52 Args: 

53 expression: The cron expression string 

54 options: Options to control the output description 

55 Raises: 

56 WrongArgumentException: if kwarg is unknown 

57 

58 """ 

59 if options is None: 

60 options = Options() 

61 self._expression = expression 

62 self._options = options 

63 self._expression_parts = [] 

64 self._parsed = False 

65 

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

67 for kwarg in kwargs: 

68 if hasattr(self._options, kwarg): 

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

70 else: 

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

72 

73 # Initializes localization 

74 self.get_text = GetText(options.locale_code) 

75 

76 def _(self, message): 

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

78 

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

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

81 

82 Args: 

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

84 Returns: 

85 The cron expression description 

86 Raises: 

87 Exception: if throw_exception_on_parse_error is True 

88 

89 """ 

90 try: 

91 if self._parsed is False: 

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

93 self._expression_parts = parser.parse() 

94 self._parsed = True 

95 

96 choices = { 

97 DescriptionTypeEnum.FULL: self.get_full_description, 

98 DescriptionTypeEnum.TIMEOFDAY: self.get_time_of_day_description, 

99 DescriptionTypeEnum.HOURS: self.get_hours_description, 

100 DescriptionTypeEnum.MINUTES: self.get_minutes_description, 

101 DescriptionTypeEnum.SECONDS: self.get_seconds_description, 

102 DescriptionTypeEnum.DAYOFMONTH: self.get_day_of_month_description, 

103 DescriptionTypeEnum.MONTH: self.get_month_description, 

104 DescriptionTypeEnum.DAYOFWEEK: self.get_day_of_week_description, 

105 DescriptionTypeEnum.YEAR: self.get_year_description, 

106 } 

107 

108 description = choices.get(description_type, self.get_seconds_description)() 

109 

110 except Exception as ex: 

111 if self._options.throw_exception_on_parse_error: 

112 raise 

113 else: 

114 description = str(ex) 

115 return description 

116 

117 def get_full_description(self): 

118 """Generates the FULL description 

119 

120 Returns: 

121 The FULL description 

122 Raises: 

123 FormatException: if formatting fails and throw_exception_on_parse_error is True 

124 

125 """ 

126 

127 try: 

128 time_segment = self.get_time_of_day_description() 

129 day_of_month_desc = self.get_day_of_month_description() 

130 month_desc = self.get_month_description() 

131 day_of_week_desc = self.get_day_of_week_description() 

132 year_desc = self.get_year_description() 

133 

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

135 time_segment, 

136 day_of_month_desc, 

137 day_of_week_desc, 

138 month_desc, 

139 year_desc) 

140 

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

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

143 except Exception: 

144 description = self._( 

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

146 ) 

147 if self._options.throw_exception_on_parse_error: 

148 raise FormatException(description) 

149 

150 return description 

151 

152 def get_time_of_day_description(self): 

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

154 

155 Returns: 

156 The TIMEOFDAY description 

157 

158 """ 

159 seconds_expression = self._expression_parts[0] 

160 minute_expression = self._expression_parts[1] 

161 hour_expression = self._expression_parts[2] 

162 

163 description = StringBuilder() 

164 

165 # handle special cases first 

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

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

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

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

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

171 description.append( 

172 self.format_time( 

173 hour_expression, 

174 minute_expression, 

175 seconds_expression)) 

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

177 "," not in minute_expression and \ 

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

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

180 minute_parts = minute_expression.split('-') 

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

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

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

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

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

186 hour_parts = hour_expression.split(',') 

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

188 for i, hour_part in enumerate(hour_parts): 

189 description.append(" ") 

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

191 

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

193 description.append(",") 

194 

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

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

197 else: 

198 # default time description 

199 seconds_description = self.get_seconds_description() 

200 minutes_description = self.get_minutes_description() 

201 hours_description = self.get_hours_description() 

202 

203 description.append(seconds_description) 

204 

205 if description and minutes_description: 

206 description.append(", ") 

207 

208 description.append(minutes_description) 

209 

210 if description and hours_description: 

211 description.append(", ") 

212 

213 description.append(hours_description) 

214 return str(description) 

215 

216 def get_seconds_description(self): 

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

218 

219 Returns: 

220 The SECONDS description 

221 

222 """ 

223 

224 def get_description_format(s): 

225 if s == "0": 

226 return "" 

227 

228 try: 

229 if int(s) < 20: 

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

231 else: 

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

233 except ValueError: 

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

235 

236 return self.get_segment_description( 

237 self._expression_parts[0], 

238 self._("every second"), 

239 lambda s: s, 

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

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

242 get_description_format, 

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

244 ) 

245 

246 def get_minutes_description(self): 

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

248 

249 Returns: 

250 The MINUTE description 

251 

252 """ 

253 seconds_expression = self._expression_parts[0] 

254 

255 def get_description_format(s): 

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

257 return "" 

258 

259 try: 

260 if int(s) < 20: 

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

262 else: 

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

264 except ValueError: 

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

266 

267 return self.get_segment_description( 

268 self._expression_parts[1], 

269 self._("every minute"), 

270 lambda s: s, 

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

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

273 get_description_format, 

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

275 ) 

276 

277 def get_hours_description(self): 

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

279 

280 Returns: 

281 The HOUR description 

282 

283 """ 

284 expression = self._expression_parts[2] 

285 return self.get_segment_description( 

286 expression, 

287 self._("every hour"), 

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

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

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

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

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

293 ) 

294 

295 def get_day_of_week_description(self): 

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

297 

298 Returns: 

299 The DAYOFWEEK description 

300 

301 """ 

302 

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

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

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

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

307 return "" 

308 

309 def get_day_name(s): 

310 exp = s 

311 if "#" in s: 

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

313 elif "L" in s: 

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

315 return ExpressionDescriptor.number_to_day(int(exp)) 

316 

317 def get_format(s): 

318 if "#" in s: 

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

320 

321 try: 

322 day_of_week_of_month_number = int(day_of_week_of_month) 

323 choices = { 

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

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

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

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

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

329 } 

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

331 except ValueError: 

332 day_of_week_of_month_description = '' 

333 

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

335 elif "L" in s: 

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

337 else: 

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

339 

340 return formatted 

341 

342 return self.get_segment_description( 

343 self._expression_parts[5], 

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

345 lambda s: get_day_name(s), 

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

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

348 lambda s: get_format(s), 

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

350 ) 

351 

352 def get_month_description(self): 

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

354 

355 Returns: 

356 The MONTH description 

357 

358 """ 

359 return self.get_segment_description( 

360 self._expression_parts[4], 

361 '', 

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

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

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

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

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

367 ) 

368 

369 def get_day_of_month_description(self): 

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

371 

372 Returns: 

373 The DAYOFMONTH description 

374 

375 """ 

376 expression = self._expression_parts[3] 

377 

378 if expression == "L": 

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

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

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

382 else: 

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

384 m = regex.match(expression) 

385 if m: # if matches 

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

387 

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

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

390 else: 

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

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

393 m = regex.match(expression) 

394 if m: # if matches 

395 off_set_days = m.group(1) 

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

397 else: 

398 description = self.get_segment_description( 

399 expression, 

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

401 lambda s: s, 

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

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

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

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

406 ) 

407 

408 return description 

409 

410 def get_year_description(self): 

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

412 

413 Returns: 

414 The YEAR description 

415 

416 """ 

417 

418 def format_year(s): 

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

420 if regex.match(s): 

421 year_int = int(s) 

422 if year_int < 1900: 

423 return year_int 

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

425 else: 

426 return s 

427 

428 return self.get_segment_description( 

429 self._expression_parts[6], 

430 '', 

431 lambda s: format_year(s), 

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

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

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

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

436 ) 

437 

438 def get_segment_description( 

439 self, 

440 expression, 

441 all_description, 

442 get_single_item_description, 

443 get_interval_description_format, 

444 get_between_description_format, 

445 get_description_format, 

446 get_range_format 

447 ): 

448 """Returns segment description 

449 Args: 

450 expression: Segment to descript 

451 all_description: * 

452 get_single_item_description: 1 

453 get_interval_description_format: 1/2 

454 get_between_description_format: 1-2 

455 get_description_format: format get_single_item_description 

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

457 Returns: 

458 segment description 

459 

460 """ 

461 description = None 

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

463 description = '' 

464 elif expression == "*": 

465 description = all_description 

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

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

468 elif "/" in expression: 

469 segments = expression.split('/') 

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

471 

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

473 if "-" in segments[0]: 

474 between_segment_description = self.generate_between_segment_description( 

475 segments[0], 

476 get_between_description_format, 

477 get_single_item_description 

478 ) 

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

480 description += ", " 

481 

482 description += between_segment_description 

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

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

485 get_single_item_description(segments[0]) 

486 ) 

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

488 

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

490 elif "," in expression: 

491 segments = expression.split(',') 

492 

493 description_content = '' 

494 for i, segment in enumerate(segments): 

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

496 description_content += "," 

497 

498 if i < len(segments) - 1: 

499 description_content += " " 

500 

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

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

503 

504 if "-" in segment: 

505 between_segment_description = self.generate_between_segment_description( 

506 segment, 

507 get_range_format, 

508 get_single_item_description 

509 ) 

510 

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

512 

513 description_content += between_segment_description 

514 else: 

515 description_content += get_single_item_description(segment) 

516 

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

518 elif "-" in expression: 

519 description = self.generate_between_segment_description( 

520 expression, 

521 get_between_description_format, 

522 get_single_item_description 

523 ) 

524 

525 return description 

526 

527 def generate_between_segment_description( 

528 self, 

529 between_expression, 

530 get_between_description_format, 

531 get_single_item_description 

532 ): 

533 """ 

534 Generates the between segment description 

535 :param between_expression: 

536 :param get_between_description_format: 

537 :param get_single_item_description: 

538 :return: The between segment description 

539 """ 

540 description = "" 

541 between_segments = between_expression.split('-') 

542 between_segment_1_description = get_single_item_description(between_segments[0]) 

543 between_segment_2_description = get_single_item_description(between_segments[1]) 

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

545 

546 between_description_format = get_between_description_format(between_expression) 

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

548 

549 return description 

550 

551 def format_time( 

552 self, 

553 hour_expression, 

554 minute_expression, 

555 second_expression='' 

556 ): 

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

558 Args: 

559 hour_expression: Hours part 

560 minute_expression: Minutes part 

561 second_expression: Seconds part 

562 Returns: 

563 Formatted time description 

564 

565 """ 

566 hour = int(hour_expression) 

567 

568 period = '' 

569 if self._options.use_24hour_time_format is False: 

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

571 if period: 

572 # add preceding space 

573 period = " " + period 

574 

575 if hour > 12: 

576 hour -= 12 

577 

578 if hour == 0: 

579 hour = 12 

580 

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

582 second = '' 

583 if second_expression is not None and second_expression: 

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

585 

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

587 

588 def transform_verbosity(self, description, use_verbose_format): 

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

590 Args: 

591 description: The description to transform 

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

593 Returns: 

594 The transformed description with proper verbosity 

595 

596 """ 

597 if use_verbose_format is False: 

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

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

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

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

602 return description 

603 

604 @staticmethod 

605 def transform_case(description, case_type): 

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

607 Args: 

608 description: The description to transform 

609 case_type: The casing type that controls the output casing 

610 Returns: 

611 The transformed description with proper casing 

612 """ 

613 if case_type == CasingTypeEnum.Sentence: 

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

615 description[0].upper(), 

616 description[1:]) 

617 elif case_type == CasingTypeEnum.Title: 

618 description = description.title() 

619 else: 

620 description = description.lower() 

621 return description 

622 

623 @staticmethod 

624 def number_to_day(day_number): 

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

626 

627 Args: 

628 day_number: Number of a day 

629 Returns: 

630 Day corresponding to day_number 

631 Raises: 

632 IndexError: When day_number is not found 

633 """ 

634 try: 

635 return [ 

636 calendar.day_name[6], 

637 calendar.day_name[0], 

638 calendar.day_name[1], 

639 calendar.day_name[2], 

640 calendar.day_name[3], 

641 calendar.day_name[4], 

642 calendar.day_name[5] 

643 ][day_number] 

644 except IndexError: 

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

646 

647 def __str__(self): 

648 return self.get_description() 

649 

650 def __repr__(self): 

651 return self.get_description() 

652 

653 

654def get_description(expression, options=None): 

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

656 Args: 

657 expression: The cron expression string 

658 options: Options to control the output description 

659 Returns: 

660 The cron expression description 

661 

662 """ 

663 descripter = ExpressionDescriptor(expression, options) 

664 return descripter.get_description(DescriptionTypeEnum.FULL)