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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

595 statements  

1#!/usr/bin/env python 

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

3 

4from __future__ import absolute_import, print_function, division 

5 

6import traceback as _traceback 

7import copy 

8import math 

9import re 

10import sys 

11import inspect 

12from time import time 

13import datetime 

14from dateutil.relativedelta import relativedelta 

15from dateutil.tz import tzutc 

16import calendar 

17import binascii 

18import random 

19 

20# as pytz is optional in thirdparty libs but we need it for good support under 

21# python2, just test that it's well installed 

22import pytz # noqa 

23 

24try: 

25 from collections import OrderedDict 

26except ImportError: 

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

28 

29 

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

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

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

33ALPHAS = {} 

34for i in M_ALPHAS, DOW_ALPHAS: 

35 ALPHAS.update(i) 

36del i 

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

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

39 

40WEEKDAYS = '|'.join(DOW_ALPHAS.keys()) 

41MONTHS = '|'.join(M_ALPHAS.keys()) 

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

43special_dow_re = re.compile( 

44 (r'^(?P<pre>((?P<he>(({WEEKDAYS})(-({WEEKDAYS}))?)').format(WEEKDAYS=WEEKDAYS) + 

45 (r'|(({MONTHS})(-({MONTHS}))?)|\w+)#)|l)(?P<last>\d+)$').format(MONTHS=MONTHS) 

46) 

47re_star = re.compile('[*]') 

48hash_expression_re = re.compile( 

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

50) 

51VALID_LEN_EXPRESSION = [5, 6] 

52EXPRESSIONS = {} 

53 

54 

55def timedelta_to_seconds(td): 

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

57 / 10**6 

58 

59 

60def datetime_to_timestamp(d): 

61 if d.tzinfo is not None: 

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

63 

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

65 

66 

67def _get_caller_globals_and_locals(): 

68 """ 

69 Returns the globals and locals of the calling frame. 

70 

71 Is there an alternative to frame hacking here? 

72 """ 

73 caller_frame = inspect.stack()[2] 

74 myglobals = caller_frame[0].f_globals 

75 mylocals = caller_frame[0].f_locals 

76 return myglobals, mylocals 

77 

78 

79class CroniterError(ValueError): 

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

81 pass 

82 

83 

84class CroniterBadTypeRangeError(TypeError): 

85 """.""" 

86 

87 

88class CroniterBadCronError(CroniterError): 

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

90 pass 

91 

92 

93class CroniterUnsupportedSyntaxError(CroniterBadCronError): 

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

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

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

97 # these will likely be handled the same way. 

98 pass 

99 

100 

101class CroniterBadDateError(CroniterError): 

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

103 pass 

104 

105 

106class CroniterNotAlphaError(CroniterBadCronError): 

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

108 pass 

109 

110 

111class croniter(object): 

112 MONTHS_IN_YEAR = 12 

113 RANGES = ( 

114 (0, 59), 

115 (0, 23), 

116 (1, 31), 

117 (1, 12), 

118 (0, 7), 

119 (0, 59) 

120 ) 

121 DAYS = ( 

122 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 

123 ) 

124 

125 ALPHACONV = ( 

126 {}, # 0: min 

127 {}, # 1: hour 

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

129 # 3: mon 

130 copy.deepcopy(M_ALPHAS), 

131 # 4: dow 

132 copy.deepcopy(DOW_ALPHAS), 

133 # command/user 

134 {} 

135 ) 

136 

137 LOWMAP = ( 

138 {}, 

139 {}, 

140 {0: 1}, 

141 {0: 1}, 

142 {7: 0}, 

143 {}, 

144 ) 

145 

146 LEN_MEANS_ALL = ( 

147 60, 

148 24, 

149 31, 

150 12, 

151 7, 

152 60 

153 ) 

154 

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

156 'expression.' 

157 

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

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

160 hash_id=None, implement_cron_bug=False): 

161 self._ret_type = ret_type 

162 self._day_or = day_or 

163 self._implement_cron_bug = implement_cron_bug 

164 

165 if hash_id: 

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

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

168 if not isinstance(hash_id, bytes): 

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

