Mercurial > repos > guerler > springsuite
comparison planemo/lib/python3.7/site-packages/dateutil/rrule.py @ 0:d30785e31577 draft
"planemo upload commit 6eee67778febed82ddd413c3ca40b3183a3898f1"
| author | guerler |
|---|---|
| date | Fri, 31 Jul 2020 00:18:57 -0400 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:d30785e31577 |
|---|---|
| 1 # -*- coding: utf-8 -*- | |
| 2 """ | |
| 3 The rrule module offers a small, complete, and very fast, implementation of | |
| 4 the recurrence rules documented in the | |
| 5 `iCalendar RFC <https://tools.ietf.org/html/rfc5545>`_, | |
| 6 including support for caching of results. | |
| 7 """ | |
| 8 import itertools | |
| 9 import datetime | |
| 10 import calendar | |
| 11 import re | |
| 12 import sys | |
| 13 | |
| 14 try: | |
| 15 from math import gcd | |
| 16 except ImportError: | |
| 17 from fractions import gcd | |
| 18 | |
| 19 from six import advance_iterator, integer_types | |
| 20 from six.moves import _thread, range | |
| 21 import heapq | |
| 22 | |
| 23 from ._common import weekday as weekdaybase | |
| 24 | |
| 25 # For warning about deprecation of until and count | |
| 26 from warnings import warn | |
| 27 | |
| 28 __all__ = ["rrule", "rruleset", "rrulestr", | |
| 29 "YEARLY", "MONTHLY", "WEEKLY", "DAILY", | |
| 30 "HOURLY", "MINUTELY", "SECONDLY", | |
| 31 "MO", "TU", "WE", "TH", "FR", "SA", "SU"] | |
| 32 | |
| 33 # Every mask is 7 days longer to handle cross-year weekly periods. | |
| 34 M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 + | |
| 35 [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7) | |
| 36 M365MASK = list(M366MASK) | |
| 37 M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32)) | |
| 38 MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) | |
| 39 MDAY365MASK = list(MDAY366MASK) | |
| 40 M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0)) | |
| 41 NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) | |
| 42 NMDAY365MASK = list(NMDAY366MASK) | |
| 43 M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366) | |
| 44 M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365) | |
| 45 WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55 | |
| 46 del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31] | |
| 47 MDAY365MASK = tuple(MDAY365MASK) | |
| 48 M365MASK = tuple(M365MASK) | |
| 49 | |
| 50 FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY'] | |
| 51 | |
| 52 (YEARLY, | |
| 53 MONTHLY, | |
| 54 WEEKLY, | |
| 55 DAILY, | |
| 56 HOURLY, | |
| 57 MINUTELY, | |
| 58 SECONDLY) = list(range(7)) | |
| 59 | |
| 60 # Imported on demand. | |
| 61 easter = None | |
| 62 parser = None | |
| 63 | |
| 64 | |
| 65 class weekday(weekdaybase): | |
| 66 """ | |
| 67 This version of weekday does not allow n = 0. | |
| 68 """ | |
| 69 def __init__(self, wkday, n=None): | |
| 70 if n == 0: | |
| 71 raise ValueError("Can't create weekday with n==0") | |
| 72 | |
| 73 super(weekday, self).__init__(wkday, n) | |
| 74 | |
| 75 | |
| 76 MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) | |
| 77 | |
| 78 | |
| 79 def _invalidates_cache(f): | |
| 80 """ | |
| 81 Decorator for rruleset methods which may invalidate the | |
| 82 cached length. | |
| 83 """ | |
| 84 def inner_func(self, *args, **kwargs): | |
| 85 rv = f(self, *args, **kwargs) | |
| 86 self._invalidate_cache() | |
| 87 return rv | |
| 88 | |
| 89 return inner_func | |
| 90 | |
| 91 | |
| 92 class rrulebase(object): | |
| 93 def __init__(self, cache=False): | |
| 94 if cache: | |
| 95 self._cache = [] | |
| 96 self._cache_lock = _thread.allocate_lock() | |
| 97 self._invalidate_cache() | |
| 98 else: | |
| 99 self._cache = None | |
| 100 self._cache_complete = False | |
| 101 self._len = None | |
| 102 | |
| 103 def __iter__(self): | |
| 104 if self._cache_complete: | |
| 105 return iter(self._cache) | |
| 106 elif self._cache is None: | |
| 107 return self._iter() | |
| 108 else: | |
| 109 return self._iter_cached() | |
| 110 | |
| 111 def _invalidate_cache(self): | |
| 112 if self._cache is not None: | |
| 113 self._cache = [] | |
| 114 self._cache_complete = False | |
| 115 self._cache_gen = self._iter() | |
| 116 | |
| 117 if self._cache_lock.locked(): | |
| 118 self._cache_lock.release() | |
| 119 | |
| 120 self._len = None | |
| 121 | |
| 122 def _iter_cached(self): | |
| 123 i = 0 | |
| 124 gen = self._cache_gen | |
| 125 cache = self._cache | |
| 126 acquire = self._cache_lock.acquire | |
| 127 release = self._cache_lock.release | |
| 128 while gen: | |
| 129 if i == len(cache): | |
| 130 acquire() | |
| 131 if self._cache_complete: | |
| 132 break | |
| 133 try: | |
| 134 for j in range(10): | |
| 135 cache.append(advance_iterator(gen)) | |
| 136 except StopIteration: | |
| 137 self._cache_gen = gen = None | |
| 138 self._cache_complete = True | |
| 139 break | |
| 140 release() | |
| 141 yield cache[i] | |
| 142 i += 1 | |
| 143 while i < self._len: | |
| 144 yield cache[i] | |
| 145 i += 1 | |
| 146 | |
| 147 def __getitem__(self, item): | |
| 148 if self._cache_complete: | |
| 149 return self._cache[item] | |
| 150 elif isinstance(item, slice): | |
| 151 if item.step and item.step < 0: | |
| 152 return list(iter(self))[item] | |
| 153 else: | |
| 154 return list(itertools.islice(self, | |
| 155 item.start or 0, | |
| 156 item.stop or sys.maxsize, | |
| 157 item.step or 1)) | |
| 158 elif item >= 0: | |
| 159 gen = iter(self) | |
| 160 try: | |
| 161 for i in range(item+1): | |
| 162 res = advance_iterator(gen) | |
| 163 except StopIteration: | |
| 164 raise IndexError | |
| 165 return res | |
| 166 else: | |
| 167 return list(iter(self))[item] | |
| 168 | |
| 169 def __contains__(self, item): | |
| 170 if self._cache_complete: | |
| 171 return item in self._cache | |
| 172 else: | |
| 173 for i in self: | |
| 174 if i == item: | |
| 175 return True | |
| 176 elif i > item: | |
| 177 return False | |
| 178 return False | |
| 179 | |
| 180 # __len__() introduces a large performance penalty. | |
| 181 def count(self): | |
| 182 """ Returns the number of recurrences in this set. It will have go | |
| 183 trough the whole recurrence, if this hasn't been done before. """ | |
| 184 if self._len is None: | |
| 185 for x in self: | |
| 186 pass | |
| 187 return self._len | |
| 188 | |
| 189 def before(self, dt, inc=False): | |
| 190 """ Returns the last recurrence before the given datetime instance. The | |
| 191 inc keyword defines what happens if dt is an occurrence. With | |
| 192 inc=True, if dt itself is an occurrence, it will be returned. """ | |
| 193 if self._cache_complete: | |
| 194 gen = self._cache | |
| 195 else: | |
| 196 gen = self | |
| 197 last = None | |
| 198 if inc: | |
| 199 for i in gen: | |
| 200 if i > dt: | |
| 201 break | |
| 202 last = i | |
| 203 else: | |
| 204 for i in gen: | |
| 205 if i >= dt: | |
| 206 break | |
| 207 last = i | |
| 208 return last | |
| 209 | |
| 210 def after(self, dt, inc=False): | |
| 211 """ Returns the first recurrence after the given datetime instance. The | |
| 212 inc keyword defines what happens if dt is an occurrence. With | |
| 213 inc=True, if dt itself is an occurrence, it will be returned. """ | |
| 214 if self._cache_complete: | |
| 215 gen = self._cache | |
| 216 else: | |
| 217 gen = self | |
| 218 if inc: | |
| 219 for i in gen: | |
| 220 if i >= dt: | |
| 221 return i | |
| 222 else: | |
| 223 for i in gen: | |
| 224 if i > dt: | |
| 225 return i | |
| 226 return None | |
| 227 | |
| 228 def xafter(self, dt, count=None, inc=False): | |
| 229 """ | |
| 230 Generator which yields up to `count` recurrences after the given | |
| 231 datetime instance, equivalent to `after`. | |
| 232 | |
| 233 :param dt: | |
| 234 The datetime at which to start generating recurrences. | |
| 235 | |
| 236 :param count: | |
| 237 The maximum number of recurrences to generate. If `None` (default), | |
| 238 dates are generated until the recurrence rule is exhausted. | |
| 239 | |
| 240 :param inc: | |
| 241 If `dt` is an instance of the rule and `inc` is `True`, it is | |
| 242 included in the output. | |
| 243 | |
| 244 :yields: Yields a sequence of `datetime` objects. | |
| 245 """ | |
| 246 | |
| 247 if self._cache_complete: | |
| 248 gen = self._cache | |
| 249 else: | |
| 250 gen = self | |
| 251 | |
| 252 # Select the comparison function | |
| 253 if inc: | |
| 254 comp = lambda dc, dtc: dc >= dtc | |
| 255 else: | |
| 256 comp = lambda dc, dtc: dc > dtc | |
| 257 | |
| 258 # Generate dates | |
| 259 n = 0 | |
| 260 for d in gen: | |
| 261 if comp(d, dt): | |
| 262 if count is not None: | |
| 263 n += 1 | |
| 264 if n > count: | |
| 265 break | |
| 266 | |
| 267 yield d | |
| 268 | |
| 269 def between(self, after, before, inc=False, count=1): | |
| 270 """ Returns all the occurrences of the rrule between after and before. | |
| 271 The inc keyword defines what happens if after and/or before are | |
| 272 themselves occurrences. With inc=True, they will be included in the | |
| 273 list, if they are found in the recurrence set. """ | |
| 274 if self._cache_complete: | |
| 275 gen = self._cache | |
| 276 else: | |
| 277 gen = self | |
| 278 started = False | |
| 279 l = [] | |
| 280 if inc: | |
| 281 for i in gen: | |
| 282 if i > before: | |
| 283 break | |
| 284 elif not started: | |
| 285 if i >= after: | |
| 286 started = True | |
| 287 l.append(i) | |
| 288 else: | |
| 289 l.append(i) | |
| 290 else: | |
| 291 for i in gen: | |
| 292 if i >= before: | |
| 293 break | |
| 294 elif not started: | |
| 295 if i > after: | |
| 296 started = True | |
| 297 l.append(i) | |
| 298 else: | |
| 299 l.append(i) | |
| 300 return l | |
| 301 | |
| 302 | |
| 303 class rrule(rrulebase): | |
| 304 """ | |
| 305 That's the base of the rrule operation. It accepts all the keywords | |
| 306 defined in the RFC as its constructor parameters (except byday, | |
| 307 which was renamed to byweekday) and more. The constructor prototype is:: | |
| 308 | |
| 309 rrule(freq) | |
| 310 | |
| 311 Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, | |
| 312 or SECONDLY. | |
| 313 | |
| 314 .. note:: | |
| 315 Per RFC section 3.3.10, recurrence instances falling on invalid dates | |
| 316 and times are ignored rather than coerced: | |
| 317 | |
| 318 Recurrence rules may generate recurrence instances with an invalid | |
| 319 date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM | |
| 320 on a day where the local time is moved forward by an hour at 1:00 | |
| 321 AM). Such recurrence instances MUST be ignored and MUST NOT be | |
| 322 counted as part of the recurrence set. | |
| 323 | |
| 324 This can lead to possibly surprising behavior when, for example, the | |
| 325 start date occurs at the end of the month: | |
| 326 | |
| 327 >>> from dateutil.rrule import rrule, MONTHLY | |
| 328 >>> from datetime import datetime | |
| 329 >>> start_date = datetime(2014, 12, 31) | |
| 330 >>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date)) | |
| 331 ... # doctest: +NORMALIZE_WHITESPACE | |
| 332 [datetime.datetime(2014, 12, 31, 0, 0), | |
| 333 datetime.datetime(2015, 1, 31, 0, 0), | |
| 334 datetime.datetime(2015, 3, 31, 0, 0), | |
| 335 datetime.datetime(2015, 5, 31, 0, 0)] | |
| 336 | |
| 337 Additionally, it supports the following keyword arguments: | |
| 338 | |
| 339 :param dtstart: | |
| 340 The recurrence start. Besides being the base for the recurrence, | |
| 341 missing parameters in the final recurrence instances will also be | |
| 342 extracted from this date. If not given, datetime.now() will be used | |
| 343 instead. | |
| 344 :param interval: | |
| 345 The interval between each freq iteration. For example, when using | |
| 346 YEARLY, an interval of 2 means once every two years, but with HOURLY, | |
| 347 it means once every two hours. The default interval is 1. | |
| 348 :param wkst: | |
| 349 The week start day. Must be one of the MO, TU, WE constants, or an | |
| 350 integer, specifying the first day of the week. This will affect | |
| 351 recurrences based on weekly periods. The default week start is got | |
| 352 from calendar.firstweekday(), and may be modified by | |
| 353 calendar.setfirstweekday(). | |
| 354 :param count: | |
| 355 If given, this determines how many occurrences will be generated. | |
| 356 | |
| 357 .. note:: | |
| 358 As of version 2.5.0, the use of the keyword ``until`` in conjunction | |
| 359 with ``count`` is deprecated, to make sure ``dateutil`` is fully | |
| 360 compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/ | |
| 361 html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count`` | |
| 362 **must not** occur in the same call to ``rrule``. | |
| 363 :param until: | |
| 364 If given, this must be a datetime instance specifying the upper-bound | |
| 365 limit of the recurrence. The last recurrence in the rule is the greatest | |
| 366 datetime that is less than or equal to the value specified in the | |
| 367 ``until`` parameter. | |
| 368 | |
| 369 .. note:: | |
| 370 As of version 2.5.0, the use of the keyword ``until`` in conjunction | |
| 371 with ``count`` is deprecated, to make sure ``dateutil`` is fully | |
| 372 compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/ | |
| 373 html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count`` | |
| 374 **must not** occur in the same call to ``rrule``. | |
| 375 :param bysetpos: | |
| 376 If given, it must be either an integer, or a sequence of integers, | |
| 377 positive or negative. Each given integer will specify an occurrence | |
| 378 number, corresponding to the nth occurrence of the rule inside the | |
| 379 frequency period. For example, a bysetpos of -1 if combined with a | |
| 380 MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will | |
| 381 result in the last work day of every month. | |
| 382 :param bymonth: | |
| 383 If given, it must be either an integer, or a sequence of integers, | |
| 384 meaning the months to apply the recurrence to. | |
| 385 :param bymonthday: | |
| 386 If given, it must be either an integer, or a sequence of integers, | |
| 387 meaning the month days to apply the recurrence to. | |
| 388 :param byyearday: | |
| 389 If given, it must be either an integer, or a sequence of integers, | |
| 390 meaning the year days to apply the recurrence to. | |
| 391 :param byeaster: | |
| 392 If given, it must be either an integer, or a sequence of integers, | |
| 393 positive or negative. Each integer will define an offset from the | |
| 394 Easter Sunday. Passing the offset 0 to byeaster will yield the Easter | |
| 395 Sunday itself. This is an extension to the RFC specification. | |
| 396 :param byweekno: | |
| 397 If given, it must be either an integer, or a sequence of integers, | |
| 398 meaning the week numbers to apply the recurrence to. Week numbers | |
| 399 have the meaning described in ISO8601, that is, the first week of | |
| 400 the year is that containing at least four days of the new year. | |
| 401 :param byweekday: | |
| 402 If given, it must be either an integer (0 == MO), a sequence of | |
| 403 integers, one of the weekday constants (MO, TU, etc), or a sequence | |
| 404 of these constants. When given, these variables will define the | |
| 405 weekdays where the recurrence will be applied. It's also possible to | |
| 406 use an argument n for the weekday instances, which will mean the nth | |
| 407 occurrence of this weekday in the period. For example, with MONTHLY, | |
| 408 or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the | |
| 409 first friday of the month where the recurrence happens. Notice that in | |
| 410 the RFC documentation, this is specified as BYDAY, but was renamed to | |
| 411 avoid the ambiguity of that keyword. | |
| 412 :param byhour: | |
| 413 If given, it must be either an integer, or a sequence of integers, | |
| 414 meaning the hours to apply the recurrence to. | |
| 415 :param byminute: | |
| 416 If given, it must be either an integer, or a sequence of integers, | |
| 417 meaning the minutes to apply the recurrence to. | |
| 418 :param bysecond: | |
| 419 If given, it must be either an integer, or a sequence of integers, | |
| 420 meaning the seconds to apply the recurrence to. | |
| 421 :param cache: | |
| 422 If given, it must be a boolean value specifying to enable or disable | |
| 423 caching of results. If you will use the same rrule instance multiple | |
| 424 times, enabling caching will improve the performance considerably. | |
| 425 """ | |
| 426 def __init__(self, freq, dtstart=None, | |
| 427 interval=1, wkst=None, count=None, until=None, bysetpos=None, | |
| 428 bymonth=None, bymonthday=None, byyearday=None, byeaster=None, | |
| 429 byweekno=None, byweekday=None, | |
| 430 byhour=None, byminute=None, bysecond=None, | |
| 431 cache=False): | |
| 432 super(rrule, self).__init__(cache) | |
| 433 global easter | |
| 434 if not dtstart: | |
| 435 if until and until.tzinfo: | |
| 436 dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0) | |
| 437 else: | |
| 438 dtstart = datetime.datetime.now().replace(microsecond=0) | |
| 439 elif not isinstance(dtstart, datetime.datetime): | |
| 440 dtstart = datetime.datetime.fromordinal(dtstart.toordinal()) | |
| 441 else: | |
| 442 dtstart = dtstart.replace(microsecond=0) | |
| 443 self._dtstart = dtstart | |
| 444 self._tzinfo = dtstart.tzinfo | |
| 445 self._freq = freq | |
| 446 self._interval = interval | |
| 447 self._count = count | |
| 448 | |
| 449 # Cache the original byxxx rules, if they are provided, as the _byxxx | |
| 450 # attributes do not necessarily map to the inputs, and this can be | |
| 451 # a problem in generating the strings. Only store things if they've | |
| 452 # been supplied (the string retrieval will just use .get()) | |
| 453 self._original_rule = {} | |
| 454 | |
| 455 if until and not isinstance(until, datetime.datetime): | |
| 456 until = datetime.datetime.fromordinal(until.toordinal()) | |
| 457 self._until = until | |
| 458 | |
| 459 if self._dtstart and self._until: | |
| 460 if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None): | |
| 461 # According to RFC5545 Section 3.3.10: | |
| 462 # https://tools.ietf.org/html/rfc5545#section-3.3.10 | |
| 463 # | |
| 464 # > If the "DTSTART" property is specified as a date with UTC | |
| 465 # > time or a date with local time and time zone reference, | |
| 466 # > then the UNTIL rule part MUST be specified as a date with | |
| 467 # > UTC time. | |
| 468 raise ValueError( | |
| 469 'RRULE UNTIL values must be specified in UTC when DTSTART ' | |
| 470 'is timezone-aware' | |
| 471 ) | |
| 472 | |
| 473 if count is not None and until: | |
| 474 warn("Using both 'count' and 'until' is inconsistent with RFC 5545" | |
| 475 " and has been deprecated in dateutil. Future versions will " | |
| 476 "raise an error.", DeprecationWarning) | |
| 477 | |
| 478 if wkst is None: | |
| 479 self._wkst = calendar.firstweekday() | |
| 480 elif isinstance(wkst, integer_types): | |
| 481 self._wkst = wkst | |
| 482 else: | |
| 483 self._wkst = wkst.weekday | |
| 484 | |
| 485 if bysetpos is None: | |
| 486 self._bysetpos = None | |
| 487 elif isinstance(bysetpos, integer_types): | |
| 488 if bysetpos == 0 or not (-366 <= bysetpos <= 366): | |
| 489 raise ValueError("bysetpos must be between 1 and 366, " | |
| 490 "or between -366 and -1") | |
| 491 self._bysetpos = (bysetpos,) | |
| 492 else: | |
| 493 self._bysetpos = tuple(bysetpos) | |
| 494 for pos in self._bysetpos: | |
| 495 if pos == 0 or not (-366 <= pos <= 366): | |
| 496 raise ValueError("bysetpos must be between 1 and 366, " | |
| 497 "or between -366 and -1") | |
| 498 | |
| 499 if self._bysetpos: | |
| 500 self._original_rule['bysetpos'] = self._bysetpos | |
| 501 | |
| 502 if (byweekno is None and byyearday is None and bymonthday is None and | |
| 503 byweekday is None and byeaster is None): | |
| 504 if freq == YEARLY: | |
| 505 if bymonth is None: | |
| 506 bymonth = dtstart.month | |
| 507 self._original_rule['bymonth'] = None | |
| 508 bymonthday = dtstart.day | |
| 509 self._original_rule['bymonthday'] = None | |
| 510 elif freq == MONTHLY: | |
| 511 bymonthday = dtstart.day | |
| 512 self._original_rule['bymonthday'] = None | |
| 513 elif freq == WEEKLY: | |
| 514 byweekday = dtstart.weekday() | |
| 515 self._original_rule['byweekday'] = None | |
| 516 | |
| 517 # bymonth | |
| 518 if bymonth is None: | |
| 519 self._bymonth = None | |
| 520 else: | |
| 521 if isinstance(bymonth, integer_types): | |
| 522 bymonth = (bymonth,) | |
| 523 | |
| 524 self._bymonth = tuple(sorted(set(bymonth))) | |
| 525 | |
| 526 if 'bymonth' not in self._original_rule: | |
| 527 self._original_rule['bymonth'] = self._bymonth | |
| 528 | |
| 529 # byyearday | |
| 530 if byyearday is None: | |
| 531 self._byyearday = None | |
| 532 else: | |
| 533 if isinstance(byyearday, integer_types): | |
| 534 byyearday = (byyearday,) | |
| 535 | |
| 536 self._byyearday = tuple(sorted(set(byyearday))) | |
| 537 self._original_rule['byyearday'] = self._byyearday | |
| 538 | |
| 539 # byeaster | |
| 540 if byeaster is not None: | |
| 541 if not easter: | |
| 542 from dateutil import easter | |
| 543 if isinstance(byeaster, integer_types): | |
| 544 self._byeaster = (byeaster,) | |
| 545 else: | |
| 546 self._byeaster = tuple(sorted(byeaster)) | |
| 547 | |
| 548 self._original_rule['byeaster'] = self._byeaster | |
| 549 else: | |
| 550 self._byeaster = None | |
| 551 | |
| 552 # bymonthday | |
| 553 if bymonthday is None: | |
| 554 self._bymonthday = () | |
| 555 self._bynmonthday = () | |
| 556 else: | |
| 557 if isinstance(bymonthday, integer_types): | |
| 558 bymonthday = (bymonthday,) | |
| 559 | |
| 560 bymonthday = set(bymonthday) # Ensure it's unique | |
| 561 | |
| 562 self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0)) | |
| 563 self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0)) | |
| 564 | |
| 565 # Storing positive numbers first, then negative numbers | |
| 566 if 'bymonthday' not in self._original_rule: | |
| 567 self._original_rule['bymonthday'] = tuple( | |
| 568 itertools.chain(self._bymonthday, self._bynmonthday)) | |
| 569 | |
| 570 # byweekno | |
| 571 if byweekno is None: | |
| 572 self._byweekno = None | |
| 573 else: | |
| 574 if isinstance(byweekno, integer_types): | |
| 575 byweekno = (byweekno,) | |
| 576 | |
| 577 self._byweekno = tuple(sorted(set(byweekno))) | |
| 578 | |
| 579 self._original_rule['byweekno'] = self._byweekno | |
| 580 | |
| 581 # byweekday / bynweekday | |
| 582 if byweekday is None: | |
| 583 self._byweekday = None | |
| 584 self._bynweekday = None | |
| 585 else: | |
| 586 # If it's one of the valid non-sequence types, convert to a | |
| 587 # single-element sequence before the iterator that builds the | |
| 588 # byweekday set. | |
| 589 if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"): | |
| 590 byweekday = (byweekday,) | |
| 591 | |
| 592 self._byweekday = set() | |
| 593 self._bynweekday = set() | |
| 594 for wday in byweekday: | |
| 595 if isinstance(wday, integer_types): | |
| 596 self._byweekday.add(wday) | |
| 597 elif not wday.n or freq > MONTHLY: | |
| 598 self._byweekday.add(wday.weekday) | |
| 599 else: | |
| 600 self._bynweekday.add((wday.weekday, wday.n)) | |
| 601 | |
| 602 if not self._byweekday: | |
| 603 self._byweekday = None | |
| 604 elif not self._bynweekday: | |
| 605 self._bynweekday = None | |
| 606 | |
| 607 if self._byweekday is not None: | |
| 608 self._byweekday = tuple(sorted(self._byweekday)) | |
| 609 orig_byweekday = [weekday(x) for x in self._byweekday] | |
| 610 else: | |
| 611 orig_byweekday = () | |
| 612 | |
| 613 if self._bynweekday is not None: | |
| 614 self._bynweekday = tuple(sorted(self._bynweekday)) | |
| 615 orig_bynweekday = [weekday(*x) for x in self._bynweekday] | |
| 616 else: | |
| 617 orig_bynweekday = () | |
| 618 | |
| 619 if 'byweekday' not in self._original_rule: | |
| 620 self._original_rule['byweekday'] = tuple(itertools.chain( | |
| 621 orig_byweekday, orig_bynweekday)) | |
| 622 | |
| 623 # byhour | |
| 624 if byhour is None: | |
| 625 if freq < HOURLY: | |
| 626 self._byhour = {dtstart.hour} | |
| 627 else: | |
| 628 self._byhour = None | |
| 629 else: | |
| 630 if isinstance(byhour, integer_types): | |
| 631 byhour = (byhour,) | |
| 632 | |
| 633 if freq == HOURLY: | |
| 634 self._byhour = self.__construct_byset(start=dtstart.hour, | |
| 635 byxxx=byhour, | |
| 636 base=24) | |
| 637 else: | |
| 638 self._byhour = set(byhour) | |
| 639 | |
| 640 self._byhour = tuple(sorted(self._byhour)) | |
| 641 self._original_rule['byhour'] = self._byhour | |
| 642 | |
| 643 # byminute | |
| 644 if byminute is None: | |
| 645 if freq < MINUTELY: | |
| 646 self._byminute = {dtstart.minute} | |
| 647 else: | |
| 648 self._byminute = None | |
| 649 else: | |
| 650 if isinstance(byminute, integer_types): | |
| 651 byminute = (byminute,) | |
| 652 | |
| 653 if freq == MINUTELY: | |
| 654 self._byminute = self.__construct_byset(start=dtstart.minute, | |
| 655 byxxx=byminute, | |
| 656 base=60) | |
| 657 else: | |
| 658 self._byminute = set(byminute) | |
| 659 | |
| 660 self._byminute = tuple(sorted(self._byminute)) | |
| 661 self._original_rule['byminute'] = self._byminute | |
| 662 | |
| 663 # bysecond | |
| 664 if bysecond is None: | |
| 665 if freq < SECONDLY: | |
| 666 self._bysecond = ((dtstart.second,)) | |
| 667 else: | |
| 668 self._bysecond = None | |
| 669 else: | |
| 670 if isinstance(bysecond, integer_types): | |
| 671 bysecond = (bysecond,) | |
| 672 | |
| 673 self._bysecond = set(bysecond) | |
| 674 | |
| 675 if freq == SECONDLY: | |
| 676 self._bysecond = self.__construct_byset(start=dtstart.second, | |
| 677 byxxx=bysecond, | |
| 678 base=60) | |
| 679 else: | |
| 680 self._bysecond = set(bysecond) | |
| 681 | |
| 682 self._bysecond = tuple(sorted(self._bysecond)) | |
| 683 self._original_rule['bysecond'] = self._bysecond | |
| 684 | |
| 685 if self._freq >= HOURLY: | |
| 686 self._timeset = None | |
| 687 else: | |
| 688 self._timeset = [] | |
| 689 for hour in self._byhour: | |
| 690 for minute in self._byminute: | |
| 691 for second in self._bysecond: | |
| 692 self._timeset.append( | |
| 693 datetime.time(hour, minute, second, | |
| 694 tzinfo=self._tzinfo)) | |
| 695 self._timeset.sort() | |
| 696 self._timeset = tuple(self._timeset) | |
| 697 | |
| 698 def __str__(self): | |
| 699 """ | |
| 700 Output a string that would generate this RRULE if passed to rrulestr. | |
| 701 This is mostly compatible with RFC5545, except for the | |
| 702 dateutil-specific extension BYEASTER. | |
| 703 """ | |
| 704 | |
| 705 output = [] | |
| 706 h, m, s = [None] * 3 | |
| 707 if self._dtstart: | |
| 708 output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S')) | |
| 709 h, m, s = self._dtstart.timetuple()[3:6] | |
| 710 | |
| 711 parts = ['FREQ=' + FREQNAMES[self._freq]] | |
| 712 if self._interval != 1: | |
| 713 parts.append('INTERVAL=' + str(self._interval)) | |
| 714 | |
| 715 if self._wkst: | |
| 716 parts.append('WKST=' + repr(weekday(self._wkst))[0:2]) | |
| 717 | |
| 718 if self._count is not None: | |
| 719 parts.append('COUNT=' + str(self._count)) | |
| 720 | |
| 721 if self._until: | |
| 722 parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S')) | |
| 723 | |
| 724 if self._original_rule.get('byweekday') is not None: | |
| 725 # The str() method on weekday objects doesn't generate | |
| 726 # RFC5545-compliant strings, so we should modify that. | |
| 727 original_rule = dict(self._original_rule) | |
| 728 wday_strings = [] | |
| 729 for wday in original_rule['byweekday']: | |
| 730 if wday.n: | |
| 731 wday_strings.append('{n:+d}{wday}'.format( | |
| 732 n=wday.n, | |
| 733 wday=repr(wday)[0:2])) | |
| 734 else: | |
| 735 wday_strings.append(repr(wday)) | |
| 736 | |
| 737 original_rule['byweekday'] = wday_strings | |
| 738 else: | |
| 739 original_rule = self._original_rule | |
| 740 | |
| 741 partfmt = '{name}={vals}' | |
| 742 for name, key in [('BYSETPOS', 'bysetpos'), | |
| 743 ('BYMONTH', 'bymonth'), | |
| 744 ('BYMONTHDAY', 'bymonthday'), | |
| 745 ('BYYEARDAY', 'byyearday'), | |
| 746 ('BYWEEKNO', 'byweekno'), | |
| 747 ('BYDAY', 'byweekday'), | |
| 748 ('BYHOUR', 'byhour'), | |
| 749 ('BYMINUTE', 'byminute'), | |
| 750 ('BYSECOND', 'bysecond'), | |
| 751 ('BYEASTER', 'byeaster')]: | |
| 752 value = original_rule.get(key) | |
| 753 if value: | |
| 754 parts.append(partfmt.format(name=name, vals=(','.join(str(v) | |
| 755 for v in value)))) | |
| 756 | |
| 757 output.append('RRULE:' + ';'.join(parts)) | |
| 758 return '\n'.join(output) | |
| 759 | |
| 760 def replace(self, **kwargs): | |
| 761 """Return new rrule with same attributes except for those attributes given new | |
| 762 values by whichever keyword arguments are specified.""" | |
| 763 new_kwargs = {"interval": self._interval, | |
| 764 "count": self._count, | |
| 765 "dtstart": self._dtstart, | |
| 766 "freq": self._freq, | |
| 767 "until": self._until, | |
| 768 "wkst": self._wkst, | |
| 769 "cache": False if self._cache is None else True } | |
| 770 new_kwargs.update(self._original_rule) | |
| 771 new_kwargs.update(kwargs) | |
| 772 return rrule(**new_kwargs) | |
| 773 | |
| 774 def _iter(self): | |
| 775 year, month, day, hour, minute, second, weekday, yearday, _ = \ | |
| 776 self._dtstart.timetuple() | |
| 777 | |
| 778 # Some local variables to speed things up a bit | |
| 779 freq = self._freq | |
| 780 interval = self._interval | |
| 781 wkst = self._wkst | |
| 782 until = self._until | |
| 783 bymonth = self._bymonth | |
| 784 byweekno = self._byweekno | |
| 785 byyearday = self._byyearday | |
| 786 byweekday = self._byweekday | |
| 787 byeaster = self._byeaster | |
| 788 bymonthday = self._bymonthday | |
| 789 bynmonthday = self._bynmonthday | |
| 790 bysetpos = self._bysetpos | |
| 791 byhour = self._byhour | |
| 792 byminute = self._byminute | |
| 793 bysecond = self._bysecond | |
| 794 | |
| 795 ii = _iterinfo(self) | |
| 796 ii.rebuild(year, month) | |
| 797 | |
| 798 getdayset = {YEARLY: ii.ydayset, | |
| 799 MONTHLY: ii.mdayset, | |
| 800 WEEKLY: ii.wdayset, | |
| 801 DAILY: ii.ddayset, | |
| 802 HOURLY: ii.ddayset, | |
| 803 MINUTELY: ii.ddayset, | |
| 804 SECONDLY: ii.ddayset}[freq] | |
| 805 | |
| 806 if freq < HOURLY: | |
| 807 timeset = self._timeset | |
| 808 else: | |
| 809 gettimeset = {HOURLY: ii.htimeset, | |
| 810 MINUTELY: ii.mtimeset, | |
| 811 SECONDLY: ii.stimeset}[freq] | |
| 812 if ((freq >= HOURLY and | |
| 813 self._byhour and hour not in self._byhour) or | |
| 814 (freq >= MINUTELY and | |
| 815 self._byminute and minute not in self._byminute) or | |
| 816 (freq >= SECONDLY and | |
| 817 self._bysecond and second not in self._bysecond)): | |
| 818 timeset = () | |
| 819 else: | |
| 820 timeset = gettimeset(hour, minute, second) | |
| 821 | |
| 822 total = 0 | |
| 823 count = self._count | |
| 824 while True: | |
| 825 # Get dayset with the right frequency | |
| 826 dayset, start, end = getdayset(year, month, day) | |
| 827 | |
| 828 # Do the "hard" work ;-) | |
| 829 filtered = False | |
| 830 for i in dayset[start:end]: | |
| 831 if ((bymonth and ii.mmask[i] not in bymonth) or | |
| 832 (byweekno and not ii.wnomask[i]) or | |
| 833 (byweekday and ii.wdaymask[i] not in byweekday) or | |
| 834 (ii.nwdaymask and not ii.nwdaymask[i]) or | |
| 835 (byeaster and not ii.eastermask[i]) or | |
| 836 ((bymonthday or bynmonthday) and | |
| 837 ii.mdaymask[i] not in bymonthday and | |
| 838 ii.nmdaymask[i] not in bynmonthday) or | |
| 839 (byyearday and | |
| 840 ((i < ii.yearlen and i+1 not in byyearday and | |
| 841 -ii.yearlen+i not in byyearday) or | |
| 842 (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and | |
| 843 -ii.nextyearlen+i-ii.yearlen not in byyearday)))): | |
| 844 dayset[i] = None | |
| 845 filtered = True | |
| 846 | |
| 847 # Output results | |
| 848 if bysetpos and timeset: | |
| 849 poslist = [] | |
| 850 for pos in bysetpos: | |
| 851 if pos < 0: | |
| 852 daypos, timepos = divmod(pos, len(timeset)) | |
| 853 else: | |
| 854 daypos, timepos = divmod(pos-1, len(timeset)) | |
| 855 try: | |
| 856 i = [x for x in dayset[start:end] | |
| 857 if x is not None][daypos] | |
| 858 time = timeset[timepos] | |
| 859 except IndexError: | |
| 860 pass | |
| 861 else: | |
| 862 date = datetime.date.fromordinal(ii.yearordinal+i) | |
| 863 res = datetime.datetime.combine(date, time) | |
| 864 if res not in poslist: | |
| 865 poslist.append(res) | |
| 866 poslist.sort() | |
| 867 for res in poslist: | |
| 868 if until and res > until: | |
| 869 self._len = total | |
| 870 return | |
| 871 elif res >= self._dtstart: | |
| 872 if count is not None: | |
| 873 count -= 1 | |
| 874 if count < 0: | |
| 875 self._len = total | |
| 876 return | |
| 877 total += 1 | |
| 878 yield res | |
| 879 else: | |
| 880 for i in dayset[start:end]: | |
| 881 if i is not None: | |
| 882 date = datetime.date.fromordinal(ii.yearordinal + i) | |
| 883 for time in timeset: | |
| 884 res = datetime.datetime.combine(date, time) | |
| 885 if until and res > until: | |
| 886 self._len = total | |
| 887 return | |
| 888 elif res >= self._dtstart: | |
| 889 if count is not None: | |
| 890 count -= 1 | |
| 891 if count < 0: | |
| 892 self._len = total | |
| 893 return | |
| 894 | |
| 895 total += 1 | |
| 896 yield res | |
| 897 | |
| 898 # Handle frequency and interval | |
| 899 fixday = False | |
| 900 if freq == YEARLY: | |
| 901 year += interval | |
| 902 if year > datetime.MAXYEAR: | |
| 903 self._len = total | |
| 904 return | |
| 905 ii.rebuild(year, month) | |
| 906 elif freq == MONTHLY: | |
| 907 month += interval | |
| 908 if month > 12: | |
| 909 div, mod = divmod(month, 12) | |
| 910 month = mod | |
| 911 year += div | |
| 912 if month == 0: | |
| 913 month = 12 | |
| 914 year -= 1 | |
| 915 if year > datetime.MAXYEAR: | |
| 916 self._len = total | |
| 917 return | |
| 918 ii.rebuild(year, month) | |
| 919 elif freq == WEEKLY: | |
| 920 if wkst > weekday: | |
| 921 day += -(weekday+1+(6-wkst))+self._interval*7 | |
| 922 else: | |
| 923 day += -(weekday-wkst)+self._interval*7 | |
| 924 weekday = wkst | |
| 925 fixday = True | |
| 926 elif freq == DAILY: | |
| 927 day += interval | |
| 928 fixday = True | |
| 929 elif freq == HOURLY: | |
| 930 if filtered: | |
| 931 # Jump to one iteration before next day | |
| 932 hour += ((23-hour)//interval)*interval | |
| 933 | |
| 934 if byhour: | |
| 935 ndays, hour = self.__mod_distance(value=hour, | |
| 936 byxxx=self._byhour, | |
| 937 base=24) | |
| 938 else: | |
| 939 ndays, hour = divmod(hour+interval, 24) | |
| 940 | |
| 941 if ndays: | |
| 942 day += ndays | |
| 943 fixday = True | |
| 944 | |
| 945 timeset = gettimeset(hour, minute, second) | |
| 946 elif freq == MINUTELY: | |
| 947 if filtered: | |
| 948 # Jump to one iteration before next day | |
| 949 minute += ((1439-(hour*60+minute))//interval)*interval | |
| 950 | |
| 951 valid = False | |
| 952 rep_rate = (24*60) | |
| 953 for j in range(rep_rate // gcd(interval, rep_rate)): | |
| 954 if byminute: | |
| 955 nhours, minute = \ | |
| 956 self.__mod_distance(value=minute, | |
| 957 byxxx=self._byminute, | |
| 958 base=60) | |
| 959 else: | |
| 960 nhours, minute = divmod(minute+interval, 60) | |
| 961 | |
| 962 div, hour = divmod(hour+nhours, 24) | |
| 963 if div: | |
| 964 day += div | |
| 965 fixday = True | |
| 966 filtered = False | |
| 967 | |
| 968 if not byhour or hour in byhour: | |
| 969 valid = True | |
| 970 break | |
| 971 | |
| 972 if not valid: | |
| 973 raise ValueError('Invalid combination of interval and ' + | |
| 974 'byhour resulting in empty rule.') | |
| 975 | |
| 976 timeset = gettimeset(hour, minute, second) | |
| 977 elif freq == SECONDLY: | |
| 978 if filtered: | |
| 979 # Jump to one iteration before next day | |
| 980 second += (((86399 - (hour * 3600 + minute * 60 + second)) | |
| 981 // interval) * interval) | |
| 982 | |
| 983 rep_rate = (24 * 3600) | |
| 984 valid = False | |
| 985 for j in range(0, rep_rate // gcd(interval, rep_rate)): | |
| 986 if bysecond: | |
| 987 nminutes, second = \ | |
| 988 self.__mod_distance(value=second, | |
| 989 byxxx=self._bysecond, | |
| 990 base=60) | |
| 991 else: | |
| 992 nminutes, second = divmod(second+interval, 60) | |
| 993 | |
| 994 div, minute = divmod(minute+nminutes, 60) | |
| 995 if div: | |
| 996 hour += div | |
| 997 div, hour = divmod(hour, 24) | |
| 998 if div: | |
| 999 day += div | |
| 1000 fixday = True | |
| 1001 | |
| 1002 if ((not byhour or hour in byhour) and | |
| 1003 (not byminute or minute in byminute) and | |
| 1004 (not bysecond or second in bysecond)): | |
| 1005 valid = True | |
| 1006 break | |
| 1007 | |
| 1008 if not valid: | |
| 1009 raise ValueError('Invalid combination of interval, ' + | |
| 1010 'byhour and byminute resulting in empty' + | |
| 1011 ' rule.') | |
| 1012 | |
| 1013 timeset = gettimeset(hour, minute, second) | |
| 1014 | |
| 1015 if fixday and day > 28: | |
| 1016 daysinmonth = calendar.monthrange(year, month)[1] | |
| 1017 if day > daysinmonth: | |
| 1018 while day > daysinmonth: | |
| 1019 day -= daysinmonth | |
| 1020 month += 1 | |
| 1021 if month == 13: | |
| 1022 month = 1 | |
| 1023 year += 1 | |
| 1024 if year > datetime.MAXYEAR: | |
| 1025 self._len = total | |
| 1026 return | |
| 1027 daysinmonth = calendar.monthrange(year, month)[1] | |
| 1028 ii.rebuild(year, month) | |
| 1029 | |
| 1030 def __construct_byset(self, start, byxxx, base): | |
| 1031 """ | |
| 1032 If a `BYXXX` sequence is passed to the constructor at the same level as | |
| 1033 `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some | |
| 1034 specifications which cannot be reached given some starting conditions. | |
| 1035 | |
| 1036 This occurs whenever the interval is not coprime with the base of a | |
| 1037 given unit and the difference between the starting position and the | |
| 1038 ending position is not coprime with the greatest common denominator | |
| 1039 between the interval and the base. For example, with a FREQ of hourly | |
| 1040 starting at 17:00 and an interval of 4, the only valid values for | |
| 1041 BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not | |
| 1042 coprime. | |
| 1043 | |
| 1044 :param start: | |
| 1045 Specifies the starting position. | |
| 1046 :param byxxx: | |
| 1047 An iterable containing the list of allowed values. | |
| 1048 :param base: | |
| 1049 The largest allowable value for the specified frequency (e.g. | |
| 1050 24 hours, 60 minutes). | |
| 1051 | |
| 1052 This does not preserve the type of the iterable, returning a set, since | |
| 1053 the values should be unique and the order is irrelevant, this will | |
| 1054 speed up later lookups. | |
| 1055 | |
| 1056 In the event of an empty set, raises a :exception:`ValueError`, as this | |
| 1057 results in an empty rrule. | |
| 1058 """ | |
| 1059 | |
| 1060 cset = set() | |
| 1061 | |
| 1062 # Support a single byxxx value. | |
| 1063 if isinstance(byxxx, integer_types): | |
| 1064 byxxx = (byxxx, ) | |
| 1065 | |
| 1066 for num in byxxx: | |
| 1067 i_gcd = gcd(self._interval, base) | |
| 1068 # Use divmod rather than % because we need to wrap negative nums. | |
| 1069 if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0: | |
| 1070 cset.add(num) | |
| 1071 | |
| 1072 if len(cset) == 0: | |
| 1073 raise ValueError("Invalid rrule byxxx generates an empty set.") | |
| 1074 | |
| 1075 return cset | |
| 1076 | |
| 1077 def __mod_distance(self, value, byxxx, base): | |
| 1078 """ | |
| 1079 Calculates the next value in a sequence where the `FREQ` parameter is | |
| 1080 specified along with a `BYXXX` parameter at the same "level" | |
| 1081 (e.g. `HOURLY` specified with `BYHOUR`). | |
| 1082 | |
| 1083 :param value: | |
| 1084 The old value of the component. | |
| 1085 :param byxxx: | |
| 1086 The `BYXXX` set, which should have been generated by | |
| 1087 `rrule._construct_byset`, or something else which checks that a | |
| 1088 valid rule is present. | |
| 1089 :param base: | |
| 1090 The largest allowable value for the specified frequency (e.g. | |
| 1091 24 hours, 60 minutes). | |
| 1092 | |
| 1093 If a valid value is not found after `base` iterations (the maximum | |
| 1094 number before the sequence would start to repeat), this raises a | |
| 1095 :exception:`ValueError`, as no valid values were found. | |
| 1096 | |
| 1097 This returns a tuple of `divmod(n*interval, base)`, where `n` is the | |
| 1098 smallest number of `interval` repetitions until the next specified | |
| 1099 value in `byxxx` is found. | |
| 1100 """ | |
| 1101 accumulator = 0 | |
| 1102 for ii in range(1, base + 1): | |
| 1103 # Using divmod() over % to account for negative intervals | |
| 1104 div, value = divmod(value + self._interval, base) | |
| 1105 accumulator += div | |
| 1106 if value in byxxx: | |
| 1107 return (accumulator, value) | |
| 1108 | |
| 1109 | |
| 1110 class _iterinfo(object): | |
| 1111 __slots__ = ["rrule", "lastyear", "lastmonth", | |
| 1112 "yearlen", "nextyearlen", "yearordinal", "yearweekday", | |
| 1113 "mmask", "mrange", "mdaymask", "nmdaymask", | |
| 1114 "wdaymask", "wnomask", "nwdaymask", "eastermask"] | |
| 1115 | |
| 1116 def __init__(self, rrule): | |
| 1117 for attr in self.__slots__: | |
| 1118 setattr(self, attr, None) | |
| 1119 self.rrule = rrule | |
| 1120 | |
| 1121 def rebuild(self, year, month): | |
| 1122 # Every mask is 7 days longer to handle cross-year weekly periods. | |
| 1123 rr = self.rrule | |
| 1124 if year != self.lastyear: | |
| 1125 self.yearlen = 365 + calendar.isleap(year) | |
| 1126 self.nextyearlen = 365 + calendar.isleap(year + 1) | |
| 1127 firstyday = datetime.date(year, 1, 1) | |
| 1128 self.yearordinal = firstyday.toordinal() | |
| 1129 self.yearweekday = firstyday.weekday() | |
| 1130 | |
| 1131 wday = datetime.date(year, 1, 1).weekday() | |
| 1132 if self.yearlen == 365: | |
| 1133 self.mmask = M365MASK | |
| 1134 self.mdaymask = MDAY365MASK | |
| 1135 self.nmdaymask = NMDAY365MASK | |
| 1136 self.wdaymask = WDAYMASK[wday:] | |
| 1137 self.mrange = M365RANGE | |
| 1138 else: | |
| 1139 self.mmask = M366MASK | |
| 1140 self.mdaymask = MDAY366MASK | |
| 1141 self.nmdaymask = NMDAY366MASK | |
| 1142 self.wdaymask = WDAYMASK[wday:] | |
| 1143 self.mrange = M366RANGE | |
| 1144 | |
| 1145 if not rr._byweekno: | |
| 1146 self.wnomask = None | |
| 1147 else: | |
| 1148 self.wnomask = [0]*(self.yearlen+7) | |
| 1149 # no1wkst = firstwkst = self.wdaymask.index(rr._wkst) | |
| 1150 no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7 | |
| 1151 if no1wkst >= 4: | |
| 1152 no1wkst = 0 | |
| 1153 # Number of days in the year, plus the days we got | |
| 1154 # from last year. | |
| 1155 wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7 | |
| 1156 else: | |
| 1157 # Number of days in the year, minus the days we | |
| 1158 # left in last year. | |
| 1159 wyearlen = self.yearlen-no1wkst | |
| 1160 div, mod = divmod(wyearlen, 7) | |
| 1161 numweeks = div+mod//4 | |
| 1162 for n in rr._byweekno: | |
| 1163 if n < 0: | |
| 1164 n += numweeks+1 | |
| 1165 if not (0 < n <= numweeks): | |
| 1166 continue | |
| 1167 if n > 1: | |
| 1168 i = no1wkst+(n-1)*7 | |
| 1169 if no1wkst != firstwkst: | |
| 1170 i -= 7-firstwkst | |
| 1171 else: | |
| 1172 i = no1wkst | |
| 1173 for j in range(7): | |
| 1174 self.wnomask[i] = 1 | |
| 1175 i += 1 | |
| 1176 if self.wdaymask[i] == rr._wkst: | |
| 1177 break | |
| 1178 if 1 in rr._byweekno: | |
| 1179 # Check week number 1 of next year as well | |
| 1180 # TODO: Check -numweeks for next year. | |
| 1181 i = no1wkst+numweeks*7 | |
| 1182 if no1wkst != firstwkst: | |
| 1183 i -= 7-firstwkst | |
| 1184 if i < self.yearlen: | |
| 1185 # If week starts in next year, we | |
| 1186 # don't care about it. | |
| 1187 for j in range(7): | |
| 1188 self.wnomask[i] = 1 | |
| 1189 i += 1 | |
| 1190 if self.wdaymask[i] == rr._wkst: | |
| 1191 break | |
| 1192 if no1wkst: | |
| 1193 # Check last week number of last year as | |
| 1194 # well. If no1wkst is 0, either the year | |
| 1195 # started on week start, or week number 1 | |
| 1196 # got days from last year, so there are no | |
| 1197 # days from last year's last week number in | |
| 1198 # this year. | |
| 1199 if -1 not in rr._byweekno: | |
| 1200 lyearweekday = datetime.date(year-1, 1, 1).weekday() | |
| 1201 lno1wkst = (7-lyearweekday+rr._wkst) % 7 | |
| 1202 lyearlen = 365+calendar.isleap(year-1) | |
| 1203 if lno1wkst >= 4: | |
| 1204 lno1wkst = 0 | |
| 1205 lnumweeks = 52+(lyearlen + | |
| 1206 (lyearweekday-rr._wkst) % 7) % 7//4 | |
| 1207 else: | |
| 1208 lnumweeks = 52+(self.yearlen-no1wkst) % 7//4 | |
| 1209 else: | |
| 1210 lnumweeks = -1 | |
| 1211 if lnumweeks in rr._byweekno: | |
| 1212 for i in range(no1wkst): | |
| 1213 self.wnomask[i] = 1 | |
| 1214 | |
| 1215 if (rr._bynweekday and (month != self.lastmonth or | |
| 1216 year != self.lastyear)): | |
| 1217 ranges = [] | |
| 1218 if rr._freq == YEARLY: | |
| 1219 if rr._bymonth: | |
| 1220 for month in rr._bymonth: | |
| 1221 ranges.append(self.mrange[month-1:month+1]) | |
| 1222 else: | |
| 1223 ranges = [(0, self.yearlen)] | |
| 1224 elif rr._freq == MONTHLY: | |
| 1225 ranges = [self.mrange[month-1:month+1]] | |
| 1226 if ranges: | |
| 1227 # Weekly frequency won't get here, so we may not | |
| 1228 # care about cross-year weekly periods. | |
| 1229 self.nwdaymask = [0]*self.yearlen | |
| 1230 for first, last in ranges: | |
| 1231 last -= 1 | |
| 1232 for wday, n in rr._bynweekday: | |
| 1233 if n < 0: | |
| 1234 i = last+(n+1)*7 | |
| 1235 i -= (self.wdaymask[i]-wday) % 7 | |
| 1236 else: | |
| 1237 i = first+(n-1)*7 | |
| 1238 i += (7-self.wdaymask[i]+wday) % 7 | |
| 1239 if first <= i <= last: | |
| 1240 self.nwdaymask[i] = 1 | |
| 1241 | |
| 1242 if rr._byeaster: | |
| 1243 self.eastermask = [0]*(self.yearlen+7) | |
| 1244 eyday = easter.easter(year).toordinal()-self.yearordinal | |
| 1245 for offset in rr._byeaster: | |
| 1246 self.eastermask[eyday+offset] = 1 | |
| 1247 | |
| 1248 self.lastyear = year | |
| 1249 self.lastmonth = month | |
| 1250 | |
| 1251 def ydayset(self, year, month, day): | |
| 1252 return list(range(self.yearlen)), 0, self.yearlen | |
| 1253 | |
| 1254 def mdayset(self, year, month, day): | |
| 1255 dset = [None]*self.yearlen | |
| 1256 start, end = self.mrange[month-1:month+1] | |
| 1257 for i in range(start, end): | |
| 1258 dset[i] = i | |
| 1259 return dset, start, end | |
| 1260 | |
| 1261 def wdayset(self, year, month, day): | |
| 1262 # We need to handle cross-year weeks here. | |
| 1263 dset = [None]*(self.yearlen+7) | |
| 1264 i = datetime.date(year, month, day).toordinal()-self.yearordinal | |
| 1265 start = i | |
| 1266 for j in range(7): | |
| 1267 dset[i] = i | |
| 1268 i += 1 | |
| 1269 # if (not (0 <= i < self.yearlen) or | |
| 1270 # self.wdaymask[i] == self.rrule._wkst): | |
| 1271 # This will cross the year boundary, if necessary. | |
| 1272 if self.wdaymask[i] == self.rrule._wkst: | |
| 1273 break | |
| 1274 return dset, start, i | |
| 1275 | |
| 1276 def ddayset(self, year, month, day): | |
| 1277 dset = [None] * self.yearlen | |
| 1278 i = datetime.date(year, month, day).toordinal() - self.yearordinal | |
| 1279 dset[i] = i | |
| 1280 return dset, i, i + 1 | |
| 1281 | |
| 1282 def htimeset(self, hour, minute, second): | |
| 1283 tset = [] | |
| 1284 rr = self.rrule | |
| 1285 for minute in rr._byminute: | |
| 1286 for second in rr._bysecond: | |
| 1287 tset.append(datetime.time(hour, minute, second, | |
| 1288 tzinfo=rr._tzinfo)) | |
| 1289 tset.sort() | |
| 1290 return tset | |
| 1291 | |
| 1292 def mtimeset(self, hour, minute, second): | |
| 1293 tset = [] | |
| 1294 rr = self.rrule | |
| 1295 for second in rr._bysecond: | |
| 1296 tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo)) | |
| 1297 tset.sort() | |
| 1298 return tset | |
| 1299 | |
| 1300 def stimeset(self, hour, minute, second): | |
| 1301 return (datetime.time(hour, minute, second, | |
| 1302 tzinfo=self.rrule._tzinfo),) | |
| 1303 | |
| 1304 | |
| 1305 class rruleset(rrulebase): | |
| 1306 """ The rruleset type allows more complex recurrence setups, mixing | |
| 1307 multiple rules, dates, exclusion rules, and exclusion dates. The type | |
| 1308 constructor takes the following keyword arguments: | |
| 1309 | |
| 1310 :param cache: If True, caching of results will be enabled, improving | |
| 1311 performance of multiple queries considerably. """ | |
| 1312 | |
| 1313 class _genitem(object): | |
| 1314 def __init__(self, genlist, gen): | |
| 1315 try: | |
| 1316 self.dt = advance_iterator(gen) | |
| 1317 genlist.append(self) | |
| 1318 except StopIteration: | |
| 1319 pass | |
| 1320 self.genlist = genlist | |
| 1321 self.gen = gen | |
| 1322 | |
| 1323 def __next__(self): | |
| 1324 try: | |
| 1325 self.dt = advance_iterator(self.gen) | |
| 1326 except StopIteration: | |
| 1327 if self.genlist[0] is self: | |
| 1328 heapq.heappop(self.genlist) | |
| 1329 else: | |
| 1330 self.genlist.remove(self) | |
| 1331 heapq.heapify(self.genlist) | |
| 1332 | |
| 1333 next = __next__ | |
| 1334 | |
| 1335 def __lt__(self, other): | |
| 1336 return self.dt < other.dt | |
| 1337 | |
| 1338 def __gt__(self, other): | |
| 1339 return self.dt > other.dt | |
| 1340 | |
| 1341 def __eq__(self, other): | |
| 1342 return self.dt == other.dt | |
| 1343 | |
| 1344 def __ne__(self, other): | |
| 1345 return self.dt != other.dt | |
| 1346 | |
| 1347 def __init__(self, cache=False): | |
| 1348 super(rruleset, self).__init__(cache) | |
| 1349 self._rrule = [] | |
| 1350 self._rdate = [] | |
| 1351 self._exrule = [] | |
| 1352 self._exdate = [] | |
| 1353 | |
| 1354 @_invalidates_cache | |
| 1355 def rrule(self, rrule): | |
| 1356 """ Include the given :py:class:`rrule` instance in the recurrence set | |
| 1357 generation. """ | |
| 1358 self._rrule.append(rrule) | |
| 1359 | |
| 1360 @_invalidates_cache | |
| 1361 def rdate(self, rdate): | |
| 1362 """ Include the given :py:class:`datetime` instance in the recurrence | |
| 1363 set generation. """ | |
| 1364 self._rdate.append(rdate) | |
| 1365 | |
| 1366 @_invalidates_cache | |
| 1367 def exrule(self, exrule): | |
| 1368 """ Include the given rrule instance in the recurrence set exclusion | |
| 1369 list. Dates which are part of the given recurrence rules will not | |
| 1370 be generated, even if some inclusive rrule or rdate matches them. | |
| 1371 """ | |
| 1372 self._exrule.append(exrule) | |
| 1373 | |
| 1374 @_invalidates_cache | |
| 1375 def exdate(self, exdate): | |
| 1376 """ Include the given datetime instance in the recurrence set | |
| 1377 exclusion list. Dates included that way will not be generated, | |
| 1378 even if some inclusive rrule or rdate matches them. """ | |
| 1379 self._exdate.append(exdate) | |
| 1380 | |
| 1381 def _iter(self): | |
| 1382 rlist = [] | |
| 1383 self._rdate.sort() | |
| 1384 self._genitem(rlist, iter(self._rdate)) | |
| 1385 for gen in [iter(x) for x in self._rrule]: | |
| 1386 self._genitem(rlist, gen) | |
| 1387 exlist = [] | |
| 1388 self._exdate.sort() | |
| 1389 self._genitem(exlist, iter(self._exdate)) | |
| 1390 for gen in [iter(x) for x in self._exrule]: | |
| 1391 self._genitem(exlist, gen) | |
| 1392 lastdt = None | |
| 1393 total = 0 | |
| 1394 heapq.heapify(rlist) | |
| 1395 heapq.heapify(exlist) | |
| 1396 while rlist: | |
| 1397 ritem = rlist[0] | |
| 1398 if not lastdt or lastdt != ritem.dt: | |
| 1399 while exlist and exlist[0] < ritem: | |
| 1400 exitem = exlist[0] | |
| 1401 advance_iterator(exitem) | |
| 1402 if exlist and exlist[0] is exitem: | |
| 1403 heapq.heapreplace(exlist, exitem) | |
| 1404 if not exlist or ritem != exlist[0]: | |
| 1405 total += 1 | |
| 1406 yield ritem.dt | |
| 1407 lastdt = ritem.dt | |
| 1408 advance_iterator(ritem) | |
| 1409 if rlist and rlist[0] is ritem: | |
| 1410 heapq.heapreplace(rlist, ritem) | |
| 1411 self._len = total | |
| 1412 | |
| 1413 | |
| 1414 | |
| 1415 | |
| 1416 class _rrulestr(object): | |
| 1417 """ Parses a string representation of a recurrence rule or set of | |
| 1418 recurrence rules. | |
| 1419 | |
| 1420 :param s: | |
| 1421 Required, a string defining one or more recurrence rules. | |
| 1422 | |
| 1423 :param dtstart: | |
| 1424 If given, used as the default recurrence start if not specified in the | |
| 1425 rule string. | |
| 1426 | |
| 1427 :param cache: | |
| 1428 If set ``True`` caching of results will be enabled, improving | |
| 1429 performance of multiple queries considerably. | |
| 1430 | |
| 1431 :param unfold: | |
| 1432 If set ``True`` indicates that a rule string is split over more | |
| 1433 than one line and should be joined before processing. | |
| 1434 | |
| 1435 :param forceset: | |
| 1436 If set ``True`` forces a :class:`dateutil.rrule.rruleset` to | |
| 1437 be returned. | |
| 1438 | |
| 1439 :param compatible: | |
| 1440 If set ``True`` forces ``unfold`` and ``forceset`` to be ``True``. | |
| 1441 | |
| 1442 :param ignoretz: | |
| 1443 If set ``True``, time zones in parsed strings are ignored and a naive | |
| 1444 :class:`datetime.datetime` object is returned. | |
| 1445 | |
| 1446 :param tzids: | |
| 1447 If given, a callable or mapping used to retrieve a | |
| 1448 :class:`datetime.tzinfo` from a string representation. | |
| 1449 Defaults to :func:`dateutil.tz.gettz`. | |
| 1450 | |
| 1451 :param tzinfos: | |
| 1452 Additional time zone names / aliases which may be present in a string | |
| 1453 representation. See :func:`dateutil.parser.parse` for more | |
| 1454 information. | |
| 1455 | |
| 1456 :return: | |
| 1457 Returns a :class:`dateutil.rrule.rruleset` or | |
| 1458 :class:`dateutil.rrule.rrule` | |
| 1459 """ | |
| 1460 | |
| 1461 _freq_map = {"YEARLY": YEARLY, | |
| 1462 "MONTHLY": MONTHLY, | |
| 1463 "WEEKLY": WEEKLY, | |
| 1464 "DAILY": DAILY, | |
| 1465 "HOURLY": HOURLY, | |
| 1466 "MINUTELY": MINUTELY, | |
| 1467 "SECONDLY": SECONDLY} | |
| 1468 | |
| 1469 _weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3, | |
| 1470 "FR": 4, "SA": 5, "SU": 6} | |
| 1471 | |
| 1472 def _handle_int(self, rrkwargs, name, value, **kwargs): | |
| 1473 rrkwargs[name.lower()] = int(value) | |
| 1474 | |
| 1475 def _handle_int_list(self, rrkwargs, name, value, **kwargs): | |
| 1476 rrkwargs[name.lower()] = [int(x) for x in value.split(',')] | |
| 1477 | |
| 1478 _handle_INTERVAL = _handle_int | |
| 1479 _handle_COUNT = _handle_int | |
| 1480 _handle_BYSETPOS = _handle_int_list | |
| 1481 _handle_BYMONTH = _handle_int_list | |
| 1482 _handle_BYMONTHDAY = _handle_int_list | |
| 1483 _handle_BYYEARDAY = _handle_int_list | |
| 1484 _handle_BYEASTER = _handle_int_list | |
| 1485 _handle_BYWEEKNO = _handle_int_list | |
| 1486 _handle_BYHOUR = _handle_int_list | |
| 1487 _handle_BYMINUTE = _handle_int_list | |
| 1488 _handle_BYSECOND = _handle_int_list | |
| 1489 | |
| 1490 def _handle_FREQ(self, rrkwargs, name, value, **kwargs): | |
| 1491 rrkwargs["freq"] = self._freq_map[value] | |
| 1492 | |
| 1493 def _handle_UNTIL(self, rrkwargs, name, value, **kwargs): | |
| 1494 global parser | |
| 1495 if not parser: | |
| 1496 from dateutil import parser | |
| 1497 try: | |
| 1498 rrkwargs["until"] = parser.parse(value, | |
| 1499 ignoretz=kwargs.get("ignoretz"), | |
| 1500 tzinfos=kwargs.get("tzinfos")) | |
| 1501 except ValueError: | |
| 1502 raise ValueError("invalid until date") | |
| 1503 | |
| 1504 def _handle_WKST(self, rrkwargs, name, value, **kwargs): | |
| 1505 rrkwargs["wkst"] = self._weekday_map[value] | |
| 1506 | |
| 1507 def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs): | |
| 1508 """ | |
| 1509 Two ways to specify this: +1MO or MO(+1) | |
| 1510 """ | |
| 1511 l = [] | |
| 1512 for wday in value.split(','): | |
| 1513 if '(' in wday: | |
| 1514 # If it's of the form TH(+1), etc. | |
| 1515 splt = wday.split('(') | |
| 1516 w = splt[0] | |
| 1517 n = int(splt[1][:-1]) | |
| 1518 elif len(wday): | |
| 1519 # If it's of the form +1MO | |
| 1520 for i in range(len(wday)): | |
| 1521 if wday[i] not in '+-0123456789': | |
| 1522 break | |
| 1523 n = wday[:i] or None | |
| 1524 w = wday[i:] | |
| 1525 if n: | |
| 1526 n = int(n) | |
| 1527 else: | |
| 1528 raise ValueError("Invalid (empty) BYDAY specification.") | |
| 1529 | |
| 1530 l.append(weekdays[self._weekday_map[w]](n)) | |
| 1531 rrkwargs["byweekday"] = l | |
| 1532 | |
| 1533 _handle_BYDAY = _handle_BYWEEKDAY | |
| 1534 | |
| 1535 def _parse_rfc_rrule(self, line, | |
| 1536 dtstart=None, | |
| 1537 cache=False, | |
| 1538 ignoretz=False, | |
| 1539 tzinfos=None): | |
| 1540 if line.find(':') != -1: | |
| 1541 name, value = line.split(':') | |
| 1542 if name != "RRULE": | |
| 1543 raise ValueError("unknown parameter name") | |
| 1544 else: | |
| 1545 value = line | |
| 1546 rrkwargs = {} | |
| 1547 for pair in value.split(';'): | |
| 1548 name, value = pair.split('=') | |
| 1549 name = name.upper() | |
| 1550 value = value.upper() | |
| 1551 try: | |
| 1552 getattr(self, "_handle_"+name)(rrkwargs, name, value, | |
| 1553 ignoretz=ignoretz, | |
| 1554 tzinfos=tzinfos) | |
| 1555 except AttributeError: | |
| 1556 raise ValueError("unknown parameter '%s'" % name) | |
| 1557 except (KeyError, ValueError): | |
| 1558 raise ValueError("invalid '%s': %s" % (name, value)) | |
| 1559 return rrule(dtstart=dtstart, cache=cache, **rrkwargs) | |
| 1560 | |
| 1561 def _parse_date_value(self, date_value, parms, rule_tzids, | |
| 1562 ignoretz, tzids, tzinfos): | |
| 1563 global parser | |
| 1564 if not parser: | |
| 1565 from dateutil import parser | |
| 1566 | |
| 1567 datevals = [] | |
| 1568 value_found = False | |
| 1569 TZID = None | |
| 1570 | |
| 1571 for parm in parms: | |
| 1572 if parm.startswith("TZID="): | |
| 1573 try: | |
| 1574 tzkey = rule_tzids[parm.split('TZID=')[-1]] | |
| 1575 except KeyError: | |
| 1576 continue | |
| 1577 if tzids is None: | |
| 1578 from . import tz | |
| 1579 tzlookup = tz.gettz | |
| 1580 elif callable(tzids): | |
| 1581 tzlookup = tzids | |
| 1582 else: | |
| 1583 tzlookup = getattr(tzids, 'get', None) | |
| 1584 if tzlookup is None: | |
| 1585 msg = ('tzids must be a callable, mapping, or None, ' | |
| 1586 'not %s' % tzids) | |
| 1587 raise ValueError(msg) | |
| 1588 | |
| 1589 TZID = tzlookup(tzkey) | |
| 1590 continue | |
| 1591 | |
| 1592 # RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found | |
| 1593 # only once. | |
| 1594 if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}: | |
| 1595 raise ValueError("unsupported parm: " + parm) | |
| 1596 else: | |
| 1597 if value_found: | |
| 1598 msg = ("Duplicate value parameter found in: " + parm) | |
| 1599 raise ValueError(msg) | |
| 1600 value_found = True | |
| 1601 | |
| 1602 for datestr in date_value.split(','): | |
| 1603 date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos) | |
| 1604 if TZID is not None: | |
| 1605 if date.tzinfo is None: | |
| 1606 date = date.replace(tzinfo=TZID) | |
| 1607 else: | |
| 1608 raise ValueError('DTSTART/EXDATE specifies multiple timezone') | |
| 1609 datevals.append(date) | |
| 1610 | |
| 1611 return datevals | |
| 1612 | |
| 1613 def _parse_rfc(self, s, | |
| 1614 dtstart=None, | |
| 1615 cache=False, | |
| 1616 unfold=False, | |
| 1617 forceset=False, | |
| 1618 compatible=False, | |
| 1619 ignoretz=False, | |
| 1620 tzids=None, | |
| 1621 tzinfos=None): | |
| 1622 global parser | |
| 1623 if compatible: | |
| 1624 forceset = True | |
| 1625 unfold = True | |
| 1626 | |
| 1627 TZID_NAMES = dict(map( | |
| 1628 lambda x: (x.upper(), x), | |
| 1629 re.findall('TZID=(?P<name>[^:]+):', s) | |
| 1630 )) | |
| 1631 s = s.upper() | |
| 1632 if not s.strip(): | |
| 1633 raise ValueError("empty string") | |
| 1634 if unfold: | |
| 1635 lines = s.splitlines() | |
| 1636 i = 0 | |
| 1637 while i < len(lines): | |
| 1638 line = lines[i].rstrip() | |
| 1639 if not line: | |
| 1640 del lines[i] | |
| 1641 elif i > 0 and line[0] == " ": | |
| 1642 lines[i-1] += line[1:] | |
| 1643 del lines[i] | |
| 1644 else: | |
| 1645 i += 1 | |
| 1646 else: | |
| 1647 lines = s.split() | |
| 1648 if (not forceset and len(lines) == 1 and (s.find(':') == -1 or | |
| 1649 s.startswith('RRULE:'))): | |
| 1650 return self._parse_rfc_rrule(lines[0], cache=cache, | |
| 1651 dtstart=dtstart, ignoretz=ignoretz, | |
| 1652 tzinfos=tzinfos) | |
| 1653 else: | |
| 1654 rrulevals = [] | |
| 1655 rdatevals = [] | |
| 1656 exrulevals = [] | |
| 1657 exdatevals = [] | |
| 1658 for line in lines: | |
| 1659 if not line: | |
| 1660 continue | |
| 1661 if line.find(':') == -1: | |
| 1662 name = "RRULE" | |
| 1663 value = line | |
| 1664 else: | |
| 1665 name, value = line.split(':', 1) | |
| 1666 parms = name.split(';') | |
| 1667 if not parms: | |
| 1668 raise ValueError("empty property name") | |
| 1669 name = parms[0] | |
| 1670 parms = parms[1:] | |
| 1671 if name == "RRULE": | |
| 1672 for parm in parms: | |
| 1673 raise ValueError("unsupported RRULE parm: "+parm) | |
| 1674 rrulevals.append(value) | |
| 1675 elif name == "RDATE": | |
| 1676 for parm in parms: | |
| 1677 if parm != "VALUE=DATE-TIME": | |
| 1678 raise ValueError("unsupported RDATE parm: "+parm) | |
| 1679 rdatevals.append(value) | |
| 1680 elif name == "EXRULE": | |
| 1681 for parm in parms: | |
| 1682 raise ValueError("unsupported EXRULE parm: "+parm) | |
| 1683 exrulevals.append(value) | |
| 1684 elif name == "EXDATE": | |
| 1685 exdatevals.extend( | |
| 1686 self._parse_date_value(value, parms, | |
| 1687 TZID_NAMES, ignoretz, | |
| 1688 tzids, tzinfos) | |
| 1689 ) | |
| 1690 elif name == "DTSTART": | |
| 1691 dtvals = self._parse_date_value(value, parms, TZID_NAMES, | |
| 1692 ignoretz, tzids, tzinfos) | |
| 1693 if len(dtvals) != 1: | |
| 1694 raise ValueError("Multiple DTSTART values specified:" + | |
| 1695 value) | |
| 1696 dtstart = dtvals[0] | |
| 1697 else: | |
| 1698 raise ValueError("unsupported property: "+name) | |
| 1699 if (forceset or len(rrulevals) > 1 or rdatevals | |
| 1700 or exrulevals or exdatevals): | |
| 1701 if not parser and (rdatevals or exdatevals): | |
| 1702 from dateutil import parser | |
| 1703 rset = rruleset(cache=cache) | |
| 1704 for value in rrulevals: | |
| 1705 rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart, | |
| 1706 ignoretz=ignoretz, | |
| 1707 tzinfos=tzinfos)) | |
| 1708 for value in rdatevals: | |
| 1709 for datestr in value.split(','): | |
| 1710 rset.rdate(parser.parse(datestr, | |
| 1711 ignoretz=ignoretz, | |
| 1712 tzinfos=tzinfos)) | |
| 1713 for value in exrulevals: | |
| 1714 rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart, | |
| 1715 ignoretz=ignoretz, | |
| 1716 tzinfos=tzinfos)) | |
| 1717 for value in exdatevals: | |
| 1718 rset.exdate(value) | |
| 1719 if compatible and dtstart: | |
| 1720 rset.rdate(dtstart) | |
| 1721 return rset | |
| 1722 else: | |
| 1723 return self._parse_rfc_rrule(rrulevals[0], | |
| 1724 dtstart=dtstart, | |
| 1725 cache=cache, | |
| 1726 ignoretz=ignoretz, | |
| 1727 tzinfos=tzinfos) | |
| 1728 | |
| 1729 def __call__(self, s, **kwargs): | |
| 1730 return self._parse_rfc(s, **kwargs) | |
| 1731 | |
| 1732 | |
| 1733 rrulestr = _rrulestr() | |
| 1734 | |
| 1735 # vim:ts=4:sw=4:et |
