示例#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, **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