Package flumotion :: Package common :: Module eventcalendar
[hide private]

Source Code for Module flumotion.common.eventcalendar

  1  # -*- Mode: Python; test-case-name: 
  2  #                flumotion.test.test_component_base_scheduler -*- 
  3  # vi:si:et:sw=4:sts=4:ts=4 
  4  # 
  5  # Flumotion - a streaming media server 
  6  # Copyright (C) 2004,2005,2006,2007 Fluendo, S.L. (www.fluendo.com). 
  7  # All rights reserved. 
  8   
  9  # This file may be distributed and/or modified under the terms of 
 10  # the GNU General Public License version 2 as published by 
 11  # the Free Software Foundation. 
 12  # This file is distributed without any warranty; without even the implied 
 13  # warranty of merchantability or fitness for a particular purpose. 
 14  # See "LICENSE.GPL" in the source distribution for more information. 
 15   
 16  # Licensees having purchased or holding a valid Flumotion Advanced 
 17  # Streaming Server license and using this file together with a Flumotion 
 18  # Advanced Streaming Server may only use this file in accordance with the 
 19  # Flumotion Advanced Streaming Server Commercial License Agreement. 
 20  # See "LICENSE.Flumotion" in the source distribution for more information. 
 21   
 22  # Headers in this file shall remain intact. 
 23   
 24  import datetime 
 25  import time 
 26   
 27  from icalendar import Calendar 
 28  from dateutil import rrule, tz, parser 
 29   
 30  from flumotion.extern.log.log import Loggable 
 31   
 32   
 33   
 34   
