thirdparty/google_appengine/google/appengine/cron/groctimespecification.py
changeset 1278 a7766286a7be
parent 828 f5fd65cc3bf3
child 2172 ac7bd3b467ff
equal deleted inserted replaced
1277:5c931bd3dc1e 1278:a7766286a7be
    20 
    20 
    21 A Groc schedule looks like '1st,2nd monday 9:00', or 'every 20 mins'. This
    21 A Groc schedule looks like '1st,2nd monday 9:00', or 'every 20 mins'. This
    22 module takes a parsed schedule (produced by Antlr) and creates objects that
    22 module takes a parsed schedule (produced by Antlr) and creates objects that
    23 can produce times that match this schedule.
    23 can produce times that match this schedule.
    24 
    24 
    25 A parsed schedule is one of two types - an Interval, and a Specific Time.
    25 A parsed schedule is one of two types - an Interval or a Specific Time.
    26 See the class docstrings for more.
    26 See the class docstrings for more.
    27 
    27 
    28 Extensions to be considered:
    28 Extensions to be considered:
    29 
    29 
    30   allowing a comma separated list of times to run
    30   allowing a comma separated list of times to run
    31   allowing the user to specify particular days of the month to run
    31   allowing the user to specify particular days of the month to run
    32 
       
    33 """
    32 """
    34 
    33 
    35 
    34 
    36 import calendar
    35 import calendar
    37 import datetime
    36 import datetime
    38 
    37 
       
    38 try:
       
    39   import pytz
       
    40 except ImportError:
       
    41   pytz = None
       
    42 
    39 import groc
    43 import groc
    40 
    44 
    41 HOURS = 'hours'
    45 HOURS = 'hours'
    42 MINUTES = 'minutes'
    46 MINUTES = 'minutes'
       
    47 
       
    48 try:
       
    49   from pytz import NonExistentTimeError
       
    50 except ImportError:
       
    51   class NonExistentTimeError(Exception):
       
    52     pass
    43 
    53 
    44 
    54 
    45 def GrocTimeSpecification(schedule):
    55 def GrocTimeSpecification(schedule):
    46   """Factory function.
    56   """Factory function.
    47 
    57 
    51     schedule: the schedule specification, as a string
    61     schedule: the schedule specification, as a string
    52 
    62 
    53   Returns:
    63   Returns:
    54     a TimeSpecification instance
    64     a TimeSpecification instance
    55   """
    65   """
    56 
       
    57   parser = groc.CreateParser(schedule)
    66   parser = groc.CreateParser(schedule)
    58   parser.timespec()
    67   parser.timespec()
    59 
    68 
    60   if parser.interval_mins:
    69   if parser.interval_mins:
    61     return IntervalTimeSpecification(parser.interval_mins, parser.period_string)
    70     return IntervalTimeSpecification(parser.interval_mins, parser.period_string)
    69 
    78 
    70   def GetMatches(self, start, n):
    79   def GetMatches(self, start, n):
    71     """Returns the next n times that match the schedule, starting at time start.
    80     """Returns the next n times that match the schedule, starting at time start.
    72 
    81 
    73     Arguments:
    82     Arguments:
    74       start: a datetime to start from. Matches will start from after this time
    83       start: a datetime to start from. Matches will start from after this time.
    75       n:     the number of matching times to return
    84       n:     the number of matching times to return
    76 
    85 
    77     Returns:
    86     Returns:
    78       a list of n datetime objects
    87       a list of n datetime objects
    79     """
    88     """
    87     """Returns the next match after time start.
    96     """Returns the next match after time start.
    88 
    97 
    89     Must be implemented in subclasses.
    98     Must be implemented in subclasses.
    90 
    99 
    91     Arguments:
   100     Arguments:
    92       start: a datetime to start with. Matches will start from this time
   101       start: a datetime to start with. Matches will start from this time.
    93 
   102 
    94     Returns:
   103     Returns:
    95       a datetime object
   104       a datetime object
    96     """
   105     """
    97     raise NotImplementedError
   106     raise NotImplementedError
    98 
   107 
    99 
   108 
   100 class IntervalTimeSpecification(TimeSpecification):
   109 class IntervalTimeSpecification(TimeSpecification):
   101   """A time specification for a given interval.
   110   """A time specification for a given interval.
   102 
   111 
   103   An Interval type spec runs at the given fixed interval. They have two
   112   An Interval type spec runs at the given fixed interval. It has two
   104   attributes:
   113   attributes:
   105   period   - the type of interval, either "hours" or "minutes"
   114   period   - the type of interval, either "hours" or "minutes"
   106   interval - the number of units of type period.
   115   interval - the number of units of type period.
       
   116   timezone - the timezone for this specification. Not used in this class.
   107   """
   117   """
   108 
   118 
   109   def __init__(self, interval, period):
   119   def __init__(self, interval, period, timezone=None):
   110     super(IntervalTimeSpecification, self).__init__(self)
   120     super(IntervalTimeSpecification, self).__init__(self)
   111     self.interval = interval
   121     self.interval = interval
   112     self.period = period
   122     self.period = period
   113 
   123 
   114   def GetMatch(self, t):
   124   def GetMatch(self, t):
   115     """Returns the next match after time 't'.
   125     """Returns the next match after time 't'.
   116 
   126 
   117     Arguments:
   127     Arguments:
   118       t: a datetime to start from. Matches will start from after this time
   128       t: a datetime to start from. Matches will start from after this time.
   119 
   129 
   120     Returns:
   130     Returns:
   121       a datetime object
   131       a datetime object
   122     """
   132     """
   123     if self.period == HOURS:
   133     if self.period == HOURS:
   127 
   137 
   128 
   138 
   129 class SpecificTimeSpecification(TimeSpecification):
   139 class SpecificTimeSpecification(TimeSpecification):
   130   """Specific time specification.
   140   """Specific time specification.
   131 
   141 
   132   A Specific interval is more complex, but define a certain time to run, on
   142   A Specific interval is more complex, but defines a certain time to run and
   133   given days. They have the following attributes:
   143   the days that it should run. It has the following attributes:
   134   time     - the time of day to run, as "HH:MM"
   144   time     - the time of day to run, as "HH:MM"
   135   ordinals - first, second, third &c, as a set of integers in 1..5
   145   ordinals - first, second, third &c, as a set of integers in 1..5
   136   months   - the months that this is valid, as a set of integers in 1..12
   146   months   - the months that this should run, as a set of integers in 1..12
   137   weekdays - the days of the week to run this, 0=Sunday, 6=Saturday.
   147   weekdays - the days of the week that this should run, as a set of integers,
   138 
   148              0=Sunday, 6=Saturday
   139   The specific time interval can be quite complex. A schedule could look like
   149   timezone - the optional timezone as a string for this specification.
       
   150              Defaults to UTC - valid entries are things like Australia/Victoria
       
   151              or PST8PDT.
       
   152 
       
   153   A specific time schedule can be quite complex. A schedule could look like
   140   this:
   154   this:
   141   "1st,third sat,sun of jan,feb,mar 09:15"
   155   "1st,third sat,sun of jan,feb,mar 09:15"
   142 
   156 
   143   In this case, ordinals would be [1,3], weekdays [0,6], months [1,2,3] and time
   157   In this case, ordinals would be {1,3}, weekdays {0,6}, months {1,2,3} and
   144   would be "09:15".
   158   time would be "09:15".
   145   """
   159   """
   146 
   160 
       
   161   timezone = None
       
   162 
   147   def __init__(self, ordinals=None, weekdays=None, months=None, monthdays=None,
   163   def __init__(self, ordinals=None, weekdays=None, months=None, monthdays=None,
   148                timestr='00:00'):
   164                timestr='00:00', timezone=None):
   149     super(SpecificTimeSpecification, self).__init__(self)
   165     super(SpecificTimeSpecification, self).__init__(self)
   150     if weekdays and monthdays:
   166     if weekdays is not None and monthdays is not None:
   151       raise ValueError("can't supply both monthdays and weekdays")
   167       raise ValueError("can't supply both monthdays and weekdays")
   152     if ordinals is None:
   168     if ordinals is None:
   153       self.ordinals = set(range(1, 6))
   169       self.ordinals = set(range(1, 6))
   154     else:
   170     else:
   155       self.ordinals = ordinals
   171       self.ordinals = set(ordinals)
   156 
   172 
   157     if weekdays is None:
   173     if weekdays is None:
   158       self.weekdays = set(range(7))
   174       self.weekdays = set(range(7))
   159     else:
   175     else:
   160       self.weekdays = weekdays
   176       self.weekdays = set(weekdays)
   161 
   177 
   162     if months is None:
   178     if months is None:
   163       self.months = set(range(1, 13))
   179       self.months = set(range(1, 13))
   164     else:
   180     else:
   165       self.months = months
   181       self.months = set(months)
   166 
   182 
   167     if monthdays is None:
   183     if monthdays is None:
   168       self.monthdays = set()
   184       self.monthdays = set()
   169     else:
   185     else:
   170       self.monthdays = monthdays
   186       self.monthdays = set(monthdays)
   171     hourstr, minutestr = timestr.split(':')
   187     hourstr, minutestr = timestr.split(':')
   172     self.time = datetime.time(int(hourstr), int(minutestr))
   188     self.time = datetime.time(int(hourstr), int(minutestr))
       
   189     if timezone and pytz is not None:
       
   190       self.timezone = pytz.timezone(timezone)
   173 
   191 
   174   def _MatchingDays(self, year, month):
   192   def _MatchingDays(self, year, month):
   175     """Returns matching days for the given year and month.
   193     """Returns matching days for the given year and month.
   176 
   194 
   177     For the given year and month, return the days that match this instance's
   195     For the given year and month, return the days that match this instance's
   223 
   241 
   224   def GetMatch(self, start):
   242   def GetMatch(self, start):
   225     """Returns the next time that matches the schedule after time start.
   243     """Returns the next time that matches the schedule after time start.
   226 
   244 
   227     Arguments:
   245     Arguments:
   228       start: a datetime to start with. Matches will start after this time
   246       start: a UTC datetime to start from. Matches will start after this time
   229 
   247 
   230     Returns:
   248     Returns:
   231       a datetime object
   249       a datetime object
   232     """
   250     """
   233     start_time = start
   251     start_time = start
       
   252     if self.timezone and pytz is not None:
       
   253       if not start_time.tzinfo:
       
   254         start_time = pytz.utc.localize(start_time)
       
   255       start_time = start_time.astimezone(self.timezone)
       
   256       start_time = start_time.replace(tzinfo=None)
   234     if self.months:
   257     if self.months:
   235       months = self._NextMonthGenerator(start.month, self.months)
   258       months = self._NextMonthGenerator(start_time.month, self.months)
   236     while True:
   259     while True:
   237       month, yearwraps = months.next()
   260       month, yearwraps = months.next()
   238       candidate = start_time.replace(day=1, month=month,
   261       candidate_month = start_time.replace(day=1, month=month,
   239                                      year=start_time.year + yearwraps)
   262                                      year=start_time.year + yearwraps)
   240 
   263 
   241       if self.monthdays:
   264       if self.monthdays:
   242         _, last_day = calendar.monthrange(candidate.year, candidate.month)
   265         _, last_day = calendar.monthrange(candidate_month.year,
   243         day_matches = sorted([x for x in self.monthdays if x <= last_day])
   266                                           candidate_month.month)
       
   267         day_matches = sorted(x for x in self.monthdays if x <= last_day)
   244       else:
   268       else:
   245         day_matches = self._MatchingDays(candidate.year, month)
   269         day_matches = self._MatchingDays(candidate_month.year, month)
   246 
   270 
   247       if ((candidate.year, candidate.month)
   271       if ((candidate_month.year, candidate_month.month)
   248           == (start_time.year, start_time.month)):
   272           == (start_time.year, start_time.month)):
   249         day_matches = [x for x in day_matches if x >= start_time.day]
   273         day_matches = [x for x in day_matches if x >= start_time.day]
   250         if day_matches and day_matches[0] == start_time.day:
   274         while (day_matches and day_matches[0] == start_time.day
   251           if start_time.time() >= self.time:
   275             and start_time.time() >= self.time):
   252             day_matches.pop(0)
   276           day_matches.pop(0)
   253       if not day_matches:
   277       while day_matches:
   254         continue
   278         out = candidate_month.replace(day=day_matches[0], hour=self.time.hour,
   255       out = candidate.replace(day=day_matches[0], hour=self.time.hour,
   279 
   256                               minute=self.time.minute, second=0, microsecond=0)
   280 
   257       return out
   281                                       minute=self.time.minute, second=0,
       
   282                                       microsecond=0)
       
   283         if self.timezone and pytz is not None:
       
   284           try:
       
   285             out = self.timezone.localize(out)
       
   286           except (NonExistentTimeError, IndexError):
       
   287             for _ in range(24):
       
   288               out = out.replace(minute=1) + datetime.timedelta(minutes=60)
       
   289               try:
       
   290                 out = self.timezone.localize(out)
       
   291               except (NonExistentTimeError, IndexError):
       
   292                 continue
       
   293               break
       
   294           out = out.astimezone(pytz.utc)
       
   295         return out