Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/croniter/croniter.py: 28%

551 statements  

« prev     ^ index     » next       coverage.py v7.0.1, created at 2022-12-25 06:11 +0000

1#!/usr/bin/env python 

2# -*- coding: utf-8 -*- 

3 

4from __future__ import absolute_import, print_function, division 

5 

6import math 

7import re 

8import sys 

9import inspect 

10from time import time 

11import datetime 

12from dateutil.relativedelta import relativedelta 

13from dateutil.tz import tzutc 

14import calendar 

15import binascii 

16import random 

17 

18try: 

19 from collections import OrderedDict 

20except ImportError: 

21 OrderedDict = dict # py26 degraded mode, expanders order will not be immutable 

22 

23 

24step_search_re = re.compile(r'^([^-]+)-([^-/]+)(/(\d+))?$') 

25only_int_re = re.compile(r'^\d+$') 

26star_or_int_re = re.compile(r'^(\d+|\*)$') 

27special_weekday_re = re.compile(r'^(\w+)#(\d+)|l(\d+)$') 

28hash_expression_re = re.compile( 

29 r'^(?P<hash_type>h|r)(\((?P<range_begin>\d+)-(?P<range_end>\d+)\))?(\/(?P<divisor>\d+))?$' 

30) 

31VALID_LEN_EXPRESSION = [5, 6] 

32 

33 

34def timedelta_to_seconds(td): 

35 return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) \ 

36 / 10**6 

37 

38 

39def datetime_to_timestamp(d): 

40 if d.tzinfo is not None: 

41 d = d.replace(tzinfo=None) - d.utcoffset() 

42 

43 return timedelta_to_seconds(d - datetime.datetime(1970, 1, 1)) 

44 

45 

46def _get_caller_globals_and_locals(): 

47 """ 

48 Returns the globals and locals of the calling frame. 

49 

50 Is there an alternative to frame hacking here? 

51 """ 

52 caller_frame = inspect.stack()[2] 

53 myglobals = caller_frame[0].f_globals 

54 mylocals = caller_frame[0].f_locals 

55 return myglobals, mylocals 

56 

57 

58class CroniterError(ValueError): 

59 """ General top-level Croniter base exception """ 

60 pass 

61 

62 

63class CroniterBadTypeRangeError(TypeError): 

64 """.""" 

65 

66 

67class CroniterBadCronError(CroniterError): 

68 """ Syntax, unknown value, or range error within a cron expression """ 

69 pass 

70 

71 

72class CroniterUnsupportedSyntaxError(CroniterBadCronError): 

73 """ Valid cron syntax, but likely to produce inaccurate results """ 

74 # Extending CroniterBadCronError, which may be contridatory, but this allows 

75 # catching both errors with a single exception. From a user perspective 

76 # these will likely be handled the same way. 

77 pass 

78 

79 

80class CroniterBadDateError(CroniterError): 

81 """ Unable to find next/prev timestamp match """ 

82 pass 

83 

84 

85class CroniterNotAlphaError(CroniterBadCronError): 

86 """ Cron syntax contains an invalid day or month abbreviation """ 

87 pass 

88 

89 

90class croniter(object): 

91 MONTHS_IN_YEAR = 12 

92 RANGES = ( 

93 (0, 59), 

94 (0, 23), 

95 (1, 31), 

96 (1, 12), 

97 (0, 7), 

98 (0, 59) 

99 ) 

100 DAYS = ( 

101 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 

102 ) 

103 