35 -class LocalTimezone(datetime.tzinfo):
36 STDOFFSET = datetime.timedelta(seconds=-time.timezone) 37 if time.daylight: 38 DSTOFFSET = datetime.timedelta(seconds=-time.altzone) 39 else: 40 DSTOFFSET = STDOFFSET 41 DSTDIFF = DSTOFFSET - STDOFFSET 42 ZERO = datetime.timedelta(0) 43
44 - def utcoffset(self, dt):
45 if self._isdst(dt): 46 return self.DSTOFFSET 47 else: 48 return self.STDOFFSET
49
50 - def dst(self, dt):
51 if self._isdst(dt): 52 return self.DSTDIFF 53 else: 54 return self.ZERO
55
56 - def tzname(self, dt):
57 return time.tzname[self._isdst(dt)]
58
59 - def _isdst(self, dt):
60 tt = (dt.year, dt.month, dt.day, 61 dt.hour, dt.minute, dt.second, 62 dt.weekday(), 0, -1) 63 return time.localtime(time.mktime(tt)).tm_isdst > 0
64 LOCAL = LocalTimezone() 65 66
67 -class Point(Loggable):
68 """ 69 I represent a start or an end point linked to an event instance 70 of an event. 71 """ 72
73 - def __init__(self, eventInstance, which, timestamp):
74 """ 75 @param eventInstance: An instance of an event. 76 @type eventInstance: EventInstance 77 @param which: 'start' or 'end' 78 @type which: str 79 @param timestamp: Timestamp of this point. It will 80 be used when comparing Points. 81 @type timestamp: datetime 82 """ 83 self.which = which 84 self.timestamp = timestamp 85 self.eventInstance = eventInstance
86
87 - def __repr__(self):
88 return "Point '%s' at %r for %r" % ( 89 self.which, self.timestamp, self.event)
90
91 - def __cmp__(self, other):
92 # compare based on timestamp 93 return cmp(self.timestamp, other.timestamp)
94 95
96 -class EventInstance(Loggable):
97 """ 98 I represent one event instance of an event. 99 """ 100
101 - def __init__(self, event, start, end):
102 """ 103 @type event: L{Event} 104 @type start: L{datetime.datetime} 105 @type end: L{datetime.datetime} 106 """ 107 self.event = event 108 self.start = start 109 self.end = end 110 #this is for recurrence events so we keep track of the 111 #original start and end but also the current ones 112 self.currentStart = start 113 self.currentEnd = end
114
115 - def getPoints(self):
116 """ 117 Get a list of start and end points. 118 @rtype: L{Point} 119 """ 120 ret = [] 121 122 ret.append(Point(self, 'start', self.start)) 123 ret.append(Point(self, 'end', self.end)) 124 125 return ret
126 127
128 -def toDateTime(d):
129 """ 130 If d is date, convert it to datetime. 131 @type d: It can be anything, even None. However, it will convert only if 132 it is an event instance of date. 133 @return: If d was an event instance of date, it returns the equivalent 134 datetime.Otherwise, it returns d. 135 """ 136 if isinstance(d, datetime.date) and not isinstance(d, datetime.datetime): 137 return datetime.datetime(d.year, d.month, d.day, tzinfo=LOCAL) 138 return d
139 140
141 -class Event(Loggable):
142 """ 143 I represent a EVENT entry in a calendar for our purposes. 144 I can have recurrence. 145 I can be scheduled between a start time and an end time, 146 returning a list of start and end points. 147 I can have exception dates. 148 """ 149
150 - def __init__(self, uid, start, end, content, rrule=None, 151 recurrenceid=None, exdates=None, now=None):
152 """ 153 @param uid: identifier of the event. 154 @type uid: str 155 @param start: start time of the event. 156 @type start: L{datetime.datetime} 157 @param end: end time of the event. 158 @type end: L{datetime.datetime} 159 @param content: label to describe the content 160 @type content: str 161 @param rrule: a RRULE string 162 @type rrule: str 163 @param recurrenceid: a RECURRENCE-ID string. It is used on 164 recurrence events. 165 @type recurrenceid: str 166 @param exdates: list of exceptions. It is commonly used with 167 recurrence events. 168 @type exdates: list of L{datetime.datetime} or None 169 """ 170 if not now: 171 now = datetime.datetime.now(LOCAL) 172 self.end = self.__addTimeZone(end, LOCAL) 173 self.start = self.__addTimeZone(start, LOCAL) 174 self.content = content 175 self.uid = uid 176 self.rrule = rrule 177 self.recurrenceid = recurrenceid 178 if exdates: 179 self.exdates = [] 180 for exdate in exdates: 181 exdate = self.__addTimeZone(exdate, LOCAL) 182 self.exdates.append(exdate) 183 else: 184 self.exdates = None 185 self.now = now 186 #this is for recurrence events so we keep track of the 187 #original start and end but also the current ones 188 self.currentStart = start 189 self.currentEnd = end
190
191 - def __addTimeZone(self, dateTime, now):
192 if dateTime.tzinfo is not None: 193 return dateTime 194 return datetime.datetime(dateTime.year, dateTime.month, dateTime.day, 195 dateTime.hour, dateTime.minute, dateTime.second, 196 dateTime.microsecond, now)
197
198 - def __repr__(self):
199 return "<Event %r >" % (self.toTuple())
200
201 - def toTuple(self):
202 return (self.uid, self.start, self.end, self.content, self.rrule, 203 self.exdates)
204
205 - def __lt__(self, other):
206 return self.toTuple() < other.toTuple()
207
208 - def __gt__(self, other):
209 return self.toTuple() > other.toTuple()
210
211 - def __eq__(self, other):
212 return self.toTuple() == other.toTuple()
213
214 - def __ne__(self, other):
215 return not self.__eq__(other)
216 217
218 -class EventSet(Loggable):
219 """ 220 I represent a set of EVENT entries in a calendar sharing the same uid. 221 I can have recurrence. 222 I can be scheduled between a start time and an end time, 223 returning a list of start and end points in UTC. 224 I can have exception dates. 225 """ 226
227 - def __init__(self, uid):
228 """ 229 @param uid: the uid shared among the events on this set 230 @type uid: str 231 """ 232 self.uid = uid 233 self._events = []
234
235 - def __repr__(self):
236 return "<EventSet for uid %r >" % ( 237 self.uid)
238
239 - def addEvent(self, event):
240 """ 241 Add an event to the set. The event must have the same uid as the set. 242 """ 243 if self.uid != event.uid: 244 self.debug("my uid %s does not match Event uid %s", 245 self.uid, event.uid) 246 return 247 self._events.append(event)
248
249 - def removeEvent(self, event):
250 """ 251 Remove and event from the set. 252 """ 253 if self.uid != event.uid: 254 self.debug("my uid %s does not match Event uid %s", 255 self.uid, event.uid) 256 self._events.remove(event)
257
258 - def getPoints(self, start, end):
259 """ 260 Get an ordered list of start and end points between the given start 261 and end for this set of Events. 262 @param start: The start point. 263 @type start: datetime 264 @param end: The end point 265 @type end: datetime 266 """ 267 points = [] 268 269 eventInstances = self._getEventInstances(start, end) 270 for i in eventInstances: 271 points.extend(i.getPoints()) 272 points.sort() 273 274 return points
275
276 - def _getEventInstances(self, start, end):
277 # get all instances between the given dates 278 279 eventInstances = [] 280 281 recurring = None 282 283 # first, find the event with the rrule if there is any 284 for v in self._events: 285 if v.rrule: 286 if recurring: 287 self.debug("Cannot have two RRULE EVENTs with UID %s", 288 self.uid) 289 return [] 290 recurring = v 291 292 # now, find all instances between the two given times 293 if recurring: 294 eventInstances = self._getEventInstancesRecur(recurring, start, end) 295 296 # now, find all events with a RECURRENCE-ID pointing to an instance, 297 # and replace with the new instance 298 for v in self._events: 299 # skip the main event 300 if v.rrule: 301 continue 302 303 if v.recurrenceid: 304 recurDateTime = parser.parse(v.recurrenceid.ical()) 305 306 # Remove recurrent instance(s) that start at this recurrenceid 307 for i in eventInstances[:]: 308 if i.start == recurDateTime: 309 eventInstances.remove(i) 310 break 311 312 313 i = self._getEventInstanceSingle(v, start, end) 314 if i: 315 eventInstances.append(i) 316 317 # fix all incidences that lie partly outside of the range 318 # to be in the range 319 for i in eventInstances[:]: 320 if i.start < start: 321 i.start = start 322 if start >= i.end: 323 eventInstances.remove(i) 324 if i.end > end: 325 i.end = end 326 327 return eventInstances
328
329 - def _getEventInstanceSingle(self, event, start, end):
330 # is this event within the range asked for ? 331 #print self, start, self.end 332 if start > event.end: 333 return None 334 if end < event.start: 335 return None 336 337 startTime = max(event.start, start) 338 endTime = min(event.end, end) 339 340 return EventInstance(event, startTime, endTime)
341
342 - def _getEventInstancesRecur(self, event, start, end):
343 # get all event instances for this recurring event that fall between 344 # the given start and end 345 346 ret = [] 347 348 # don't calculate endPoint based on end recurrence rule, because 349 # if the next one after a start point is past UNTIL then the rrule 350 # returns None 351 delta = event.end - event.start 352 353 startRecurRule = rrule.rrulestr(event.rrule, dtstart=event.start) 354 355 for startTime in startRecurRule: 356 # ignore everything stopping before our start time 357 if startTime + delta < start: 358 continue 359 360 # stop looping if it's past the requested end time 361 if startTime >= end: 362 break 363 364 # skip if it's on our list of exceptions 365 if event.exdates: 366 if startTime in event.exdates: 367 continue 368 369 endTime = startTime + delta 370 371 i = EventInstance(event, startTime, endTime) 372 373 ret.append(i) 374 375 return ret
376
377 - def getEvents(self):
378 """ 379 Return the list of events 380 @rtype: L{Event} 381 """ 382 return self._events
383 384
385 -def parseCalendar(cal):
386 """ 387 Take a Calendar object and return a list of 388 EventSet objects. 389 390 @param cal: The calendar to "parse" 391 @type cal: icalendar.Calendar 392 393 @rtype: list of {EventSet} 394 """ 395 events = [] 396 397 def vDDDToDatetime(v): 398 """ 399 Convert a vDDDType to a datetime, respecting timezones 400 @param v: the time to convert 401 @type v: vDDDType 402 403 """ 404 dt = toDateTime(v.dt) 405 if dt.tzinfo is None: 406 tzinfo = tz.gettz(v.params['TZID']) 407 dt = datetime.datetime(dt.year, dt.month, dt.day, 408 dt.hour, dt.minute, dt.second, 409 dt.microsecond, tzinfo) 410 return dt
411 412 for event in cal.walk('vevent'): 413 # extract to function ? 414 start = vDDDToDatetime(event.get('dtstart', None)) 415 end = vDDDToDatetime(event.get('dtend', None)) 416 summary = event.decoded('SUMMARY', None) 417 uid = event['UID'] 418 recur = event.get('RRULE', None) 419 recurrenceid = event.get('RECURRENCE-ID', None) 420 exdates = event.get('EXDATE', []) 421 # When there is only one exdate, we don't get a list, but the 422 # single exdate. Bad API 423 if not isinstance(exdates, list): 424 exdates = [exdates, ] 425 426 # this is a list of icalendar.propvDDDTypes on which we can call 427 # .dt() or .ical() 428 exdates = [vDDDToDatetime(i) for i in exdates] 429 430 # FIXME: we're not handling EXDATE at all here 431 432 #if not start: 433 # raise AssertionError, "event %r does not have start" % event 434 #if not end: 435 # raise AssertionError, "event %r does not have end" % event 436 e = Event(uid, start, end, summary, 437 recur and recur.ical() or None, recurrenceid, exdates) 438 439 events.append(e) 440 eventSets = {} # uid -> VEventSet 441 for event in events: 442 if not event.uid in eventSets.keys(): 443 eventSets[event.uid] = EventSet(event.uid) 444 445 eventSets[event.uid].addEvent(event) 446 return eventSets.values() 447
448 -def parseCalendarFromFile(file):
449 """ 450 Parse a given file into EventSets. 451 452 @type file: file object 453 454 @rtype: list of {EventSet} 455 """ 456 data = file.read() 457 cal = Calendar.from_string(data) 458 file.close() 459 return parseCalendar(cal)
460