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

Source Code for Module flumotion.common.eventcalendar

  1  # -*- Mode:Python; test-case-name:flumotion.test.test_common_eventcalendar -*- 
  2  # vi:si:et:sw=4:sts=4:ts=4 
  3  # 
  4  # Flumotion - a streaming media server 
  5  # Copyright (C) 2004,2005,2006,2007,2008 Fluendo, S.L. (www.fluendo.com). 
  6  # All rights reserved. 
  7   
  8  # This file may be distributed and/or modified under the terms of 
  9  # the GNU General Public License version 2 as published by 
 10  # the Free Software Foundation. 
 11  # This file is distributed without any warranty; without even the implied 
 12  # warranty of merchantability or fitness for a particular purpose. 
 13  # See "LICENSE.GPL" in the source distribution for more information. 
 14   
 15  # Licensees having purchased or holding a valid Flumotion Advanced 
 16  # Streaming Server license and using this file together with a Flumotion 
 17  # Advanced Streaming Server may only use this file in accordance with the 
 18  # Flumotion Advanced Streaming Server Commercial License Agreement. 
 19  # See "LICENSE.Flumotion" in the source distribution for more information. 
 20   
 21  # Headers in this file shall remain intact. 
 22   
 23  import datetime 
 24  import time 
 25   
 26  HAS_ICALENDAR = False 
 27  try: 
 28      import icalendar 
 29      HAS_ICALENDAR = True 
 30  except ImportError: 
 31      pass 
 32   
 33  # for documentation on dateutil, see http://labix.org/python-dateutil 
 34  HAS_DATEUTIL = False 
 35  try: 
 36      from dateutil import rrule, tz 
 37      HAS_DATEUTIL = True 
 38  except ImportError: 
 39      pass 
 40   
 41  from flumotion.extern.log import log 
 42   
 43  """ 
 44  Implementation of a calendar that can inform about events beginning and 
 45  ending, as well as active event instances at a given time. 
 46   
 47  This uses iCalendar as defined in 
 48  http://www.ietf.org/rfc/rfc2445.txt 
 49   
 50  The users of this module should check if it has both HAS_ICALENDAR 
 51  and HAS_DATEUTIL properties and if any of them is False, they should 
 52  withhold from further using the module. 
 53  """ 
 54   
 55   
