def _testRRule(self, freq, rrule): """Create a simple rrule, make sure it behaves as we expect.""" self.assertEqual(rrule[0], getattr(self, freq)['start']) self.assertEqual(rrule[-1], getattr(self, freq)['end']) self.assertEqual(rrule.count(), getattr(self, freq)['count'])
def expand(vevent, href=''): """ Constructs a list of start and end dates for all recurring instances of the event defined in vevent. It considers RRULE as well as RDATE and EXDATE properties. In case of unsupported recursion rules an UnsupportedRecurrence exception is thrown. If the vevent contains a RECURRENCE-ID property, no expansion is done, the function still returns a tuple of start and end (date)times. :param vevent: vevent to be expanded :type vevent: icalendar.cal.Event :param href: the href of the vevent, used for more informative logging and nothing else :type href: str :returns: list of start and end (date)times of the expanded event :rtype: list(tuple(datetime, datetime)) """ # we do this now and than never care about the "real" end time again if 'DURATION' in vevent: duration = vevent['DURATION'].dt else: duration = vevent['DTEND'].dt - vevent['DTSTART'].dt # if this vevent has a RECURRENCE_ID property, no expansion will be # performed expand = not bool(vevent.get('RECURRENCE-ID')) events_tz = getattr(vevent['DTSTART'].dt, 'tzinfo', None) allday = not isinstance(vevent['DTSTART'].dt, dt.datetime) def sanitize_datetime(date): if allday and isinstance(date, dt.datetime): date = date.date() if events_tz is not None: date = events_tz.localize(date) return date rrule_param = vevent.get('RRULE') if expand and rrule_param is not None: vevent = sanitize_rrule(vevent) # dst causes problem while expanding the rrule, therefore we transform # everything to naive datetime objects and transform back after # expanding # See https://github.com/dateutil/dateutil/issues/102 dtstart = vevent['DTSTART'].dt if events_tz: dtstart = dtstart.replace(tzinfo=None) rrule = dateutil.rrule.rrulestr( rrule_param.to_ical().decode(), dtstart=dtstart, ignoretz=True, ) if rrule._until is None: # rrule really doesn't like to calculate all recurrences until # eternity, so we only do it until 2037, because a) I'm not sure # if python can deal with larger datetime values yet and b) pytz # doesn't know any larger transition times rrule._until = dt.datetime(2037, 12, 31) else: if events_tz and 'Z' in rrule_param.to_ical().decode(): rrule._until = pytz.UTC.localize( rrule._until).astimezone(events_tz).replace(tzinfo=None) # rrule._until and dtstart could be dt.date or dt.datetime. They # need to be the same for comparison testuntil = rrule._until if (type(dtstart) == dt.date and type(testuntil) == dt.datetime): testuntil = testuntil.date() teststart = dtstart if (type(testuntil) == dt.date and type(teststart) == dt.datetime): teststart = teststart.date() if testuntil < teststart: logger.warning( f'{href}: Unsupported recurrence. UNTIL is before DTSTART.\n' 'This event will not be available in khal.') return False if rrule.count() == 0: logger.warning( f'{href}: Recurrence defined but will never occur.\n' 'This event will not be available in khal.') return False rrule = map(sanitize_datetime, rrule) logger.debug( f'calculating recurrence dates for {href}, this might take some time.' ) # RRULE and RDATE may specify the same date twice, it is recommended by # the RFC to consider this as only one instance dtstartl = set(rrule) if not dtstartl: raise UnsupportedRecurrence() else: dtstartl = {vevent['DTSTART'].dt} def get_dates(vevent, key): # TODO replace with get_all_properties dates = vevent.get(key) if dates is None: return if not isinstance(dates, list): dates = [dates] dates = (leaf.dt for tree in dates for leaf in tree.dts) dates = localize_strip_tz(dates, events_tz) return map(sanitize_datetime, dates) # include explicitly specified recursion dates if expand: dtstartl.update(get_dates(vevent, 'RDATE') or ()) # remove excluded dates if expand: for date in get_dates(vevent, 'EXDATE') or (): try: dtstartl.remove(date) except KeyError: logger.warning( f'In event {href}, excluded instance starting at {date} ' 'not found, event might be invalid.') dtstartend = [(start, start + duration) for start in dtstartl] # not necessary, but I prefer deterministic output dtstartend.sort() return dtstartend