Esempio n. 1
0
 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'])
Esempio n. 2
0
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
 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'])