Spaces:
Running
Running
# -*- coding: utf-8 -*- | |
""" | |
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 | |
# For warning about deprecation of until and count | |
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"] | |
# Every mask is 7 days longer to handle cross-year weekly periods. | |
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)) | |
# Imported on demand. | |
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. | |
""" | |
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 | |
# __len__() introduces a large performance penalty. | |
def count(self): | |
""" Returns the number of recurrences in this set. It will have go | |
through 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 | |
# Select the comparison function | |
if inc: | |
comp = lambda dc, dtc: dc >= dtc | |
else: | |
comp = lambda dc, dtc: dc > dtc | |
# Generate dates | |
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 | |
# Cache the original byxxx rules, if they are provided, as the _byxxx | |
# attributes do not necessarily map to the inputs, and this can be | |
# a problem in generating the strings. Only store things if they've | |
# been supplied (the string retrieval will just use .get()) | |
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): | |
# According to RFC5545 Section 3.3.10: | |
# https://tools.ietf.org/html/rfc5545#section-3.3.10 | |
# | |
# > If the "DTSTART" property is specified as a date with UTC | |
# > time or a date with local time and time zone reference, | |
# > then the UNTIL rule part MUST be specified as a date with | |
# > UTC time. | |
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 | |
# bymonth | |
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 | |
# byyearday | |
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 | |
# byeaster | |
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 | |
# bymonthday | |
if bymonthday is None: | |
self._bymonthday = () | |
self._bynmonthday = () | |
else: | |
if isinstance(bymonthday, integer_types): | |
bymonthday = (bymonthday,) | |
bymonthday = set(bymonthday) # Ensure it's unique | |
self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0)) | |
self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0)) | |
# Storing positive numbers first, then negative numbers | |
if 'bymonthday' not in self._original_rule: | |
self._original_rule['bymonthday'] = tuple( | |
itertools.chain(self._bymonthday, self._bynmonthday)) | |
# byweekno | |
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 | |
# byweekday / bynweekday | |
if byweekday is None: | |
self._byweekday = None | |
self._bynweekday = None | |
else: | |
# If it's one of the valid non-sequence types, convert to a | |
# single-element sequence before the iterator that builds the | |
# byweekday set. | |
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)) | |
# byhour | |
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 | |
# byminute | |
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 | |
# bysecond | |
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: | |
# The str() method on weekday objects doesn't generate | |
# RFC5545-compliant strings, so we should modify that. | |
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() | |
# Some local variables to speed things up a bit | |
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: | |
# Get dayset with the right frequency | |
dayset, start, end = getdayset(year, month, day) | |
# Do the "hard" work ;-) | |
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 | |
# Output results | |
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 | |
# Handle frequency and interval | |
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: | |
# Jump to one iteration before next day | |
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: | |
# Jump to one iteration before next day | |
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: | |
# Jump to one iteration before next day | |
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() | |
# Support a single byxxx value. | |
if isinstance(byxxx, integer_types): | |
byxxx = (byxxx, ) | |
for num in byxxx: | |
i_gcd = gcd(self._interval, base) | |
# Use divmod rather than % because we need to wrap negative nums. | |
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): | |
# Using divmod() over % to account for negative intervals | |
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): | |
# Every mask is 7 days longer to handle cross-year weekly periods. | |
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 = self.wdaymask.index(rr._wkst) | |
no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7 | |
if no1wkst >= 4: | |
no1wkst = 0 | |
# Number of days in the year, plus the days we got | |
# from last year. | |
wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7 | |
else: | |
# Number of days in the year, minus the days we | |
# left in last year. | |
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: | |
# Check week number 1 of next year as well | |
# TODO: Check -numweeks for next year. | |
i = no1wkst+numweeks*7 | |
if no1wkst != firstwkst: | |
i -= 7-firstwkst | |
if i < self.yearlen: | |
# If week starts in next year, we | |
# don't care about it. | |
for j in range(7): | |
self.wnomask[i] = 1 | |
i += 1 | |
if self.wdaymask[i] == rr._wkst: | |
break | |
if no1wkst: | |
# Check last week number of last year as | |
# well. If no1wkst is 0, either the year | |
# started on week start, or week number 1 | |
# got days from last year, so there are no | |
# days from last year's last week number in | |
# this year. | |
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: | |
# Weekly frequency won't get here, so we may not | |
# care about cross-year weekly periods. | |
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): | |
# We need to handle cross-year weeks here. | |
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 (not (0 <= i < self.yearlen) or | |
# self.wdaymask[i] == self.rrule._wkst): | |
# This will cross the year boundary, if necessary. | |
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 = [] | |
def rrule(self, rrule): | |
""" Include the given :py:class:`rrule` instance in the recurrence set | |
generation. """ | |
self._rrule.append(rrule) | |
def rdate(self, rdate): | |
""" Include the given :py:class:`datetime` instance in the recurrence | |
set generation. """ | |
self._rdate.append(rdate) | |
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) | |
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: | |
# If it's of the form TH(+1), etc. | |
splt = wday.split('(') | |
w = splt[0] | |
n = int(splt[1][:-1]) | |
elif len(wday): | |
# If it's of the form +1MO | |
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 | |
# 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, | |
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() | |
# vim:ts=4:sw=4:et | |