| | |
| | """ |
| | The rrule module offers a small, complete, and very fast, implementation of |
| | the recurrence rules documented in the |
| | `iCalendar RFC <https://tools.ietf.org/html/rfc5545>`_, |
| | including support for caching of results. |
| | """ |
| | import calendar |
| | import datetime |
| | import heapq |
| | import itertools |
| | import re |
| | import sys |
| | from functools import wraps |
| | |
| | from warnings import warn |
| |
|
| | from six import advance_iterator, integer_types |
| |
|
| | from six.moves import _thread, range |
| |
|
| | from ._common import weekday as weekdaybase |
| |
|
| | try: |
| | from math import gcd |
| | except ImportError: |
| | from fractions import gcd |
| |
|
| | __all__ = ["rrule", "rruleset", "rrulestr", |
| | "YEARLY", "MONTHLY", "WEEKLY", "DAILY", |
| | "HOURLY", "MINUTELY", "SECONDLY", |
| | "MO", "TU", "WE", "TH", "FR", "SA", "SU"] |
| |
|
| | |
| | M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 + |
| | [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7) |
| | M365MASK = list(M366MASK) |
| | M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32)) |
| | MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) |
| | MDAY365MASK = list(MDAY366MASK) |
| | M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0)) |
| | NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) |
| | NMDAY365MASK = list(NMDAY366MASK) |
| | M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366) |
| | M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365) |
| | WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55 |
| | del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31] |
| | MDAY365MASK = tuple(MDAY365MASK) |
| | M365MASK = tuple(M365MASK) |
| |
|
| | FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY'] |
| |
|
| | (YEARLY, |
| | MONTHLY, |
| | WEEKLY, |
| | DAILY, |
| | HOURLY, |
| | MINUTELY, |
| | SECONDLY) = list(range(7)) |
| |
|
| | |
| | easter = None |
| | parser = None |
| |
|
| |
|
| | class weekday(weekdaybase): |
| | """ |
| | This version of weekday does not allow n = 0. |
| | """ |
| | def __init__(self, wkday, n=None): |
| | if n == 0: |
| | raise ValueError("Can't create weekday with n==0") |
| |
|
| | super(weekday, self).__init__(wkday, n) |
| |
|
| |
|
| | MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) |
| |
|
| |
|
| | def _invalidates_cache(f): |
| | """ |
| | Decorator for rruleset methods which may invalidate the |
| | cached length. |
| | """ |
| | @wraps(f) |
| | def inner_func(self, *args, **kwargs): |
| | rv = f(self, *args, **kwargs) |
| | self._invalidate_cache() |
| | return rv |
| |
|
| | return inner_func |
| |
|
| |
|
| | class rrulebase(object): |
| | def __init__(self, cache=False): |
| | if cache: |
| | self._cache = [] |
| | self._cache_lock = _thread.allocate_lock() |
| | self._invalidate_cache() |
| | else: |
| | self._cache = None |
| | self._cache_complete = False |
| | self._len = None |
| |
|
| | def __iter__(self): |
| | if self._cache_complete: |
| | return iter(self._cache) |
| | elif self._cache is None: |
| | return self._iter() |
| | else: |
| | return self._iter_cached() |
| |
|
| | def _invalidate_cache(self): |
| | if self._cache is not None: |
| | self._cache = [] |
| | self._cache_complete = False |
| | self._cache_gen = self._iter() |
| |
|
| | if self._cache_lock.locked(): |
| | self._cache_lock.release() |
| |
|
| | self._len = None |
| |
|
| | def _iter_cached(self): |
| | i = 0 |
| | gen = self._cache_gen |
| | cache = self._cache |
| | acquire = self._cache_lock.acquire |
| | release = self._cache_lock.release |
| | while gen: |
| | if i == len(cache): |
| | acquire() |
| | if self._cache_complete: |
| | break |
| | try: |
| | for j in range(10): |
| | cache.append(advance_iterator(gen)) |
| | except StopIteration: |
| | self._cache_gen = gen = None |
| | self._cache_complete = True |
| | break |
| | release() |
| | yield cache[i] |
| | i += 1 |
| | while i < self._len: |
| | yield cache[i] |
| | i += 1 |
| |
|
| | def __getitem__(self, item): |
| | if self._cache_complete: |
| | return self._cache[item] |
| | elif isinstance(item, slice): |
| | if item.step and item.step < 0: |
| | return list(iter(self))[item] |
| | else: |
| | return list(itertools.islice(self, |
| | item.start or 0, |
| | item.stop or sys.maxsize, |
| | item.step or 1)) |
| | elif item >= 0: |
| | gen = iter(self) |
| | try: |
| | for i in range(item+1): |
| | res = advance_iterator(gen) |
| | except StopIteration: |
| | raise IndexError |
| | return res |
| | else: |
| | return list(iter(self))[item] |
| |
|
| | def __contains__(self, item): |
| | if self._cache_complete: |
| | return item in self._cache |
| | else: |
| | for i in self: |
| | if i == item: |
| | return True |
| | elif i > item: |
| | return False |
| | return False |
| |
|
| | |
| | def count(self): |
| | """ Returns the number of recurrences in this set. It will have go |
| | trough the whole recurrence, if this hasn't been done before. """ |
| | if self._len is None: |
| | for x in self: |
| | pass |
| | return self._len |
| |
|
| | def before(self, dt, inc=False): |
| | """ Returns the last recurrence before the given datetime instance. The |
| | inc keyword defines what happens if dt is an occurrence. With |
| | inc=True, if dt itself is an occurrence, it will be returned. """ |
| | if self._cache_complete: |
| | gen = self._cache |
| | else: |
| | gen = self |
| | last = None |
| | if inc: |
| | for i in gen: |
| | if i > dt: |
| | break |
| | last = i |
| | else: |
| | for i in gen: |
| | if i >= dt: |
| | break |
| | last = i |
| | return last |
| |
|
| | def after(self, dt, inc=False): |
| | """ Returns the first recurrence after the given datetime instance. The |
| | inc keyword defines what happens if dt is an occurrence. With |
| | inc=True, if dt itself is an occurrence, it will be returned. """ |
| | if self._cache_complete: |
| | gen = self._cache |
| | else: |
| | gen = self |
| | if inc: |
| | for i in gen: |
| | if i >= dt: |
| | return i |
| | else: |
| | for i in gen: |
| | if i > dt: |
| | return i |
| | return None |
| |
|
| | def xafter(self, dt, count=None, inc=False): |
| | """ |
| | Generator which yields up to `count` recurrences after the given |
| | datetime instance, equivalent to `after`. |
| | |
| | :param dt: |
| | The datetime at which to start generating recurrences. |
| | |
| | :param count: |
| | The maximum number of recurrences to generate. If `None` (default), |
| | dates are generated until the recurrence rule is exhausted. |
| | |
| | :param inc: |
| | If `dt` is an instance of the rule and `inc` is `True`, it is |
| | included in the output. |
| | |
| | :yields: Yields a sequence of `datetime` objects. |
| | """ |
| |
|
| | if self._cache_complete: |
| | gen = self._cache |
| | else: |
| | gen = self |
| |
|
| | |
| | if inc: |
| | comp = lambda dc, dtc: dc >= dtc |
| | else: |
| | comp = lambda dc, dtc: dc > dtc |
| |
|
| | |
| | n = 0 |
| | for d in gen: |
| | if comp(d, dt): |
| | if count is not None: |
| | n += 1 |
| | if n > count: |
| | break |
| |
|
| | yield d |
| |
|
| | def between(self, after, before, inc=False, count=1): |
| | """ Returns all the occurrences of the rrule between after and before. |
| | The inc keyword defines what happens if after and/or before are |
| | themselves occurrences. With inc=True, they will be included in the |
| | list, if they are found in the recurrence set. """ |
| | if self._cache_complete: |
| | gen = self._cache |
| | else: |
| | gen = self |
| | started = False |
| | l = [] |
| | if inc: |
| | for i in gen: |
| | if i > before: |
| | break |
| | elif not started: |
| | if i >= after: |
| | started = True |
| | l.append(i) |
| | else: |
| | l.append(i) |
| | else: |
| | for i in gen: |
| | if i >= before: |
| | break |
| | elif not started: |
| | if i > after: |
| | started = True |
| | l.append(i) |
| | else: |
| | l.append(i) |
| | return l |
| |
|
| |
|
| | class rrule(rrulebase): |
| | """ |
| | That's the base of the rrule operation. It accepts all the keywords |
| | defined in the RFC as its constructor parameters (except byday, |
| | which was renamed to byweekday) and more. The constructor prototype is:: |
| | |
| | rrule(freq) |
| | |
| | Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, |
| | or SECONDLY. |
| | |
| | .. note:: |
| | Per RFC section 3.3.10, recurrence instances falling on invalid dates |
| | and times are ignored rather than coerced: |
| | |
| | Recurrence rules may generate recurrence instances with an invalid |
| | date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM |
| | on a day where the local time is moved forward by an hour at 1:00 |
| | AM). Such recurrence instances MUST be ignored and MUST NOT be |
| | counted as part of the recurrence set. |
| | |
| | This can lead to possibly surprising behavior when, for example, the |
| | start date occurs at the end of the month: |
| | |
| | >>> from dateutil.rrule import rrule, MONTHLY |
| | >>> from datetime import datetime |
| | >>> start_date = datetime(2014, 12, 31) |
| | >>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date)) |
| | ... # doctest: +NORMALIZE_WHITESPACE |
| | [datetime.datetime(2014, 12, 31, 0, 0), |
| | datetime.datetime(2015, 1, 31, 0, 0), |
| | datetime.datetime(2015, 3, 31, 0, 0), |
| | datetime.datetime(2015, 5, 31, 0, 0)] |
| | |
| | Additionally, it supports the following keyword arguments: |
| | |
| | :param dtstart: |
| | The recurrence start. Besides being the base for the recurrence, |
| | missing parameters in the final recurrence instances will also be |
| | extracted from this date. If not given, datetime.now() will be used |
| | instead. |
| | :param interval: |
| | The interval between each freq iteration. For example, when using |
| | YEARLY, an interval of 2 means once every two years, but with HOURLY, |
| | it means once every two hours. The default interval is 1. |
| | :param wkst: |
| | The week start day. Must be one of the MO, TU, WE constants, or an |
| | integer, specifying the first day of the week. This will affect |
| | recurrences based on weekly periods. The default week start is got |
| | from calendar.firstweekday(), and may be modified by |
| | calendar.setfirstweekday(). |
| | :param count: |
| | If given, this determines how many occurrences will be generated. |
| | |
| | .. note:: |
| | 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 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 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 |
| | number, corresponding to the nth occurrence of the rule inside the |
| | frequency period. For example, a bysetpos of -1 if combined with a |
| | MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will |
| | result in the last work day of every month. |
| | :param bymonth: |
| | If given, it must be either an integer, or a sequence of integers, |
| | meaning the months to apply the recurrence to. |
| | :param bymonthday: |
| | If given, it must be either an integer, or a sequence of integers, |
| | meaning the month days to apply the recurrence to. |
| | :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 |
| | have the meaning described in ISO8601, that is, the first week of |
| | the year is that containing at least four days of the new year. |
| | :param byweekday: |
| | If given, it must be either an integer (0 == MO), a sequence of |
| | integers, one of the weekday constants (MO, TU, etc), or a sequence |
| | of these constants. When given, these variables will define the |
| | weekdays where the recurrence will be applied. It's also possible to |
| | use an argument n for the weekday instances, which will mean the nth |
| | occurrence of this weekday in the period. For example, with MONTHLY, |
| | or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the |
| | first friday of the month where the recurrence happens. Notice that in |
| | the RFC documentation, this is specified as BYDAY, but was renamed to |
| | avoid the ambiguity of that keyword. |
| | :param byhour: |
| | If given, it must be either an integer, or a sequence of integers, |
| | meaning the hours to apply the recurrence to. |
| | :param byminute: |
| | If given, it must be either an integer, or a sequence of integers, |
| | meaning the minutes to apply the recurrence to. |
| | :param bysecond: |
| | If given, it must be either an integer, or a sequence of integers, |
| | meaning the seconds to apply the recurrence to. |
| | :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, |
| | bymonth=None, bymonthday=None, byyearday=None, byeaster=None, |
| | byweekno=None, byweekday=None, |
| | byhour=None, byminute=None, bysecond=None, |
| | cache=False): |
| | super(rrule, self).__init__(cache) |
| | global easter |
| | if not dtstart: |
| | 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: |
| | dtstart = dtstart.replace(microsecond=0) |
| | self._dtstart = dtstart |
| | self._tzinfo = dtstart.tzinfo |
| | self._freq = freq |
| | self._interval = interval |
| | self._count = count |
| |
|
| | |
| | |
| | |
| | |
| | self._original_rule = {} |
| |
|
| | if until and not isinstance(until, datetime.datetime): |
| | until = datetime.datetime.fromordinal(until.toordinal()) |
| | self._until = until |
| |
|
| | if self._dtstart and self._until: |
| | if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None): |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | raise ValueError( |
| | 'RRULE UNTIL values must be specified in UTC when DTSTART ' |
| | 'is timezone-aware' |
| | ) |
| |
|
| | if count is not None and until: |
| | warn("Using both 'count' and 'until' is inconsistent with RFC 5545" |
| | " and has been deprecated in dateutil. Future versions will " |
| | "raise an error.", DeprecationWarning) |
| |
|
| | if wkst is None: |
| | self._wkst = calendar.firstweekday() |
| | elif isinstance(wkst, integer_types): |
| | self._wkst = wkst |
| | else: |
| | self._wkst = wkst.weekday |
| |
|
| | if bysetpos is None: |
| | self._bysetpos = None |
| | elif isinstance(bysetpos, integer_types): |
| | if bysetpos == 0 or not (-366 <= bysetpos <= 366): |
| | raise ValueError("bysetpos must be between 1 and 366, " |
| | "or between -366 and -1") |
| | self._bysetpos = (bysetpos,) |
| | else: |
| | self._bysetpos = tuple(bysetpos) |
| | for pos in self._bysetpos: |
| | if pos == 0 or not (-366 <= pos <= 366): |
| | raise ValueError("bysetpos must be between 1 and 366, " |
| | "or between -366 and -1") |
| |
|
| | if self._bysetpos: |
| | self._original_rule['bysetpos'] = self._bysetpos |
| |
|
| | if (byweekno is None and byyearday is None and bymonthday is None and |
| | byweekday is None and byeaster is None): |
| | if freq == YEARLY: |
| | if bymonth is None: |
| | bymonth = dtstart.month |
| | self._original_rule['bymonth'] = None |
| | bymonthday = dtstart.day |
| | self._original_rule['bymonthday'] = None |
| | elif freq == MONTHLY: |
| | bymonthday = dtstart.day |
| | self._original_rule['bymonthday'] = None |
| | elif freq == WEEKLY: |
| | byweekday = dtstart.weekday() |
| | self._original_rule['byweekday'] = None |
| |
|
| | |
| | if bymonth is None: |
| | self._bymonth = None |
| | else: |
| | if isinstance(bymonth, integer_types): |
| | bymonth = (bymonth,) |
| |
|
| | self._bymonth = tuple(sorted(set(bymonth))) |
| |
|
| | if 'bymonth' not in self._original_rule: |
| | self._original_rule['bymonth'] = self._bymonth |
| |
|
| | |
| | if byyearday is None: |
| | self._byyearday = None |
| | else: |
| | if isinstance(byyearday, integer_types): |
| | byyearday = (byyearday,) |
| |
|
| | self._byyearday = tuple(sorted(set(byyearday))) |
| | self._original_rule['byyearday'] = self._byyearday |
| |
|
| | |
| | if byeaster is not None: |
| | if not easter: |
| | from dateutil import easter |
| | if isinstance(byeaster, integer_types): |
| | self._byeaster = (byeaster,) |
| | else: |
| | self._byeaster = tuple(sorted(byeaster)) |
| |
|
| | self._original_rule['byeaster'] = self._byeaster |
| | else: |
| | self._byeaster = None |
| |
|
| | |
| | if bymonthday is None: |
| | self._bymonthday = () |
| | self._bynmonthday = () |
| | else: |
| | if isinstance(bymonthday, integer_types): |
| | bymonthday = (bymonthday,) |
| |
|
| | bymonthday = set(bymonthday) |
| |
|
| | self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0)) |
| | self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0)) |
| |
|
| | |
| | if 'bymonthday' not in self._original_rule: |
| | self._original_rule['bymonthday'] = tuple( |
| | itertools.chain(self._bymonthday, self._bynmonthday)) |
| |
|
| | |
| | if byweekno is None: |
| | self._byweekno = None |
| | else: |
| | if isinstance(byweekno, integer_types): |
| | byweekno = (byweekno,) |
| |
|
| | self._byweekno = tuple(sorted(set(byweekno))) |
| |
|
| | self._original_rule['byweekno'] = self._byweekno |
| |
|
| | |
| | if byweekday is None: |
| | self._byweekday = None |
| | self._bynweekday = None |
| | else: |
| | |
| | |
| | |
| | if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"): |
| | byweekday = (byweekday,) |
| |
|
| | self._byweekday = set() |
| | self._bynweekday = set() |
| | for wday in byweekday: |
| | if isinstance(wday, integer_types): |
| | self._byweekday.add(wday) |
| | elif not wday.n or freq > MONTHLY: |
| | self._byweekday.add(wday.weekday) |
| | else: |
| | self._bynweekday.add((wday.weekday, wday.n)) |
| |
|
| | if not self._byweekday: |
| | self._byweekday = None |
| | elif not self._bynweekday: |
| | self._bynweekday = None |
| |
|
| | if self._byweekday is not None: |
| | self._byweekday = tuple(sorted(self._byweekday)) |
| | orig_byweekday = [weekday(x) for x in self._byweekday] |
| | else: |
| | orig_byweekday = () |
| |
|
| | if self._bynweekday is not None: |
| | self._bynweekday = tuple(sorted(self._bynweekday)) |
| | orig_bynweekday = [weekday(*x) for x in self._bynweekday] |
| | else: |
| | orig_bynweekday = () |
| |
|
| | if 'byweekday' not in self._original_rule: |
| | self._original_rule['byweekday'] = tuple(itertools.chain( |
| | orig_byweekday, orig_bynweekday)) |
| |
|
| | |
| | if byhour is None: |
| | if freq < HOURLY: |
| | self._byhour = {dtstart.hour} |
| | else: |
| | self._byhour = None |
| | else: |
| | if isinstance(byhour, integer_types): |
| | byhour = (byhour,) |
| |
|
| | if freq == HOURLY: |
| | self._byhour = self.__construct_byset(start=dtstart.hour, |
| | byxxx=byhour, |
| | base=24) |
| | else: |
| | self._byhour = set(byhour) |
| |
|
| | self._byhour = tuple(sorted(self._byhour)) |
| | self._original_rule['byhour'] = self._byhour |
| |
|
| | |
| | if byminute is None: |
| | if freq < MINUTELY: |
| | self._byminute = {dtstart.minute} |
| | else: |
| | self._byminute = None |
| | else: |
| | if isinstance(byminute, integer_types): |
| | byminute = (byminute,) |
| |
|
| | if freq == MINUTELY: |
| | self._byminute = self.__construct_byset(start=dtstart.minute, |
| | byxxx=byminute, |
| | base=60) |
| | else: |
| | self._byminute = set(byminute) |
| |
|
| | self._byminute = tuple(sorted(self._byminute)) |
| | self._original_rule['byminute'] = self._byminute |
| |
|
| | |
| | if bysecond is None: |
| | if freq < SECONDLY: |
| | self._bysecond = ((dtstart.second,)) |
| | else: |
| | self._bysecond = None |
| | else: |
| | if isinstance(bysecond, integer_types): |
| | bysecond = (bysecond,) |
| |
|
| | self._bysecond = set(bysecond) |
| |
|
| | if freq == SECONDLY: |
| | self._bysecond = self.__construct_byset(start=dtstart.second, |
| | byxxx=bysecond, |
| | base=60) |
| | else: |
| | self._bysecond = set(bysecond) |
| |
|
| | self._bysecond = tuple(sorted(self._bysecond)) |
| | self._original_rule['bysecond'] = self._bysecond |
| |
|
| | if self._freq >= HOURLY: |
| | self._timeset = None |
| | else: |
| | self._timeset = [] |
| | for hour in self._byhour: |
| | for minute in self._byminute: |
| | for second in self._bysecond: |
| | self._timeset.append( |
| | datetime.time(hour, minute, second, |
| | tzinfo=self._tzinfo)) |
| | self._timeset.sort() |
| | self._timeset = tuple(self._timeset) |
| |
|
| | def __str__(self): |
| | """ |
| | Output a string that would generate this RRULE if passed to rrulestr. |
| | This is mostly compatible with RFC5545, except for the |
| | dateutil-specific extension BYEASTER. |
| | """ |
| |
|
| | output = [] |
| | h, m, s = [None] * 3 |
| | if self._dtstart: |
| | output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S')) |
| | h, m, s = self._dtstart.timetuple()[3:6] |
| |
|
| | parts = ['FREQ=' + FREQNAMES[self._freq]] |
| | if self._interval != 1: |
| | parts.append('INTERVAL=' + str(self._interval)) |
| |
|
| | if self._wkst: |
| | parts.append('WKST=' + repr(weekday(self._wkst))[0:2]) |
| |
|
| | if self._count is not None: |
| | parts.append('COUNT=' + str(self._count)) |
| |
|
| | if self._until: |
| | parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S')) |
| |
|
| | if self._original_rule.get('byweekday') is not None: |
| | |
| | |
| | original_rule = dict(self._original_rule) |
| | wday_strings = [] |
| | for wday in original_rule['byweekday']: |
| | if wday.n: |
| | wday_strings.append('{n:+d}{wday}'.format( |
| | n=wday.n, |
| | wday=repr(wday)[0:2])) |
| | else: |
| | wday_strings.append(repr(wday)) |
| |
|
| | original_rule['byweekday'] = wday_strings |
| | else: |
| | original_rule = self._original_rule |
| |
|
| | partfmt = '{name}={vals}' |
| | for name, key in [('BYSETPOS', 'bysetpos'), |
| | ('BYMONTH', 'bymonth'), |
| | ('BYMONTHDAY', 'bymonthday'), |
| | ('BYYEARDAY', 'byyearday'), |
| | ('BYWEEKNO', 'byweekno'), |
| | ('BYDAY', 'byweekday'), |
| | ('BYHOUR', 'byhour'), |
| | ('BYMINUTE', 'byminute'), |
| | ('BYSECOND', 'bysecond'), |
| | ('BYEASTER', 'byeaster')]: |
| | value = original_rule.get(key) |
| | if value: |
| | parts.append(partfmt.format(name=name, vals=(','.join(str(v) |
| | for v in value)))) |
| |
|
| | output.append('RRULE:' + ';'.join(parts)) |
| | return '\n'.join(output) |
| |
|
| | def replace(self, **kwargs): |
| | """Return new rrule with same attributes except for those attributes given new |
| | values by whichever keyword arguments are specified.""" |
| | new_kwargs = {"interval": self._interval, |
| | "count": self._count, |
| | "dtstart": self._dtstart, |
| | "freq": self._freq, |
| | "until": self._until, |
| | "wkst": self._wkst, |
| | "cache": False if self._cache is None else True } |
| | new_kwargs.update(self._original_rule) |
| | new_kwargs.update(kwargs) |
| | return rrule(**new_kwargs) |
| |
|
| | def _iter(self): |
| | year, month, day, hour, minute, second, weekday, yearday, _ = \ |
| | self._dtstart.timetuple() |
| |
|
| | |
| | freq = self._freq |
| | interval = self._interval |
| | wkst = self._wkst |
| | until = self._until |
| | bymonth = self._bymonth |
| | byweekno = self._byweekno |
| | byyearday = self._byyearday |
| | byweekday = self._byweekday |
| | byeaster = self._byeaster |
| | bymonthday = self._bymonthday |
| | bynmonthday = self._bynmonthday |
| | bysetpos = self._bysetpos |
| | byhour = self._byhour |
| | byminute = self._byminute |
| | bysecond = self._bysecond |
| |
|
| | ii = _iterinfo(self) |
| | ii.rebuild(year, month) |
| |
|
| | getdayset = {YEARLY: ii.ydayset, |
| | MONTHLY: ii.mdayset, |
| | WEEKLY: ii.wdayset, |
| | DAILY: ii.ddayset, |
| | HOURLY: ii.ddayset, |
| | MINUTELY: ii.ddayset, |
| | SECONDLY: ii.ddayset}[freq] |
| |
|
| | if freq < HOURLY: |
| | timeset = self._timeset |
| | else: |
| | gettimeset = {HOURLY: ii.htimeset, |
| | MINUTELY: ii.mtimeset, |
| | SECONDLY: ii.stimeset}[freq] |
| | if ((freq >= HOURLY and |
| | self._byhour and hour not in self._byhour) or |
| | (freq >= MINUTELY and |
| | self._byminute and minute not in self._byminute) or |
| | (freq >= SECONDLY and |
| | self._bysecond and second not in self._bysecond)): |
| | timeset = () |
| | else: |
| | timeset = gettimeset(hour, minute, second) |
| |
|
| | total = 0 |
| | count = self._count |
| | while True: |
| | |
| | dayset, start, end = getdayset(year, month, day) |
| |
|
| | |
| | filtered = False |
| | for i in dayset[start:end]: |
| | if ((bymonth and ii.mmask[i] not in bymonth) or |
| | (byweekno and not ii.wnomask[i]) or |
| | (byweekday and ii.wdaymask[i] not in byweekday) or |
| | (ii.nwdaymask and not ii.nwdaymask[i]) or |
| | (byeaster and not ii.eastermask[i]) or |
| | ((bymonthday or bynmonthday) and |
| | ii.mdaymask[i] not in bymonthday and |
| | ii.nmdaymask[i] not in bynmonthday) or |
| | (byyearday and |
| | ((i < ii.yearlen and i+1 not in byyearday and |
| | -ii.yearlen+i not in byyearday) or |
| | (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and |
| | -ii.nextyearlen+i-ii.yearlen not in byyearday)))): |
| | dayset[i] = None |
| | filtered = True |
| |
|
| | |
| | if bysetpos and timeset: |
| | poslist = [] |
| | for pos in bysetpos: |
| | if pos < 0: |
| | daypos, timepos = divmod(pos, len(timeset)) |
| | else: |
| | daypos, timepos = divmod(pos-1, len(timeset)) |
| | try: |
| | i = [x for x in dayset[start:end] |
| | if x is not None][daypos] |
| | time = timeset[timepos] |
| | except IndexError: |
| | pass |
| | else: |
| | date = datetime.date.fromordinal(ii.yearordinal+i) |
| | res = datetime.datetime.combine(date, time) |
| | if res not in poslist: |
| | poslist.append(res) |
| | poslist.sort() |
| | for res in poslist: |
| | if until and res > until: |
| | self._len = total |
| | return |
| | elif res >= self._dtstart: |
| | if count is not None: |
| | count -= 1 |
| | if count < 0: |
| | self._len = total |
| | return |
| | total += 1 |
| | yield res |
| | else: |
| | for i in dayset[start:end]: |
| | if i is not None: |
| | date = datetime.date.fromordinal(ii.yearordinal + i) |
| | for time in timeset: |
| | res = datetime.datetime.combine(date, time) |
| | if until and res > until: |
| | self._len = total |
| | return |
| | elif res >= self._dtstart: |
| | if count is not None: |
| | count -= 1 |
| | if count < 0: |
| | self._len = total |
| | return |
| |
|
| | total += 1 |
| | yield res |
| |
|
| | |
| | fixday = False |
| | if freq == YEARLY: |
| | year += interval |
| | if year > datetime.MAXYEAR: |
| | self._len = total |
| | return |
| | ii.rebuild(year, month) |
| | elif freq == MONTHLY: |
| | month += interval |
| | if month > 12: |
| | div, mod = divmod(month, 12) |
| | month = mod |
| | year += div |
| | if month == 0: |
| | month = 12 |
| | year -= 1 |
| | if year > datetime.MAXYEAR: |
| | self._len = total |
| | return |
| | ii.rebuild(year, month) |
| | elif freq == WEEKLY: |
| | if wkst > weekday: |
| | day += -(weekday+1+(6-wkst))+self._interval*7 |
| | else: |
| | day += -(weekday-wkst)+self._interval*7 |
| | weekday = wkst |
| | fixday = True |
| | elif freq == DAILY: |
| | day += interval |
| | fixday = True |
| | elif freq == HOURLY: |
| | if filtered: |
| | |
| | hour += ((23-hour)//interval)*interval |
| |
|
| | if byhour: |
| | ndays, hour = self.__mod_distance(value=hour, |
| | byxxx=self._byhour, |
| | base=24) |
| | else: |
| | ndays, hour = divmod(hour+interval, 24) |
| |
|
| | if ndays: |
| | day += ndays |
| | fixday = True |
| |
|
| | timeset = gettimeset(hour, minute, second) |
| | elif freq == MINUTELY: |
| | if filtered: |
| | |
| | minute += ((1439-(hour*60+minute))//interval)*interval |
| |
|
| | valid = False |
| | rep_rate = (24*60) |
| | for j in range(rep_rate // gcd(interval, rep_rate)): |
| | if byminute: |
| | nhours, minute = \ |
| | self.__mod_distance(value=minute, |
| | byxxx=self._byminute, |
| | base=60) |
| | else: |
| | nhours, minute = divmod(minute+interval, 60) |
| |
|
| | div, hour = divmod(hour+nhours, 24) |
| | if div: |
| | day += div |
| | fixday = True |
| | filtered = False |
| |
|
| | if not byhour or hour in byhour: |
| | valid = True |
| | break |
| |
|
| | if not valid: |
| | raise ValueError('Invalid combination of interval and ' + |
| | 'byhour resulting in empty rule.') |
| |
|
| | timeset = gettimeset(hour, minute, second) |
| | elif freq == SECONDLY: |
| | if filtered: |
| | |
| | second += (((86399 - (hour * 3600 + minute * 60 + second)) |
| | // interval) * interval) |
| |
|
| | rep_rate = (24 * 3600) |
| | valid = False |
| | for j in range(0, rep_rate // gcd(interval, rep_rate)): |
| | if bysecond: |
| | nminutes, second = \ |
| | self.__mod_distance(value=second, |
| | byxxx=self._bysecond, |
| | base=60) |
| | else: |
| | nminutes, second = divmod(second+interval, 60) |
| |
|
| | div, minute = divmod(minute+nminutes, 60) |
| | if div: |
| | hour += div |
| | div, hour = divmod(hour, 24) |
| | if div: |
| | day += div |
| | fixday = True |
| |
|
| | if ((not byhour or hour in byhour) and |
| | (not byminute or minute in byminute) and |
| | (not bysecond or second in bysecond)): |
| | valid = True |
| | break |
| |
|
| | if not valid: |
| | raise ValueError('Invalid combination of interval, ' + |
| | 'byhour and byminute resulting in empty' + |
| | ' rule.') |
| |
|
| | timeset = gettimeset(hour, minute, second) |
| |
|
| | if fixday and day > 28: |
| | daysinmonth = calendar.monthrange(year, month)[1] |
| | if day > daysinmonth: |
| | while day > daysinmonth: |
| | day -= daysinmonth |
| | month += 1 |
| | if month == 13: |
| | month = 1 |
| | year += 1 |
| | if year > datetime.MAXYEAR: |
| | self._len = total |
| | return |
| | daysinmonth = calendar.monthrange(year, month)[1] |
| | ii.rebuild(year, month) |
| |
|
| | def __construct_byset(self, start, byxxx, base): |
| | """ |
| | If a `BYXXX` sequence is passed to the constructor at the same level as |
| | `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some |
| | specifications which cannot be reached given some starting conditions. |
| | |
| | This occurs whenever the interval is not coprime with the base of a |
| | given unit and the difference between the starting position and the |
| | ending position is not coprime with the greatest common denominator |
| | between the interval and the base. For example, with a FREQ of hourly |
| | starting at 17:00 and an interval of 4, the only valid values for |
| | BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not |
| | coprime. |
| | |
| | :param start: |
| | Specifies the starting position. |
| | :param byxxx: |
| | An iterable containing the list of allowed values. |
| | :param base: |
| | The largest allowable value for the specified frequency (e.g. |
| | 24 hours, 60 minutes). |
| | |
| | This does not preserve the type of the iterable, returning a set, since |
| | the values should be unique and the order is irrelevant, this will |
| | speed up later lookups. |
| | |
| | In the event of an empty set, raises a :exception:`ValueError`, as this |
| | results in an empty rrule. |
| | """ |
| |
|
| | cset = set() |
| |
|
| | |
| | if isinstance(byxxx, integer_types): |
| | byxxx = (byxxx, ) |
| |
|
| | for num in byxxx: |
| | i_gcd = gcd(self._interval, base) |
| | |
| | if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0: |
| | cset.add(num) |
| |
|
| | if len(cset) == 0: |
| | raise ValueError("Invalid rrule byxxx generates an empty set.") |
| |
|
| | return cset |
| |
|
| | def __mod_distance(self, value, byxxx, base): |
| | """ |
| | Calculates the next value in a sequence where the `FREQ` parameter is |
| | specified along with a `BYXXX` parameter at the same "level" |
| | (e.g. `HOURLY` specified with `BYHOUR`). |
| | |
| | :param value: |
| | The old value of the component. |
| | :param byxxx: |
| | The `BYXXX` set, which should have been generated by |
| | `rrule._construct_byset`, or something else which checks that a |
| | valid rule is present. |
| | :param base: |
| | The largest allowable value for the specified frequency (e.g. |
| | 24 hours, 60 minutes). |
| | |
| | If a valid value is not found after `base` iterations (the maximum |
| | number before the sequence would start to repeat), this raises a |
| | :exception:`ValueError`, as no valid values were found. |
| | |
| | This returns a tuple of `divmod(n*interval, base)`, where `n` is the |
| | smallest number of `interval` repetitions until the next specified |
| | value in `byxxx` is found. |
| | """ |
| | accumulator = 0 |
| | for ii in range(1, base + 1): |
| | |
| | div, value = divmod(value + self._interval, base) |
| | accumulator += div |
| | if value in byxxx: |
| | return (accumulator, value) |
| |
|
| |
|
| | class _iterinfo(object): |
| | __slots__ = ["rrule", "lastyear", "lastmonth", |
| | "yearlen", "nextyearlen", "yearordinal", "yearweekday", |
| | "mmask", "mrange", "mdaymask", "nmdaymask", |
| | "wdaymask", "wnomask", "nwdaymask", "eastermask"] |
| |
|
| | def __init__(self, rrule): |
| | for attr in self.__slots__: |
| | setattr(self, attr, None) |
| | self.rrule = rrule |
| |
|
| | def rebuild(self, year, month): |
| | |
| | rr = self.rrule |
| | if year != self.lastyear: |
| | self.yearlen = 365 + calendar.isleap(year) |
| | self.nextyearlen = 365 + calendar.isleap(year + 1) |
| | firstyday = datetime.date(year, 1, 1) |
| | self.yearordinal = firstyday.toordinal() |
| | self.yearweekday = firstyday.weekday() |
| |
|
| | wday = datetime.date(year, 1, 1).weekday() |
| | if self.yearlen == 365: |
| | self.mmask = M365MASK |
| | self.mdaymask = MDAY365MASK |
| | self.nmdaymask = NMDAY365MASK |
| | self.wdaymask = WDAYMASK[wday:] |
| | self.mrange = M365RANGE |
| | else: |
| | self.mmask = M366MASK |
| | self.mdaymask = MDAY366MASK |
| | self.nmdaymask = NMDAY366MASK |
| | self.wdaymask = WDAYMASK[wday:] |
| | self.mrange = M366RANGE |
| |
|
| | if not rr._byweekno: |
| | self.wnomask = None |
| | else: |
| | self.wnomask = [0]*(self.yearlen+7) |
| | |
| | no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7 |
| | if no1wkst >= 4: |
| | no1wkst = 0 |
| | |
| | |
| | wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7 |
| | else: |
| | |
| | |
| | wyearlen = self.yearlen-no1wkst |
| | div, mod = divmod(wyearlen, 7) |
| | numweeks = div+mod//4 |
| | for n in rr._byweekno: |
| | if n < 0: |
| | n += numweeks+1 |
| | if not (0 < n <= numweeks): |
| | continue |
| | if n > 1: |
| | i = no1wkst+(n-1)*7 |
| | if no1wkst != firstwkst: |
| | i -= 7-firstwkst |
| | else: |
| | i = no1wkst |
| | for j in range(7): |
| | self.wnomask[i] = 1 |
| | i += 1 |
| | if self.wdaymask[i] == rr._wkst: |
| | break |
| | if 1 in rr._byweekno: |
| | |
| | |
| | i = no1wkst+numweeks*7 |
| | if no1wkst != firstwkst: |
| | i -= 7-firstwkst |
| | if i < self.yearlen: |
| | |
| | |
| | for j in range(7): |
| | self.wnomask[i] = 1 |
| | i += 1 |
| | if self.wdaymask[i] == rr._wkst: |
| | break |
| | if no1wkst: |
| | |
| | |
| | |
| | |
| | |
| | |
| | if -1 not in rr._byweekno: |
| | lyearweekday = datetime.date(year-1, 1, 1).weekday() |
| | lno1wkst = (7-lyearweekday+rr._wkst) % 7 |
| | lyearlen = 365+calendar.isleap(year-1) |
| | if lno1wkst >= 4: |
| | lno1wkst = 0 |
| | lnumweeks = 52+(lyearlen + |
| | (lyearweekday-rr._wkst) % 7) % 7//4 |
| | else: |
| | lnumweeks = 52+(self.yearlen-no1wkst) % 7//4 |
| | else: |
| | lnumweeks = -1 |
| | if lnumweeks in rr._byweekno: |
| | for i in range(no1wkst): |
| | self.wnomask[i] = 1 |
| |
|
| | if (rr._bynweekday and (month != self.lastmonth or |
| | year != self.lastyear)): |
| | ranges = [] |
| | if rr._freq == YEARLY: |
| | if rr._bymonth: |
| | for month in rr._bymonth: |
| | ranges.append(self.mrange[month-1:month+1]) |
| | else: |
| | ranges = [(0, self.yearlen)] |
| | elif rr._freq == MONTHLY: |
| | ranges = [self.mrange[month-1:month+1]] |
| | if ranges: |
| | |
| | |
| | self.nwdaymask = [0]*self.yearlen |
| | for first, last in ranges: |
| | last -= 1 |
| | for wday, n in rr._bynweekday: |
| | if n < 0: |
| | i = last+(n+1)*7 |
| | i -= (self.wdaymask[i]-wday) % 7 |
| | else: |
| | i = first+(n-1)*7 |
| | i += (7-self.wdaymask[i]+wday) % 7 |
| | if first <= i <= last: |
| | self.nwdaymask[i] = 1 |
| |
|
| | if rr._byeaster: |
| | self.eastermask = [0]*(self.yearlen+7) |
| | eyday = easter.easter(year).toordinal()-self.yearordinal |
| | for offset in rr._byeaster: |
| | self.eastermask[eyday+offset] = 1 |
| |
|
| | self.lastyear = year |
| | self.lastmonth = month |
| |
|
| | def ydayset(self, year, month, day): |
| | return list(range(self.yearlen)), 0, self.yearlen |
| |
|
| | def mdayset(self, year, month, day): |
| | dset = [None]*self.yearlen |
| | start, end = self.mrange[month-1:month+1] |
| | for i in range(start, end): |
| | dset[i] = i |
| | return dset, start, end |
| |
|
| | def wdayset(self, year, month, day): |
| | |
| | dset = [None]*(self.yearlen+7) |
| | i = datetime.date(year, month, day).toordinal()-self.yearordinal |
| | start = i |
| | for j in range(7): |
| | dset[i] = i |
| | i += 1 |
| | |
| | |
| | |
| | if self.wdaymask[i] == self.rrule._wkst: |
| | break |
| | return dset, start, i |
| |
|
| | def ddayset(self, year, month, day): |
| | dset = [None] * self.yearlen |
| | i = datetime.date(year, month, day).toordinal() - self.yearordinal |
| | dset[i] = i |
| | return dset, i, i + 1 |
| |
|
| | def htimeset(self, hour, minute, second): |
| | tset = [] |
| | rr = self.rrule |
| | for minute in rr._byminute: |
| | for second in rr._bysecond: |
| | tset.append(datetime.time(hour, minute, second, |
| | tzinfo=rr._tzinfo)) |
| | tset.sort() |
| | return tset |
| |
|
| | def mtimeset(self, hour, minute, second): |
| | tset = [] |
| | rr = self.rrule |
| | for second in rr._bysecond: |
| | tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo)) |
| | tset.sort() |
| | return tset |
| |
|
| | def stimeset(self, hour, minute, second): |
| | return (datetime.time(hour, minute, second, |
| | tzinfo=self.rrule._tzinfo),) |
| |
|
| |
|
| | class rruleset(rrulebase): |
| | """ The rruleset type allows more complex recurrence setups, mixing |
| | multiple rules, dates, exclusion rules, and exclusion dates. The type |
| | constructor takes the following keyword arguments: |
| | |
| | :param cache: If True, caching of results will be enabled, improving |
| | performance of multiple queries considerably. """ |
| |
|
| | class _genitem(object): |
| | def __init__(self, genlist, gen): |
| | try: |
| | self.dt = advance_iterator(gen) |
| | genlist.append(self) |
| | except StopIteration: |
| | pass |
| | self.genlist = genlist |
| | self.gen = gen |
| |
|
| | def __next__(self): |
| | try: |
| | self.dt = advance_iterator(self.gen) |
| | except StopIteration: |
| | if self.genlist[0] is self: |
| | heapq.heappop(self.genlist) |
| | else: |
| | self.genlist.remove(self) |
| | heapq.heapify(self.genlist) |
| |
|
| | next = __next__ |
| |
|
| | def __lt__(self, other): |
| | return self.dt < other.dt |
| |
|
| | def __gt__(self, other): |
| | return self.dt > other.dt |
| |
|
| | def __eq__(self, other): |
| | return self.dt == other.dt |
| |
|
| | def __ne__(self, other): |
| | return self.dt != other.dt |
| |
|
| | def __init__(self, cache=False): |
| | super(rruleset, self).__init__(cache) |
| | self._rrule = [] |
| | self._rdate = [] |
| | self._exrule = [] |
| | self._exdate = [] |
| |
|
| | @_invalidates_cache |
| | def rrule(self, rrule): |
| | """ Include the given :py:class:`rrule` instance in the recurrence set |
| | generation. """ |
| | self._rrule.append(rrule) |
| |
|
| | @_invalidates_cache |
| | def rdate(self, rdate): |
| | """ Include the given :py:class:`datetime` instance in the recurrence |
| | set generation. """ |
| | self._rdate.append(rdate) |
| |
|
| | @_invalidates_cache |
| | def exrule(self, exrule): |
| | """ Include the given rrule instance in the recurrence set exclusion |
| | list. Dates which are part of the given recurrence rules will not |
| | be generated, even if some inclusive rrule or rdate matches them. |
| | """ |
| | self._exrule.append(exrule) |
| |
|
| | @_invalidates_cache |
| | def exdate(self, exdate): |
| | """ Include the given datetime instance in the recurrence set |
| | exclusion list. Dates included that way will not be generated, |
| | even if some inclusive rrule or rdate matches them. """ |
| | self._exdate.append(exdate) |
| |
|
| | def _iter(self): |
| | rlist = [] |
| | self._rdate.sort() |
| | self._genitem(rlist, iter(self._rdate)) |
| | for gen in [iter(x) for x in self._rrule]: |
| | self._genitem(rlist, gen) |
| | exlist = [] |
| | self._exdate.sort() |
| | self._genitem(exlist, iter(self._exdate)) |
| | for gen in [iter(x) for x in self._exrule]: |
| | self._genitem(exlist, gen) |
| | lastdt = None |
| | total = 0 |
| | heapq.heapify(rlist) |
| | heapq.heapify(exlist) |
| | while rlist: |
| | ritem = rlist[0] |
| | if not lastdt or lastdt != ritem.dt: |
| | while exlist and exlist[0] < ritem: |
| | exitem = exlist[0] |
| | advance_iterator(exitem) |
| | if exlist and exlist[0] is exitem: |
| | heapq.heapreplace(exlist, exitem) |
| | if not exlist or ritem != exlist[0]: |
| | total += 1 |
| | yield ritem.dt |
| | lastdt = ritem.dt |
| | advance_iterator(ritem) |
| | if rlist and rlist[0] is ritem: |
| | heapq.heapreplace(rlist, ritem) |
| | 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, |
| | "WEEKLY": WEEKLY, |
| | "DAILY": DAILY, |
| | "HOURLY": HOURLY, |
| | "MINUTELY": MINUTELY, |
| | "SECONDLY": SECONDLY} |
| |
|
| | _weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3, |
| | "FR": 4, "SA": 5, "SU": 6} |
| |
|
| | def _handle_int(self, rrkwargs, name, value, **kwargs): |
| | rrkwargs[name.lower()] = int(value) |
| |
|
| | def _handle_int_list(self, rrkwargs, name, value, **kwargs): |
| | rrkwargs[name.lower()] = [int(x) for x in value.split(',')] |
| |
|
| | _handle_INTERVAL = _handle_int |
| | _handle_COUNT = _handle_int |
| | _handle_BYSETPOS = _handle_int_list |
| | _handle_BYMONTH = _handle_int_list |
| | _handle_BYMONTHDAY = _handle_int_list |
| | _handle_BYYEARDAY = _handle_int_list |
| | _handle_BYEASTER = _handle_int_list |
| | _handle_BYWEEKNO = _handle_int_list |
| | _handle_BYHOUR = _handle_int_list |
| | _handle_BYMINUTE = _handle_int_list |
| | _handle_BYSECOND = _handle_int_list |
| |
|
| | def _handle_FREQ(self, rrkwargs, name, value, **kwargs): |
| | rrkwargs["freq"] = self._freq_map[value] |
| |
|
| | def _handle_UNTIL(self, rrkwargs, name, value, **kwargs): |
| | global parser |
| | if not parser: |
| | from dateutil import parser |
| | try: |
| | rrkwargs["until"] = parser.parse(value, |
| | ignoretz=kwargs.get("ignoretz"), |
| | tzinfos=kwargs.get("tzinfos")) |
| | except ValueError: |
| | raise ValueError("invalid until date") |
| |
|
| | def _handle_WKST(self, rrkwargs, name, value, **kwargs): |
| | rrkwargs["wkst"] = self._weekday_map[value] |
| |
|
| | def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs): |
| | """ |
| | Two ways to specify this: +1MO or MO(+1) |
| | """ |
| | l = [] |
| | for wday in value.split(','): |
| | if '(' in wday: |
| | |
| | splt = wday.split('(') |
| | w = splt[0] |
| | n = int(splt[1][:-1]) |
| | elif len(wday): |
| | |
| | for i in range(len(wday)): |
| | if wday[i] not in '+-0123456789': |
| | break |
| | n = wday[:i] or None |
| | w = wday[i:] |
| | if n: |
| | n = int(n) |
| | else: |
| | raise ValueError("Invalid (empty) BYDAY specification.") |
| |
|
| | l.append(weekdays[self._weekday_map[w]](n)) |
| | rrkwargs["byweekday"] = l |
| |
|
| | _handle_BYDAY = _handle_BYWEEKDAY |
| |
|
| | def _parse_rfc_rrule(self, line, |
| | dtstart=None, |
| | cache=False, |
| | ignoretz=False, |
| | tzinfos=None): |
| | if line.find(':') != -1: |
| | name, value = line.split(':') |
| | if name != "RRULE": |
| | raise ValueError("unknown parameter name") |
| | else: |
| | value = line |
| | rrkwargs = {} |
| | for pair in value.split(';'): |
| | name, value = pair.split('=') |
| | name = name.upper() |
| | value = value.upper() |
| | try: |
| | getattr(self, "_handle_"+name)(rrkwargs, name, value, |
| | ignoretz=ignoretz, |
| | tzinfos=tzinfos) |
| | except AttributeError: |
| | raise ValueError("unknown parameter '%s'" % name) |
| | except (KeyError, ValueError): |
| | 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 |
| |
|
| | |
| | |
| | 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, |
| | unfold=False, |
| | forceset=False, |
| | compatible=False, |
| | ignoretz=False, |
| | tzids=None, |
| | tzinfos=None): |
| | global parser |
| | if compatible: |
| | forceset = True |
| | unfold = True |
| |
|
| | TZID_NAMES = dict(map( |
| | lambda x: (x.upper(), x), |
| | re.findall('TZID=(?P<name>[^:]+):', s) |
| | )) |
| | s = s.upper() |
| | if not s.strip(): |
| | raise ValueError("empty string") |
| | if unfold: |
| | lines = s.splitlines() |
| | i = 0 |
| | while i < len(lines): |
| | line = lines[i].rstrip() |
| | if not line: |
| | del lines[i] |
| | elif i > 0 and line[0] == " ": |
| | lines[i-1] += line[1:] |
| | del lines[i] |
| | else: |
| | i += 1 |
| | else: |
| | lines = s.split() |
| | if (not forceset and len(lines) == 1 and (s.find(':') == -1 or |
| | s.startswith('RRULE:'))): |
| | return self._parse_rfc_rrule(lines[0], cache=cache, |
| | dtstart=dtstart, ignoretz=ignoretz, |
| | tzinfos=tzinfos) |
| | else: |
| | rrulevals = [] |
| | rdatevals = [] |
| | exrulevals = [] |
| | exdatevals = [] |
| | for line in lines: |
| | if not line: |
| | continue |
| | if line.find(':') == -1: |
| | name = "RRULE" |
| | value = line |
| | else: |
| | name, value = line.split(':', 1) |
| | parms = name.split(';') |
| | if not parms: |
| | raise ValueError("empty property name") |
| | name = parms[0] |
| | parms = parms[1:] |
| | if name == "RRULE": |
| | for parm in parms: |
| | raise ValueError("unsupported RRULE parm: "+parm) |
| | rrulevals.append(value) |
| | elif name == "RDATE": |
| | for parm in parms: |
| | if parm != "VALUE=DATE-TIME": |
| | raise ValueError("unsupported RDATE parm: "+parm) |
| | rdatevals.append(value) |
| | elif name == "EXRULE": |
| | for parm in parms: |
| | raise ValueError("unsupported EXRULE parm: "+parm) |
| | exrulevals.append(value) |
| | elif name == "EXDATE": |
| | exdatevals.extend( |
| | self._parse_date_value(value, parms, |
| | TZID_NAMES, ignoretz, |
| | tzids, tzinfos) |
| | ) |
| | elif name == "DTSTART": |
| | 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 |
| | or exrulevals or exdatevals): |
| | if not parser and (rdatevals or exdatevals): |
| | from dateutil import parser |
| | rset = rruleset(cache=cache) |
| | for value in rrulevals: |
| | rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart, |
| | ignoretz=ignoretz, |
| | tzinfos=tzinfos)) |
| | for value in rdatevals: |
| | for datestr in value.split(','): |
| | rset.rdate(parser.parse(datestr, |
| | ignoretz=ignoretz, |
| | tzinfos=tzinfos)) |
| | for value in exrulevals: |
| | rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart, |
| | ignoretz=ignoretz, |
| | tzinfos=tzinfos)) |
| | for value in exdatevals: |
| | rset.exdate(value) |
| | if compatible and dtstart: |
| | rset.rdate(dtstart) |
| | return rset |
| | else: |
| | return self._parse_rfc_rrule(rrulevals[0], |
| | dtstart=dtstart, |
| | cache=cache, |
| | ignoretz=ignoretz, |
| | tzinfos=tzinfos) |
| |
|
| | def __call__(self, s, **kwargs): |
| | return self._parse_rfc(s, **kwargs) |
| |
|
| |
|
| | rrulestr = _rrulestr() |
| |
|
| | |
| |
|