104 ALPHACONV = ( 

105 {}, # 0: min 

106 {}, # 1: hour 

107 {"l": "l"}, # 2: dom 

108 # 3: mon 

109 {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6, 

110 'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12}, 

111 # 4: dow 

112 {'sun': 0, 'mon': 1, 'tue': 2, 'wed': 3, 'thu': 4, 'fri': 5, 'sat': 6}, 

113 # command/user 

114 {} 

115 ) 

116 

117 LOWMAP = ( 

118 {}, 

119 {}, 

120 {0: 1}, 

121 {0: 1}, 

122 {7: 0}, 

123 {}, 

124 ) 

125 

126 LEN_MEANS_ALL = ( 

127 60, 

128 24, 

129 31, 

130 12, 

131 7, 

132 60 

133 ) 

134 

135 bad_length = 'Exactly 5 or 6 columns has to be specified for iterator ' \ 

136 'expression.' 

137 

138 def __init__(self, expr_format, start_time=None, ret_type=float, 

139 day_or=True, max_years_between_matches=None, is_prev=False, 

140 hash_id=None): 

141 self._ret_type = ret_type 

142 self._day_or = day_or 

143 

144 if hash_id: 

145 if not isinstance(hash_id, (bytes, str)): 

146 raise TypeError('hash_id must be bytes or UTF-8 string') 

147 if not isinstance(hash_id, bytes): 

148 hash_id = hash_id.encode('UTF-8') 

149 

150 self._max_years_btw_matches_explicitly_set = ( 

151 max_years_between_matches is not None) 

152 if not self._max_years_btw_matches_explicitly_set: 

153 max_years_between_matches = 50 

154 self._max_years_between_matches = max(int(max_years_between_matches), 1) 

155 

156 if start_time is None: 

157 start_time = time() 

158 

159 self.tzinfo = None 

160 

161 self.start_time = None 

162 self.dst_start_time = None 

163 self.cur = None 

164 self.set_current(start_time, force=False) 

165 

166 self.expanded, self.nth_weekday_of_month = self.expand(expr_format, hash_id=hash_id) 

167 self._is_prev = is_prev 

168 

169 @classmethod 

170 def _alphaconv(cls, index, key, expressions): 

171 try: 

172 return cls.ALPHACONV[index][key] 

173 except KeyError: 

174 raise CroniterNotAlphaError( 

175 "[{0}] is not acceptable".format(" ".join(expressions))) 

176 

177 def get_next(self, ret_type=None, start_time=None): 

178 self.set_current(start_time, force=True) 

179 return self._get_next(ret_type or self._ret_type, is_prev=False) 

180 

181 def get_prev(self, ret_type=None): 

182 return self._get_next(ret_type or self._ret_type, is_prev=True) 

183 

184 def get_current(self, ret_type=None): 

185 ret_type = ret_type or self._ret_type 

186 if issubclass(ret_type, datetime.datetime): 

187 return self._timestamp_to_datetime(self.cur) 

188 return self.cur 

189 

190 def set_current(self, start_time, force=True): 

191 if (force or (self.cur is None)) and start_time is not None: 

192 if isinstance(start_time, datetime.datetime): 

193 self.tzinfo = start_time.tzinfo 

194 start_time = self._datetime_to_timestamp(start_time) 

195 

196 self.start_time = start_time 

197 self.dst_start_time = start_time 

198 self.cur = start_time 

199 return self.cur 

200 

201 @classmethod 

202 def _datetime_to_timestamp(cls, d): 

203 """ 

204 Converts a `datetime` object `d` into a UNIX timestamp. 

205 """ 

206 return datetime_to_timestamp(d) 

207 

208 def _timestamp_to_datetime(self, timestamp): 

209 """ 

210 Converts a UNIX timestamp `timestamp` into a `datetime` object. 

211 """ 

212 result = datetime.datetime.utcfromtimestamp(timestamp) 

213 if self.tzinfo: 

214 result = result.replace(tzinfo=tzutc()).astimezone(self.tzinfo) 

215 

216 return result 

217 

218 @classmethod 

219 def _timedelta_to_seconds(cls, td): 

220 """ 

221 Converts a 'datetime.timedelta' object `td` into seconds contained in 

222 the duration. 

223 Note: We cannot use `timedelta.total_seconds()` because this is not 

224 supported by Python 2.6. 

225 """ 

226 return timedelta_to_seconds(td) 

227 

228 def _get_next(self, ret_type=None, start_time=None, is_prev=None): 

229 self.set_current(start_time, force=True) 

230 if is_prev is None: 

231 is_prev = self._is_prev 

232 self._is_prev = is_prev 

233 expanded = self.expanded[:] 

234 nth_weekday_of_month = self.nth_weekday_of_month.copy() 

235 

236 ret_type = ret_type or self._ret_type 

237 

238 if not issubclass(ret_type, (float, datetime.datetime)): 

239 raise TypeError("Invalid ret_type, only 'float' or 'datetime' " 

240 "is acceptable.") 

241 

242 # exception to support day of month and day of week as defined in cron 

243 if (expanded[2][0] != '*' and expanded[4][0] != '*') and self._day_or: 

244 bak = expanded[4] 

245 expanded[4] = ['*'] 

246 t1 = self._calc(self.cur, expanded, nth_weekday_of_month, is_prev) 

247 expanded[4] = bak 

248 expanded[2] = ['*'] 

249 

250 t2 = self._calc(self.cur, expanded, nth_weekday_of_month, is_prev) 

251 if not is_prev: 

252 result = t1 if t1 < t2 else t2 

253 else: 

254 result = t1 if t1 > t2 else t2 

255 else: 

256 result = self._calc(self.cur, expanded, 

257 nth_weekday_of_month, is_prev) 

258 

259 # DST Handling for cron job spanning across days 

260 dtstarttime = self._timestamp_to_datetime(self.dst_start_time) 

261 dtstarttime_utcoffset = ( 

262 dtstarttime.utcoffset() or datetime.timedelta(0)) 

263 dtresult = self._timestamp_to_datetime(result) 

264 lag = lag_hours = 0 

265 # do we trigger DST on next crontab (handle backward changes) 

266 dtresult_utcoffset = dtstarttime_utcoffset 

267 if dtresult and self.tzinfo: 

268 dtresult_utcoffset = dtresult.utcoffset() 

269 lag_hours = ( 

270 self._timedelta_to_seconds(dtresult - dtstarttime) / (60 * 60) 

271 ) 

272 lag = self._timedelta_to_seconds( 

273 dtresult_utcoffset - dtstarttime_utcoffset 

274 ) 

275 hours_before_midnight = 24 - dtstarttime.hour 

276 if dtresult_utcoffset != dtstarttime_utcoffset: 

277 if ( 

278 (lag > 0 and abs(lag_hours) >= hours_before_midnight) 

279 or (lag < 0 and 

280 ((3600 * abs(lag_hours) + abs(lag)) >= hours_before_midnight * 3600)) 

281 ): 

282 dtresult_adjusted = dtresult - datetime.timedelta(seconds=lag) 

283 result_adjusted = self._datetime_to_timestamp(dtresult_adjusted) 

284 # Do the actual adjust only if the result time actually exists 

285 if self._timestamp_to_datetime(result_adjusted).tzinfo == dtresult_adjusted.tzinfo: 

286 dtresult = dtresult_adjusted 

287 result = result_adjusted 

288 self.dst_start_time = result 

289 self.cur = result 

290 if issubclass(ret_type, datetime.datetime): 

291 result = dtresult 

292 return result 

293 

294 # iterator protocol, to enable direct use of croniter 

295 # objects in a loop, like "for dt in croniter('5 0 * * *'): ..." 

296 # or for combining multiple croniters into single 

297 # dates feed using 'itertools' module 

298 def all_next(self, ret_type=None): 

299 '''Generator of all consecutive dates. Can be used instead of 

300 implicit call to __iter__, whenever non-default 

301 'ret_type' has to be specified. 

302 ''' 

303 # In a Python 3.7+ world: contextlib.suppress and contextlib.nullcontext could be used instead 

304 try: 

305 while True: 

306 self._is_prev = False 

307 yield self._get_next(ret_type or self._ret_type) 

308 except CroniterBadDateError: 

309 if self._max_years_btw_matches_explicitly_set: 

310 return 

311 else: 

312 raise 

313 

314 def all_prev(self, ret_type=None): 

315 '''Generator of all previous dates.''' 

316 try: 

317 while True: 

318 self._is_prev = True 

319 yield self._get_next(ret_type or self._ret_type) 

320 except CroniterBadDateError: 

321 if self._max_years_btw_matches_explicitly_set: 

322 return 

323 else: 

324 raise 

325 

326 def iter(self, *args, **kwargs): 

327 return (self._is_prev and self.all_prev or self.all_next) 

328 

329 def __iter__(self): 

330 return self 

331 __next__ = next = _get_next 

332 

333 def _calc(self, now, expanded, nth_weekday_of_month, is_prev): 

334 if is_prev: 

335 now = math.ceil(now) 

336 nearest_diff_method = self._get_prev_nearest_diff 

337 sign = -1 

338 offset = (len(expanded) == 6 or now % 60 > 0) and 1 or 60 

339 else: 

340 now = math.floor(now) 

341 nearest_diff_method = self._get_next_nearest_diff 

342 sign = 1 

343 offset = (len(expanded) == 6) and 1 or 60 

344 

345 dst = now = self._timestamp_to_datetime(now + sign * offset) 

346 

347 month, year = dst.month, dst.year 

348 current_year = now.year 

349 DAYS = self.DAYS 

350 

351 def proc_month(d): 

352 try: 

353 expanded[3].index('*') 

354 except ValueError: 

355 diff_month = nearest_diff_method( 

356 d.month, expanded[3], self.MONTHS_IN_YEAR) 

357 days = DAYS[month - 1] 

358 if month == 2 and self.is_leap(year) is True: 

359 days += 1 

360 

361 reset_day = 1 

362 

363 if diff_month is not None and diff_month != 0: 

364 if is_prev: 

365 d += relativedelta(months=diff_month) 

366 reset_day = DAYS[d.month - 1] 

367 d += relativedelta( 

368 day=reset_day, hour=23, minute=59, second=59) 

369 else: 

370 d += relativedelta(months=diff_month, day=reset_day, 

371 hour=0, minute=0, second=0) 

372 return True, d 

373 return False, d 

374 

375 def proc_day_of_month(d): 

376 try: 

377 expanded[2].index('*') 

378 except ValueError: 

379 days = DAYS[month - 1] 

380 if month == 2 and self.is_leap(year) is True: 

381 days += 1 

382 if 'l' in expanded[2] and days == d.day: 

383 return False, d 

384 

385 if is_prev: 

386 days_in_prev_month = DAYS[ 

387 (month - 2) % self.MONTHS_IN_YEAR] 

388 diff_day = nearest_diff_method( 

389 d.day, expanded[2], days_in_prev_month) 

390 else: 

391 diff_day = nearest_diff_method(d.day, expanded[2], days) 

392 

393 if diff_day is not None and diff_day != 0: 

394 if is_prev: 

395 d += relativedelta( 

396 days=diff_day, hour=23, minute=59, second=59) 

397 else: 

398 d += relativedelta( 

399 days=diff_day, hour=0, minute=0, second=0) 

400 return True, d 

401 return False, d 

402 

403 def proc_day_of_week(d): 

404 try: 

405 expanded[4].index('*') 

406 except ValueError: 

407 diff_day_of_week = nearest_diff_method( 

408 d.isoweekday() % 7, expanded[4], 7) 

409 if diff_day_of_week is not None and diff_day_of_week != 0: 

410 if is_prev: 

411 d += relativedelta(days=diff_day_of_week, 

412 hour=23, minute=59, second=59) 

413 else: 

414 d += relativedelta(days=diff_day_of_week, 

415 hour=0, minute=0, second=0) 

416 return True, d 

417 return False, d 

418 

419 def proc_day_of_week_nth(d): 

420 if '*' in nth_weekday_of_month: 

421 s = nth_weekday_of_month['*'] 

422 for i in range(0, 7): 

423 if i in nth_weekday_of_month: 

424 nth_weekday_of_month[i].update(s) 

425 else: 

426 nth_weekday_of_month[i] = s 

427 del nth_weekday_of_month['*'] 

428 

429 candidates = [] 

430 for wday, nth in nth_weekday_of_month.items(): 

431 c = self._get_nth_weekday_of_month(d.year, d.month, wday) 

432 for n in nth: 

433 if n == "l": 

434 candidate = c[-1] 

435 elif len(c) < n: 

436 continue 

437 else: 

438 candidate = c[n - 1] 

439 if ( 

440 (is_prev and candidate <= d.day) or 

441 (not is_prev and d.day <= candidate) 

442 ): 

443 candidates.append(candidate) 

444 

445 if not candidates: 

446 if is_prev: 

447 d += relativedelta(days=-d.day, 

448 hour=23, minute=59, second=59) 

449 else: 

450 days = DAYS[month - 1] 

451 if month == 2 and self.is_leap(year) is True: 

452 days += 1 

453 d += relativedelta(days=(days - d.day + 1), 

454 hour=0, minute=0, second=0) 

455 return True, d 

456 

457 candidates.sort() 

458 diff_day = (candidates[-1] if is_prev else candidates[0]) - d.day 

459 if diff_day != 0: 

460 if is_prev: 

461 d += relativedelta(days=diff_day, 

462 hour=23, minute=59, second=59) 

463 else: 

464 d += relativedelta(days=diff_day, 

465 hour=0, minute=0, second=0) 

466 return True, d 

467 return False, d 

468 

469 def proc_hour(d): 

470 try: 

471 expanded[1].index('*') 

472 except ValueError: 

473 diff_hour = nearest_diff_method(d.hour, expanded[1], 24) 

474 if diff_hour is not None and diff_hour != 0: 

475 if is_prev: 

476 d += relativedelta( 

477 hours=diff_hour, minute=59, second=59) 

478 else: 

479 d += relativedelta(hours=diff_hour, minute=0, second=0) 

480 return True, d 

481 return False, d 

482 

483 def proc_minute(d): 

484 try: 

485 expanded[0].index('*') 

486 except ValueError: 

487 diff_min = nearest_diff_method(d.minute, expanded[0], 60) 

488 if diff_min is not None and diff_min != 0: 

489 if is_prev: 

490 d += relativedelta(minutes=diff_min, second=59) 

491 else: 

492 d += relativedelta(minutes=diff_min, second=0) 

493 return True, d 

494 return False, d 

495 

496 def proc_second(d): 

497 if len(expanded) == 6: 

498 try: 

499 expanded[5].index('*') 

500 except ValueError: 

501 diff_sec = nearest_diff_method(d.second, expanded[5], 60) 

502 if diff_sec is not None and diff_sec != 0: 

503 d += relativedelta(seconds=diff_sec) 

504 return True, d 

505 else: 

506 d += relativedelta(second=0) 

507 return False, d 

508 

509 procs = [proc_month, 

510 proc_day_of_month, 

511 (proc_day_of_week_nth if nth_weekday_of_month 

512 else proc_day_of_week), 

513 proc_hour, 

514 proc_minute, 

515 proc_second] 

516 

517 while abs(year - current_year) <= self._max_years_between_matches: 

518 next = False 

519 for proc in procs: 

520 (changed, dst) = proc(dst) 

521 if changed: 

522 month, year = dst.month, dst.year 

523 next = True 

524 break 

525 if next: 

526 continue 

527 return self._datetime_to_timestamp(dst.replace(microsecond=0)) 

528 

529 if is_prev: 

530 raise CroniterBadDateError("failed to find prev date") 

531 raise CroniterBadDateError("failed to find next date") 

532 

533 def _get_next_nearest(self, x, to_check): 

534 small = [item for item in to_check if item < x] 

535 large = [item for item in to_check if item >= x] 

536 large.extend(small) 

537 return large[0] 

538 

539 def _get_prev_nearest(self, x, to_check): 

540 small = [item for item in to_check if item <= x] 

541 large = [item for item in to_check if item > x] 

542 small.reverse() 

543 large.reverse() 

544 small.extend(large) 

545 return small[0] 

546 

547 def _get_next_nearest_diff(self, x, to_check, range_val): 

548 for i, d in enumerate(to_check): 

549 if d == "l": 

550 # if 'l' then it is the last day of month 

551 # => its value of range_val 

552 d = range_val 

553 if d >= x: 

554 return d - x 

555 return to_check[0] - x + range_val 

556 

557 def _get_prev_nearest_diff(self, x, to_check, range_val): 

558 candidates = to_check[:] 

559 candidates.reverse() 

560 for d in candidates: 

561 if d != 'l' and d <= x: 

562 return d - x 

563 if 'l' in candidates: 

564 return -x 

565 candidate = candidates[0] 

566 for c in candidates: 

567 # fixed: c < range_val 

568 # this code will reject all 31 day of month, 12 month, 59 second, 

569 # 23 hour and so on. 

570 # if candidates has just a element, this will not harmful. 

571 # but candidates have multiple elements, then values equal to 

572 # range_val will rejected. 

573 if c <= range_val: 

574 candidate = c 

575 break 

576 if candidate > range_val: 

577 # fix crontab "0 6 30 3 *" condidates only a element, 

578 # then get_prev error return 2021-03-02 06:00:00 

579 return - x 

580 return (candidate - x - range_val) 

581 

582 @staticmethod 

583 def _get_nth_weekday_of_month(year, month, day_of_week): 

584 """ For a given year/month return a list of days in nth-day-of-month order. 

585 The last weekday of the month is always [-1]. 

586 """ 

587 w = (day_of_week + 6) % 7 

588 c = calendar.Calendar(w).monthdayscalendar(year, month) 

589 if c[0][0] == 0: 

590 c.pop(0) 

591 return tuple(i[0] for i in c) 

592 

593 def is_leap(self, year): 

594 if year % 400 == 0 or (year % 4 == 0 and year % 100 != 0): 

595 return True 

596 else: 

597 return False 

598 

599 @classmethod 

600 def _expand(cls, expr_format, hash_id=None): 

601 # Split the expression in components, and normalize L -> l, MON -> mon, 

602 # etc. Keep expr_format untouched so we can use it in the exception 

603 # messages. 

604 expr_aliases = { 

605 '@midnight': ('0 0 * * *', 'h h(0-2) * * * h'), 

606 '@hourly': ('0 * * * *', 'h * * * * h'), 

607 '@daily': ('0 0 * * *', 'h h * * * h'), 

608 '@weekly': ('0 0 * * 0', 'h h * * h h'), 

609 '@monthly': ('0 0 1 * *', 'h h h * * h'), 

610 '@yearly': ('0 0 1 1 *', 'h h h h * h'), 

611 '@annually': ('0 0 1 1 *', 'h h h h * h'), 

612 } 

613 

614 efl = expr_format.lower() 

615 hash_id_expr = hash_id is not None and 1 or 0 

616 try: 

617 efl = expr_aliases[efl][hash_id_expr] 

618 except KeyError: 

619 pass 

620 

621 expressions = efl.split() 

622 

623 if len(expressions) not in VALID_LEN_EXPRESSION: 

624 raise CroniterBadCronError(cls.bad_length) 

625 

626 expanded = [] 

627 nth_weekday_of_month = {} 

628 

629 for i, expr in enumerate(expressions): 

630 for expanderid, expander in EXPANDERS.items(): 

631 expr = expander(cls).expand(efl, i, expr, hash_id=hash_id) 

632 

633 e_list = expr.split(',') 

634 res = [] 

635 

636 while len(e_list) > 0: 

637 e = e_list.pop() 

638 

639 if i == 4: 

640 # Handle special case in the day-of-week expression 

641 m = special_weekday_re.match(str(e)) 

642 if m: 

643 orig_e = e 

644 e, nth, last = m.groups() 

645 if nth: 

646 try: 

647 nth = int(nth) 

648 assert (nth >= 1 and nth <= 5) 

649 except (ValueError, AssertionError): 

650 raise CroniterBadCronError( 

651 "[{0}] is not acceptable. Invalid day_of_week " 

652 "value: '{1}'".format(expr_format, orig_e)) 

653 elif last: 

654 nth = "l" 

655 e = last 

656 del last, orig_e 

657 else: 

658 nth = None 

659 

660 # Before matching step_search_re, normalize "*" to "{min}-{max}". 

661 # Example: in the minute field, "*/5" normalizes to "0-59/5" 

662 t = re.sub(r'^\*(\/.+)$', r'%d-%d\1' % ( 

663 cls.RANGES[i][0], 

664 cls.RANGES[i][1]), 

665 str(e)) 

666 m = step_search_re.search(t) 

667 

668 if not m: 

669 # Before matching step_search_re, 

670 # normalize "{start}/{step}" to "{start}-{max}/{step}". 

671 # Example: in the minute field, "10/5" normalizes to "10-59/5" 

672 t = re.sub(r'^(.+)\/(.+)$', r'\1-%d/\2' % ( 

673 cls.RANGES[i][1]), 

674 str(e)) 

675 m = step_search_re.search(t) 

676 

677 if m: 

678 # early abort if low/high are out of bounds 

679 

680 (low, high, step) = m.group(1), m.group(2), m.group(4) or 1 

681 if i == 2 and high == 'l': 

682 high = '31' 

683 

684 if not only_int_re.search(low): 

685 low = "{0}".format(cls._alphaconv(i, low, expressions)) 

686 

687 if not only_int_re.search(high): 

688 high = "{0}".format(cls._alphaconv(i, high, expressions)) 

689 

690 if ( 

691 not low or not high or int(low) > int(high) 

692 or not only_int_re.search(str(step)) 

693 ): 

694 if i == 4 and high == '0': 

695 # handle -Sun notation -> 7 

696 high = '7' 

697 else: 

698 raise CroniterBadCronError( 

699 "[{0}] is not acceptable".format(expr_format)) 

700 

701 low, high, step = map(int, [low, high, step]) 

702 if ( 

703 max(low, high) > max(cls.RANGES[i][0], cls.RANGES[i][1]) 

704 ): 

705 raise CroniterBadCronError( 

706 "{0} is out of bands".format(expr_format)) 

707 try: 

708 rng = range(low, high + 1, step) 

709 except ValueError as exc: 

710 raise CroniterBadCronError( 

711 'invalid range: {0}'.format(exc)) 

712 e_list += (["{0}#{1}".format(item, nth) for item in rng] 

713 if i == 4 and nth and nth != "l" else rng) 

714 else: 

715 if t.startswith('-'): 

716 raise CroniterBadCronError(( 

717 "[{0}] is not acceptable," 

718 "negative numbers not allowed" 

719 ).format(expr_format)) 

720 if not star_or_int_re.search(t): 

721 t = cls._alphaconv(i, t, expressions) 

722 

723 try: 

724 t = int(t) 

725 except ValueError: 

726 pass 

727 

728 if t in cls.LOWMAP[i] and not ( 

729 # do not support 0 as a month either for classical 5 fields cron 

730 # or 6fields second repeat form 

731 # but still let conversion happen if day field is shifted 

732 (i in [2, 3] and len(expressions) == 5) or 

733 (i in [3, 4] and len(expressions) == 6) 

734 ): 

735 t = cls.LOWMAP[i][t] 

736 

737 if ( 

738 t not in ["*", "l"] 

739 and (int(t) < cls.RANGES[i][0] or 

740 int(t) > cls.RANGES[i][1]) 

741 ): 

742 raise CroniterBadCronError( 

743 "[{0}] is not acceptable, out of range".format( 

744 expr_format)) 

745 

746 res.append(t) 

747 

748 if i == 4 and nth: 

749 if t not in nth_weekday_of_month: 

750 nth_weekday_of_month[t] = set() 

751 nth_weekday_of_month[t].add(nth) 

752 

753 res = set(res) 

754 res = sorted(res, key=lambda i: "{:02}".format(i) if isinstance(i, int) else i) 

755 if len(res) == cls.LEN_MEANS_ALL[i]: 

756 res = ['*'] 

757 

758 expanded.append(['*'] if (len(res) == 1 

759 and res[0] == '*') 

760 else res) 

761 

762 # Check to make sure the dow combo in use is supported 

763 if nth_weekday_of_month: 

764 dow_expanded_set = set(expanded[4]) 

765 dow_expanded_set = dow_expanded_set.difference(nth_weekday_of_month.keys()) 

766 dow_expanded_set.discard("*") 

767 if dow_expanded_set: 

768 raise CroniterUnsupportedSyntaxError( 

769 "day-of-week field does not support mixing literal values and nth day of week syntax. " 

770 "Cron: '{}' dow={} vs nth={}".format(expr_format, dow_expanded_set, nth_weekday_of_month)) 

771 

772 return expanded, nth_weekday_of_month 

773 

774 @classmethod 

775 def expand(cls, expr_format, hash_id=None): 

776 """Shallow non Croniter ValueError inside a nice CroniterBadCronError""" 

777 try: 

778 return cls._expand(expr_format, hash_id=hash_id) 

779 except ValueError as exc: 

780 error_type, error_instance, traceback = sys.exc_info() 

781 if isinstance(exc, CroniterError): 

782 raise 

783 if int(sys.version[0]) >= 3: 

784 globs, locs = _get_caller_globals_and_locals() 

785 exec("raise CroniterBadCronError from exc", globs, locs) 

786 else: 

787 raise CroniterBadCronError("{0}".format(exc)) 

788 

789 @classmethod 

790 def is_valid(cls, expression, hash_id=None): 

791 try: 

792 cls.expand(expression, hash_id=hash_id) 

793 except CroniterError: 

794 return False 

795 else: 

796 return True 

797 

798 @classmethod 

799 def match(cls, cron_expression, testdate, day_or=True): 

800 cron = cls(cron_expression, testdate, ret_type=datetime.datetime, day_or=day_or) 

801 td, ms1 = cron.get_current(datetime.datetime), relativedelta(microseconds=1) 

802 if not td.microsecond: 

803 td = td + ms1 

804 cron.set_current(td, force=True) 

805 tdp, tdt = cron.get_current(), cron.get_prev() 

806 return (max(tdp, tdt) - min(tdp, tdt)).total_seconds() < 60 

807 

808 

809def croniter_range(start, stop, expr_format, ret_type=None, day_or=True, exclude_ends=False, 

810 _croniter=None): 

811 """ 

812 Generator that provides all times from start to stop matching the given cron expression. 

813 If the cron expression matches either 'start' and/or 'stop', those times will be returned as 

814 well unless 'exclude_ends=True' is passed. 

815 

816 You can think of this function as sibling to the builtin range function for datetime objects. 

817 Like range(start,stop,step), except that here 'step' is a cron expression. 

818 """ 

819 _croniter = _croniter or croniter 

820 auto_rt = datetime.datetime 

821 # type is used in first if branch for perfs reasons 

822 if ( 

823 type(start) != type(stop) and not ( 

824 isinstance(start, type(stop)) or 

825 isinstance(stop, type(start))) 

826 ): 

827 raise CroniterBadTypeRangeError( 

828 "The start and stop must be same type. {0} != {1}". 

829 format(type(start), type(stop))) 

830 if isinstance(start, (float, int)): 

831 start, stop = (datetime.datetime.utcfromtimestamp(t) for t in (start, stop)) 

832 auto_rt = float 

833 if ret_type is None: 

834 ret_type = auto_rt 

835 if not exclude_ends: 

836 ms1 = relativedelta(microseconds=1) 

837 if start < stop: # Forward (normal) time order 

838 start -= ms1 

839 stop += ms1 

840 else: # Reverse time order 

841 start += ms1 

842 stop -= ms1 

843 year_span = math.floor(abs(stop.year - start.year)) + 1 

844 ic = _croniter(expr_format, start, ret_type=datetime.datetime, day_or=day_or, 

845 max_years_between_matches=year_span) 

846 # define a continue (cont) condition function and step function for the main while loop 

847 if start < stop: # Forward 

848 def cont(v): 

849 return v < stop 

850 step = ic.get_next 

851 else: # Reverse 

852 def cont(v): 

853 return v > stop 

854 step = ic.get_prev 

855 try: 

856 dt = step() 

857 while cont(dt): 

858 if ret_type is float: 

859 yield ic.get_current(float) 

860 else: 

861 yield dt 

862 dt = step() 

863 except CroniterBadDateError: 

864 # Stop iteration when this exception is raised; no match found within the given year range 

865 return 

866 

867 

868class HashExpander: 

869 

870 def __init__(self, cronit): 

871 self.cron = cronit 

872 

873 def do(self, idx, hash_type="h", hash_id=None, range_end=None, range_begin=None): 

874 """Return a hashed/random integer given range/hash information""" 

875 if range_end is None: 

876 range_end = self.cron.RANGES[idx][1] 

877 if range_begin is None: 

878 range_begin = self.cron.RANGES[idx][0] 

879 if hash_type == 'r': 

880 crc = random.randint(0, 0xFFFFFFFF) 

881 else: 

882 crc = binascii.crc32(hash_id) & 0xFFFFFFFF 

883 return ((crc >> idx) % (range_end - range_begin + 1)) + range_begin 

884 

885 def match(self, efl, idx, expr, hash_id=None, **kw): 

886 return hash_expression_re.match(expr) 

887 

888 def expand(self, efl, idx, expr, hash_id=None, match='', **kw): 

889 """Expand a hashed/random expression to its normal representation""" 

890 if match == '': 

891 match = self.match(efl, idx, expr, hash_id, **kw) 

892 if not match: 

893 return expr 

894 m = match.groupdict() 

895 

896 if m['hash_type'] == 'h' and hash_id is None: 

897 raise CroniterBadCronError('Hashed definitions must include hash_id') 

898 

899 if m['range_begin'] and m['range_end'] and m['divisor']: 

900 # Example: H(30-59)/10 -> 34-59/10 (i.e. 34,44,54) 

901 if int(m["divisor"]) == 0: 

902 raise CroniterBadCronError("Bad expression: {0}".format(expr)) 

903 

904 return '{0}-{1}/{2}'.format( 

905 self.do( 

906 idx, 

907 hash_type=m['hash_type'], 

908 hash_id=hash_id, 

909 range_end=int(m['divisor']), 

910 ) + int(m['range_begin']), 

911 int(m['range_end']), 

912 int(m['divisor']), 

913 ) 

914 elif m['range_begin'] and m['range_end']: 

915 # Example: H(0-29) -> 12 

916 return str( 

917 self.do( 

918 idx, 

919 hash_type=m['hash_type'], 

920 hash_id=hash_id, 

921 range_end=int(m['range_end']), 

922 range_begin=int(m['range_begin']), 

923 ) 

924 ) 

925 elif m['divisor']: 

926 # Example: H/15 -> 7-59/15 (i.e. 7,22,37,52) 

927 if int(m["divisor"]) == 0: 

928 raise CroniterBadCronError("Bad expression: {0}".format(expr)) 

929 

930 return '{0}-{1}/{2}'.format( 

931 self.do( 

932 idx, 

933 hash_type=m['hash_type'], 

934 hash_id=hash_id, 

935 range_end=int(m['divisor']), 

936 ), 

937 self.cron.RANGES[idx][1], 

938 int(m['divisor']), 

939 ) 

940 else: 

941 # Example: H -> 32 

942 return str( 

943 self.do( 

944 idx, 

945 hash_type=m['hash_type'], 

946 hash_id=hash_id, 

947 ) 

948 ) 

949 

950 

951EXPANDERS = OrderedDict([ 

952 ('hash', HashExpander), 

953])