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

571 statements  

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

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 

20try: 

21 from collections import OrderedDict 

22except ImportError: 

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

24 

25 

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

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

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

29ALPHAS = {} 

30for i in M_ALPHAS, DOW_ALPHAS: 

31 ALPHAS.update(i) 

32del i 

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

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

35 

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

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

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

39special_dow_re = re.compile( 

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

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

42) 

43hash_expression_re = re.compile( 

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

45) 

46VALID_LEN_EXPRESSION = [5, 6] 

47 

48 

49def timedelta_to_seconds(td): 

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

51 / 10**6 

52 

53 

54def datetime_to_timestamp(d): 

55 if d.tzinfo is not None: 

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

57 

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

59 

60 

61def _get_caller_globals_and_locals(): 

62 """ 

63 Returns the globals and locals of the calling frame. 

64 

65 Is there an alternative to frame hacking here? 

66 """ 

67 caller_frame = inspect.stack()[2] 

68 myglobals = caller_frame[0].f_globals 

69 mylocals = caller_frame[0].f_locals 

70 return myglobals, mylocals 

71 

72 

73class CroniterError(ValueError): 

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

75 pass 

76 

77 

78class CroniterBadTypeRangeError(TypeError): 

79 """.""" 

80 

81 

82class CroniterBadCronError(CroniterError): 

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

84 pass 

85 

86 

87class CroniterUnsupportedSyntaxError(CroniterBadCronError): 

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

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

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

91 # these will likely be handled the same way. 

92 pass 

93 

94 

95class CroniterBadDateError(CroniterError): 

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

97 pass 

98 

99 

100class CroniterNotAlphaError(CroniterBadCronError): 

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

102 pass 

103 

104 

105class croniter(object): 

106 MONTHS_IN_YEAR = 12 

107 RANGES = ( 

108 (0, 59), 

109 (0, 23), 

110 (1, 31), 

111 (1, 12), 

112 (0, 7), 

113 (0, 59) 

114 ) 

115 DAYS = ( 

116 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 

117 ) 

118 

119 ALPHACONV = ( 

120 {}, # 0: min 

121 {}, # 1: hour 

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

123 # 3: mon 

124 copy.deepcopy(M_ALPHAS), 

125 # 4: dow 

126 copy.deepcopy(DOW_ALPHAS), 

127 # command/user 

128 {} 

129 ) 

130 

131 LOWMAP = ( 

132 {}, 

133 {}, 

134 {0: 1}, 

135 {0: 1}, 

136 {7: 0}, 

137 {}, 

138 ) 

139 

140 LEN_MEANS_ALL = ( 

141 60, 

142 24, 

143 31, 

144 12, 

145 7, 

146 60 

147 ) 

148 

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

150 'expression.' 

151 

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

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

154 hash_id=None): 

155 self._ret_type = ret_type 

156 self._day_or = day_or 

157 

158 if hash_id: 

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

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

161 if not isinstance(hash_id, bytes): 

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

163 

164 self._max_years_btw_matches_explicitly_set = ( 

165 max_years_between_matches is not None) 

166 if not self._max_years_btw_matches_explicitly_set: 

167 max_years_between_matches = 50 

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

169 

170 if start_time is None: 

171 start_time = time() 

172 

173 self.tzinfo = None 

174 

175 self.start_time = None 

176 self.dst_start_time = None 

177 self.cur = None 

178 self.set_current(start_time, force=False) 

179 

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

181 self._is_prev = is_prev 

182 

183 @classmethod 

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

185 try: 

186 return cls.ALPHACONV[index][key] 

187 except KeyError: 

