diff -r 88c186556a80 -r f5fd65cc3bf3 thirdparty/google_appengine/google/appengine/cron/groctimespecification.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/thirdparty/google_appengine/google/appengine/cron/groctimespecification.py Tue Jan 20 13:19:45 2009 +0000 @@ -0,0 +1,257 @@ +#!/usr/bin/env python +# +# Copyright 2007 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +"""Implementation of scheduling for Groc format schedules. + +A Groc schedule looks like '1st,2nd monday 9:00', or 'every 20 mins'. This +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. +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 + +import groc + +HOURS = 'hours' +MINUTES = 'minutes' + + +def GrocTimeSpecification(schedule): + """Factory function. + + Turns a schedule specification into a TimeSpecification. + + Arguments: + schedule: the schedule specification, as a string + + Returns: + a TimeSpecification instance + """ + + parser = groc.CreateParser(schedule) + parser.timespec() + + if parser.interval_mins: + return IntervalTimeSpecification(parser.interval_mins, parser.period_string) + else: + return SpecificTimeSpecification(parser.ordinal_set, parser.weekday_set, + parser.month_set, None, parser.time_string) + + +class TimeSpecification(object): + """Base class for time specifications.""" + + def GetMatches(self, start, n): + """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 + n: the number of matching times to return + + Returns: + a list of n datetime objects + """ + out = [] + for _ in range(n): + start = self.GetMatch(start) + out.append(start) + return out + + def GetMatch(self, start): + """Returns the next match after time start. + + Must be implemented in subclasses. + + Arguments: + start: a datetime to start with. Matches will start from this time + + Returns: + a datetime object + """ + raise NotImplementedError + + +class IntervalTimeSpecification(TimeSpecification): + """A time specification for a given interval. + + An Interval type spec runs at the given fixed interval. They have two + attributes: + period - the type of interval, either "hours" or "minutes" + interval - the number of units of type period. + """ + + def __init__(self, interval, period): + super(IntervalTimeSpecification, self).__init__(self) + self.interval = interval + self.period = period + + def GetMatch(self, t): + """Returns the next match after time 't'. + + Arguments: + t: a datetime to start from. Matches will start from after this time + + Returns: + a datetime object + """ + if self.period == HOURS: + return t + datetime.timedelta(hours=self.interval) + else: + return t + datetime.timedelta(minutes=self.interval) + + +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: + 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. + + The specific time interval 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". + """ + + def __init__(self, ordinals=None, weekdays=None, months=None, monthdays=None, + timestr='00:00'): + super(SpecificTimeSpecification, self).__init__(self) + if weekdays and monthdays: + raise ValueError("can't supply both monthdays and weekdays") + if ordinals is None: + self.ordinals = set(range(1, 6)) + else: + self.ordinals = ordinals + + if weekdays is None: + self.weekdays = set(range(7)) + else: + self.weekdays = weekdays + + if months is None: + self.months = set(range(1, 13)) + else: + self.months = months + + if monthdays is None: + self.monthdays = set() + else: + self.monthdays = monthdays + hourstr, minutestr = timestr.split(':') + self.time = datetime.time(int(hourstr), int(minutestr)) + + def _MatchingDays(self, year, month): + """Returns matching days for the given year and month. + + For the given year and month, return the days that match this instance's + day specification, based on the ordinals and weekdays. + + Arguments: + year: the year as an integer + month: the month as an integer, in range 1-12 + + Returns: + a list of matching days, as ints in range 1-31 + """ + out_days = [] + start_day, last_day = calendar.monthrange(year, month) + start_day = (start_day + 1) % 7 + for ordinal in self.ordinals: + for weekday in self.weekdays: + day = ((weekday - start_day) % 7) + 1 + day += 7 * (ordinal - 1) + if day <= last_day: + out_days.append(day) + return sorted(out_days) + + def _NextMonthGenerator(self, start, matches): + """Creates a generator that produces results from the set 'matches'. + + Matches must be >= 'start'. If none match, the wrap counter is incremented, + and the result set is reset to the full set. Yields a 2-tuple of (match, + wrapcount). + + Arguments: + start: first set of matches will be >= this value (an int) + matches: the set of potential matches (a sequence of ints) + + Yields: + a two-tuple of (match, wrap counter). match is an int in range (1-12), + wrapcount is a int indicating how many times we've wrapped around. + """ + potential = matches = sorted(matches) + after = start - 1 + wrapcount = 0 + while True: + potential = [x for x in potential if x > after] + if not potential: + wrapcount += 1 + potential = matches + after = potential[0] + yield (after, wrapcount) + + def GetMatch(self, start): + """Returns the next time that matches the schedule after time start. + + Arguments: + start: a datetime to start with. Matches will start after this time + + Returns: + a datetime object + """ + start_time = start + if self.months: + months = self._NextMonthGenerator(start.month, self.months) + while True: + month, yearwraps = months.next() + candidate = 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]) + else: + day_matches = self._MatchingDays(candidate.year, month) + + if ((candidate.year, candidate.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