예제 #1
0
    def coerce_naive_until(cls, rrule):
        #
        # RFC5545 specifies that the UNTIL rule part MUST ALWAYS be a date
        # with UTC time.  This is extra work for API implementers because
        # it requires them to perform DTSTART local -> UTC datetime coercion on
        # POST and UTC -> DTSTART local coercion on GET.
        #
        # This block of code is a departure from the RFC.  If you send an
        # rrule like this to the API (without a Z on the UNTIL):
        #
        # DTSTART;TZID=America/New_York:20180502T150000 RRULE:FREQ=HOURLY;INTERVAL=1;UNTIL=20180502T180000
        #
        # ...we'll assume that the naive UNTIL is intended to match the DTSTART
        # timezone (America/New_York), and so we'll coerce to UTC _for you_
        # automatically.
        #
        if 'until=' in rrule.lower():
            # if DTSTART;TZID= is used, coerce "naive" UNTIL values
            # to the proper UTC date
            match_until = re.match(
                r".*?(?P<until>UNTIL\=[0-9]+T[0-9]+)(?P<utcflag>Z?)", rrule)
            if not len(match_until.group('utcflag')):
                # rrule = DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T170000

                # Find the UNTIL=N part of the string
                # naive_until = UNTIL=20200601T170000
                naive_until = match_until.group('until')

                # What is the DTSTART timezone for:
                # DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T170000Z
                # local_tz = tzfile('/usr/share/zoneinfo/America/New_York')
                local_tz = dateutil.rrule.rrulestr(
                    rrule.replace(naive_until, naive_until + 'Z'),
                    tzinfos=UTC_TIMEZONES)._dtstart.tzinfo

                # Make a datetime object with tzinfo=<the DTSTART timezone>
                # localized_until = datetime.datetime(2020, 6, 1, 17, 0, tzinfo=tzfile('/usr/share/zoneinfo/America/New_York'))
                localized_until = make_aware(
                    datetime.datetime.strptime(
                        re.sub('^UNTIL=', '', naive_until), "%Y%m%dT%H%M%S"),
                    local_tz)

                # Coerce the datetime to UTC and format it as a string w/ Zulu format
                # utc_until = UNTIL=20200601T220000Z
                utc_until = 'UNTIL=' + localized_until.astimezone(
                    pytz.utc).strftime('%Y%m%dT%H%M%SZ')

                # rrule was:    DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T170000
                # rrule is now: DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T220000Z
                rrule = rrule.replace(naive_until, utc_until)
        return rrule
예제 #2
0
    def rrulestr(cls, rrule, fast_forward=True, **kwargs):
        """
        Apply our own custom rrule parsing requirements
        """
        rrule = Schedule.coerce_naive_until(rrule)
        kwargs['forceset'] = True
        x = dateutil.rrule.rrulestr(rrule, tzinfos=UTC_TIMEZONES, **kwargs)

        for r in x._rrule:
            if r._dtstart and r._dtstart.tzinfo is None:
                raise ValueError(
                    'A valid TZID must be provided (e.g., America/New_York)')

        if (fast_forward and ('MINUTELY' in rrule or 'HOURLY' in rrule)
                and 'COUNT=' not in rrule):
            try:
                first_event = x[0]
                # If the first event was over a week ago...
                if (now() - first_event).days > 7:
                    # hourly/minutely rrules with far-past DTSTART values
                    # are *really* slow to precompute
                    # start *from* one week ago to speed things up drastically
                    dtstart = x._rrule[0]._dtstart.strftime(':%Y%m%dT')
                    new_start = (
                        now() -
                        datetime.timedelta(days=7)).strftime(':%Y%m%dT')
                    new_rrule = rrule.replace(dtstart, new_start)
                    return Schedule.rrulestr(new_rrule, fast_forward=False)
            except IndexError:
                pass
        return x