188 raise CroniterNotAlphaError( 

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

190 

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

192 self.set_current(start_time, force=True) 

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

194 

195 def get_prev(self, ret_type=None): 

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

197 

198 def get_current(self, ret_type=None): 

199 ret_type = ret_type or self._ret_type 

200 if issubclass(ret_type, datetime.datetime): 

201 return self._timestamp_to_datetime(self.cur) 

202 return self.cur 

203 

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

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

206 if isinstance(start_time, datetime.datetime): 

207 self.tzinfo = start_time.tzinfo 

208 start_time = self._datetime_to_timestamp(start_time) 

209 

210 self.start_time = start_time 

211 self.dst_start_time = start_time 

212 self.cur = start_time 

213 return self.cur 

214 

215 @classmethod 

216 def _datetime_to_timestamp(cls, d): 

217 """ 

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

219 """ 

220 return datetime_to_timestamp(d) 

221 

222 def _timestamp_to_datetime(self, timestamp): 

223 """ 

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

225 """ 

226 result = datetime.datetime.utcfromtimestamp(timestamp) 

227 if self.tzinfo: 

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

229 

230 return result 

231 

232 @classmethod 

233 def _timedelta_to_seconds(cls, td): 

234 """ 

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

236 the duration. 

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

238 supported by Python 2.6. 

239 """ 

240 return timedelta_to_seconds(td) 

241 

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

243 self.set_current(start_time, force=True) 

244 if is_prev is None: 

245 is_prev = self._is_prev 

246 self._is_prev = is_prev 

247 expanded = self.expanded[:] 

248 nth_weekday_of_month = self.nth_weekday_of_month.copy() 

249 

250 ret_type = ret_type or self._ret_type 

251 

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

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

254 "is acceptable.") 

255 

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

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

258 bak = expanded[4] 

259 expanded[4] = ['*'] 

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

261 expanded[4] = bak 

262 expanded[2] = ['*'] 

263 

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

265 if not is_prev: 

266 result = t1 if t1 < t2 else t2 

267 else: 

268 result = t1 if t1 > t2 else t2 

269 else: 

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

271 nth_weekday_of_month, is_prev) 

272 

273 # DST Handling for cron job spanning across days 

274 dtstarttime = self._timestamp_to_datetime(self.dst_start_time) 

