1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 import time
24 from datetime import datetime, timedelta, tzinfo
25
26 from twisted.internet import reactor
27
28 from flumotion.common import log, avltree
29 from flumotion.component.base import watcher
30
31
32
33
35 STDOFFSET = timedelta(seconds=-time.timezone)
36 if time.daylight:
37 DSTOFFSET = timedelta(seconds=-time.altzone)
38 else:
39 DSTOFFSET = STDOFFSET
40 DSTDIFF = DSTOFFSET - STDOFFSET
41 ZERO = timedelta(0)
42
48
54
57
59 tt = (dt.year, dt.month, dt.day,
60 dt.hour, dt.minute, dt.second,
61 dt.weekday(), 0, -1)
62 return time.localtime(time.mktime(tt)).tm_isdst > 0
63 LOCAL = LocalTimezone()
64
65
67 return datetime.now(tz)
68
69
70 -class Event(log.Loggable):
71 """
72 I am an event. I have a start and stop time and a "content" that can
73 be anything. I can recur.
74 """
75
76 - def __init__(self, start, end, content, recur=None, now=None):
77 self.debug('new event, content=%r, start=%r, end=%r', content,
78 start, end)
79
80 assert start < end
81
82 if recur:
83 from dateutil import rrule
84 if now is None:
85 now = datetime.now(LOCAL)
86 if end.tzinfo is None:
87 end = datetime(end.year, end.month, end.day, end.hour,
88 end.minute, end.second, end.microsecond, LOCAL)
89 if start.tzinfo is None:
90 start = datetime(start.year, start.month, start.day,
91 start.hour, start.minute, start.second,
92 start.microsecond, LOCAL)
93
94 if isinstance(recur, timedelta):
95 interval = recur.days*24*60*60 + recur.seconds
96 endRecurRule = rrule.rrule(rrule.SECONDLY,
97 interval=interval,
98 dtstart=end)
99 startRecurRule = rrule.rrule(rrule.SECONDLY,
100 interval=interval,
101 dtstart=start)
102 else:
103 endRecurRule = rrule.rrulestr(recur, dtstart=end)
104 startRecurRule = rrule.rrulestr(recur, dtstart=start)
105
106 if end < now:
107 end = endRecurRule.after(now)
108 start = startRecurRule.before(end)
109 self.debug("adjusting start and end times to %r, %r",
110 start, end)
111
112 if not start.tzinfo:
113 self.info('event starting at %r does not have timezone '
114 'info; using local time zone', start)
115 start = start.replace(tzinfo=LOCAL)
116 if not end.tzinfo:
117 self.info('event ending at %r does not have timezone '
118 'info; using local time zone', end)
119 end = end.replace(tzinfo=LOCAL)
120
121 self.start = start
122 self.end = end
123 self.content = content
124 self.recur = recur
125
127 if self.recur:
128 return Event(self.start, self.end, self.content, self.recur,
129 now)
130 else:
131 return None
132
134 return self.start, self.end, self.content, self.recur
135
137 return '<Event %r>' % (self.toTuple(),)
138
141
144
147
148
154
156 try:
157 avltree.AVLTree.insert(self, event)
158 return True
159 except ValueError:
160 self.warning('an identical event to %r already exists in '
161 'store', event)
162 return False
163
164
166 """
167 I keep track of upcoming events.
168
169 I can provide notifications when events stop and start, and maintain
170 a set of current events.
171 """
172
174 self.current = []
175 self._delayedCall = None
176 self._subscribeId = 0
177 self.subscribers = {}
178 self.replaceEvents([])
179
180 - def addEvent(self, start, end, content, recur=None, now=None):
181 """Add a new event to the scheduler.
182
183 @param start: wall-clock time of event start
184 @type start: datetime
185 @param end: wall-clock time of event end
186 @type end: datetime
187 @param content: content of this event
188 @type content: str
189 @param recur: recurrence rule, either as a string parseable by
190 datetime.rrule.rrulestr or as a datetime.timedelta
191 @type recur: None, str, or datetime.timedelta
192
193 @returns: an Event that can later be passed to removeEvent, if
194 so desired. The event will be removed or rescheduled
195 automatically when it stops.
196 """
197 if now is None:
198 now = datetime.now(LOCAL)
199 event = Event(start, end, content, recur, now)
200 if event.end < now:
201 self.warning('attempted to schedule event in the past: %r',
202 event)
203 else:
204 if self.events.insert(event):
205 if event.start < now:
206 self._eventStarted(event)
207 self._reschedule()
208 return event
209
211 """Remove an event from the scheduler.
212
213 @param event: an event, as returned from addEvent()
214 @type event: Event
215 """
216 currentEvent = event.reschedule() or event
217 self.events.delete(currentEvent)
218 if currentEvent in self.current:
219 self._eventStopped(currentEvent)
220 self._reschedule()
221
223 return [e.content for e in self.current]
224
240
270
271 - def subscribe(self, eventStarted, eventStopped):
272 """Subscribe to event happenings in the scheduler.
273
274 @param eventStarted: Function that will be called when an event
275 starts.
276 @type eventStarted: Event -> None
277 @param eventStopped: Function that will be called when an event
278 stops.
279 @type eventStopped: Event -> None
280
281 @returns: A subscription ID that can later be passed to
282 unsubscribe().
283 """
284 sid = self._subscribeId
285 self._subscribeId += 1
286 self.subscribers[sid] = (eventStarted, eventStopped)
287 return sid
288
290 """Unsubscribe from event happenings in the scheduler.
291
292 @param id: Subscription ID received from subscribe()
293 """
294 del self.subscribers[id]
295
297 self.current.append(event)
298 for started, _ in self.subscribers.values():
299 started(event)
300
302 self.current.remove(event)
303 for _, stopped in self.subscribers.values():
304 stopped(event)
305
307 def _getNextStart():
308 for event in self.events:
309 if event not in self.current:
310 return event
311 return None
312
313 def _getNextStop():
314 t = None
315 e = None
316 for event in self.current:
317 if not t or event.end < t:
318 t = event.end
319 e = event
320 return e
321
322 def doStart(e):
323 self._eventStarted(e)
324 self._reschedule()
325
326 def doStop(e):
327 self._eventStopped(e)
328 self.events.delete(e)
329 new = e.reschedule()
330 if new:
331 self.events.insert(new)
332 self._reschedule()
333
334 if self._delayedCall:
335 if self._delayedCall.active():
336 self._delayedCall.cancel()
337 self._delayedCall = None
338
339 start = _getNextStart()
340 stop = _getNextStop()
341 now = datetime.now(LOCAL)
342
343 def toSeconds(td):
344 return max(td.days*24*3600 + td.seconds + td.microseconds/1e6, 0)
345
346 if start and (not stop or start.start < stop.end):
347 dc = reactor.callLater(toSeconds(start.start - now),
348 doStart, start)
349 elif stop:
350 dc = reactor.callLater(toSeconds(stop.end - now),
351 doStop, stop)
352 else:
353 dc = None
354
355 self._delayedCall = dc
356
357
359 """
360 I am a scheduler that takes its data from an ical file.
361 """
362
372 parseCalendarFromFile(fileObj)
373
374 if hasattr(fileObj, 'name'):
375 def fileChanged(f):
376 parseCalendarFromFile(open(f,'r'))
377 self.watcher = watcher.FilesWatcher([fileObj.name])
378 self.watcher.subscribe(fileChanged=fileChanged)
379 self.watcher.start()
380
382 """
383 Take a Calendar object and return a list of
384 Event objects.
385
386 @param cal: The calendar to "parse"
387 @type cal: icalendar.Calendar
388 @rtype List of {flumotion.component.base.scheduler.Event}
389 """
390 events = []
391 for event in cal.walk('vevent'):
392 try:
393 start = event.decoded('dtstart', None)
394 end = event.decoded('dtend', None)
395 summary = event.decoded('summary', None)
396 recur = event.get('rrule', None)
397 if start and end:
398 self.debug("start %r tzname %s end %r recur %r", start,
399 start.tzname(), end, recur)
400 if recur:
401 e = Event(start, end, summary, recur.ical())
402 else:
403 e = Event(start, end, summary)
404 events.append(e)
405 else:
406 self.warning('ical has event without start or end: '
407 '%r', event)
408 except Exception:
409 self.warning("could not parse ical event %r", event)
410 return events
411