diff options
Diffstat (limited to 'dateutil/rrule.py')
-rw-r--r-- | dateutil/rrule.py | 201 |
1 files changed, 134 insertions, 67 deletions
diff --git a/dateutil/rrule.py b/dateutil/rrule.py index ef4607a..20a0c4a 100644 --- a/dateutil/rrule.py +++ b/dateutil/rrule.py @@ -337,10 +337,6 @@ class rrule(rrulebase): Additionally, it supports the following keyword arguments: - :param cache: - If given, it must be a boolean value specifying to enable or disable - caching of results. If you will use the same rrule instance multiple - times, enabling caching will improve the performance considerably. :param dtstart: The recurrence start. Besides being the base for the recurrence, missing parameters in the final recurrence instances will also be @@ -357,20 +353,26 @@ class rrule(rrulebase): from calendar.firstweekday(), and may be modified by calendar.setfirstweekday(). :param count: - How many occurrences will be generated. + If given, this determines how many occurrences will be generated. .. note:: - As of version 2.5.0, the use of the ``until`` keyword together - with the ``count`` keyword is deprecated per RFC-5545 Sec. 3.3.10. + As of version 2.5.0, the use of the keyword ``until`` in conjunction + with ``count`` is deprecated, to make sure ``dateutil`` is fully + compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/ + html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count`` + **must not** occur in the same call to ``rrule``. :param until: - If given, this must be a datetime instance, that will specify the + If given, this must be a datetime instance specifying the upper-bound limit of the recurrence. The last recurrence in the rule is the greatest datetime that is less than or equal to the value specified in the ``until`` parameter. .. note:: - As of version 2.5.0, the use of the ``until`` keyword together - with the ``count`` keyword is deprecated per RFC-5545 Sec. 3.3.10. + As of version 2.5.0, the use of the keyword ``until`` in conjunction + with ``count`` is deprecated, to make sure ``dateutil`` is fully + compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/ + html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count`` + **must not** occur in the same call to ``rrule``. :param bysetpos: If given, it must be either an integer, or a sequence of integers, positive or negative. Each given integer will specify an occurrence @@ -387,6 +389,11 @@ class rrule(rrulebase): :param byyearday: If given, it must be either an integer, or a sequence of integers, meaning the year days to apply the recurrence to. + :param byeaster: + If given, it must be either an integer, or a sequence of integers, + positive or negative. Each integer will define an offset from the + Easter Sunday. Passing the offset 0 to byeaster will yield the Easter + Sunday itself. This is an extension to the RFC specification. :param byweekno: If given, it must be either an integer, or a sequence of integers, meaning the week numbers to apply the recurrence to. Week numbers @@ -412,11 +419,10 @@ class rrule(rrulebase): :param bysecond: If given, it must be either an integer, or a sequence of integers, meaning the seconds to apply the recurrence to. - :param byeaster: - If given, it must be either an integer, or a sequence of integers, - positive or negative. Each integer will define an offset from the - Easter Sunday. Passing the offset 0 to byeaster will yield the Easter - Sunday itself. This is an extension to the RFC specification. + :param cache: + If given, it must be a boolean value specifying to enable or disable + caching of results. If you will use the same rrule instance multiple + times, enabling caching will improve the performance considerably. """ def __init__(self, freq, dtstart=None, interval=1, wkst=None, count=None, until=None, bysetpos=None, @@ -427,7 +433,10 @@ class rrule(rrulebase): super(rrule, self).__init__(cache) global easter if not dtstart: - dtstart = datetime.datetime.now().replace(microsecond=0) + if until and until.tzinfo: + dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0) + else: + dtstart = datetime.datetime.now().replace(microsecond=0) elif not isinstance(dtstart, datetime.datetime): dtstart = datetime.datetime.fromordinal(dtstart.toordinal()) else: @@ -1403,7 +1412,52 @@ class rruleset(rrulebase): self._len = total + + class _rrulestr(object): + """ Parses a string representation of a recurrence rule or set of + recurrence rules. + + :param s: + Required, a string defining one or more recurrence rules. + + :param dtstart: + If given, used as the default recurrence start if not specified in the + rule string. + + :param cache: + If set ``True`` caching of results will be enabled, improving + performance of multiple queries considerably. + + :param unfold: + If set ``True`` indicates that a rule string is split over more + than one line and should be joined before processing. + + :param forceset: + If set ``True`` forces a :class:`dateutil.rrule.rruleset` to + be returned. + + :param compatible: + If set ``True`` forces ``unfold`` and ``forceset`` to be ``True``. + + :param ignoretz: + If set ``True``, time zones in parsed strings are ignored and a naive + :class:`datetime.datetime` object is returned. + + :param tzids: + If given, a callable or mapping used to retrieve a + :class:`datetime.tzinfo` from a string representation. + Defaults to :func:`dateutil.tz.gettz`. + + :param tzinfos: + Additional time zone names / aliases which may be present in a string + representation. See :func:`dateutil.parser.parse` for more + information. + + :return: + Returns a :class:`dateutil.rrule.rruleset` or + :class:`dateutil.rrule.rrule` + """ _freq_map = {"YEARLY": YEARLY, "MONTHLY": MONTHLY, @@ -1505,6 +1559,58 @@ class _rrulestr(object): raise ValueError("invalid '%s': %s" % (name, value)) return rrule(dtstart=dtstart, cache=cache, **rrkwargs) + def _parse_date_value(self, date_value, parms, rule_tzids, + ignoretz, tzids, tzinfos): + global parser + if not parser: + from dateutil import parser + + datevals = [] + value_found = False + TZID = None + + for parm in parms: + if parm.startswith("TZID="): + try: + tzkey = rule_tzids[parm.split('TZID=')[-1]] + except KeyError: + continue + if tzids is None: + from . import tz + tzlookup = tz.gettz + elif callable(tzids): + tzlookup = tzids + else: + tzlookup = getattr(tzids, 'get', None) + if tzlookup is None: + msg = ('tzids must be a callable, mapping, or None, ' + 'not %s' % tzids) + raise ValueError(msg) + + TZID = tzlookup(tzkey) + continue + + # RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found + # only once. + if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}: + raise ValueError("unsupported parm: " + parm) + else: + if value_found: + msg = ("Duplicate value parameter found in: " + parm) + raise ValueError(msg) + value_found = True + + for datestr in date_value.split(','): + date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos) + if TZID is not None: + if date.tzinfo is None: + date = date.replace(tzinfo=TZID) + else: + raise ValueError('DTSTART/EXDATE specifies multiple timezone') + datevals.append(date) + + return datevals + def _parse_rfc(self, s, dtstart=None, cache=False, @@ -1577,54 +1683,18 @@ class _rrulestr(object): raise ValueError("unsupported EXRULE parm: "+parm) exrulevals.append(value) elif name == "EXDATE": - for parm in parms: - if parm != "VALUE=DATE-TIME": - raise ValueError("unsupported EXDATE parm: "+parm) - exdatevals.append(value) + exdatevals.extend( + self._parse_date_value(value, parms, + TZID_NAMES, ignoretz, + tzids, tzinfos) + ) elif name == "DTSTART": - # RFC 5445 3.8.2.4: The VALUE parameter is optional, but - # may be found only once. - value_found = False - TZID = None - valid_values = {"VALUE=DATE-TIME", "VALUE=DATE"} - for parm in parms: - if parm.startswith("TZID="): - try: - tzkey = TZID_NAMES[parm.split('TZID=')[-1]] - except KeyError: - continue - if tzids is None: - from . import tz - tzlookup = tz.gettz - elif callable(tzids): - tzlookup = tzids - else: - tzlookup = getattr(tzids, 'get', None) - if tzlookup is None: - msg = ('tzids must be a callable, ' + - 'mapping, or None, ' + - 'not %s' % tzids) - raise ValueError(msg) - - TZID = tzlookup(tzkey) - continue - if parm not in valid_values: - raise ValueError("unsupported DTSTART parm: "+parm) - else: - if value_found: - msg = ("Duplicate value parameter found in " + - "DTSTART: " + parm) - raise ValueError(msg) - value_found = True - if not parser: - from dateutil import parser - dtstart = parser.parse(value, ignoretz=ignoretz, - tzinfos=tzinfos) - if TZID is not None: - if dtstart.tzinfo is None: - dtstart = dtstart.replace(tzinfo=TZID) - else: - raise ValueError('DTSTART specifies multiple timezones') + dtvals = self._parse_date_value(value, parms, TZID_NAMES, + ignoretz, tzids, tzinfos) + if len(dtvals) != 1: + raise ValueError("Multiple DTSTART values specified:" + + value) + dtstart = dtvals[0] else: raise ValueError("unsupported property: "+name) if (forceset or len(rrulevals) > 1 or rdatevals @@ -1646,10 +1716,7 @@ class _rrulestr(object): ignoretz=ignoretz, tzinfos=tzinfos)) for value in exdatevals: - for datestr in value.split(','): - rset.exdate(parser.parse(datestr, - ignoretz=ignoretz, - tzinfos=tzinfos)) + rset.exdate(value) if compatible and dtstart: rset.rdate(dtstart) return rset |