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
« 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 -*-
4from __future__ import absolute_import, print_function, division
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
18try:
19 from collections import OrderedDict
20except ImportError:
21 OrderedDict = dict # py26 degraded mode, expanders order will not be immutable
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]
34def timedelta_to_seconds(td):
35 return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) \
36 / 10**6
39def datetime_to_timestamp(d):
40 if d.tzinfo is not None:
41 d = d.replace(tzinfo=None) - d.utcoffset()
43 return timedelta_to_seconds(d - datetime.datetime(1970, 1, 1))
46def _get_caller_globals_and_locals():
47 """
48 Returns the globals and locals of the calling frame.
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
58class CroniterError(ValueError):
59 """ General top-level Croniter base exception """
60 pass
63class CroniterBadTypeRangeError(TypeError):
64 """."""
67class CroniterBadCronError(CroniterError):
68 """ Syntax, unknown value, or range error within a cron expression """
69 pass
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
80class CroniterBadDateError(CroniterError):
81 """ Unable to find next/prev timestamp match """
82 pass
85class CroniterNotAlphaError(CroniterBadCronError):
86 """ Cron syntax contains an invalid day or month abbreviation """
87 pass
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 )
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 )
117 LOWMAP = (
118 {},
119 {},
120 {0: 1},
121 {0: 1},
122 {7: 0},
123 {},
124 )
126 LEN_MEANS_ALL = (
127 60,
128 24,
129 31,
130 12,
131 7,
132 60
133 )
135 bad_length = 'Exactly 5 or 6 columns has to be specified for iterator ' \
136 'expression.'
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
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')
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)
156 if start_time is None:
157 start_time = time()
159 self.tzinfo = None
161 self.start_time = None
162 self.dst_start_time = None
163 self.cur = None
164 self.set_current(start_time, force=False)
166 self.expanded, self.nth_weekday_of_month = self.expand(expr_format, hash_id=hash_id)
167 self._is_prev = is_prev
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)))
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)
181 def get_prev(self, ret_type=None):
182 return self._get_next(ret_type or self._ret_type, is_prev=True)
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
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)
196 self.start_time = start_time
197 self.dst_start_time = start_time
198 self.cur = start_time
199 return self.cur
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)
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)
216 return result
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)
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()
236 ret_type = ret_type or self._ret_type
238 if not issubclass(ret_type, (float, datetime.datetime)):
239 raise TypeError("Invalid ret_type, only 'float' or 'datetime' "
240 "is acceptable.")
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] = ['*']
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)
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
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
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
326 def iter(self, *args, **kwargs):
327 return (self._is_prev and self.all_prev or self.all_next)
329 def __iter__(self):
330 return self
331 __next__ = next = _get_next
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
345 dst = now = self._timestamp_to_datetime(now + sign * offset)
347 month, year = dst.month, dst.year
348 current_year = now.year
349 DAYS = self.DAYS
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
361 reset_day = 1
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
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
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)
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
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
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['*']
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)
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
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
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
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
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
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]
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))
529 if is_prev:
530 raise CroniterBadDateError("failed to find prev date")
531 raise CroniterBadDateError("failed to find next date")
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]
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]
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
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)
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)
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
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 }
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
621 expressions = efl.split()
623 if len(expressions) not in VALID_LEN_EXPRESSION:
624 raise CroniterBadCronError(cls.bad_length)
626 expanded = []
627 nth_weekday_of_month = {}
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)
633 e_list = expr.split(',')
634 res = []
636 while len(e_list) > 0:
637 e = e_list.pop()
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
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)
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)
677 if m:
678 # early abort if low/high are out of bounds
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'
684 if not only_int_re.search(low):
685 low = "{0}".format(cls._alphaconv(i, low, expressions))
687 if not only_int_re.search(high):
688 high = "{0}".format(cls._alphaconv(i, high, expressions))
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))
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)
723 try:
724 t = int(t)
725 except ValueError:
726 pass
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]
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))
746 res.append(t)
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)
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 = ['*']
758 expanded.append(['*'] if (len(res) == 1
759 and res[0] == '*')
760 else res)
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))
772 return expanded, nth_weekday_of_month
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))
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
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
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.
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
868class HashExpander:
870 def __init__(self, cronit):
871 self.cron = cronit
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
885 def match(self, efl, idx, expr, hash_id=None, **kw):
886 return hash_expression_re.match(expr)
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()
896 if m['hash_type'] == 'h' and hash_id is None:
897 raise CroniterBadCronError('Hashed definitions must include hash_id')
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))
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))
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 )
951EXPANDERS = OrderedDict([
952 ('hash', HashExpander),
953])