--- 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