170 

171 self._max_years_btw_matches_explicitly_set = ( 

172 max_years_between_matches is not None) 

173 if not self._max_years_btw_matches_explicitly_set: 

174 max_years_between_matches = 50 

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

176 

177 if start_time is None: 

178 start_time = time() 

179 

180 self.tzinfo = None 

181 

182 self.start_time = None 

183 self.dst_start_time = None 

184 self.cur = None 

185 self.set_current(start_time, force=False) 

186 

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

188 self.expressions = EXPRESSIONS[(expr_format, hash_id)] 

189 self._is_prev = is_prev 

190 

191 @classmethod 

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

193 try: 

194 return cls.ALPHACONV[index][key] 

195 except KeyError: 

196 raise CroniterNotAlphaError( 

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

198 

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

200 self.set_current(start_time, force=True) 

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

202 

203 def get_prev(self, ret_type=None): 

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

205 

206 def get_current(self, ret_type=None): 

207 ret_type = ret_type or self._ret_type 

208 if issubclass(ret_type, datetime.datetime): 

209 return self._timestamp_to_datetime(self.cur) 

210 return self.cur 

211 

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

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

214 if isinstance(start_time, datetime.datetime): 

215 self.tzinfo = start_time.tzinfo 

216 start_time = self._datetime_to_timestamp(start_time) 

217 

218 self.start_time = start_time 

219 self.dst_start_time = start_time 

220 self.cur = start_time 

221 return self.cur 

222 

223 @classmethod 

224 def _datetime_to_timestamp(cls, d): 

225 """ 

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

227 """ 

228 return datetime_to_timestamp(d) 

229 

230 def _timestamp_to_datetime(self, timestamp): 

231 """ 

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

233 """ 

234 result = datetime.datetime.fromtimestamp(timestamp, tz=tzutc()).replace(tzinfo=None) 

235 if self.tzinfo: 

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

237 

238 return result 

239 

240 @classmethod 

241 def _timedelta_to_seconds(cls, td): 

242 """ 

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

244 the duration. 

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

246 supported by Python 2.6. 

247 """ 

248 return timedelta_to_seconds(td) 

249 

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

251 self.set_current(start_time, force=True) 

252 if is_prev is None: 

253 is_prev = self._is_prev 

254 self._is_prev = is_prev 

255 expanded = self.expanded[:] 

256 nth_weekday_of_month = self.nth_weekday_of_month.copy() 

257 

258 ret_type = ret_type or self._ret_type 

259 

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

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

262 "is acceptable.") 

263 

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

265 dom_dow_exception_processed = False 

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

267 # If requested, handle a bug in vixie cron/ISC cron where day_of_month and day_of_week form 

268 # an intersection (AND) instead of a union (OR) if either field is an asterisk or starts with an asterisk 

269 # (https://crontab.guru/cron-bug.html) 

270 if self._implement_cron_bug and (re_star.match(self.expressions[2]) or re_star.match(self.expressions[4])): 

271 # To produce a schedule identical to the cron bug, we'll bypass the code that 

272 # makes a union of DOM and DOW, and instead skip to the code that does an intersect instead 

273 pass 

274 else: 

275 bak = expanded[4] 

276 expanded[4] = ['*'] 

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

278 expanded[4] = bak 

279 expanded[2] = ['*'] 

280 

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

282 if not is_prev: 

283 result = t1 if t1 < t2 else t2 

284 else: 

285 result = t1 if t1 > t2 else t2 

286 dom_dow_exception_processed = True 

287 

288 if not dom_dow_exception_processed: 

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

290 nth_weekday_of_month, is_prev) 

291 

292 # DST Handling for cron job spanning across days 

293 dtstarttime = self._timestamp_to_datetime(self.dst_start_time) 

