1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
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
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
72 """A tzinfo class representing the system's idea of the local timezone"""
73 STDOFFSET = datetime.timedelta(seconds=-time.timezone)
74 if time.daylight:
75 DSTOFFSET = datetime.timedelta(seconds=-time.altzone)
76 else:
77 DSTOFFSET = STDOFFSET
78 DSTDIFF = DSTOFFSET - STDOFFSET
79 ZERO = datetime.timedelta(0)
80
86
92
95
97 tt = (dt.year, dt.month, dt.day,
98 dt.hour, dt.minute, dt.second,
99 dt.weekday(), 0, -1)
100 return time.localtime(time.mktime(tt)).tm_isdst > 0
101 LOCAL = LocalTimezone()
102
103
104
105
107 """A tzinfo class representing UTC"""
108 ZERO = datetime.timedelta(0)
109
112
115
118 UTC = UTCTimezone()
119
120
121 -class Point(log.Loggable):
122 """
123 I represent a start or an end point linked to an event instance
124 of an event.
125
126 @type eventInstance: L{EventInstance}
127 @type which: str
128 @type dt: L{datetime.datetime}
129 """
130
131 - def __init__(self, eventInstance, which, dt):
132 """
133 @param eventInstance: An instance of an event.
134 @type eventInstance: L{EventInstance}
135 @param which: 'start' or 'end'
136 @type which: str
137 @param dt: Timestamp of this point. It will
138 be used when comparing Points.
139 @type dt: L{datetime.datetime}
140 """
141 self.which = which
142 self.dt = dt
143 self.eventInstance = eventInstance
144
146 return "Point '%s' at %r for %r" % (
147 self.which, self.dt, self.eventInstance)
148
150
151
152 return cmp(self.dt, other.dt) \
153 or cmp(self.which, other.which)
154
155
157 """
158 I represent one event instance of an event.
159
160 @type event: L{Event}
161 @type start: L{datetime.datetime}
162 @type end: L{datetime.datetime}
163 """
164
166 """
167 @type event: L{Event}
168 @type start: L{datetime.datetime}
169 @type end: L{datetime.datetime}
170 """
171 self.event = event
172 self.start = start
173 self.end = end
174
176 """
177 Get a list of start and end points.
178
179 @rtype: list of L{Point}
180 """
181 ret = []
182
183 ret.append(Point(self, 'start', self.start))
184 ret.append(Point(self, 'end', self.end))
185
186 return ret
187
191
193 return not self.__eq__(other)
194
195
196 -class Event(log.Loggable):
197 """
198 I represent a VEVENT entry in a calendar for our purposes.
199 I can have recurrence.
200 I can be scheduled between a start time and an end time,
201 returning a list of start and end points.
202 I can have exception dates.
203 """
204
205 - def __init__(self, uid, start, end, content, rrules=None,
206 recurrenceid=None, exdates=None):
207 """
208 @param uid: identifier of the event
209 @type uid: str
210 @param start: start time of the event
211 @type start: L{datetime.datetime}
212 @param end: end time of the event
213 @type end: L{datetime.datetime}
214 @param content: label to describe the content
215 @type content: unicode
216 @param rrules: a list of RRULE string
217 @type rrules: list of str
218 @param recurrenceid: a RECURRENCE-ID, used with
219 recurrence events
220 @type recurrenceid: L{datetime.datetime}
221 @param exdates: list of exceptions to the recurrence rule
222 @type exdates: list of L{datetime.datetime} or None
223 """
224
225 self.start = self._ensureTimeZone(start)
226 self.end = self._ensureTimeZone(end)
227 self.content = content
228 self.uid = uid
229 self.rrules = rrules
230 if rrules and len(rrules) > 1:
231 raise NotImplementedError(
232 "Events with multiple RRULE are not yet supported")
233 self.recurrenceid = recurrenceid
234 if exdates:
235 self.exdates = []
236 for exdate in exdates:
237 exdate = self._ensureTimeZone(exdate)
238 self.exdates.append(exdate)
239 else:
240 self.exdates = None
241
243
244 if dateTime.tzinfo:
245 return dateTime
246
247 return datetime.datetime(dateTime.year, dateTime.month, dateTime.day,
248 dateTime.hour, dateTime.minute, dateTime.second,
249 dateTime.microsecond, tz)
250
252 return "<Event %r >" % (self.toTuple(), )
253
255 return (self.uid, self.start, self.end, self.content, self.rrules,
256 self.exdates)
257
258
259
260
263
266
267
268
269
272
274 return not self.__eq__(other)
275
276
278 """
279 I represent a set of VEVENT entries in a calendar sharing the same uid.
280 I can have recurrence.
281 I can be scheduled between a start time and an end time,
282 returning a list of start and end points in UTC.
283 I can have exception dates.
284 """
285
287 """
288 @param uid: the uid shared among the events on this set
289 @type uid: str
290 """
291 self.uid = uid
292 self._events = []
293
295 return "<EventSet for uid %r >" % (
296 self.uid)
297
299 """
300 Add an event to the set. The event must have the same uid as the set.
301
302 @param event: the event to add.
303 @type event: L{Event}
304 """
305 assert self.uid == event.uid, \
306 "my uid %s does not match Event uid %s" % (self.uid, event.uid)
307 assert event not in self._events, "event %r already in set %r" % (
308 event, self._events)
309
310 self._events.append(event)
311
313 """
314 Remove an event from the set.
315
316 @param event: the event to add.
317 @type event: L{Event}
318 """
319 assert self.uid == event.uid, \
320 "my uid %s does not match Event uid %s" % (self.uid, event.uid)
321 self._events.remove(event)
322
323 - def getPoints(self, start=None, delta=None, clip=True):
324 """
325 Get an ordered list of start and end points from the given start
326 point, with the given delta, in this set of Events.
327
328 start defaults to now.
329 delta defaults to 0, effectively returning all points at this time.
330 the returned list includes the extremes (start and start + delta)
331
332 @param start: the start time
333 @type start: L{datetime.datetime}
334 @param delta: the delta
335 @type delta: L{datetime.timedelta}
336 @param clip: whether to clip all event instances to the given
337 start and end
338 """
339 if start is None:
340 start = datetime.datetime.now(UTC)
341
342 if delta is None:
343 delta = datetime.timedelta(seconds=0)
344
345 points = []
346
347 eventInstances = self._getEventInstances(start, start + delta, clip)
348 for i in eventInstances:
349 for p in i.getPoints():
350 if p.dt >= start and p.dt <= start + delta:
351 points.append(p)
352 points.sort()
353
354 return points
355
357 recurring = None
358
359
360 for v in self._events:
361 if v.rrules:
362 assert not recurring, \
363 "Cannot have two RRULE VEVENTs with UID %s" % self.uid
364 recurring = v
365 else:
366 if len(self._events) > 1:
367 assert v.recurrenceid, \
368 "With multiple VEVENTs with UID %s, " \
369 "each VEVENT should either have a " \
370 "reccurrence rule or have a recurrence id" % self.uid
371
372 return recurring
373
375
376
377
378
379
380
381 eventInstances = []
382
383 recurring = self._getRecurringEvent()
384
385
386 if recurring:
387 eventInstances = self._getEventInstancesRecur(
388 recurring, start, end)
389
390
391
392
393 for event in self._events:
394
395 if event is recurring:
396 continue
397
398 if event.recurrenceid:
399
400 for i in eventInstances[:]:
401 if i.start == event.recurrenceid:
402 eventInstances.remove(i)
403 break
404
405 i = self._getEventInstanceSingle(event, start, end)
406 if i:
407 eventInstances.append(i)
408
409 if clip:
410
411
412 for i in eventInstances[:]:
413 if i.start < start:
414 i.start = start
415 if start >= i.end:
416 eventInstances.remove(i)
417 if i.end > end:
418 i.end = end
419
420 return eventInstances
421
430
432
433
434
435
436
437
438
439 ret = []
440
441
442
443
444 delta = event.end - event.start
445
446
447 r = None
448 if event.rrules:
449 r = event.rrules[0]
450 startRecurRule = rrule.rrulestr(r, dtstart=event.start)
451
452 for startTime in startRecurRule:
453
454 if startTime + delta < start:
455 continue
456
457
458 if startTime >= end:
459 break
460
461
462 if event.exdates:
463 if startTime in event.exdates:
464 self.debug("startTime %r is listed as EXDATE, skipping",
465 startTime)
466 continue
467
468 endTime = startTime + delta
469
470 i = EventInstance(event, startTime, endTime)
471
472 ret.append(i)
473
474 return ret
475
477 """
478 Get all event instances active at the given dt.
479
480 @type dt: L{datetime.datetime}
481
482 @rtype: list of L{EventInstance}
483 """
484 if not dt:
485 dt = datetime.datetime.now(tz=UTC)
486
487 result = []
488
489
490 recurring = self._getRecurringEvent()
491 if recurring:
492
493 startRecurRule = rrule.rrulestr(recurring.rrules[0],
494 dtstart=recurring.start)
495 dtstart = startRecurRule.before(dt)
496
497 if dtstart:
498 skip = False
499
500 for event in self._events:
501 if event.recurrenceid:
502 if event.recurrenceid == dtstart:
503 self.log(
504 'event %r, recurrenceid %r matches dtstart %r',
505 event, event.recurrenceid, dtstart)
506 skip = True
507
508
509 if recurring.exdates and dtstart in recurring.exdates:
510 self.log('recurring event %r has exdate for %r',
511 recurring, dtstart)
512 skip = True
513
514 if not skip:
515 delta = recurring.end - recurring.start
516 dtend = dtstart + delta
517 if dtend >= dt:
518
519 result.append(EventInstance(recurring, dtstart, dtend))
520
521
522 for event in self._events:
523 if event is recurring:
524 continue
525
526 if event.start < dt < event.end:
527 result.append(EventInstance(event, event.start, event.end))
528
529 self.log('events active at %s: %r', str(dt), result)
530
531 return result
532
534 """
535 Return the list of events.
536
537 @rtype: list of L{Event}
538 """
539 return self._events
540
541
543 """
544 I represent a parsed iCalendar resource.
545 I have a list of VEVENT sets from which I can be asked to schedule
546 points marking the start or end of event instances.
547 """
548
549 logCategory = 'calendar'
550
553
555 """
556 Add a parsed VEVENT definition.
557
558 @type event: L{Event}
559 """
560 uid = event.uid
561 self.log("adding event %s with content %r", uid, event.content)
562 if uid not in self._eventSets:
563 self._eventSets[uid] = EventSet(uid)
564 self._eventSets[uid].addEvent(event)
565
566 - def getPoints(self, start=None, delta=None):
567 """
568 Get all points from the given start time within the given delta.
569 End Points will be ordered before Start Points with the same time.
570
571 All points have a dt in the timezone as specified in the calendar.
572
573 start defaults to now.
574 delta defaults to 0, effectively returning all points at this time.
575
576 @type start: L{datetime.datetime}
577 @type delta: L{datetime.timedelta}
578
579 @rtype: list of L{Point}
580 """
581 result = []
582
583 for eventSet in self._eventSets.values():
584 points = eventSet.getPoints(start, delta=delta, clip=False)
585 result.extend(points)
586
587 result.sort()
588
589 return result
590
592 """
593 Get a list of active event instances at the given time.
594
595 @param when: the time to check; defaults to right now
596 @type when: L{datetime.datetime}
597
598 @rtype: list of L{EventInstance}
599 """
600 result = []
601
602 if not when:
603 when = datetime.datetime.now(UTC)
604
605 for eventSet in self._eventSets.values():
606 result.extend(eventSet.getActiveEventInstances(when))
607
608 self.debug('%d active event instances at %s', len(result), str(when))
609 return result
610
611
613 """
614 Convert a vDDDType to a datetime, respecting timezones.
615
616 @param v: the time to convert
617 @type v: L{icalendar.prop.vDDDTypes}
618
619 """
620 dt = _toDateTime(v.dt)
621 if dt.tzinfo is None:
622
623
624
625
626
627
628 tzinfo = tz.gettz(v.params.get('TZID', None))
629 dt = datetime.datetime(dt.year, dt.month, dt.day,
630 dt.hour, dt.minute, dt.second,
631 dt.microsecond, tzinfo)
632 return dt
633
634
636 """
637 Parse an icalendar Calendar object into our Calendar object.
638
639 @param iCalendar: The calendar to parse
640 @type iCalendar: L{icalendar.Calendar}
641
642 @rtype: L{Calendar}
643 """
644 calendar = Calendar()
645
646 for event in iCalendar.walk('vevent'):
647
648
649
650 start = vDDDToDatetime(event.get('dtstart'))
651
652 end = vDDDToDatetime(event.get('dtend', None))
653
654
655
656
657
658 if not end:
659 continue
660
661 if end == start:
662 continue
663
664 assert end >= start, "end %r should not be before start %r" % (
665 end, start)
666
667 summary = event.decoded('SUMMARY', None)
668 uid = event['UID']
669
670
671 recur = event.get('RRULE', [])
672 if not isinstance(recur, list):
673 recur = [recur, ]
674 recur = [r.ical() for r in recur]
675
676 recurrenceid = event.get('RECURRENCE-ID', None)
677 if recurrenceid:
678 recurrenceid = vDDDToDatetime(recurrenceid)
679
680 exdates = event.get('EXDATE', [])
681
682
683 if not isinstance(exdates, list):
684 exdates = [exdates, ]
685
686
687
688 exdates = [vDDDToDatetime(i) for i in exdates]
689
690 if event.get('RDATE'):
691 raise NotImplementedError("We don't handle RDATE yet")
692
693 if event.get('EXRULE'):
694 raise NotImplementedError("We don't handle EXRULE yet")
695
696
697
698
699
700 e = Event(uid, start, end, summary, recur, recurrenceid, exdates)
701
702 calendar.addEvent(e)
703
704 return calendar
705
706
708 """
709 Create a new calendar from an open file object.
710
711 @type file: file object
712
713 @rtype: L{Calendar}
714 """
715 data = file.read()
716
717
718
719
720 data = data.replace('\nCREATED:0000', '\nCREATED:2008')
721 cal = icalendar.Calendar.from_string(data)
722 return fromICalendar(cal)
723