|
@@ -1,15 +1,19 @@
|
|
|
from __future__ import absolute_import
|
|
|
|
|
|
+import re
|
|
|
+
|
|
|
from datetime import datetime, timedelta
|
|
|
from dateutil.relativedelta import relativedelta
|
|
|
-from pyparsing import (Word, Literal, ZeroOrMore, Optional,
|
|
|
- Group, StringEnd, alphas)
|
|
|
|
|
|
from .utils import is_iterable
|
|
|
from .utils.timeutils import (timedelta_seconds, weekday,
|
|
|
remaining, humanize_seconds)
|
|
|
|
|
|
|
|
|
+class ParseException(Exception):
|
|
|
+ """Raised by crontab_parser when the input can't be parsed."""
|
|
|
+
|
|
|
+
|
|
|
class schedule(object):
|
|
|
relative = False
|
|
|
|
|
@@ -50,8 +54,8 @@ class schedule(object):
|
|
|
return False, rem
|
|
|
|
|
|
def __repr__(self):
|
|
|
- return "<freq: %s>" % humanize_seconds(
|
|
|
- timedelta_seconds(self.run_every))
|
|
|
+ return "<freq: %s>" % (
|
|
|
+ humanize_seconds(timedelta_seconds(self.run_every)), )
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
if isinstance(other, schedule):
|
|
@@ -85,70 +89,74 @@ class crontab_parser(object):
|
|
|
[0, 1, 2, 3, 4, 5, 6]
|
|
|
|
|
|
"""
|
|
|
+ ParseException = ParseException
|
|
|
|
|
|
- def __init__(self, max_=60):
|
|
|
- # define the grammar structure
|
|
|
- digits = "0123456789"
|
|
|
- star = Literal('*')
|
|
|
- number = Word(digits) | Word(alphas)
|
|
|
- steps = number
|
|
|
- range_ = number + Optional(Literal('-') + number)
|
|
|
- numspec = star | range_
|
|
|
- expr = Group(numspec) + Optional(Literal('/') + steps)
|
|
|
- extra_groups = ZeroOrMore(Literal(',') + expr)
|
|
|
- groups = expr + extra_groups + StringEnd()
|
|
|
-
|
|
|
- # define parse actions
|
|
|
- star.setParseAction(self._expand_star)
|
|
|
- number.setParseAction(self._expand_number)
|
|
|
- range_.setParseAction(self._expand_range)
|
|
|
- expr.setParseAction(self._filter_steps)
|
|
|
- extra_groups.setParseAction(self._ignore_comma)
|
|
|
- groups.setParseAction(self._join_to_set)
|
|
|
+ _range = r'(\w+?)-(\w+)'
|
|
|
+ _steps = r'/(\w+)?'
|
|
|
+ _star = r'\*'
|
|
|
|
|
|
+ def __init__(self, max_=60):
|
|
|
+ _range = self._range
|
|
|
+ _steps = self._steps
|
|
|
+ _star = self._star
|
|
|
+ self.pats = ((re.compile(_range + _steps), self._range_steps),
|
|
|
+ (re.compile(_range), self._expand_range),
|
|
|
+ (re.compile(_star + _steps), self._star_steps))
|
|
|
self.max_ = max_
|
|
|
- self.parser = groups
|
|
|
-
|
|
|
- @staticmethod
|
|
|
- def _expand_number(toks):
|
|
|
- try:
|
|
|
- i = int(toks[0])
|
|
|
- except ValueError:
|
|
|
- try:
|
|
|
- i = weekday(toks[0])
|
|
|
- except KeyError:
|
|
|
- raise ValueError("Invalid weekday literal '%s'." % toks[0])
|
|
|
- return [i]
|
|
|
|
|
|
- @staticmethod
|
|
|
- def _expand_range(toks):
|
|
|
+ def parse(self, spec):
|
|
|
+ acc = set()
|
|
|
+ for part in spec.split(','):
|
|
|
+ if part:
|
|
|
+ acc |= set(self._parse_part(part))
|
|
|
+ else:
|
|
|
+ raise self.ParseException("empty part")
|
|
|
+ return acc
|
|
|
+
|
|
|
+ def _parse_part(self, part):
|
|
|
+ for regex, handler in self.pats:
|
|
|
+ m = regex.match(part)
|
|
|
+ if m:
|
|
|
+ return handler(m.groups())
|
|
|
+ if part == '*':
|
|
|
+ return self._expand_star()
|
|
|
+ return self._expand_range((part, ))
|
|
|
+
|
|
|
+ def _expand_range(self, toks):
|
|
|
+ fr = self._expand_number(toks[0])
|
|
|
if len(toks) > 1:
|
|
|
- return range(toks[0], int(toks[2]) + 1)
|
|
|
+ to = self._expand_number(toks[1])
|
|
|
+ return range(fr, min(to + 1, self.max_ + 1))
|
|
|
else:
|
|
|
- return toks[0]
|
|
|
+ return [fr]
|
|
|
|
|
|
- def _expand_star(self, toks):
|
|
|
- return range(self.max_)
|
|
|
+ def _range_steps(self, toks):
|
|
|
+ if len(toks) != 3 or not toks[2]:
|
|
|
+ raise self.ParseException("empty filter")
|
|
|
+ return self._filter_steps(self._expand_range(toks[:2]), int(toks[2]))
|
|
|
|
|
|
- @staticmethod
|
|
|
- def _filter_steps(toks):
|
|
|
- numbers = toks[0]
|
|
|
- if len(toks) > 1:
|
|
|
- steps = toks[2]
|
|
|
- return [n for n in numbers if n % steps == 0]
|
|
|
- else:
|
|
|
- return numbers
|
|
|
+ def _star_steps(self, toks):
|
|
|
+ if not toks or not toks[0]:
|
|
|
+ raise self.ParseException("empty filter")
|
|
|
+ return self._filter_steps(self._expand_star(), int(toks[0]))
|
|
|
|
|
|
- @staticmethod
|
|
|
- def _ignore_comma(toks):
|
|
|
- return [x for x in toks if x != ',']
|
|
|
+ def _filter_steps(self, numbers, steps):
|
|
|
+ return [n for n in numbers if n % steps == 0]
|
|
|
|
|
|
- @staticmethod
|
|
|
- def _join_to_set(toks):
|
|
|
- return set(toks.asList())
|
|
|
+ def _expand_star(self):
|
|
|
+ return range(self.max_)
|
|
|
|
|
|
- def parse(self, cronspec):
|
|
|
- return self.parser.parseString(cronspec).pop()
|
|
|
+ def _expand_number(self, s):
|
|
|
+ if isinstance(s, basestring) and s[0] == '-':
|
|
|
+ raise self.ParseException("negative numbers not supported")
|
|
|
+ try:
|
|
|
+ i = int(s)
|
|
|
+ except ValueError:
|
|
|
+ try:
|
|
|
+ i = weekday(s)
|
|
|
+ except KeyError:
|
|
|
+ raise ValueError("Invalid weekday literal '%s'." % s)
|
|
|
+ return i
|
|
|
|
|
|
|
|
|
class crontab(schedule):
|