294 dtstarttime_utcoffset = ( 

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

296 dtresult = self._timestamp_to_datetime(result) 

297 lag = lag_hours = 0 

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

299 dtresult_utcoffset = dtstarttime_utcoffset 

300 if dtresult and self.tzinfo: 

301 dtresult_utcoffset = dtresult.utcoffset() 

302 lag_hours = ( 

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

304 ) 

305 lag = self._timedelta_to_seconds( 

306 dtresult_utcoffset - dtstarttime_utcoffset 

307 ) 

308 hours_before_midnight = 24 - dtstarttime.hour 

309 if dtresult_utcoffset != dtstarttime_utcoffset: 

310 if ( 

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

312 or (lag < 0 and 

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

314 ): 

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

316 result_adjusted = self._datetime_to_timestamp(dtresult_adjusted) 

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

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

319 dtresult = dtresult_adjusted 

320 result = result_adjusted 

321 self.dst_start_time = result 

322 self.cur = result 

323 if issubclass(ret_type, datetime.datetime): 

324 result = dtresult 

325 return result 

326 

327 # iterator protocol, to enable direct use of croniter 

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

329 # or for combining multiple croniters into single 

330 # dates feed using 'itertools' module 

331 def all_next(self, ret_type=None): 

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

333 implicit call to __iter__, whenever non-default 

334 'ret_type' has to be specified. 

335 ''' 

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

337 try: 

338 while True: 

339 self._is_prev = False 

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

341 except CroniterBadDateError: 

342 if self._max_years_btw_matches_explicitly_set: 

343 return 

344 else: 

345 raise 

346 

347 def all_prev(self, ret_type=None): 

348 '''Generator of all previous dates.''' 

349 try: 

350 while True: 

351 self._is_prev = True 

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

353 except CroniterBadDateError: 

354 if self._max_years_btw_matches_explicitly_set: 

355 return 

356 else: 

357 raise 

358 

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

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

361 

362 def __iter__(self): 

363 return self 

364 __next__ = next = _get_next 

365 

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

367 if is_prev: 

368 now = math.ceil(now) 

369 nearest_diff_method = self._get_prev_nearest_diff 

370 sign = -1 

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

372 else: 

373 now = math.floor(now) 

374 nearest_diff_method = self._get_next_nearest_diff 

375 sign = 1 

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

377 

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

379 

380 month, year = dst.month, dst.year 

381 current_year = now.year 

382 DAYS = self.DAYS 

383 

384 def proc_month(d): 

385 try: 

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

387 except ValueError: 

388 diff_month = nearest_diff_method( 

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

390 days = DAYS[month - 1] 

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

392 days += 1 

393 

394 reset_day = 1 

395 

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

397 if is_prev: 

398 d += relativedelta(months=diff_month) 

399 reset_day = DAYS[d.month - 1] 

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

401 reset_day += 1 

402 d += relativedelta( 

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

404 else: 

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

406 hour=0, minute=0, second=0) 

407 return True, d 

408 return False, d 

409 

410 def proc_day_of_month(d): 

411 try: 

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

413 except ValueError: 

414 days = DAYS[month - 1] 

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

416 days += 1 

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

418 return False, d 

419 

420 if is_prev: 

421 days_in_prev_month = DAYS[ 

422 (month - 2) % self.MONTHS_IN_YEAR] 

423 diff_day = nearest_diff_method( 

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

425 else: 

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

427 

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

429 if is_prev: 

430 d += relativedelta( 

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

432 else: 

433 d += relativedelta( 

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

435 return True, d 

436 return False, d 

437 

438 def proc_day_of_week(d): 

439 try: 

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

441 except ValueError: 

442 diff_day_of_week = nearest_diff_method( 

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

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

445 if is_prev: 

446 d += relativedelta(days=diff_day_of_week, 

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

448 else: 

449 d += relativedelta(days=diff_day_of_week, 

450 hour=0, minute=0, second=0) 

451 return True, d 

452 return False, d 

453 

454 def proc_day_of_week_nth(d): 

455 if '*' in nth_weekday_of_month: 

456 s = nth_weekday_of_month['*'] 

457 for i in range(0, 7): 

458 if i in nth_weekday_of_month: 

459 nth_weekday_of_month[i].update(s) 

460 else: 

461 nth_weekday_of_month[i] = s 

462 del nth_weekday_of_month['*'] 

463 

464 candidates = [] 

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

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

467 for n in nth: 

468 if n == "l": 

469 candidate = c[-1] 

470 elif len(c) < n: 

471 continue 

472 else: 

473 candidate = c[n - 1] 

474 if ( 

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

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

477 ): 

478 candidates.append(candidate) 

479 

480 if not candidates: 

481 if is_prev: 

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

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

484 else: 

485 days = DAYS[month - 1] 

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

487 days += 1 

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

489 hour=0, minute=0, second=0) 

490 return True, d 

491 

492 candidates.sort() 

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

494 if diff_day != 0: 

495 if is_prev: 

496 d += relativedelta(days=diff_day, 

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

498 else: 

499 d += relativedelta(days=diff_day, 

500 hour=0, minute=0, second=0) 

501 return True, d 

502 return False, d 

503 

504 def proc_hour(d): 

505 try: 

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

507 except ValueError: 

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

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

510 if is_prev: 

511 d += relativedelta( 

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

513 else: 

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

515 return True, d 

516 return False, d 

517 

518 def proc_minute(d): 

519 try: 

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

521 except ValueError: 

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

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

524 if is_prev: 

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

526 else: 

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

528 return True, d 

529 return False, d 

530 

531 def proc_second(d): 

532 if len(expanded) == 6: 

533 try: 

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

535 except ValueError: 

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

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

538 d += relativedelta(seconds=diff_sec) 

539 return True, d 

540 else: 

541 d += relativedelta(second=0) 

542 return False, d 

543 

544 procs = [proc_month, 

545 proc_day_of_month, 

546 (proc_day_of_week_nth if nth_weekday_of_month 

547 else proc_day_of_week), 

548 proc_hour, 

549 proc_minute, 

550 proc_second] 

551 

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

553 next = False 

554 for proc in procs: 

555 (changed, dst) = proc(dst) 

556 if changed: 

557 month, year = dst.month, dst.year 

558 next = True 

559 break 

560 if next: 

561 continue 

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

563 

564 if is_prev: 

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

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

567 

568 def _get_next_nearest(self, x, to_check): 

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

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

571 large.extend(small) 

572 return large[0] 

573 

574 def _get_prev_nearest(self, x, to_check): 

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

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

577 small.reverse() 

578 large.reverse() 

579 small.extend(large) 

580 return small[0] 

581 

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

583 for i, d in enumerate(to_check): 

584 if d == "l": 

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

586 # => its value of range_val 

587 d = range_val 

588 if d >= x: 

589 return d - x 

590 return to_check[0] - x + range_val 

591 

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

593 candidates = to_check[:] 

594 candidates.reverse() 

595 for d in candidates: 

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

597 return d - x 

598 if 'l' in candidates: 

599 return -x 

600 candidate = candidates[0] 

601 for c in candidates: 

602 # fixed: c < range_val 

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

604 # 23 hour and so on. 

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

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

607 # range_val will rejected. 

608 if c <= range_val: 

609 candidate = c 

610 break 

611 if candidate > range_val: 

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

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

614 return - x 

615 return (candidate - x - range_val) 

616 

617 @staticmethod 

618 def _get_nth_weekday_of_month(year, month, day_of_week): 

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

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

621 """ 

622 w = (day_of_week + 6) % 7 

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

624 if c[0][0] == 0: 

625 c.pop(0) 

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

627 

628 def is_leap(self, year): 

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

630 return True 

631 else: 

632 return False 

633 

634 @classmethod 

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

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

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

638 # messages. 

639 expr_aliases = { 

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

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

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

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

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

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

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

647 } 

648 

649 efl = expr_format.lower() 

650 hash_id_expr = hash_id is not None and 1 or 0 

651 try: 

652 efl = expr_aliases[efl][hash_id_expr] 

653 except KeyError: 

654 pass 

655 

656 expressions = efl.split() 

657 

658 if len(expressions) not in VALID_LEN_EXPRESSION: 

659 raise CroniterBadCronError(cls.bad_length) 

660 

661 expanded = [] 

662 nth_weekday_of_month = {} 

663 

664 for i, expr in enumerate(expressions): 

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

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

667 

668 e_list = expr.split(',') 

669 res = [] 

670 

671 while len(e_list) > 0: 

672 e = e_list.pop() 

673 nth = None 

674 

675 if i == 4: 

676 # Handle special case in the dow expression: 2#3, l3 

677 special_dow_rem = special_dow_re.match(str(e)) 

678 if special_dow_rem: 

679 g = special_dow_rem.groupdict() 

680 he, last = g.get('he', ''), g.get('last', '') 

681 if he: 

682 e = he 

683 try: 

684 nth = int(last) 

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

686 except (KeyError, ValueError, AssertionError): 

687 raise CroniterBadCronError( 

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

689 "value: '{1}'".format(expr_format, nth)) 

690 elif last: 

691 e = last 

692 nth = g['pre'] # 'l' 

693 

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

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

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

697 cls.RANGES[i][0], 

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

699 str(e)) 

700 m = step_search_re.search(t) 

701 

702 if not m: 

703 # Before matching step_search_re, 

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

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

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

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

708 str(e)) 

709 m = step_search_re.search(t) 

710 

711 if m: 

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

713 

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

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

716 high = '31' 

717 

718 if not only_int_re.search(low): 

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

720 

721 if not only_int_re.search(high): 

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

723 

724 if ( 

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

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

727 ): 

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

729 # handle -Sun notation -> 7 

730 high = '7' 

731 else: 

732 raise CroniterBadCronError( 

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

734 

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

736 if ( 

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

738 ): 

739 raise CroniterBadCronError( 

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

741 try: 

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

743 except ValueError as exc: 

744 raise CroniterBadCronError( 

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

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

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

748 else: 

749 if t.startswith('-'): 

750 raise CroniterBadCronError(( 

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

752 "negative numbers not allowed" 

753 ).format(expr_format)) 

754 if not star_or_int_re.search(t): 

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

756 

757 try: 

758 t = int(t) 

759 except ValueError: 

760 pass 

761 

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

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

764 # or 6fields second repeat form 

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

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

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

768 ): 

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

770 

771 if ( 

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

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

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

775 ): 

776 raise CroniterBadCronError( 

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

778 expr_format)) 

779 

780 res.append(t) 

781 

782 if i == 4 and nth: 

783 if t not in nth_weekday_of_month: 

784 nth_weekday_of_month[t] = set() 

785 nth_weekday_of_month[t].add(nth) 

786 

787 res = set(res) 

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

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

790 # Make sure the wildcard is used in the correct way (avoid over-optimization) 

791 if ( 

792 (i == 2 and '*' not in expressions[4]) or 

793 (i == 4 and '*' not in expressions[2]) 

794 ): 

795 pass 

796 else: 

797 res = ['*'] 

798 

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

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

801 else res) 

802 

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

804 if nth_weekday_of_month: 

805 dow_expanded_set = set(expanded[4]) 

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

807 dow_expanded_set.discard("*") 

808 # Skip: if it's all weeks instead of wildcard 

809 if dow_expanded_set and len(set(expanded[4])) != cls.LEN_MEANS_ALL[4]: 

810 raise CroniterUnsupportedSyntaxError( 

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

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

813 

814 EXPRESSIONS[(expr_format, hash_id)] = expressions 

815 return expanded, nth_weekday_of_month 

816 

817 @classmethod 

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

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

820 try: 

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

822 except (ValueError,) as exc: 

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

824 if isinstance(exc, CroniterError): 

825 raise 

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

827 trace = _traceback.format_exc() 

828 globs, locs = _get_caller_globals_and_locals() 

829 raise CroniterBadCronError(trace) 

830 else: 

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

832 

833 @classmethod 

834 def is_valid(cls, expression, hash_id=None, encoding='UTF-8'): 

835 if hash_id: 

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

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

838 if not isinstance(hash_id, bytes): 

839 hash_id = hash_id.encode(encoding) 

840 try: 

841 cls.expand(expression, hash_id=hash_id) 

842 except CroniterError: 

843 return False 

844 else: 

845 return True 

846 

847 @classmethod 

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

849 return cls.match_range(cron_expression, testdate, testdate, day_or) 

850 

851 @classmethod 

852 def match_range(cls, cron_expression, from_datetime, to_datetime, day_or=True): 

853 cron = cls(cron_expression, to_datetime, ret_type=datetime.datetime, day_or=day_or) 

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

855 if not td.microsecond: 

856 td = td + ms1 

857 cron.set_current(td, force=True) 

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

859 precision_in_seconds = 1 if len(cron.expanded) == 6 else 60 

860 duration_in_second = (to_datetime - from_datetime).total_seconds() + precision_in_seconds 

861 return (max(tdp, tdt) - min(tdp, tdt)).total_seconds() < duration_in_second 

862 

863 

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

865 _croniter=None): 

866 """ 

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

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

869 well unless 'exclude_ends=True' is passed. 

870 

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

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

873 """ 

874 _croniter = _croniter or croniter 

875 auto_rt = datetime.datetime 

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

877 if ( 

878 type(start) is not type(stop) and not ( 

879 isinstance(start, type(stop)) or 

880 isinstance(stop, type(start))) 

881 ): 

882 raise CroniterBadTypeRangeError( 

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

884 format(type(start), type(stop))) 

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

886 start, stop = (datetime.datetime.fromtimestamp(t, tzutc()).replace(tzinfo=None) for t in (start, stop)) 

887 auto_rt = float 

888 if ret_type is None: 

889 ret_type = auto_rt 

890 if not exclude_ends: 

891 ms1 = relativedelta(microseconds=1) 

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

893 start -= ms1 

894 stop += ms1 

895 else: # Reverse time order 

896 start += ms1 

897 stop -= ms1 

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

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

900 max_years_between_matches=year_span) 

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

902 if start < stop: # Forward 

903 def cont(v): 

904 return v < stop 

905 step = ic.get_next 

906 else: # Reverse 

907 def cont(v): 

908 return v > stop 

909 step = ic.get_prev 

910 try: 

911 dt = step() 

912 while cont(dt): 

913 if ret_type is float: 

914 yield ic.get_current(float) 

915 else: 

916 yield dt 

917 dt = step() 

918 except CroniterBadDateError: 

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

920 return 

921 

922 

923class HashExpander: 

924 

925 def __init__(self, cronit): 

926 self.cron = cronit 

927 

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

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

930 hours_or_minutes = idx in {0, 1} 

931 if range_end is None: 

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

933 if hours_or_minutes: 

934 range_end += 1 

935 if range_begin is None: 

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

937 if hash_type == 'r': 

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

939 else: 

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

941 if not hours_or_minutes: 

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

943 return ((crc >> idx) % (range_end - range_begin)) + range_begin 

944 

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

946 return hash_expression_re.match(expr) 

947 

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

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

950 if match == '': 

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

952 if not match: 

953 return expr 

954 m = match.groupdict() 

955 

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

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

958 

959 if m['range_begin'] and m['range_end']: 

960 if int(m['range_begin']) >= int(m['range_end']): 

961 raise CroniterBadCronError('Range end must be greater than range begin') 

962 

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

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

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

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

967 

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

969 self.do( 

970 idx, 

971 hash_type=m['hash_type'], 

972 hash_id=hash_id, 

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

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

975 int(m['range_end']), 

976 int(m['divisor']), 

977 ) 

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

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

980 return str( 

981 self.do( 

982 idx, 

983 hash_type=m['hash_type'], 

984 hash_id=hash_id, 

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

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

987 ) 

988 ) 

989 elif m['divisor']: 

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

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

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

993 

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

995 self.do( 

996 idx, 

997 hash_type=m['hash_type'], 

998 hash_id=hash_id, 

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

1000 ), 

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

1002 int(m['divisor']), 

1003 ) 

1004 else: 

1005 # Example: H -> 32 

1006 return str( 

1007 self.do( 

1008 idx, 

1009 hash_type=m['hash_type'], 

1010 hash_id=hash_id, 

1011 ) 

1012 ) 

1013 

1014 

1015EXPANDERS = OrderedDict([ 

1016 ('hash', HashExpander), 

1017])