예제 #3
0
    def as_ical(self, description=None, rrule=None, url=None):
        """ Returns the occurrence as iCalendar string. """

        event = icalendar.Event()
        event.add("summary", self.title)
        event.add("dtstart", to_timezone(self.start, UTC))
        event.add("dtend", to_timezone(self.end, UTC))
        event.add("last-modified", self.modified or self.created or datetime.utcnow())
        event["location"] = icalendar.vText(self.location)
        if description:
            event["description"] = icalendar.vText(description)
        if rrule:
            event["rrule"] = icalendar.vRecur(icalendar.vRecur.from_ical(rrule.replace("RRULE:", "")))
        if url:
            event.add("url", url)

        cal = icalendar.Calendar()
        cal.add("prodid", "-//OneGov//onegov.event//")
        cal.add("version", "2.0")
        cal.add_component(event)

        return cal.to_ical()
예제 #4
0
파일: schedules.py 프로젝트: humadmin/Jrepo
    def rrulestr(cls, rrule, **kwargs):
        """
        Apply our own custom rrule parsing logic.  This applies some extensions
        and limitations to parsing that are specific to our supported
        implementation.  Namely:

        * python-dateutil doesn't _natively_ support `DTSTART;TZID=`; this
          function parses out the TZID= component and uses it to produce the
          `tzinfos` keyword argument to `dateutil.rrule.rrulestr()`. In this
          way, we translate:

          DTSTART;TZID=America/New_York:20180601T120000 RRULE:FREQ=DAILY;INTERVAL=1

          ...into...

          DTSTART:20180601T120000TZID RRULE:FREQ=DAILY;INTERVAL=1

          ...and we pass a hint about the local timezone to dateutil's parser:
          `dateutil.rrule.rrulestr(rrule, {
              'tzinfos': {
                  'TZID': dateutil.tz.gettz('America/New_York')
                }
           })`

          it's possible that we can remove the custom code that performs this
          parsing if TZID= gains support in upstream dateutil:
          https://github.com/dateutil/dateutil/pull/615

        * RFC5545 specifies that: if the "DTSTART" property is specified as
          a date with local time (in our case, TZID=), then the UNTIL rule part
          MUST also be treated as a date with local time.  If the "DTSTART"
          property is specified as a date with UTC time (with a Z suffix),
          then the UNTIL rule part MUST be specified as a date with UTC time.

          this function provides additional parsing to translate RRULES like this:

          DTSTART;TZID=America/New_York:20180601T120000 RRULE:FREQ=DAILY;INTERVAL=1;UNTIL=20180602T170000

          ...into this (under the hood):

          DTSTART;TZID=America/New_York:20180601T120000 RRULE:FREQ=DAILY;INTERVAL=1;UNTIL=20180602T210000Z
        """
        source_rrule = rrule
        kwargs['tzinfos'] = {}
        match = cls.TZID_REGEX.match(rrule)
        if match is not None:
            rrule = cls.TZID_REGEX.sub("DTSTART\g<stamp>TZI\g<rrule>", rrule)
            timezone = gettz(match.group('tzid'))
            if 'until' in rrule.lower():
                # if DTSTART;TZID= is used, coerce "naive" UNTIL values
                # to the proper UTC date
                match_until = re.match(
                    ".*?(?P<until>UNTIL\=[0-9]+T[0-9]+)(?P<utcflag>Z?)", rrule)
                until_date = match_until.group('until').split("=")[1]
                if len(match_until.group('utcflag')):
                    raise ValueError(
                        six.text_type(
                            _('invalid rrule `{}` includes TZINFO= stanza and UTC-based UNTIL clause'
                              ).format(source_rrule)))  # noqa
                localized = make_aware(
                    datetime.datetime.strptime(until_date, "%Y%m%dT%H%M%S"),
                    timezone)
                utc = localized.astimezone(pytz.utc).strftime('%Y%m%dT%H%M%SZ')
                rrule = rrule.replace(match_until.group('until'),
                                      'UNTIL={}'.format(utc))
            kwargs['tzinfos']['TZI'] = timezone
        x = dateutil.rrule.rrulestr(rrule, **kwargs)
        return x