56 -def _toDateTime(d):
57 """ 58 If d is a L{datetime.date}, convert it to L{datetime.datetime}. 59 60 @type d: anything 61 62 @rtype: L{datetime.datetime} or anything 63 @returns: The equivalent datetime.datetime if d is a datetime.date; 64 d if not 65 """ 66 if isinstance(d, datetime.date) and not isinstance(d, datetime.datetime): 67 return datetime.datetime(d.year, d.month, d.day, tzinfo=UTC) 68 return d
69 70
71 -def _first_sunday_on_or_after(dt):
72 """ 73 Looks for the closest last sunday in the month 74 75 @param dt: Reference date 76 @type dt: L{datetime.datetime} 77 78 @rtype: L{datetime.datetime} or None 79 @returns: Last sunday of the month 80 """ 81 82 days_to_go = 6 - dt.weekday() 83 if days_to_go: 84 dt += datetime.timedelta(days_to_go) 85 return dt
86 87
88 -class DSTTimezone(datetime.tzinfo):
89 """ A tzinfo class representing a DST timezone """ 90 91 ZERO = datetime.timedelta(0) 92
93 - def __init__(self, tzid, stdname, dstname, stdoffset, dstoffset, 94 stdoffsetfrom, dstoffsetfrom, dststart, dstend):
95 ''' 96 @param tzid: Timezone unique ID 97 @type tzid: str 98 @param stdname: Name of the Standard observance 99 @type stdname: str 100 @param dstname: Name of the DST observance 101 @type dstname: str 102 @param stdoffset: UTC offset for the standard observance 103 @type stdoffset: L{datetime.timedelta} 104 @param dstoffset: UTC offset for the DST observance 105 @type dstoffset: L{datetime.timedelta} 106 @param stdoffsetfrom: UTC offset which is in use when the onset of 107 Standard observance begins 108 @type stdoffsetfrom: l{datetime.timedelta} 109 @param dstoffsetfrom: UTC offset which is in use when the onset of 110 DST observance begins 111 @type stdoffsetfrom: L{datetime.timedelta} 112 @param dststart: Start of the DST observance 113 @type dststart: L{datetime.datetime} 114 @param dstend: End of the DST observance 115 @type dstend: L{datetime.datetime} 116 ''' 117 118 self._tzid = str(tzid) 119 self._stdname = str(stdname) 120 self._dstname = str(dstname) 121 self._stdoffset = stdoffset 122 self._dstoffset = dstoffset 123 self._stdoffsetfrom = stdoffsetfrom 124 self._dstoffsetfrom = dstoffsetfrom 125 self._dststart = dststart 126 self._dstend = dstend
127
128 - def __str__(self):
129 return self._tzid
130
131 - def tzname(self, dt):
132 return self._isdst(dt) and self._dstname or self._stdname
133
134 - def utcoffset(self, dt):
135 return self._isdst(dt) and self._dstoffset or self._stdoffset
136
137 - def fromutc(self, dt):
138 dt = dt.replace(tzinfo=None) 139 return self._isdst(dt) and \ 140 dt + self._dstoffsetfrom or dt + self._stdoffsetfrom
141
142 - def dst(self, dt):
143 # The substraction is done converting the datetime values to UTC and 144 # adding the utcoffset of each one (see 9.1.4 datetime Objects) 145 # which is done only if both datetime are 'aware' and have different 146 # tzinfo member. 147 if dt is None or dt.tzinfo is None: 148 return self.ZERO 149 assert dt.tzinfo is self 150 return self._isdst(dt) and self._dstoffset - self._stdoffset or \ 151 self.ZERO
152
153 - def copy(self):
154 return DSTTimezone(self._tzid, self._stdname, self._dstname, 155 self._stdoffset, self._dstoffset, self._stdoffsetfrom, 156 self._dstoffsetfrom, self._dststart, self._dstend)
157
158 - def _isdst(self, dt):
159 if self._dstoffset is None or dt.year < self._dststart.year: 160 return False 161 start = _first_sunday_on_or_after(self._dststart.replace(year=dt.year)) 162 end = _first_sunday_on_or_after(self._dstend.replace(year=dt.year)) 163 return start <= dt.replace(tzinfo=None) < end
164 165
166 -class FixedOffsetTimezone(datetime.tzinfo):
167 """Fixed offset in hours from UTC.""" 168
169 - def __init__(self, offset, name):
170 self.__offset = offset 171 self.__name = name
172
173 - def utcoffset(self, dt):
174 return self.__offset
175
176 - def tzname(self, dt):
177 return self.__name
178
179 - def dst(self, dt):
180 return datetime.deltatime(0)
181
182 - def copy(self):
183 return FixedOffsetTimezone(self.__offset, self.__name)
184 185
186 -class LocalTimezone(datetime.tzinfo):
187 """A tzinfo class representing the system's idea of the local timezone""" 188 STDOFFSET = datetime.timedelta(seconds=-time.timezone) 189 if time.daylight: 190 DSTOFFSET = datetime.timedelta(seconds=-time.altzone) 191 else: 192 DSTOFFSET = STDOFFSET 193 DSTDIFF = DSTOFFSET - STDOFFSET 194 ZERO = datetime.timedelta(0) 195
196 - def utcoffset(self, dt):
197 if self._isdst(dt): 198 return self.DSTOFFSET 199 else: 200 return self.STDOFFSET
201
202 - def dst(self, dt):
203 if self._isdst(dt): 204 return self.DSTDIFF 205 else: 206 return self.ZERO
207
208 - def tzname(self, dt):
209 return time.tzname[self._isdst(dt)]
210
211 - def _isdst(self, dt):
212 tt = (dt.year, dt.month, dt.day, 213 dt.hour, dt.minute, dt.second, 214 dt.weekday(), 0, -1) 215 return time.localtime(time.mktime(tt)).tm_isdst > 0
216 LOCAL = LocalTimezone() 217 218 # A UTC class; see datetime.tzinfo documentation 219 220
221 -class UTCTimezone(datetime.tzinfo):
222 """A tzinfo class representing UTC""" 223 ZERO = datetime.timedelta(0) 224
225 - def utcoffset(self, dt):
226 return self.ZERO
227
228 - def tzname(self, dt):
229 return "UTC"
230
231 - def dst(self, dt):
232 return self.ZERO
233 UTC = UTCTimezone() 234 235
236 -class Point(log.Loggable):
237 """ 238 I represent a start or an end point linked to an event instance 239 of an event. 240 241 @type eventInstance: L{EventInstance} 242 @type which: str 243 @type dt: L{datetime.datetime} 244 """ 245
246 - def __init__(self, eventInstance, which, dt):
247 """ 248 @param eventInstance: An instance of an event. 249 @type eventInstance: L{EventInstance} 250 @param which: 'start' or 'end' 251 @type which: str 252 @param dt: Timestamp of this point. It will 253 be used when comparing Points. 254 @type dt: L{datetime.datetime} 255 """ 256 self.which = which 257 self.dt = dt 258 self.eventInstance = eventInstance
259
260 - def __repr__(self):
261 return "Point '%s' at %r for %r" % ( 262 self.which, self.dt, self.eventInstance)
263
264 - def __cmp__(self, other):
265 # compare based on dt, then end before start 266 # relies on alphabetic order of end before start 267 return cmp(self.dt, other.dt) \ 268 or cmp(self.which, other.which)
269 270
271 -class EventInstance(log.Loggable):
272 """ 273 I represent one event instance of an event. 274 275 @type event: L{Event} 276 @type start: L{datetime.datetime} 277 @type end: L{datetime.datetime} 278 """ 279
280 - def __init__(self, event, start, end):
281 """ 282 @type event: L{Event} 283 @type start: L{datetime.datetime} 284 @type end: L{datetime.datetime} 285 """ 286 self.event = event 287 self.start = start 288 self.end = end
289
290 - def getPoints(self):
291 """ 292 Get a list of start and end points. 293 294 @rtype: list of L{Point} 295 """ 296 ret = [] 297 298 ret.append(Point(self, 'start', self.start)) 299 ret.append(Point(self, 'end', self.end)) 300 301 return ret
302
303 - def __eq__(self, other):
304 return self.start == other.start and self.end == other.end and \ 305 self.event == other.event
306
307 - def __ne__(self, other):
308 return not self.__eq__(other)
309 310
311 -class Event(log.Loggable):
312 """ 313 I represent a VEVENT entry in a calendar for our purposes. 314 I can have recurrence. 315 I can be scheduled between a start time and an end time, 316 returning a list of start and end points. 317 I can have exception dates. 318 """ 319
320 - def __init__(self, uid, start, end, content, rrules=None, 321 recurrenceid=None, exdates=None):
322 """ 323 @param uid: identifier of the event 324 @type uid: str 325 @param start: start time of the event 326 @type start: L{datetime.datetime} 327 @param end: end time of the event 328 @type end: L{datetime.datetime} 329 @param content: label to describe the content 330 @type content: unicode 331 @param rrules: a list of RRULE string 332 @type rrules: list of str 333 @param recurrenceid: a RECURRENCE-ID, used with 334 recurrence events 335 @type recurrenceid: L{datetime.datetime} 336 @param exdates: list of exceptions to the recurrence rule 337 @type exdates: list of L{datetime.datetime} or None 338 """ 339 340 self.start = self._ensureTimeZone(start) 341 self.end = self._ensureTimeZone(end) 342 self.content = content 343 self.uid = uid 344 self.rrules = rrules 345 if rrules and len(rrules) > 1: 346 raise NotImplementedError( 347 "Events with multiple RRULE are not yet supported") 348 self.recurrenceid = recurrenceid 349 if exdates: 350 self.exdates = [] 351 for exdate in exdates: 352 exdate = self._ensureTimeZone(exdate) 353 self.exdates.append(exdate) 354 else: 355 self.exdates = None
356
357 - def _ensureTimeZone(self, dateTime, tz=UTC):
358 # add timezone information if it is not specified for some reason 359 if dateTime.tzinfo: 360 return dateTime 361 362 return datetime.datetime(dateTime.year, dateTime.month, dateTime.day, 363 dateTime.hour, dateTime.minute, dateTime.second, 364 dateTime.microsecond, tz)
365
366 - def __repr__(self):
367 return "<Event %r >" % (self.toTuple(), )
368
369 - def toTuple(self):
370 return (self.uid, self.start, self.end, self.content, self.rrules, 371 self.exdates)
372 373 # FIXME: these are only here so the rrdmon stuff can use Event instances 374 # in an avltree 375
376 - def __lt__(self, other):
377 return self.toTuple() < other.toTuple()
378
379 - def __gt__(self, other):
380 return self.toTuple() > other.toTuple()
381 382 # FIXME: but these should be kept, so that events with different id 383 # but same properties are the same 384
385 - def __eq__(self, other):
386 return self.toTuple() == other.toTuple()
387
388 - def __ne__(self, other):
389 return not self.__eq__(other)
390 391
392 -class EventSet(log.Loggable):
393 """ 394 I represent a set of VEVENT entries in a calendar sharing the same uid. 395 I can have recurrence. 396 I can be scheduled between a start time and an end time, 397 returning a list of start and end points in UTC. 398 I can have exception dates. 399 """ 400
401 - def __init__(self, uid):
402 """ 403 @param uid: the uid shared among the events on this set 404 @type uid: str 405 """ 406 self.uid = uid 407 self._events = []
408
409 - def __repr__(self):
410 return "<EventSet for uid %r >" % ( 411 self.uid)
412
413 - def addEvent(self, event):
414 """ 415 Add an event to the set. The event must have the same uid as the set. 416 417 @param event: the event to add. 418 @type event: L{Event} 419 """ 420 assert self.uid == event.uid, \ 421 "my uid %s does not match Event uid %s" % (self.uid, event.uid) 422 assert event not in self._events, "event %r already in set %r" % ( 423 event, self._events) 424 425 self._events.append(event)
426
427 - def removeEvent(self, event):
428 """ 429 Remove an event from the set. 430 431 @param event: the event to add. 432 @type event: L{Event} 433 """ 434 assert self.uid == event.uid, \ 435 "my uid %s does not match Event uid %s" % (self.uid, event.uid) 436 self._events.remove(event)
437
438 - def getPoints(self, start=None, delta=None, clip=True):
439 """ 440 Get an ordered list of start and end points from the given start 441 point, with the given delta, in this set of Events. 442 443 start defaults to now. 444 delta defaults to 0, effectively returning all points at this time. 445 the returned list includes the extremes (start and start + delta) 446 447 @param start: the start time 448 @type start: L{datetime.datetime} 449 @param delta: the delta 450 @type delta: L{datetime.timedelta} 451 @param clip: whether to clip all event instances to the given 452 start and end 453 """ 454 if start is None: 455 start = datetime.datetime.now(UTC) 456 457 if delta is None: 458 delta = datetime.timedelta(seconds=0) 459 460 points = [] 461 462 eventInstances = self._getEventInstances(start, start + delta, clip) 463 for i in eventInstances: 464 for p in i.getPoints(): 465 if p.dt >= start and p.dt <= start + delta: 466 points.append(p) 467 points.sort() 468 469 return points
470
471 - def _getRecurringEvent(self):
472 recurring = None 473 474 # get the event in the event set that is recurring, if any 475 for v in self._events: 476 if v.rrules: 477 assert not recurring, \ 478 "Cannot have two RRULE VEVENTs with UID %s" % self.uid 479 recurring = v 480 else: 481 if len(self._events) > 1: 482 assert v.recurrenceid, \ 483 "With multiple VEVENTs with UID %s, " \ 484 "each VEVENT should either have a " \ 485 "reccurrence rule or have a recurrence id" % self.uid 486 487 return recurring
488
489 - def _getEventInstances(self, start, end, clip):
490 # get all instances whose start and/or end fall between the given 491 # datetimes 492 # clips the event to the given start and end if asked for 493 # FIXME: decide if clip is inclusive or exclusive; maybe compare 494 # to dateutil's solution 495 496 eventInstances = [] 497 498 recurring = self._getRecurringEvent() 499 500 # find all instances between the two given times 501 if recurring: 502 eventInstances = self._getEventInstancesRecur( 503 recurring, start, end) 504 505 # an event that has a recurrence id overrides the instance of the 506 # recurrence with a start time matching the recurrence id, so 507 # throw it out 508 for event in self._events: 509 # skip the main event 510 if event is recurring: 511 continue 512 513 if event.recurrenceid: 514 # Remove recurrent instance(s) that start at this recurrenceid 515 for i in eventInstances[:]: 516 if i.start == event.recurrenceid: 517 eventInstances.remove(i) 518 break 519 520 i = self._getEventInstanceSingle(event, start, end) 521 if i: 522 eventInstances.append(i) 523 524 if clip: 525 # fix all incidences that lie partly outside of the range 526 # to be in the range 527 for i in eventInstances[:]: 528 if i.start < start: 529 i.start = start 530 if start >= i.end: 531 eventInstances.remove(i) 532 if i.end > end: 533 i.end = end 534 535 return eventInstances
536
537 - def _getEventInstanceSingle(self, event, start, end):
538 # is this event within the range asked for ? 539 if start > event.end: 540 return None 541 if end < event.start: 542 return None 543 544 return EventInstance(event, event.start, event.end)
545
546 - def _getEventInstancesRecur(self, event, start, end):
547 # get all event instances for this recurring event that start before 548 # the given end time and end after the given start time. 549 # The UNTIL value applies to the start of a recurring event, 550 # not to the end. So if you would calculate based on the end for the 551 # recurrence rule, and there is a recurring instance that starts before 552 # UNTIL but ends after UNTIL, it would not be taken into account. 553 554 ret = [] 555 556 # don't calculate endPoint based on end recurrence rule, because 557 # if the next one after a start point is past UNTIL then the rrule 558 # returns None 559 delta = event.end - event.start 560 561 # FIXME: support multiple RRULE; see 4.8.5.4 Recurrence Rule 562 r = None 563 if event.rrules: 564 r = event.rrules[0] 565 startRecurRule = rrule.rrulestr(r, dtstart=event.start) 566 567 for startTime in startRecurRule: 568 # ignore everything stopping before our start time 569 if startTime + delta < start: 570 continue 571 572 # stop looping if it's past the requested end time 573 if startTime >= end: 574 break 575 576 # skip if it's on our list of exceptions 577 if event.exdates: 578 if startTime in event.exdates: 579 self.debug("startTime %r is listed as EXDATE, skipping", 580 startTime) 581 continue 582 583 endTime = startTime + delta 584 585 i = EventInstance(event, startTime, endTime) 586 587 ret.append(i) 588 589 return ret
590
591 - def getActiveEventInstances(self, dt=None):
592 """ 593 Get all event instances active at the given dt. 594 595 @type dt: L{datetime.datetime} 596 597 @rtype: list of L{EventInstance} 598 """ 599 if not dt: 600 dt = datetime.datetime.now(tz=UTC) 601 602 result = [] 603 604 # handle recurrence events first 605 recurring = self._getRecurringEvent() 606 if recurring: 607 # FIXME: support multiple RRULE; see 4.8.5.4 Recurrence Rule 608 startRecurRule = rrule.rrulestr(recurring.rrules[0], 609 dtstart=recurring.start) 610 dtstart = startRecurRule.before(dt) 611 612 if dtstart: 613 skip = False 614 # ignore if we have another event with this recurrence-id 615 for event in self._events: 616 if event.recurrenceid: 617 if event.recurrenceid == dtstart: 618 self.log( 619 'event %r, recurrenceid %r matches dtstart %r', 620 event, event.recurrenceid, dtstart) 621 skip = True 622 623 # add if it's not on our list of exceptions 624 if recurring.exdates and dtstart in recurring.exdates: 625 self.log('recurring event %r has exdate for %r', 626 recurring, dtstart) 627 skip = True 628 629 if not skip: 630 delta = recurring.end - recurring.start 631 dtend = dtstart + delta 632 if dtend >= dt: 633 # starts before our dt, and ends after, so add 634 result.append(EventInstance(recurring, dtstart, dtend)) 635 636 # handle all other events 637 for event in self._events: 638 if event is recurring: 639 continue 640 641 if event.start < dt < event.end: 642 result.append(EventInstance(event, event.start, event.end)) 643 644 self.log('events active at %s: %r', str(dt), result) 645 646 return result
647
648 - def getEvents(self):
649 """ 650 Return the list of events. 651 652 @rtype: list of L{Event} 653 """ 654 return self._events
655 656
657 -class Calendar(log.Loggable):
658 """ 659 I represent a parsed iCalendar resource. 660 I have a list of VEVENT sets from which I can be asked to schedule 661 points marking the start or end of event instances. 662 """ 663 664 logCategory = 'calendar' 665
666 - def __init__(self):
667 self._eventSets = {} # uid -> EventSet
668
669 - def addEvent(self, event):
670 """ 671 Add a parsed VEVENT definition. 672 673 @type event: L{Event} 674 """ 675 uid = event.uid 676 self.log("adding event %s with content %r", uid, event.content) 677 if uid not in self._eventSets: 678 self._eventSets[uid] = EventSet(uid) 679 self._eventSets[uid].addEvent(event)
680
681 - def getPoints(self, start=None, delta=None):
682 """ 683 Get all points from the given start time within the given delta. 684 End Points will be ordered before Start Points with the same time. 685 686 All points have a dt in the timezone as specified in the calendar. 687 688 start defaults to now. 689 delta defaults to 0, effectively returning all points at this time. 690 691 @type start: L{datetime.datetime} 692 @type delta: L{datetime.timedelta} 693 694 @rtype: list of L{Point} 695 """ 696 result = [] 697 698 for eventSet in self._eventSets.values(): 699 points = eventSet.getPoints(start, delta=delta, clip=False) 700 result.extend(points) 701 702 result.sort() 703 704 return result
705
706 - def getActiveEventInstances(self, when=None):
707 """ 708 Get a list of active event instances at the given time. 709 710 @param when: the time to check; defaults to right now 711 @type when: L{datetime.datetime} 712 713 @rtype: list of L{EventInstance} 714 """ 715 result = [] 716 717 if not when: 718 when = datetime.datetime.now(UTC) 719 720 for eventSet in self._eventSets.values(): 721 result.extend(eventSet.getActiveEventInstances(when)) 722 723 self.debug('%d active event instances at %s', len(result), str(when)) 724 return result
725 726
727 -class NotCompilantError(Exception):
728
729 - def __init__(self, value):
730 self.value = value
731
732 - def __str__(self):
733 return "The calendar is not compilant. " + repr(self.value)
734 735
736 -def vDDDToDatetime(v, timezones):
737 """ 738 Convert a vDDDType to a datetime, respecting timezones. 739 740 @param v: the time to convert 741 @type v: L{icalendar.prop.vDDDTypes} 742 743 @param timezones: Defined timezones in the calendar 744 745 """ 746 if v is None: 747 return None 748 dt = _toDateTime(v.dt) 749 if dt.tzinfo is None: 750 # We might have a "floating" DATE-TIME value here, in 751 # which case we will not have a TZID parameter; see 752 # 4.3.5, FORM #3 753 tzid = v.params.get('TZID') 754 if tzid is None: 755 timezone = tz.gettz(None) 756 else: 757 # If the timezone is not in the calendar, try one last time 758 # with the system's timezones 759 timezone = timezones.get(tzid, tz.gettz(tzid)) 760 if timezone is None: 761 raise NotCompilantError("You are trying to use a timezone\ 762 that is not defined in this calendar") 763 elif timezone != UTC: 764 timezone = timezone.copy() 765 dt = datetime.datetime(dt.year, dt.month, dt.day, 766 dt.hour, dt.minute, dt.second, 767 dt.microsecond, timezone) 768 return dt
769 770
771 -def vDDDToTimedelta(v):
772 """ 773 Convert a vDDDType (vDuration) to a timedelta. 774 775 @param v: the duration to convert 776 @type v: L{icalendar.prop.vDDDTypes} 777 778 @rtype : L{datetime.timedelta} 779 """ 780 if v is None or not isinstance(v.dt, datetime.timedelta): 781 return None 782 return v.dt
783 784
785 -def fromICalendar(iCalendar):
786 """ 787 Parse an icalendar Calendar object into our Calendar object. 788 789 @param iCalendar: The calendar to parse 790 @type iCalendar: L{icalendar.Calendar} 791 792 @rtype: L{Calendar} 793 """ 794 calendar = Calendar() 795 timezones = {'UTC': UTC} 796 797 for vtimezone in iCalendar.walk('vtimezone'): 798 799 def parseObservance(observance): 800 try: 801 return (observance['TZNAME'], observance['TZOFFSETFROM'], 802 observance['TZOFFSETTO'], observance['DTSTART']) 803 except: 804 raise NotCompilantError( 805 "VTIMEZONE does not define one of the following required " 806 "elements: TZNAME, TZOFFSETFROM, TZOFFSETTO or DTSTART")
807 808 # We need to parse all the timezone defined for the current iCalendar 809 tzid = vtimezone.get('tzid') 810 standard = vtimezone.walk('standard')[0] 811 stdname, stdoffsetfrom, stdoffset, dstend = parseObservance(standard) 812 try: 813 daylight = vtimezone.walk('daylight')[0] 814 except: 815 timezone = FixedOffsetTimezone(stdoffset.td, stdname) 816 else: 817 dstname, dstoffsetfrom, dstoffset, dststart = parseObservance( 818 daylight) 819 timezone = DSTTimezone(tzid, stdname, dstname, 820 stdoffset.td, dstoffset.td, 821 stdoffsetfrom.td, dstoffsetfrom.td, 822 dststart.dt, dstend.dt) 823 if tzid not in timezones: 824 timezones[tzid] = timezone 825 else: 826 raise NotCompilantError("Timezones must have a unique TZID") 827 828 for event in iCalendar.walk('vevent'): 829 # extract to function ? 830 831 # DTSTART is REQUIRED in VEVENT; see 4.8.2.4 832 start = vDDDToDatetime(event.get('dtstart'), timezones) 833 # DTEND is optional; see 4.8.2.3 834 end = vDDDToDatetime(event.get('dtend', None), timezones) 835 # DURATION can replace DTEND; see 4.8.2.5 836 if not end: 837 duration = vDDDToTimedelta(event.get('duration', None)) 838 end = duration and start + duration or None 839 840 # an event without DURATION or DTEND is defined to not consume any 841 # time; see 6; so we skip it 842 if not end: 843 continue 844 845 if end == start: 846 continue 847 848 assert end > start, "end %r should not be before start %r" % ( 849 end, start) 850 851 summary = event.decoded('SUMMARY', None) 852 uid = event['UID'] 853 # When there is only one rrule, we don't get a list, but the 854 # single rrule Bad API 855 recur = event.get('RRULE', []) 856 if not isinstance(recur, list): 857 recur = [recur, ] 858 recur = [r.ical() for r in recur] 859 860 recurrenceid = event.get('RECURRENCE-ID', None) 861 if recurrenceid: 862 recurrenceid = vDDDToDatetime(recurrenceid, timezones) 863 864 exdates = event.get('EXDATE', []) 865 # When there is only one exdate, we don't get a list, but the 866 # single exdate. Bad API 867 if not isinstance(exdates, list): 868 exdates = [exdates, ] 869 870 # this is a list of icalendar.propvDDDTypes on which we can call 871 # .dt() or .ical() 872 exdates = [vDDDToDatetime(i, timezones) for i in exdates] 873 874 if event.get('RDATE'): 875 raise NotImplementedError("We don't handle RDATE yet") 876 877 if event.get('EXRULE'): 878 raise NotImplementedError("We don't handle EXRULE yet") 879 880 #if not start: 881 # raise AssertionError, "event %r does not have start" % event 882 #if not end: 883 # raise AssertionError, "event %r does not have end" % event 884 e = Event(uid, start, end, summary, recur, recurrenceid, exdates) 885 886 calendar.addEvent(e) 887 888 return calendar 889 890
891 -def fromFile(file):
892 """ 893 Create a new calendar from an open file object. 894 895 @type file: file object 896 897 @rtype: L{Calendar} 898 """ 899 data = file.read() 900 901 # FIXME Google calendar recently started introducing things like 902 # CREATED:0000XXXXTXXXXXXZ, which means: created in year 0000 903 # this breaks the icalendar parsing code. Guard against that. 904 data = data.replace('\nCREATED:0000', '\nCREATED:2008') 905 cal = icalendar.Calendar.from_string(data) 906 return fromICalendar(cal)
907