|
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 |