thirdparty/google_appengine/google/appengine/cron/groctimespecification.py
changeset 828 f5fd65cc3bf3
child 1278 a7766286a7be
equal deleted inserted replaced
827:88c186556a80 828:f5fd65cc3bf3
       
     1 #!/usr/bin/env python
       
     2 #
       
     3 # Copyright 2007 Google Inc.
       
     4 #
       
     5 # Licensed under the Apache License, Version 2.0 (the "License");
       
     6 # you may not use this file except in compliance with the License.
       
     7 # You may obtain a copy of the License at
       
     8 #
       
     9 #     http://www.apache.org/licenses/LICENSE-2.0
       
    10 #
       
    11 # Unless required by applicable law or agreed to in writing, software
       
    12 # distributed under the License is distributed on an "AS IS" BASIS,
       
    13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
       
    14 # See the License for the specific language governing permissions and
       
    15 # limitations under the License.
       
    16 #
       
    17 
       
    18 
       
    19 """Implementation of scheduling for Groc format schedules.
       
    20 
       
    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
       
    23 can produce times that match this schedule.
       
    24 
       
    25 A parsed schedule is one of two types - an Interval, and a Specific Time.
       
    26 See the class docstrings for more.
       
    27 
       
    28 Extensions to be considered:
       
    29 
       
    30   allowing a comma separated list of times to run
       
    31   allowing the user to specify particular days of the month to run
       
    32 
       
    33 """
       
    34 
       
    35 
       
    36 import calendar
       
    37 import datetime
       
    38 
       
    39 import groc
       
    40 
       
    41 HOURS = 'hours'
       
    42 MINUTES = 'minutes'
       
    43 
       
    44 
       
    45 def GrocTimeSpecification(schedule):
       
    46   """Factory function.
       
    47 
       
    48   Turns a schedule specification into a TimeSpecification.
       
    49 
       
    50   Arguments:
       
    51     schedule: the schedule specification, as a string
       
    52 
       
    53   Returns:
       
    54     a TimeSpecification instance
       
    55   """
       
    56 
       
    57   parser = groc.CreateParser(schedule)
       
    58   parser.timespec()
       
    59 
       
    60   if parser.interval_mins:
       
    61     return IntervalTimeSpecification(parser.interval_mins, parser.period_string)
       
    62   else:
       
    63     return SpecificTimeSpecification(parser.ordinal_set, parser.weekday_set,
       
    64                                      parser.month_set, None, parser.time_string)
       
    65 
       
    66 
       
    67 class TimeSpecification(object):
       
    68   """Base class for time specifications."""
       
    69 
       
    70   def GetMatches(self, start, n):
       
    71     """Returns the next n times that match the schedule, starting at time start.
       
    72 
       
    73     Arguments:
       
    74       start: a datetime to start from. Matches will start from after this time
       
    75       n:     the number of matching times to return
       
    76 
       
    77     Returns:
       
    78       a list of n datetime objects
       
    79     """
       
    80     out = []
       
    81     for _ in range(n):
       
    82       start = self.GetMatch(start)
       
    83       out.append(start)
       
    84     return out
       
    85 
       
    86   def GetMatch(self, start):
       
    87     """Returns the next match after time start.
       
    88 
       
    89     Must be implemented in subclasses.
       
    90 
       
    91     Arguments:
       
    92       start: a datetime to start with. Matches will start from this time
       
    93 
       
    94     Returns:
       
    95       a datetime object
       
    96     """
       
    97     raise NotImplementedError
       
    98 
       
    99 
       
   100 class IntervalTimeSpecification(TimeSpecification):
       
   101   """A time specification for a given interval.
       
   102 
       
   103   An Interval type spec runs at the given fixed interval. They have two
       
   104   attributes:
       
   105   period   - the type of interval, either "hours" or "minutes"
       
   106   interval - the number of units of type period.
       
   107   """
       
   108 
       
   109   def __init__(self, interval, period):
       
   110     super(IntervalTimeSpecification, self).__init__(self)
       
   111     self.interval = interval
       
   112     self.period = period
       
   113 
       
   114   def GetMatch(self, t):
       
   115     """Returns the next match after time 't'.
       
   116 
       
   117     Arguments:
       
   118       t: a datetime to start from. Matches will start from after this time
       
   119 
       
   120     Returns:
       
   121       a datetime object
       
   122     """
       
   123     if self.period == HOURS:
       
   124       return t + datetime.timedelta(hours=self.interval)
       
   125     else:
       
   126       return t + datetime.timedelta(minutes=self.interval)
       
   127 
       
   128 
       
   129 class SpecificTimeSpecification(TimeSpecification):
       
   130   """Specific time specification.
       
   131 
       
   132   A Specific interval is more complex, but define a certain time to run, on
       
   133   given days. They have the following attributes:
       
   134   time     - the time of day to run, as "HH:MM"
       
   135   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
       
   137   weekdays - the days of the week to run this, 0=Sunday, 6=Saturday.
       
   138 
       
   139   The specific time interval can be quite complex. A schedule could look like
       
   140   this:
       
   141   "1st,third sat,sun of jan,feb,mar 09:15"
       
   142 
       
   143   In this case, ordinals would be [1,3], weekdays [0,6], months [1,2,3] and time
       
   144   would be "09:15".
       
   145   """
       
   146 
       
   147   def __init__(self, ordinals=None, weekdays=None, months=None, monthdays=None,
       
   148                timestr='00:00'):
       
   149     super(SpecificTimeSpecification, self).__init__(self)
       
   150     if weekdays and monthdays:
       
   151       raise ValueError("can't supply both monthdays and weekdays")
       
   152     if ordinals is None:
       
   153       self.ordinals = set(range(1, 6))
       
   154     else:
       
   155       self.ordinals = ordinals
       
   156 
       
   157     if weekdays is None:
       
   158       self.weekdays = set(range(7))
       
   159     else:
       
   160       self.weekdays = weekdays
       
   161 
       
   162     if months is None:
       
   163       self.months = set(range(1, 13))
       
   164     else:
       
   165       self.months = months
       
   166 
       
   167     if monthdays is None:
       
   168       self.monthdays = set()
       
   169     else:
       
   170       self.monthdays = monthdays
       
   171     hourstr, minutestr = timestr.split(':')
       
   172     self.time = datetime.time(int(hourstr), int(minutestr))
       
   173 
       
   174   def _MatchingDays(self, year, month):
       
   175     """Returns matching days for the given year and month.
       
   176 
       
   177     For the given year and month, return the days that match this instance's
       
   178     day specification, based on the ordinals and weekdays.
       
   179 
       
   180     Arguments:
       
   181       year: the year as an integer
       
   182       month: the month as an integer, in range 1-12
       
   183 
       
   184     Returns:
       
   185       a list of matching days, as ints in range 1-31
       
   186     """
       
   187     out_days = []
       
   188     start_day, last_day = calendar.monthrange(year, month)
       
   189     start_day = (start_day + 1) % 7
       
   190     for ordinal in self.ordinals:
       
   191       for weekday in self.weekdays:
       
   192         day = ((weekday - start_day) % 7) + 1
       
   193         day += 7 * (ordinal - 1)
       
   194         if day <= last_day:
       
   195           out_days.append(day)
       
   196     return sorted(out_days)
       
   197 
       
   198   def _NextMonthGenerator(self, start, matches):
       
   199     """Creates a generator that produces results from the set 'matches'.
       
   200 
       
   201     Matches must be >= 'start'. If none match, the wrap counter is incremented,
       
   202     and the result set is reset to the full set. Yields a 2-tuple of (match,
       
   203     wrapcount).
       
   204 
       
   205     Arguments:
       
   206       start: first set of matches will be >= this value (an int)
       
   207       matches: the set of potential matches (a sequence of ints)
       
   208 
       
   209     Yields:
       
   210       a two-tuple of (match, wrap counter). match is an int in range (1-12),
       
   211       wrapcount is a int indicating how many times we've wrapped around.
       
   212     """
       
   213     potential = matches = sorted(matches)
       
   214     after = start - 1
       
   215     wrapcount = 0
       
   216     while True:
       
   217       potential = [x for x in potential if x > after]
       
   218       if not potential:
       
   219         wrapcount += 1
       
   220         potential = matches
       
   221       after = potential[0]
       
   222       yield (after, wrapcount)
       
   223 
       
   224   def GetMatch(self, start):
       
   225     """Returns the next time that matches the schedule after time start.
       
   226 
       
   227     Arguments:
       
   228       start: a datetime to start with. Matches will start after this time
       
   229 
       
   230     Returns:
       
   231       a datetime object
       
   232     """
       
   233     start_time = start
       
   234     if self.months:
       
   235       months = self._NextMonthGenerator(start.month, self.months)
       
   236     while True:
       
   237       month, yearwraps = months.next()
       
   238       candidate = start_time.replace(day=1, month=month,
       
   239                                      year=start_time.year + yearwraps)
       
   240 
       
   241       if self.monthdays:
       
   242         _, last_day = calendar.monthrange(candidate.year, candidate.month)
       
   243         day_matches = sorted([x for x in self.monthdays if x <= last_day])
       
   244       else:
       
   245         day_matches = self._MatchingDays(candidate.year, month)
       
   246 
       
   247       if ((candidate.year, candidate.month)
       
   248           == (start_time.year, start_time.month)):
       
   249         day_matches = [x for x in day_matches if x >= start_time.day]
       
   250         if day_matches and day_matches[0] == start_time.day:
       
   251           if start_time.time() >= self.time:
       
   252             day_matches.pop(0)
       
   253       if not day_matches:
       
   254         continue
       
   255       out = candidate.replace(day=day_matches[0], hour=self.time.hour,
       
   256                               minute=self.time.minute, second=0, microsecond=0)
       
   257       return out