275 dtstarttime_utcoffset = ( 

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

277 dtresult = self._timestamp_to_datetime(result) 

278 lag = lag_hours = 0 

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

280 dtresult_utcoffset = dtstarttime_utcoffset 

281 if dtresult and self.tzinfo: 

282 dtresult_utcoffset = dtresult.utcoffset() 

283 lag_hours = ( 

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

285 ) 

286 lag = self._timedelta_to_seconds( 

287 dtresult_utcoffset - dtstarttime_utcoffset 

288 ) 

289 hours_before_midnight = 24 - dtstarttime.hour 

290 if dtresult_utcoffset != dtstarttime_utcoffset: 

291 if ( 

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

293 or (lag < 0 and 

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

295 ): 

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

297 result_adjusted = self._datetime_to_timestamp(dtresult_adjusted) 

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

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

300 dtresult = dtresult_adjusted 

301 result = result_adjusted 

302 self.dst_start_time = result 

303 self.cur = result 

304 if issubclass(ret_type, datetime.datetime): 

305 result = dtresult 

306 return result 

307 

308 # iterator protocol, to enable direct use of croniter 

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

310 # or for combining multiple croniters into single 

311 # dates feed using 'itertools' module 

312 def all_next(self, ret_type=None): 

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

314 implicit call to __iter__, whenever non-default 

315 'ret_type' has to be specified. 

316 ''' 

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

318 try: 

319 while True: 

320 self._is_prev = False 

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

322 except CroniterBadDateError: 

323 if self._max_years_btw_matches_explicitly_set: 

324 return 

325 else: 

326 raise 

327 

328 def all_prev(self, ret_type=None): 

329 '''Generator of all previous dates.''' 

330 try: 

331 while True: 

332 self._is_prev = True 

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

334 except CroniterBadDateError: 

335 if self._max_years_btw_matches_explicitly_set: 

336 return 

337 else: 

338 raise 

339 

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

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

342 

343 def __iter__(self): 

344 return self 

345 __next__ = next = _get_next 

346 

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

348 if is_prev: 

349 now = math.ceil(now) 

350 nearest_diff_method = self._get_prev_nearest_diff 

351 sign = -1 

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

353 else: 

354 now = math.floor(now) 

355 nearest_diff_method = self._get_next_nearest_diff 

356 sign = 1 

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

358 

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

360 

361 month, year = dst.month, dst.year 

362 current_year = now.year 

363 DAYS = self.DAYS 

364 

365 def proc_month(d): 

366 try: 

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

368 except ValueError: 

369 diff_month = nearest_diff_method( 

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

371 days = DAYS[month - 1] 

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

373 days += 1 

374 

375 reset_day = 1 

376 

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

378 if is_prev: 

379 d += relativedelta(months=diff_month) 

380 reset_day = DAYS[d.month - 1] 

381 d += relativedelta( 

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

383 else: 

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

385 hour=0, minute=0, second=0) 

386 return True, d 

387 return False, d 

388 

389 def proc_day_of_month(d): 

390 try: 

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

392 except ValueError: 

393 days = DAYS[month - 1] 

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

395 days += 1 

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

397 return False, d 

398 

399 if is_prev: 

400 days_in_prev_month = DAYS[ 

401 (month - 2) % self.MONTHS_IN_YEAR] 

402 diff_day = nearest_diff_method( 

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

404 else: 

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

406 

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

408 if is_prev: 

409 d += relativedelta( 

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

411 else: 

412 d += relativedelta( 

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

414 return True, d 

415 return False, d 

416 

417 def proc_day_of_week(d): 

418 try: 

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

420 except ValueError: 

421 diff_day_of_week = nearest_diff_method( 

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

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

424 if is_prev: 

425 d += relativedelta(days=diff_day_of_week, 

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

427 else: 

428 d += relativedelta(days=diff_day_of_week, 

429 hour=0, minute=0, second=0) 

430 return True, d 

431 return False, d 

432 

433 def proc_day_of_week_nth(d): 

434 if '*' in nth_weekday_of_month: 

435 s = nth_weekday_of_month['*'] 

436 for i in range(0, 7): 

437 if i in nth_weekday_of_month: 

438 nth_weekday_of_month[i].update(s) 

439 else: 

440 nth_weekday_of_month[i] = s 

441 del nth_weekday_of_month['*'] 

442 

443 candidates = [] 

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

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

446 for n in nth: 

447 if n == "l": 

448 candidate = c[-1] 

449 elif len(c) < n: 

450 continue 

451 else: 

452 candidate = c[n - 1] 

453 if ( 

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

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

456 ): 

457 candidates.append(candidate) 

458 

459 if not candidates: 

460 if is_prev: 

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

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

463 else: 

464 days = DAYS[month - 1] 

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

466 days += 1 

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

468 hour=0, minute=0, second=0) 

469 return True, d 

470 

471 candidates.sort() 

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

473 if diff_day != 0: 

474 if is_prev: 

475 d += relativedelta(days=diff_day, 

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

477 else: 

478 d += relativedelta(days=diff_day, 

479 hour=0, minute=0, second=0) 

480 return True, d 

481 return False, d 

482 

483 def proc_hour(d): 

484 try: 

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

486 except ValueError: 

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

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

489 if is_prev: 

490 d += relativedelta( 

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

492 else: 

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

494 return True, d 

495 return False, d 

496 

497 def proc_minute(d): 

498 try: 

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

500 except ValueError: 

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

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

503 if is_prev: 

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

505 else: 

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

507 return True, d 

508 return False, d 

509 

510 def proc_second(d): 

511 if len(expanded) == 6: 

512 try: 

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

514 except ValueError: 

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

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

517 d += relativedelta(seconds=diff_sec) 

518 return True, d 

519 else: 

520 d += relativedelta(second=0) 

521 return False, d 

522 

523 procs = [proc_month, 

524 proc_day_of_month, 

525 (proc_day_of_week_nth if nth_weekday_of_month 

526 else proc_day_of_week), 

527 proc_hour, 

528 proc_minute, 

529 proc_second] 

530 

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

532 next = False 

533 for proc in procs: 

534 (changed, dst) = proc(dst) 

535 if changed: 

536 month, year = dst.month, dst.year 

537 next = True 

538 break 

539 if next: 

540 continue 

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

542 

543 if is_prev: 

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

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

546 

547 def _get_next_nearest(self, x, to_check): 

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

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

550 large.extend(small) 

551 return large[0] 

552 

553 def _get_prev_nearest(self, x, to_check): 

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

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

556 small.reverse() 

557 large.reverse() 

558 small.extend(large) 

559 return small[0] 

560 

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

562 for i, d in enumerate(to_check): 

563 if d == "l": 

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

565 # => its value of range_val 

566 d = range_val 

567 if d >= x: 

568 return d - x 

569 return to_check[0] - x + range_val 

570 

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

572 candidates = to_check[:] 

573 candidates.reverse() 

574 for d in candidates: 

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

576 return d - x 

577 if 'l' in candidates: 

578 return -x 

579 candidate = candidates[0] 

580 for c in candidates: 

581 # fixed: c < range_val 

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

583 # 23 hour and so on. 

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

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

586 # range_val will rejected. 

587 if c <= range_val: 

588 candidate = c 

589 break 

590 if candidate > range_val: 

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

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

593 return - x 

594 return (candidate - x - range_val) 

595 

596 @staticmethod 

597 def _get_nth_weekday_of_month(year, month, day_of_week): 

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

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

600 """ 

601 w = (day_of_week + 6) % 7 

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

603 if c[0][0] == 0: 

604 c.pop(0) 

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

606 

607 def is_leap(self, year): 

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

609 return True 

610 else: 

611 return False 

612 

613 @classmethod 

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

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

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

617 # messages. 

618 expr_aliases = { 

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

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

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

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

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

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

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

626 } 

627 

628 efl = expr_format.lower() 

629 hash_id_expr = hash_id is not None and 1 or 0 

630 try: 

631 efl = expr_aliases[efl][hash_id_expr] 

632 except KeyError: 

633 pass 

634 

635 expressions = efl.split() 

636 

637 if len(expressions) not in VALID_LEN_EXPRESSION: 

638 raise CroniterBadCronError(cls.bad_length) 

639 

640 expanded = [] 

641 nth_weekday_of_month = {} 

642 

643 for i, expr in enumerate(expressions): 

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

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

646 

647 e_list = expr.split(',') 

648 res = [] 

649 

650 while len(e_list) > 0: 

651 e = e_list.pop() 

652 nth = None 

653 

654 if i == 4: 

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

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

657 if special_dow_rem: 

658 g = special_dow_rem.groupdict() 

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

660 if he: 

661 e = he 

662 try: 

663 nth = int(last) 

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

665 except (KeyError, ValueError, AssertionError): 

666 raise CroniterBadCronError( 

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

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

669 elif last: 

670 e = last 

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

672 

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

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

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

676 cls.RANGES[i][0], 

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

678 str(e)) 

679 m = step_search_re.search(t) 

680 

681 if not m: 

682 # Before matching step_search_re, 

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

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

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

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

687 str(e)) 

688 m = step_search_re.search(t) 

689 

690 if m: 

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

692 

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

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

695 high = '31' 

696 

697 if not only_int_re.search(low): 

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

699 

700 if not only_int_re.search(high): 

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

702 

703 if ( 

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

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

706 ): 

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

708 # handle -Sun notation -> 7 

709 high = '7' 

710 else: 

711 raise CroniterBadCronError( 

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

713 

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

715 if ( 

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

717 ): 

718 raise CroniterBadCronError( 

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

720 try: 

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

722 except ValueError as exc: 

723 raise CroniterBadCronError( 

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

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

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

727 else: 

728 if t.startswith('-'): 

729 raise CroniterBadCronError(( 

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

731 "negative numbers not allowed" 

732 ).format(expr_format)) 

733 if not star_or_int_re.search(t): 

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

735 

736 try: 

737 t = int(t) 

738 except ValueError: 

739 pass 

740 

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

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

743 # or 6fields second repeat form 

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

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

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

747 ): 

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

749 

750 if ( 

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

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

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

754 ): 

755 raise CroniterBadCronError( 

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

757 expr_format)) 

758 

759 res.append(t) 

760 

761 if i == 4 and nth: 

762 if t not in nth_weekday_of_month: 

763 nth_weekday_of_month[t] = set() 

764 nth_weekday_of_month[t].add(nth) 

765 

766 res = set(res) 

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

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

769 res = ['*'] 

770 

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

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

773 else res) 

774 

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

776 if nth_weekday_of_month: 

777 dow_expanded_set = set(expanded[4]) 

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

779 dow_expanded_set.discard("*") 

780 if dow_expanded_set: 

781 raise CroniterUnsupportedSyntaxError( 

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

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

784 

785 return expanded, nth_weekday_of_month 

786 

787 @classmethod 

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

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

790 try: 

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

792 except (ValueError,) as exc: 

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

794 if isinstance(exc, CroniterError): 

795 raise 

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

797 trace = _traceback.format_exc() 

798 globs, locs = _get_caller_globals_and_locals() 

799 raise CroniterBadCronError(trace) 

800 else: 

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

802 

803 @classmethod 

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

805 try: 

806 cls.expand(expression, hash_id=hash_id) 

807 except CroniterError: 

808 return False 

809 else: 

810 return True 

811 

812 @classmethod 

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

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

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

816 if not td.microsecond: 

817 td = td + ms1 

818 cron.set_current(td, force=True) 

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

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

821 return (max(tdp, tdt) - min(tdp, tdt)).total_seconds() < precision_in_seconds 

822 

823 

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

825 _croniter=None): 

826 """ 

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

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

829 well unless 'exclude_ends=True' is passed. 

830 

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

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

833 """ 

834 _croniter = _croniter or croniter 

835 auto_rt = datetime.datetime 

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

837 if ( 

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

839 isinstance(start, type(stop)) or 

840 isinstance(stop, type(start))) 

841 ): 

842 raise CroniterBadTypeRangeError( 

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

844 format(type(start), type(stop))) 

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

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

847 auto_rt = float 

848 if ret_type is None: 

849 ret_type = auto_rt 

850 if not exclude_ends: 

851 ms1 = relativedelta(microseconds=1) 

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

853 start -= ms1 

854 stop += ms1 

855 else: # Reverse time order 

856 start += ms1 

857 stop -= ms1 

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

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

860 max_years_between_matches=year_span) 

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

862 if start < stop: # Forward 

863 def cont(v): 

864 return v < stop 

865 step = ic.get_next 

866 else: # Reverse 

867 def cont(v): 

868 return v > stop 

869 step = ic.get_prev 

870 try: 

871 dt = step() 

872 while cont(dt): 

873 if ret_type is float: 

874 yield ic.get_current(float) 

875 else: 

876 yield dt 

877 dt = step() 

878 except CroniterBadDateError: 

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

880 return 

881 

882 

883class HashExpander: 

884 

885 def __init__(self, cronit): 

886 self.cron = cronit 

887 

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

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

890 hours_or_minutes = idx in {0, 1} 

891 if range_end is None: 

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

893 if hours_or_minutes: 

894 range_end += 1 

895 if range_begin is None: 

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

897 if hash_type == 'r': 

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

899 else: 

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

901 if not hours_or_minutes: 

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

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

904 

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

906 return hash_expression_re.match(expr) 

907 

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

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

910 if match == '': 

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

912 if not match: 

913 return expr 

914 m = match.groupdict() 

915 

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

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

918 

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

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

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

922 

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

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

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

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

927 

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

929 self.do( 

930 idx, 

931 hash_type=m['hash_type'], 

932 hash_id=hash_id, 

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

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

935 int(m['range_end']), 

936 int(m['divisor']), 

937 ) 

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

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

940 return str( 

941 self.do( 

942 idx, 

943 hash_type=m['hash_type'], 

944 hash_id=hash_id, 

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

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

947 ) 

948 ) 

949 elif m['divisor']: 

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

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

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

953 

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

955 self.do( 

956 idx, 

957 hash_type=m['hash_type'], 

958 hash_id=hash_id, 

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

960 ), 

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

962 int(m['divisor']), 

963 ) 

964 else: 

965 # Example: H -> 32 

966 return str( 

967 self.do( 

968 idx, 

969 hash_type=m['hash_type'], 

970 hash_id=hash_id, 

971 ) 

972 ) 

973 

974 

975EXPANDERS = OrderedDict([ 

976 ('hash', HashExpander), 

977])