123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737 |
- # -*- 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.
- """
- @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
- # __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. """
- 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 = []
- @_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:
- # 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
|