thirdparty/google_appengine/google/appengine/cron/groctimespecification.py
changeset 1278 a7766286a7be
parent 828 f5fd65cc3bf3
child 2172 ac7bd3b467ff
--- 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