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
« 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 -*-
4from __future__ import absolute_import, print_function, division
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
20try:
21 from collections import OrderedDict
22except ImportError:
23 OrderedDict = dict # py26 degraded mode, expanders order will not be immutable
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+$')
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]
49def timedelta_to_seconds(td):
50 return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) \
51 / 10**6
54def datetime_to_timestamp(d):
55 if d.tzinfo is not None:
56 d = d.replace(tzinfo=None) - d.utcoffset()
58 return timedelta_to_seconds(d - datetime.datetime(1970, 1, 1))
61def _get_caller_globals_and_locals():
62 """
63 Returns the globals and locals of the calling frame.
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
73class CroniterError(ValueError):
74 """ General top-level Croniter base exception """
75 pass
78class CroniterBadTypeRangeError(TypeError):
79 """."""
82class CroniterBadCronError(CroniterError):
83 """ Syntax, unknown value, or range error within a cron expression """
84 pass
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
95class CroniterBadDateError(CroniterError):
96 """ Unable to find next/prev timestamp match """
97 pass
100class CroniterNotAlphaError(CroniterBadCronError):
101 """ Cron syntax contains an invalid day or month abbreviation """
102 pass
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 )
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 )
131 LOWMAP = (
132 {},
133 {},
134 {0: 1},
135 {0: 1},
136 {7: 0},
137 {},
138 )
140 LEN_MEANS_ALL = (
141 60,
142 24,
143 31,
144 12,
145 7,
146 60
147 )
149 bad_length = 'Exactly 5 or 6 columns has to be specified for iterator ' \
150 'expression.'
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
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')
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)
170 if start_time is None:
171 start_time = time()
173 self.tzinfo = None
175 self.start_time = None
176 self.dst_start_time = None
177 self.cur = None
178 self.set_current(start_time, force=False)
180 self.expanded, self.nth_weekday_of_month = self.expand(expr_format, hash_id=hash_id)
181 self._is_prev = is_prev
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)))
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)
195 def get_prev(self, ret_type=None):
196 return self._get_next(ret_type or self._ret_type, is_prev=True)
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
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)
210 self.start_time = start_time
211 self.dst_start_time = start_time
212 self.cur = start_time
213 return self.cur
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)
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)
230 return result
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)
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()
250 ret_type = ret_type or self._ret_type
252 if not issubclass(ret_type, (float, datetime.datetime)):
253 raise TypeError("Invalid ret_type, only 'float' or 'datetime' "
254 "is acceptable.")
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] = ['*']
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)
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
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
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
340 def iter(self, *args, **kwargs):
341 return (self._is_prev and self.all_prev or self.all_next)
343 def __iter__(self):
344 return self
345 __next__ = next = _get_next
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
359 dst = now = self._timestamp_to_datetime(now + sign * offset)
361 month, year = dst.month, dst.year
362 current_year = now.year
363 DAYS = self.DAYS
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
375 reset_day = 1
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
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
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)
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
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
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['*']
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)
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
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
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
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
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
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]
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))
543 if is_prev:
544 raise CroniterBadDateError("failed to find prev date")
545 raise CroniterBadDateError("failed to find next date")
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]
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]
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
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)
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)
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
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 }
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
635 expressions = efl.split()
637 if len(expressions) not in VALID_LEN_EXPRESSION:
638 raise CroniterBadCronError(cls.bad_length)
640 expanded = []
641 nth_weekday_of_month = {}
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)
647 e_list = expr.split(',')
648 res = []
650 while len(e_list) > 0:
651 e = e_list.pop()
652 nth = None
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'
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)
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)
690 if m:
691 # early abort if low/high are out of bounds
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'
697 if not only_int_re.search(low):
698 low = "{0}".format(cls._alphaconv(i, low, expressions))
700 if not only_int_re.search(high):
701 high = "{0}".format(cls._alphaconv(i, high, expressions))
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))
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)
736 try:
737 t = int(t)
738 except ValueError:
739 pass
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]
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))
759 res.append(t)
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)
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 = ['*']
771 expanded.append(['*'] if (len(res) == 1
772 and res[0] == '*')
773 else res)
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))
785 return expanded, nth_weekday_of_month
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))
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
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
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.
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
883class HashExpander:
885 def __init__(self, cronit):
886 self.cron = cronit
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
905 def match(self, efl, idx, expr, hash_id=None, **kw):
906 return hash_expression_re.match(expr)
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()
916 if m['hash_type'] == 'h' and hash_id is None:
917 raise CroniterBadCronError('Hashed definitions must include hash_id')
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')
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))
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))
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 )
975EXPANDERS = OrderedDict([
976 ('hash', HashExpander),
977])