diff -r 5c931bd3dc1e -r a7766286a7be thirdparty/google_appengine/google/appengine/cron/groctimespecification.py --- a/thirdparty/google_appengine/google/appengine/cron/groctimespecification.py Thu Feb 12 10:24:37 2009 +0000 +++ b/thirdparty/google_appengine/google/appengine/cron/groctimespecification.py Thu Feb 12 12:30:36 2009 +0000 @@ -22,25 +22,35 @@ module takes a parsed schedule (produced by Antlr) and creates objects that can produce times that match this schedule. -A parsed schedule is one of two types - an Interval, and a Specific Time. +A parsed schedule is one of two types - an Interval or a Specific Time. See the class docstrings for more. Extensions to be considered: allowing a comma separated list of times to run allowing the user to specify particular days of the month to run - """ import calendar import datetime +try: + import pytz +except ImportError: + pytz = None + import groc HOURS = 'hours' MINUTES = 'minutes' +try: + from pytz import NonExistentTimeError +except ImportError: + class NonExistentTimeError(Exception): + pass + def GrocTimeSpecification(schedule): """Factory function. @@ -53,7 +63,6 @@ Returns: a TimeSpecification instance """ - parser = groc.CreateParser(schedule) parser.timespec() @@ -71,7 +80,7 @@ """Returns the next n times that match the schedule, starting at time start. Arguments: - start: a datetime to start from. Matches will start from after this time + start: a datetime to start from. Matches will start from after this time. n: the number of matching times to return Returns: @@ -89,7 +98,7 @@ Must be implemented in subclasses. Arguments: - start: a datetime to start with. Matches will start from this time + start: a datetime to start with. Matches will start from this time. Returns: a datetime object @@ -100,13 +109,14 @@ class IntervalTimeSpecification(TimeSpecification): """A time specification for a given interval. - An Interval type spec runs at the given fixed interval. They have two + An Interval type spec runs at the given fixed interval. It has two attributes: period - the type of interval, either "hours" or "minutes" interval - the number of units of type period. + timezone - the timezone for this specification. Not used in this class. """ - def __init__(self, interval, period): + def __init__(self, interval, period, timezone=None): super(IntervalTimeSpecification, self).__init__(self) self.interval = interval self.period = period @@ -115,7 +125,7 @@ """Returns the next match after time 't'. Arguments: - t: a datetime to start from. Matches will start from after this time + t: a datetime to start from. Matches will start from after this time. Returns: a datetime object @@ -129,47 +139,55 @@ class SpecificTimeSpecification(TimeSpecification): """Specific time specification. - A Specific interval is more complex, but define a certain time to run, on - given days. They have the following attributes: + A Specific interval is more complex, but defines a certain time to run and + the days that it should run. It has the following attributes: time - the time of day to run, as "HH:MM" ordinals - first, second, third &c, as a set of integers in 1..5 - months - the months that this is valid, as a set of integers in 1..12 - weekdays - the days of the week to run this, 0=Sunday, 6=Saturday. + months - the months that this should run, as a set of integers in 1..12 + weekdays - the days of the week that this should run, as a set of integers, + 0=Sunday, 6=Saturday + timezone - the optional timezone as a string for this specification. + Defaults to UTC - valid entries are things like Australia/Victoria + or PST8PDT. - The specific time interval can be quite complex. A schedule could look like + A specific time schedule can be quite complex. A schedule could look like this: "1st,third sat,sun of jan,feb,mar 09:15" - In this case, ordinals would be [1,3], weekdays [0,6], months [1,2,3] and time - would be "09:15". + In this case, ordinals would be {1,3}, weekdays {0,6}, months {1,2,3} and + time would be "09:15". """ + timezone = None + def __init__(self, ordinals=None, weekdays=None, months=None, monthdays=None, - timestr='00:00'): + timestr='00:00', timezone=None): super(SpecificTimeSpecification, self).__init__(self) - if weekdays and monthdays: + if weekdays is not None and monthdays is not None: raise ValueError("can't supply both monthdays and weekdays") if ordinals is None: self.ordinals = set(range(1, 6)) else: - self.ordinals = ordinals + self.ordinals = set(ordinals) if weekdays is None: self.weekdays = set(range(7)) else: - self.weekdays = weekdays + self.weekdays = set(weekdays) if months is None: self.months = set(range(1, 13)) else: - self.months = months + self.months = set(months) if monthdays is None: self.monthdays = set() else: - self.monthdays = monthdays + self.monthdays = set(monthdays) hourstr, minutestr = timestr.split(':') self.time = datetime.time(int(hourstr), int(minutestr)) + if timezone and pytz is not None: + self.timezone = pytz.timezone(timezone) def _MatchingDays(self, year, month): """Returns matching days for the given year and month. @@ -225,33 +243,53 @@ """Returns the next time that matches the schedule after time start. Arguments: - start: a datetime to start with. Matches will start after this time + start: a UTC datetime to start from. Matches will start after this time Returns: a datetime object """ start_time = start + if self.timezone and pytz is not None: + if not start_time.tzinfo: + start_time = pytz.utc.localize(start_time) + start_time = start_time.astimezone(self.timezone) + start_time = start_time.replace(tzinfo=None) if self.months: - months = self._NextMonthGenerator(start.month, self.months) + months = self._NextMonthGenerator(start_time.month, self.months) while True: month, yearwraps = months.next() - candidate = start_time.replace(day=1, month=month, + candidate_month = start_time.replace(day=1, month=month, year=start_time.year + yearwraps) if self.monthdays: - _, last_day = calendar.monthrange(candidate.year, candidate.month) - day_matches = sorted([x for x in self.monthdays if x <= last_day]) + _, last_day = calendar.monthrange(candidate_month.year, + candidate_month.month) + day_matches = sorted(x for x in self.monthdays if x <= last_day) else: - day_matches = self._MatchingDays(candidate.year, month) + day_matches = self._MatchingDays(candidate_month.year, month) - if ((candidate.year, candidate.month) + if ((candidate_month.year, candidate_month.month) == (start_time.year, start_time.month)): day_matches = [x for x in day_matches if x >= start_time.day] - if day_matches and day_matches[0] == start_time.day: - if start_time.time() >= self.time: - day_matches.pop(0) - if not day_matches: - continue - out = candidate.replace(day=day_matches[0], hour=self.time.hour, - minute=self.time.minute, second=0, microsecond=0) - return out + while (day_matches and day_matches[0] == start_time.day + and start_time.time() >= self.time): + day_matches.pop(0) + while day_matches: + out = candidate_month.replace(day=day_matches[0], hour=self.time.hour, + + + minute=self.time.minute, second=0, + microsecond=0) + if self.timezone and pytz is not None: + try: + out = self.timezone.localize(out) + except (NonExistentTimeError, IndexError): + for _ in range(24): + out = out.replace(minute=1) + datetime.timedelta(minutes=60) + try: + out = self.timezone.localize(out) + except (NonExistentTimeError, IndexError): + continue + break + out = out.astimezone(pytz.utc) + return out