Load /Users/solydzajs/Downloads/google_appengine into
[Melange.git] / thirdparty / google_appengine / google / appengine / cron / groctimespecification.py
blob462527043e422fb12d00921a9d788087d85621c8
1 #!/usr/bin/env python
3 # Copyright 2007 Google Inc.
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
19 """Implementation of scheduling for Groc format schedules.
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.
25 A parsed schedule is one of two types - an Interval or a Specific Time.
26 See the class docstrings for more.
28 Extensions to be considered:
30 allowing a comma separated list of times to run
31 allowing the user to specify particular days of the month to run
32 """
35 import calendar
36 import datetime
38 try:
39 import pytz
40 except ImportError:
41 pytz = None
43 import groc
45 HOURS = 'hours'
46 MINUTES = 'minutes'
48 try:
49 from pytz import NonExistentTimeError
50 except ImportError:
51 class NonExistentTimeError(Exception):
52 pass
55 def GrocTimeSpecification(schedule):
56 """Factory function.
58 Turns a schedule specification into a TimeSpecification.
60 Arguments:
61 schedule: the schedule specification, as a string
63 Returns:
64 a TimeSpecification instance
65 """
66 parser = groc.CreateParser(schedule)
67 parser.timespec()
69 if parser.interval_mins:
70 return IntervalTimeSpecification(parser.interval_mins,
71 parser.period_string)
72 else:
73 return SpecificTimeSpecification(parser.ordinal_set, parser.weekday_set,
74 parser.month_set,
75 None,
76 parser.time_string)
79 class TimeSpecification(object):
80 """Base class for time specifications."""
82 def GetMatches(self, start, n):
83 """Returns the next n times that match the schedule, starting at time start.
85 Arguments:
86 start: a datetime to start from. Matches will start from after this time.
87 n: the number of matching times to return
89 Returns:
90 a list of n datetime objects
91 """
92 out = []
93 for _ in range(n):
94 start = self.GetMatch(start)
95 out.append(start)
96 return out
98 def GetMatch(self, start):
99 """Returns the next match after time start.
101 Must be implemented in subclasses.
103 Arguments:
104 start: a datetime to start with. Matches will start from this time.
106 Returns:
107 a datetime object
109 raise NotImplementedError
112 class IntervalTimeSpecification(TimeSpecification):
113 """A time specification for a given interval.
115 An Interval type spec runs at the given fixed interval. It has two
116 attributes:
117 period - the type of interval, either "hours" or "minutes"
118 interval - the number of units of type period.
121 def __init__(self, interval, period):
122 super(IntervalTimeSpecification, self).__init__(self)
123 self.interval = interval
124 self.period = period
126 def GetMatch(self, t):
127 """Returns the next match after time 't'.
129 Arguments:
130 t: a datetime to start from. Matches will start from after this time.
132 Returns:
133 a datetime object
135 if self.period == HOURS:
136 return t + datetime.timedelta(hours=self.interval)
137 else:
138 return t + datetime.timedelta(minutes=self.interval)
141 class SpecificTimeSpecification(TimeSpecification):
142 """Specific time specification.
144 A Specific interval is more complex, but defines a certain time to run and
145 the days that it should run. It has the following attributes:
146 time - the time of day to run, as "HH:MM"
147 ordinals - first, second, third &c, as a set of integers in 1..5
148 months - the months that this should run, as a set of integers in 1..12
149 weekdays - the days of the week that this should run, as a set of integers,
150 0=Sunday, 6=Saturday
151 timezone - the optional timezone as a string for this specification.
152 Defaults to UTC - valid entries are things like Australia/Victoria
153 or PST8PDT.
155 A specific time schedule can be quite complex. A schedule could look like
156 this:
157 "1st,third sat,sun of jan,feb,mar 09:15"
159 In this case, ordinals would be {1,3}, weekdays {0,6}, months {1,2,3} and
160 time would be "09:15".
163 timezone = None
165 def __init__(self, ordinals=None, weekdays=None, months=None, monthdays=None,
166 timestr='00:00', timezone=None):
167 super(SpecificTimeSpecification, self).__init__(self)
168 if weekdays is not None and monthdays is not None:
169 raise ValueError("can't supply both monthdays and weekdays")
170 if ordinals is None:
171 self.ordinals = set(range(1, 6))
172 else:
173 self.ordinals = set(ordinals)
175 if weekdays is None:
176 self.weekdays = set(range(7))
177 else:
178 self.weekdays = set(weekdays)
180 if months is None:
181 self.months = set(range(1, 13))
182 else:
183 self.months = set(months)
185 if monthdays is None:
186 self.monthdays = set()
187 else:
188 self.monthdays = set(monthdays)
189 hourstr, minutestr = timestr.split(':')
190 self.time = datetime.time(int(hourstr), int(minutestr))
191 if timezone:
192 if pytz is None:
193 raise ValueError("need pytz in order to specify a timezone")
194 self.timezone = pytz.timezone(timezone)
196 def _MatchingDays(self, year, month):
197 """Returns matching days for the given year and month.
199 For the given year and month, return the days that match this instance's
200 day specification, based on the ordinals and weekdays.
202 Arguments:
203 year: the year as an integer
204 month: the month as an integer, in range 1-12
206 Returns:
207 a list of matching days, as ints in range 1-31
209 out_days = []
210 start_day, last_day = calendar.monthrange(year, month)
211 start_day = (start_day + 1) % 7
212 for ordinal in self.ordinals:
213 for weekday in self.weekdays:
214 day = ((weekday - start_day) % 7) + 1
215 day += 7 * (ordinal - 1)
216 if day <= last_day:
217 out_days.append(day)
218 return sorted(out_days)
220 def _NextMonthGenerator(self, start, matches):
221 """Creates a generator that produces results from the set 'matches'.
223 Matches must be >= 'start'. If none match, the wrap counter is incremented,
224 and the result set is reset to the full set. Yields a 2-tuple of (match,
225 wrapcount).
227 Arguments:
228 start: first set of matches will be >= this value (an int)
229 matches: the set of potential matches (a sequence of ints)
231 Yields:
232 a two-tuple of (match, wrap counter). match is an int in range (1-12),
233 wrapcount is a int indicating how many times we've wrapped around.
235 potential = matches = sorted(matches)
236 after = start - 1
237 wrapcount = 0
238 while True:
239 potential = [x for x in potential if x > after]
240 if not potential:
241 wrapcount += 1
242 potential = matches
243 after = potential[0]
244 yield (after, wrapcount)
246 def GetMatch(self, start):
247 """Returns the next time that matches the schedule after time start.
249 Arguments:
250 start: a UTC datetime to start from. Matches will start after this time
252 Returns:
253 a datetime object
255 start_time = start
256 if self.timezone and pytz is not None:
257 if not start_time.tzinfo:
258 start_time = pytz.utc.localize(start_time)
259 start_time = start_time.astimezone(self.timezone)
260 start_time = start_time.replace(tzinfo=None)
261 if self.months:
262 months = self._NextMonthGenerator(start_time.month, self.months)
263 while True:
264 month, yearwraps = months.next()
265 candidate_month = start_time.replace(day=1, month=month,
266 year=start_time.year + yearwraps)
268 if self.monthdays:
269 _, last_day = calendar.monthrange(candidate_month.year,
270 candidate_month.month)
271 day_matches = sorted(x for x in self.monthdays if x <= last_day)
272 else:
273 day_matches = self._MatchingDays(candidate_month.year, month)
275 if ((candidate_month.year, candidate_month.month)
276 == (start_time.year, start_time.month)):
277 day_matches = [x for x in day_matches if x >= start_time.day]
278 while (day_matches and day_matches[0] == start_time.day
279 and start_time.time() >= self.time):
280 day_matches.pop(0)
281 while day_matches:
282 out = candidate_month.replace(day=day_matches[0], hour=self.time.hour,
285 minute=self.time.minute, second=0,
286 microsecond=0)
287 if self.timezone and pytz is not None:
288 try:
289 out = self.timezone.localize(out)
290 except (NonExistentTimeError, IndexError):
291 for _ in range(24):
292 out = out.replace(minute=1) + datetime.timedelta(minutes=60)
293 try:
294 out = self.timezone.localize(out)
295 except (NonExistentTimeError, IndexError):
296 continue
297 break
298 out = out.astimezone(pytz.utc)
299 return out