mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-06 13:11:15 -07:00
Update datutil-2.8.2
This commit is contained in:
parent
439ca8ebb8
commit
3b645cf6c3
37 changed files with 16696 additions and 2664 deletions
|
@ -2,18 +2,29 @@
|
|||
"""
|
||||
The rrule module offers a small, complete, and very fast, implementation of
|
||||
the recurrence rules documented in the
|
||||
`iCalendar RFC <http://www.ietf.org/rfc/rfc2445.txt>`_,
|
||||
`iCalendar RFC <https://tools.ietf.org/html/rfc5545>`_,
|
||||
including support for caching of results.
|
||||
"""
|
||||
import itertools
|
||||
import datetime
|
||||
import calendar
|
||||
import datetime
|
||||
import heapq
|
||||
import itertools
|
||||
import re
|
||||
import sys
|
||||
|
||||
from fractions import gcd
|
||||
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
|
||||
|
||||
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",
|
||||
|
@ -37,6 +48,8 @@ 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,
|
||||
|
@ -50,37 +63,32 @@ easter = None
|
|||
parser = None
|
||||
|
||||
|
||||
class weekday(object):
|
||||
__slots__ = ["weekday", "n"]
|
||||
|
||||
def __init__(self, weekday, n=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")
|
||||
self.weekday = weekday
|
||||
self.n = n
|
||||
raise ValueError("Can't create weekday with n==0")
|
||||
|
||||
def __call__(self, n):
|
||||
if n == self.n:
|
||||
return self
|
||||
else:
|
||||
return self.__class__(self.weekday, n)
|
||||
super(weekday, self).__init__(wkday, n)
|
||||
|
||||
def __eq__(self, other):
|
||||
try:
|
||||
if self.weekday != other.weekday or self.n != other.n:
|
||||
return False
|
||||
except AttributeError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
|
||||
if not self.n:
|
||||
return s
|
||||
else:
|
||||
return "%s(%+d)" % (s, self.n)
|
||||
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
|
||||
|
||||
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):
|
||||
|
@ -88,12 +96,11 @@ class rrulebase(object):
|
|||
if cache:
|
||||
self._cache = []
|
||||
self._cache_lock = _thread.allocate_lock()
|
||||
self._cache_gen = self._iter()
|
||||
self._cache_complete = False
|
||||
self._invalidate_cache()
|
||||
else:
|
||||
self._cache = None
|
||||
self._cache_complete = False
|
||||
self._len = None
|
||||
self._len = None
|
||||
|
||||
def __iter__(self):
|
||||
if self._cache_complete:
|
||||
|
@ -103,6 +110,17 @@ class rrulebase(object):
|
|||
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
|
||||
|
@ -161,7 +179,7 @@ class rrulebase(object):
|
|||
return False
|
||||
return False
|
||||
|
||||
# __len__() introduces a large performance penality.
|
||||
# __len__() introduces a large performance penalty.
|
||||
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. """
|
||||
|
@ -209,7 +227,48 @@ class rrulebase(object):
|
|||
return i
|
||||
return None
|
||||
|
||||
def between(self, after, before, inc=False):
|
||||
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
|
||||
|
@ -254,12 +313,31 @@ class rrule(rrulebase):
|
|||
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 cache:
|
||||
If given, it must be a boolean value specifying to enable or disable
|
||||
caching of results. If you will use the same rrule instance multiple
|
||||
times, enabling caching will improve the performance considerably.
|
||||
:param dtstart:
|
||||
The recurrence start. Besides being the base for the recurrence,
|
||||
missing parameters in the final recurrence instances will also be
|
||||
|
@ -276,12 +354,26 @@ class rrule(rrulebase):
|
|||
from calendar.firstweekday(), and may be modified by
|
||||
calendar.setfirstweekday().
|
||||
:param count:
|
||||
How many occurrences will be generated.
|
||||
If given, this determines how many occurrences will be generated.
|
||||
|
||||
.. note::
|
||||
As of version 2.5.0, the use of the keyword ``until`` in conjunction
|
||||
with ``count`` is deprecated, to make sure ``dateutil`` is fully
|
||||
compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/
|
||||
html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count``
|
||||
**must not** occur in the same call to ``rrule``.
|
||||
:param until:
|
||||
If given, this must be a datetime instance, that will specify the
|
||||
limit of the recurrence. If a recurrence instance happens to be the
|
||||
same as the datetime instance given in the until keyword, this will
|
||||
be the last occurrence.
|
||||
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
|
||||
|
@ -298,6 +390,11 @@ class rrule(rrulebase):
|
|||
:param byyearday:
|
||||
If given, it must be either an integer, or a sequence of integers,
|
||||
meaning the year days to apply the recurrence to.
|
||||
:param byeaster:
|
||||
If given, it must be either an integer, or a sequence of integers,
|
||||
positive or negative. Each integer will define an offset from the
|
||||
Easter Sunday. Passing the offset 0 to byeaster will yield the Easter
|
||||
Sunday itself. This is an extension to the RFC specification.
|
||||
:param byweekno:
|
||||
If given, it must be either an integer, or a sequence of integers,
|
||||
meaning the week numbers to apply the recurrence to. Week numbers
|
||||
|
@ -323,11 +420,10 @@ class rrule(rrulebase):
|
|||
:param bysecond:
|
||||
If given, it must be either an integer, or a sequence of integers,
|
||||
meaning the seconds to apply the recurrence to.
|
||||
:param byeaster:
|
||||
If given, it must be either an integer, or a sequence of integers,
|
||||
positive or negative. Each integer will define an offset from the
|
||||
Easter Sunday. Passing the offset 0 to byeaster will yield the Easter
|
||||
Sunday itself. This is an extension to the RFC specification.
|
||||
:param cache:
|
||||
If given, it must be a boolean value specifying to enable or disable
|
||||
caching of results. If you will use the same rrule instance multiple
|
||||
times, enabling caching will improve the performance considerably.
|
||||
"""
|
||||
def __init__(self, freq, dtstart=None,
|
||||
interval=1, wkst=None, count=None, until=None, bysetpos=None,
|
||||
|
@ -338,7 +434,10 @@ class rrule(rrulebase):
|
|||
super(rrule, self).__init__(cache)
|
||||
global easter
|
||||
if not dtstart:
|
||||
dtstart = datetime.datetime.now().replace(microsecond=0)
|
||||
if until and until.tzinfo:
|
||||
dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0)
|
||||
else:
|
||||
dtstart = datetime.datetime.now().replace(microsecond=0)
|
||||
elif not isinstance(dtstart, datetime.datetime):
|
||||
dtstart = datetime.datetime.fromordinal(dtstart.toordinal())
|
||||
else:
|
||||
|
@ -349,10 +448,35 @@ class rrule(rrulebase):
|
|||
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):
|
||||
|
@ -374,16 +498,23 @@ class rrule(rrulebase):
|
|||
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:
|
||||
|
@ -394,6 +525,9 @@ class rrule(rrulebase):
|
|||
|
||||
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
|
||||
|
@ -402,6 +536,7 @@ class rrule(rrulebase):
|
|||
byyearday = (byyearday,)
|
||||
|
||||
self._byyearday = tuple(sorted(set(byyearday)))
|
||||
self._original_rule['byyearday'] = self._byyearday
|
||||
|
||||
# byeaster
|
||||
if byeaster is not None:
|
||||
|
@ -411,10 +546,12 @@ class rrule(rrulebase):
|
|||
self._byeaster = (byeaster,)
|
||||
else:
|
||||
self._byeaster = tuple(sorted(byeaster))
|
||||
|
||||
self._original_rule['byeaster'] = self._byeaster
|
||||
else:
|
||||
self._byeaster = None
|
||||
|
||||
# bymonthay
|
||||
# bymonthday
|
||||
if bymonthday is None:
|
||||
self._bymonthday = ()
|
||||
self._bynmonthday = ()
|
||||
|
@ -422,8 +559,15 @@ class rrule(rrulebase):
|
|||
if isinstance(bymonthday, integer_types):
|
||||
bymonthday = (bymonthday,)
|
||||
|
||||
self._bymonthday = tuple(sorted(set([x for x in bymonthday if x > 0])))
|
||||
self._bynmonthday = tuple(sorted(set([x for x in bymonthday if x < 0])))
|
||||
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:
|
||||
|
@ -434,6 +578,8 @@ class rrule(rrulebase):
|
|||
|
||||
self._byweekno = tuple(sorted(set(byweekno)))
|
||||
|
||||
self._original_rule['byweekno'] = self._byweekno
|
||||
|
||||
# byweekday / bynweekday
|
||||
if byweekday is None:
|
||||
self._byweekday = None
|
||||
|
@ -462,14 +608,24 @@ class rrule(rrulebase):
|
|||
|
||||
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 = set((dtstart.hour,))
|
||||
self._byhour = {dtstart.hour}
|
||||
else:
|
||||
self._byhour = None
|
||||
else:
|
||||
|
@ -484,11 +640,12 @@ class rrule(rrulebase):
|
|||
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 = set((dtstart.minute,))
|
||||
self._byminute = {dtstart.minute}
|
||||
else:
|
||||
self._byminute = None
|
||||
else:
|
||||
|
@ -503,6 +660,7 @@ class rrule(rrulebase):
|
|||
self._byminute = set(byminute)
|
||||
|
||||
self._byminute = tuple(sorted(self._byminute))
|
||||
self._original_rule['byminute'] = self._byminute
|
||||
|
||||
# bysecond
|
||||
if bysecond is None:
|
||||
|
@ -524,6 +682,7 @@ class rrule(rrulebase):
|
|||
self._bysecond = set(bysecond)
|
||||
|
||||
self._bysecond = tuple(sorted(self._bysecond))
|
||||
self._original_rule['bysecond'] = self._bysecond
|
||||
|
||||
if self._freq >= HOURLY:
|
||||
self._timeset = None
|
||||
|
@ -538,6 +697,82 @@ class rrule(rrulebase):
|
|||
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()
|
||||
|
@ -636,31 +871,32 @@ class rrule(rrulebase):
|
|||
self._len = total
|
||||
return
|
||||
elif res >= self._dtstart:
|
||||
total += 1
|
||||
yield res
|
||||
if count:
|
||||
if count is not None:
|
||||
count -= 1
|
||||
if not count:
|
||||
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)
|
||||
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:
|
||||
total += 1
|
||||
yield res
|
||||
if count:
|
||||
if count is not None:
|
||||
count -= 1
|
||||
if not count:
|
||||
if count < 0:
|
||||
self._len = total
|
||||
return
|
||||
|
||||
total += 1
|
||||
yield res
|
||||
|
||||
# Handle frequency and interval
|
||||
fixday = False
|
||||
if freq == YEARLY:
|
||||
|
@ -743,10 +979,10 @@ class rrule(rrulebase):
|
|||
elif freq == SECONDLY:
|
||||
if filtered:
|
||||
# Jump to one iteration before next day
|
||||
second += (((86399-(hour*3600+minute*60+second))
|
||||
// interval)*interval)
|
||||
second += (((86399 - (hour * 3600 + minute * 60 + second))
|
||||
// interval) * interval)
|
||||
|
||||
rep_rate = (24*3600)
|
||||
rep_rate = (24 * 3600)
|
||||
valid = False
|
||||
for j in range(0, rep_rate // gcd(interval, rep_rate)):
|
||||
if bysecond:
|
||||
|
@ -809,9 +1045,9 @@ class rrule(rrulebase):
|
|||
|
||||
:param start:
|
||||
Specifies the starting position.
|
||||
:param byxxx:
|
||||
:param byxxx:
|
||||
An iterable containing the list of allowed values.
|
||||
:param base:
|
||||
:param base:
|
||||
The largest allowable value for the specified frequency (e.g.
|
||||
24 hours, 60 minutes).
|
||||
|
||||
|
@ -846,9 +1082,9 @@ class rrule(rrulebase):
|
|||
specified along with a `BYXXX` parameter at the same "level"
|
||||
(e.g. `HOURLY` specified with `BYHOUR`).
|
||||
|
||||
:param value:
|
||||
:param value:
|
||||
The old value of the component.
|
||||
:param byxxx:
|
||||
: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.
|
||||
|
@ -888,8 +1124,8 @@ class _iterinfo(object):
|
|||
# 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)
|
||||
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()
|
||||
|
@ -1040,10 +1276,10 @@ class _iterinfo(object):
|
|||
return dset, start, i
|
||||
|
||||
def ddayset(self, year, month, day):
|
||||
dset = [None]*self.yearlen
|
||||
i = datetime.date(year, month, day).toordinal()-self.yearordinal
|
||||
dset = [None] * self.yearlen
|
||||
i = datetime.date(year, month, day).toordinal() - self.yearordinal
|
||||
dset[i] = i
|
||||
return dset, i, i+1
|
||||
return dset, i, i + 1
|
||||
|
||||
def htimeset(self, hour, minute, second):
|
||||
tset = []
|
||||
|
@ -1051,7 +1287,7 @@ class _iterinfo(object):
|
|||
for minute in rr._byminute:
|
||||
for second in rr._bysecond:
|
||||
tset.append(datetime.time(hour, minute, second,
|
||||
tzinfo=rr._tzinfo))
|
||||
tzinfo=rr._tzinfo))
|
||||
tset.sort()
|
||||
return tset
|
||||
|
||||
|
@ -1090,7 +1326,11 @@ class rruleset(rrulebase):
|
|||
try:
|
||||
self.dt = advance_iterator(self.gen)
|
||||
except StopIteration:
|
||||
self.genlist.remove(self)
|
||||
if self.genlist[0] is self:
|
||||
heapq.heappop(self.genlist)
|
||||
else:
|
||||
self.genlist.remove(self)
|
||||
heapq.heapify(self.genlist)
|
||||
|
||||
next = __next__
|
||||
|
||||
|
@ -1113,16 +1353,19 @@ class rruleset(rrulebase):
|
|||
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
|
||||
|
@ -1130,6 +1373,7 @@ class rruleset(rrulebase):
|
|||
"""
|
||||
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,
|
||||
|
@ -1142,31 +1386,79 @@ class rruleset(rrulebase):
|
|||
self._genitem(rlist, iter(self._rdate))
|
||||
for gen in [iter(x) for x in self._rrule]:
|
||||
self._genitem(rlist, gen)
|
||||
rlist.sort()
|
||||
exlist = []
|
||||
self._exdate.sort()
|
||||
self._genitem(exlist, iter(self._exdate))
|
||||
for gen in [iter(x) for x in self._exrule]:
|
||||
self._genitem(exlist, gen)
|
||||
exlist.sort()
|
||||
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:
|
||||
advance_iterator(exlist[0])
|
||||
exlist.sort()
|
||||
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)
|
||||
rlist.sort()
|
||||
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,
|
||||
|
@ -1214,16 +1506,29 @@ class _rrulestr(object):
|
|||
def _handle_WKST(self, rrkwargs, name, value, **kwargs):
|
||||
rrkwargs["wkst"] = self._weekday_map[value]
|
||||
|
||||
def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwarsg):
|
||||
def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs):
|
||||
"""
|
||||
Two ways to specify this: +1MO or MO(+1)
|
||||
"""
|
||||
l = []
|
||||
for wday in value.split(','):
|
||||
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)
|
||||
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
|
||||
|
||||
|
@ -1255,6 +1560,58 @@ class _rrulestr(object):
|
|||
raise ValueError("invalid '%s': %s" % (name, value))
|
||||
return rrule(dtstart=dtstart, cache=cache, **rrkwargs)
|
||||
|
||||
def _parse_date_value(self, date_value, parms, rule_tzids,
|
||||
ignoretz, tzids, tzinfos):
|
||||
global parser
|
||||
if not parser:
|
||||
from dateutil import parser
|
||||
|
||||
datevals = []
|
||||
value_found = False
|
||||
TZID = None
|
||||
|
||||
for parm in parms:
|
||||
if parm.startswith("TZID="):
|
||||
try:
|
||||
tzkey = rule_tzids[parm.split('TZID=')[-1]]
|
||||
except KeyError:
|
||||
continue
|
||||
if tzids is None:
|
||||
from . import tz
|
||||
tzlookup = tz.gettz
|
||||
elif callable(tzids):
|
||||
tzlookup = tzids
|
||||
else:
|
||||
tzlookup = getattr(tzids, 'get', None)
|
||||
if tzlookup is None:
|
||||
msg = ('tzids must be a callable, mapping, or None, '
|
||||
'not %s' % tzids)
|
||||
raise ValueError(msg)
|
||||
|
||||
TZID = tzlookup(tzkey)
|
||||
continue
|
||||
|
||||
# RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found
|
||||
# only once.
|
||||
if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}:
|
||||
raise ValueError("unsupported parm: " + parm)
|
||||
else:
|
||||
if value_found:
|
||||
msg = ("Duplicate value parameter found in: " + parm)
|
||||
raise ValueError(msg)
|
||||
value_found = True
|
||||
|
||||
for datestr in date_value.split(','):
|
||||
date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos)
|
||||
if TZID is not None:
|
||||
if date.tzinfo is None:
|
||||
date = date.replace(tzinfo=TZID)
|
||||
else:
|
||||
raise ValueError('DTSTART/EXDATE specifies multiple timezone')
|
||||
datevals.append(date)
|
||||
|
||||
return datevals
|
||||
|
||||
def _parse_rfc(self, s,
|
||||
dtstart=None,
|
||||
cache=False,
|
||||
|
@ -1262,11 +1619,17 @@ class _rrulestr(object):
|
|||
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")
|
||||
|
@ -1321,17 +1684,18 @@ class _rrulestr(object):
|
|||
raise ValueError("unsupported EXRULE parm: "+parm)
|
||||
exrulevals.append(value)
|
||||
elif name == "EXDATE":
|
||||
for parm in parms:
|
||||
if parm != "VALUE=DATE-TIME":
|
||||
raise ValueError("unsupported RDATE parm: "+parm)
|
||||
exdatevals.append(value)
|
||||
exdatevals.extend(
|
||||
self._parse_date_value(value, parms,
|
||||
TZID_NAMES, ignoretz,
|
||||
tzids, tzinfos)
|
||||
)
|
||||
elif name == "DTSTART":
|
||||
for parm in parms:
|
||||
raise ValueError("unsupported DTSTART parm: "+parm)
|
||||
if not parser:
|
||||
from dateutil import parser
|
||||
dtstart = parser.parse(value, ignoretz=ignoretz,
|
||||
tzinfos=tzinfos)
|
||||
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
|
||||
|
@ -1353,10 +1717,7 @@ class _rrulestr(object):
|
|||
ignoretz=ignoretz,
|
||||
tzinfos=tzinfos))
|
||||
for value in exdatevals:
|
||||
for datestr in value.split(','):
|
||||
rset.exdate(parser.parse(datestr,
|
||||
ignoretz=ignoretz,
|
||||
tzinfos=tzinfos))
|
||||
rset.exdate(value)
|
||||
if compatible and dtstart:
|
||||
rset.rdate(dtstart)
|
||||
return rset
|
||||
|
@ -1370,6 +1731,7 @@ class _rrulestr(object):
|
|||
def __call__(self, s, **kwargs):
|
||||
return self._parse_rfc(s, **kwargs)
|
||||
|
||||
|
||||
rrulestr = _rrulestr()
|
||||
|
||||
# vim:ts=4:sw=4:et
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue