Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/dateutil/rrule.py: 12%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

979 statements  

1# -*- coding: utf-8 -*- 

2""" 

3The rrule module offers a small, complete, and very fast, implementation of 

4the recurrence rules documented in the 

5`iCalendar RFC <https://tools.ietf.org/html/rfc5545>`_, 

6including support for caching of results. 

7""" 

8import calendar 

9import datetime 

10import heapq 

11import itertools 

12import re 

13import sys 

14from functools import wraps 

15# For warning about deprecation of until and count 

16from warnings import warn 

17 

18from six import advance_iterator, integer_types 

19 

20from six.moves import _thread, range 

21 

22from ._common import weekday as weekdaybase 

23 

24try: 

25 from math import gcd 

26except ImportError: 

27 from fractions import gcd 

28 

29__all__ = ["rrule", "rruleset", "rrulestr", 

30 "YEARLY", "MONTHLY", "WEEKLY", "DAILY", 

31 "HOURLY", "MINUTELY", "SECONDLY", 

32 "MO", "TU", "WE", "TH", "FR", "SA", "SU"] 

33 

34# Every mask is 7 days longer to handle cross-year weekly periods. 

35M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 + 

36 [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7) 

37M365MASK = list(M366MASK) 

38M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32)) 

39MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) 

40MDAY365MASK = list(MDAY366MASK) 

41M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0)) 

42NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) 

43NMDAY365MASK = list(NMDAY366MASK) 

44M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366) 

45M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365) 

46WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55 

47del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31] 

48MDAY365MASK = tuple(MDAY365MASK) 

49M365MASK = tuple(M365MASK) 

50 

51FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY'] 

52 

53(YEARLY, 

54 MONTHLY, 

55 WEEKLY, 

56 DAILY, 

57 HOURLY, 

58 MINUTELY, 

59 SECONDLY) = list(range(7)) 

60 

61# Imported on demand. 

62easter = None 

63parser = None 

64 

65 

66class weekday(weekdaybase): 

67 """ 

68 This version of weekday does not allow n = 0. 

69 """ 

70 def __init__(self, wkday, n=None): 

71 if n == 0: 

72 raise ValueError("Can't create weekday with n==0") 

73 

74 super(weekday, self).__init__(wkday, n) 

75 

76 

77MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) 

78 

79 

80def _invalidates_cache(f): 

81 """ 

82 Decorator for rruleset methods which may invalidate the 

83 cached length. 

84 """ 

85 @wraps(f) 

86 def inner_func(self, *args, **kwargs): 

87 rv = f(self, *args, **kwargs) 

88 self._invalidate_cache() 

89 return rv 

90 

91 return inner_func 

92 

93 

94class rrulebase(object): 

95 def __init__(self, cache=False): 

96 if cache: 

97 self._cache = [] 

98 self._cache_lock = _thread.allocate_lock() 

99 self._invalidate_cache() 

100 else: 

101 self._cache = None 

102 self._cache_complete = False 

103 self._len = None 

104 

105 def __iter__(self): 

106 if self._cache_complete: 

107 return iter(self._cache) 

108 elif self._cache is None: 

109 return self._iter() 

110 else: 

111 return self._iter_cached() 

112 

113 def _invalidate_cache(self): 

114 if self._cache is not None: 

115 self._cache = [] 

116 self._cache_complete = False 

117 self._cache_gen = self._iter() 

118 

119 if self._cache_lock.locked(): 

120 self._cache_lock.release() 

121 

122 self._len = None 

123 

124 def _iter_cached(self): 

125 i = 0 

126 gen = self._cache_gen 

127 cache = self._cache 

128 acquire = self._cache_lock.acquire 

129 release = self._cache_lock.release 

130 while gen: 

131 if i == len(cache): 

132 acquire() 

133 if self._cache_complete: 

134 break 

135 try: 

136 for j in range(10): 

137 cache.append(advance_iterator(gen)) 

138 except StopIteration: 

139 self._cache_gen = gen = None 

140 self._cache_complete = True 

141 break 

142 release() 

143 yield cache[i] 

144 i += 1 

145 while i < self._len: 

146 yield cache[i] 

147 i += 1 

148 

149 def __getitem__(self, item): 

150 if self._cache_complete: 

151 return self._cache[item] 

152 elif isinstance(item, slice): 

153 if item.step and item.step < 0: 

154 return list(iter(self))[item] 

155 else: 

156 return list(itertools.islice(self, 

157 item.start or 0, 

158 item.stop or sys.maxsize, 

159 item.step or 1)) 

160 elif item >= 0: 

161 gen = iter(self) 

162 try: 

163 for i in range(item+1): 

164 res = advance_iterator(gen) 

165 except StopIteration: 

166 raise IndexError 

167 return res 

168 else: 

169 return list(iter(self))[item] 

170 

171 def __contains__(self, item): 

172 if self._cache_complete: 

173 return item in self._cache 

174 else: 

175 for i in self: 

176 if i == item: 

177 return True 

178 elif i > item: 

179 return False 

180 return False 

181 

182 # __len__() introduces a large performance penalty. 

183 def count(self): 

184 """ Returns the number of recurrences in this set. It will have go 

185 through the whole recurrence, if this hasn't been done before. """ 

186 if self._len is None: 

187 for x in self: 

188 pass 

189 return self._len 

190 

191 def before(self, dt, inc=False): 

192 """ Returns the last recurrence before the given datetime instance. The 

193 inc keyword defines what happens if dt is an occurrence. With 

194 inc=True, if dt itself is an occurrence, it will be returned. """ 

195 if self._cache_complete: 

196 gen = self._cache 

197 else: 

198 gen = self 

199 last = None 

200 if inc: 

201 for i in gen: 

202 if i > dt: 

203 break 

204 last = i 

205 else: 

206 for i in gen: 

207 if i >= dt: 

208 break 

209 last = i 

210 return last 

211 

212 def after(self, dt, inc=False): 

213 """ Returns the first recurrence after the given datetime instance. The 

214 inc keyword defines what happens if dt is an occurrence. With 

215 inc=True, if dt itself is an occurrence, it will be returned. """ 

216 if self._cache_complete: 

217 gen = self._cache 

218 else: 

219 gen = self 

220 if inc: 

221 for i in gen: 

222 if i >= dt: 

223 return i 

224 else: 

225 for i in gen: 

226 if i > dt: 

227 return i 

228 return None 

229 

230 def xafter(self, dt, count=None, inc=False): 

231 """ 

232 Generator which yields up to `count` recurrences after the given 

233 datetime instance, equivalent to `after`. 

234 

235 :param dt: 

236 The datetime at which to start generating recurrences. 

237 

238 :param count: 

239 The maximum number of recurrences to generate. If `None` (default), 

240 dates are generated until the recurrence rule is exhausted. 

241 

242 :param inc: 

243 If `dt` is an instance of the rule and `inc` is `True`, it is 

244 included in the output. 

245 

246 :yields: Yields a sequence of `datetime` objects. 

247 """ 

248 

249 if self._cache_complete: 

250 gen = self._cache 

251 else: 

252 gen = self 

253 

254 # Select the comparison function 

255 if inc: 

256 comp = lambda dc, dtc: dc >= dtc 

257 else: 

258 comp = lambda dc, dtc: dc > dtc 

259 

260 # Generate dates 

261 n = 0 

262 for d in gen: 

263 if comp(d, dt): 

264 if count is not None: 

265 n += 1 

266 if n > count: 

267 break 

268 

269 yield d 

270 

271 def between(self, after, before, inc=False, count=1): 

272 """ Returns all the occurrences of the rrule between after and before. 

273 The inc keyword defines what happens if after and/or before are 

274 themselves occurrences. With inc=True, they will be included in the 

275 list, if they are found in the recurrence set. """ 

276 if self._cache_complete: 

277 gen = self._cache 

278 else: 

279 gen = self 

280 started = False 

281 l = [] 

282 if inc: 

283 for i in gen: 

284 if i > before: 

285 break 

286 elif not started: 

287 if i >= after: 

288 started = True 

289 l.append(i) 

290 else: 

291 l.append(i) 

292 else: 

293 for i in gen: 

294 if i >= before: 

295 break 

296 elif not started: 

297 if i > after: 

298 started = True 

299 l.append(i) 

300 else: 

301 l.append(i) 

302 return l 

303 

304 

305class rrule(rrulebase): 

306 """ 

307 That's the base of the rrule operation. It accepts all the keywords 

308 defined in the RFC as its constructor parameters (except byday, 

309 which was renamed to byweekday) and more. The constructor prototype is:: 

310 

311 rrule(freq) 

312 

313 Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, 

314 or SECONDLY. 

315 

316 .. note:: 

317 Per RFC section 3.3.10, recurrence instances falling on invalid dates 

318 and times are ignored rather than coerced: 

319 

320 Recurrence rules may generate recurrence instances with an invalid 

321 date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM 

322 on a day where the local time is moved forward by an hour at 1:00 

323 AM). Such recurrence instances MUST be ignored and MUST NOT be 

324 counted as part of the recurrence set. 

325 

326 This can lead to possibly surprising behavior when, for example, the 

327 start date occurs at the end of the month: 

328 

329 >>> from dateutil.rrule import rrule, MONTHLY 

330 >>> from datetime import datetime 

331 >>> start_date = datetime(2014, 12, 31) 

332 >>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date)) 

333 ... # doctest: +NORMALIZE_WHITESPACE 

334 [datetime.datetime(2014, 12, 31, 0, 0), 

335 datetime.datetime(2015, 1, 31, 0, 0), 

336 datetime.datetime(2015, 3, 31, 0, 0), 

337 datetime.datetime(2015, 5, 31, 0, 0)] 

338 

339 Additionally, it supports the following keyword arguments: 

340 

341 :param dtstart: 

342 The recurrence start. Besides being the base for the recurrence, 

343 missing parameters in the final recurrence instances will also be 

344 extracted from this date. If not given, datetime.now() will be used 

345 instead. 

346 :param interval: 

347 The interval between each freq iteration. For example, when using 

348 YEARLY, an interval of 2 means once every two years, but with HOURLY, 

349 it means once every two hours. The default interval is 1. 

350 :param wkst: 

351 The week start day. Must be one of the MO, TU, WE constants, or an 

352 integer, specifying the first day of the week. This will affect 

353 recurrences based on weekly periods. The default week start is got 

354 from calendar.firstweekday(), and may be modified by 

355 calendar.setfirstweekday(). 

356 :param count: 

357 If given, this determines how many occurrences will be generated. 

358 

359 .. note:: 

360 As of version 2.5.0, the use of the keyword ``until`` in conjunction 

361 with ``count`` is deprecated, to make sure ``dateutil`` is fully 

362 compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/ 

363 html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count`` 

364 **must not** occur in the same call to ``rrule``. 

365 :param until: 

366 If given, this must be a datetime instance specifying the upper-bound 

367 limit of the recurrence. The last recurrence in the rule is the greatest 

368 datetime that is less than or equal to the value specified in the 

369 ``until`` parameter. 

370 

371 .. note:: 

372 As of version 2.5.0, the use of the keyword ``until`` in conjunction 

373 with ``count`` is deprecated, to make sure ``dateutil`` is fully 

374 compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/ 

375 html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count`` 

376 **must not** occur in the same call to ``rrule``. 

377 :param bysetpos: 

378 If given, it must be either an integer, or a sequence of integers, 

379 positive or negative. Each given integer will specify an occurrence 

380 number, corresponding to the nth occurrence of the rule inside the 

381 frequency period. For example, a bysetpos of -1 if combined with a 

382 MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will 

383 result in the last work day of every month. 

384 :param bymonth: 

385 If given, it must be either an integer, or a sequence of integers, 

386 meaning the months to apply the recurrence to. 

387 :param bymonthday: 

388 If given, it must be either an integer, or a sequence of integers, 

389 meaning the month days to apply the recurrence to. 

390 :param byyearday: 

391 If given, it must be either an integer, or a sequence of integers, 

392 meaning the year days to apply the recurrence to. 

393 :param byeaster: 

394 If given, it must be either an integer, or a sequence of integers, 

395 positive or negative. Each integer will define an offset from the 

396 Easter Sunday. Passing the offset 0 to byeaster will yield the Easter 

397 Sunday itself. This is an extension to the RFC specification. 

398 :param byweekno: 

399 If given, it must be either an integer, or a sequence of integers, 

400 meaning the week numbers to apply the recurrence to. Week numbers 

401 have the meaning described in ISO8601, that is, the first week of 

402 the year is that containing at least four days of the new year. 

403 :param byweekday: 

404 If given, it must be either an integer (0 == MO), a sequence of 

405 integers, one of the weekday constants (MO, TU, etc), or a sequence 

406 of these constants. When given, these variables will define the 

407 weekdays where the recurrence will be applied. It's also possible to 

408 use an argument n for the weekday instances, which will mean the nth 

409 occurrence of this weekday in the period. For example, with MONTHLY, 

410 or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the 

411 first friday of the month where the recurrence happens. Notice that in 

412 the RFC documentation, this is specified as BYDAY, but was renamed to 

413 avoid the ambiguity of that keyword. 

414 :param byhour: 

415 If given, it must be either an integer, or a sequence of integers, 

416 meaning the hours to apply the recurrence to. 

417 :param byminute: 

418 If given, it must be either an integer, or a sequence of integers, 

419 meaning the minutes to apply the recurrence to. 

420 :param bysecond: 

421 If given, it must be either an integer, or a sequence of integers, 

422 meaning the seconds to apply the recurrence to. 

423 :param cache: 

424 If given, it must be a boolean value specifying to enable or disable 

425 caching of results. If you will use the same rrule instance multiple 

426 times, enabling caching will improve the performance considerably. 

427 """ 

428 def __init__(self, freq, dtstart=None, 

429 interval=1, wkst=None, count=None, until=None, bysetpos=None, 

430 bymonth=None, bymonthday=None, byyearday=None, byeaster=None, 

431 byweekno=None, byweekday=None, 

432 byhour=None, byminute=None, bysecond=None, 

433 cache=False): 

434 super(rrule, self).__init__(cache) 

435 global easter 

436 if not dtstart: 

437 if until and until.tzinfo: 

438 dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0) 

439 else: 

440 dtstart = datetime.datetime.now().replace(microsecond=0) 

441 elif not isinstance(dtstart, datetime.datetime): 

442 dtstart = datetime.datetime.fromordinal(dtstart.toordinal()) 

443 else: 

444 dtstart = dtstart.replace(microsecond=0) 

445 self._dtstart = dtstart 

446 self._tzinfo = dtstart.tzinfo 

447 self._freq = freq 

448 self._interval = interval 

449 self._count = count 

450 

451 # Cache the original byxxx rules, if they are provided, as the _byxxx 

452 # attributes do not necessarily map to the inputs, and this can be 

453 # a problem in generating the strings. Only store things if they've 

454 # been supplied (the string retrieval will just use .get()) 

455 self._original_rule = {} 

456 

457 if until and not isinstance(until, datetime.datetime): 

458 until = datetime.datetime.fromordinal(until.toordinal()) 

459 self._until = until 

460 

461 if self._dtstart and self._until: 

462 if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None): 

463 # According to RFC5545 Section 3.3.10: 

464 # https://tools.ietf.org/html/rfc5545#section-3.3.10 

465 # 

466 # > If the "DTSTART" property is specified as a date with UTC 

467 # > time or a date with local time and time zone reference, 

468 # > then the UNTIL rule part MUST be specified as a date with 

469 # > UTC time. 

470 raise ValueError( 

471 'RRULE UNTIL values must be specified in UTC when DTSTART ' 

472 'is timezone-aware' 

473 ) 

474 

475 if count is not None and until: 

476 warn("Using both 'count' and 'until' is inconsistent with RFC 5545" 

477 " and has been deprecated in dateutil. Future versions will " 

478 "raise an error.", DeprecationWarning) 

479 

480 if wkst is None: 

481 self._wkst = calendar.firstweekday() 

482 elif isinstance(wkst, integer_types): 

483 self._wkst = wkst 

484 else: 

485 self._wkst = wkst.weekday 

486 

487 if bysetpos is None: 

488 self._bysetpos = None 

489 elif isinstance(bysetpos, integer_types): 

490 if bysetpos == 0 or not (-366 <= bysetpos <= 366): 

491 raise ValueError("bysetpos must be between 1 and 366, " 

492 "or between -366 and -1") 

493 self._bysetpos = (bysetpos,) 

494 else: 

495 self._bysetpos = tuple(bysetpos) 

496 for pos in self._bysetpos: 

497 if pos == 0 or not (-366 <= pos <= 366): 

498 raise ValueError("bysetpos must be between 1 and 366, " 

499 "or between -366 and -1") 

500 

501 if self._bysetpos: 

502 self._original_rule['bysetpos'] = self._bysetpos 

503 

504 if (byweekno is None and byyearday is None and bymonthday is None and 

505 byweekday is None and byeaster is None): 

506 if freq == YEARLY: 

507 if bymonth is None: 

508 bymonth = dtstart.month 

509 self._original_rule['bymonth'] = None 

510 bymonthday = dtstart.day 

511 self._original_rule['bymonthday'] = None 

512 elif freq == MONTHLY: 

513 bymonthday = dtstart.day 

514 self._original_rule['bymonthday'] = None 

515 elif freq == WEEKLY: 

516 byweekday = dtstart.weekday() 

517 self._original_rule['byweekday'] = None 

518 

519 # bymonth 

520 if bymonth is None: 

521 self._bymonth = None 

522 else: 

523 if isinstance(bymonth, integer_types): 

524 bymonth = (bymonth,) 

525 

526 self._bymonth = tuple(sorted(set(bymonth))) 

527 

528 if 'bymonth' not in self._original_rule: 

529 self._original_rule['bymonth'] = self._bymonth 

530 

531 # byyearday 

532 if byyearday is None: 

533 self._byyearday = None 

534 else: 

535 if isinstance(byyearday, integer_types): 

536 byyearday = (byyearday,) 

537 

538 self._byyearday = tuple(sorted(set(byyearday))) 

539 self._original_rule['byyearday'] = self._byyearday 

540 

541 # byeaster 

542 if byeaster is not None: 

543 if not easter: 

544 from dateutil import easter 

545 if isinstance(byeaster, integer_types): 

546 self._byeaster = (byeaster,) 

547 else: 

548 self._byeaster = tuple(sorted(byeaster)) 

549 

550 self._original_rule['byeaster'] = self._byeaster 

551 else: 

552 self._byeaster = None 

553 

554 # bymonthday 

555 if bymonthday is None: 

556 self._bymonthday = () 

557 self._bynmonthday = () 

558 else: 

559 if isinstance(bymonthday, integer_types): 

560 bymonthday = (bymonthday,) 

561 

562 bymonthday = set(bymonthday) # Ensure it's unique 

563 

564 self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0)) 

565 self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0)) 

566 

567 # Storing positive numbers first, then negative numbers 

568 if 'bymonthday' not in self._original_rule: 

569 self._original_rule['bymonthday'] = tuple( 

570 itertools.chain(self._bymonthday, self._bynmonthday)) 

571 

572 # byweekno 

573 if byweekno is None: 

574 self._byweekno = None 

575 else: 

576 if isinstance(byweekno, integer_types): 

577 byweekno = (byweekno,) 

578 

579 self._byweekno = tuple(sorted(set(byweekno))) 

580 

581 self._original_rule['byweekno'] = self._byweekno 

582 

583 # byweekday / bynweekday 

584 if byweekday is None: 

585 self._byweekday = None 

586 self._bynweekday = None 

587 else: 

588 # If it's one of the valid non-sequence types, convert to a 

589 # single-element sequence before the iterator that builds the 

590 # byweekday set. 

591 if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"): 

592 byweekday = (byweekday,) 

593 

594 self._byweekday = set() 

595 self._bynweekday = set() 

596 for wday in byweekday: 

597 if isinstance(wday, integer_types): 

598 self._byweekday.add(wday) 

599 elif not wday.n or freq > MONTHLY: 

600 self._byweekday.add(wday.weekday) 

601 else: 

602 self._bynweekday.add((wday.weekday, wday.n)) 

603 

604 if not self._byweekday: 

605 self._byweekday = None 

606 elif not self._bynweekday: 

607 self._bynweekday = None 

608 

609 if self._byweekday is not None: 

610 self._byweekday = tuple(sorted(self._byweekday)) 

611 orig_byweekday = [weekday(x) for x in self._byweekday] 

612 else: 

613 orig_byweekday = () 

614 

615 if self._bynweekday is not None: 

616 self._bynweekday = tuple(sorted(self._bynweekday)) 

617 orig_bynweekday = [weekday(*x) for x in self._bynweekday] 

618 else: 

619 orig_bynweekday = () 

620 

621 if 'byweekday' not in self._original_rule: 

622 self._original_rule['byweekday'] = tuple(itertools.chain( 

623 orig_byweekday, orig_bynweekday)) 

624 

625 # byhour 

626 if byhour is None: 

627 if freq < HOURLY: 

628 self._byhour = {dtstart.hour} 

629 else: 

630 self._byhour = None 

631 else: 

632 if isinstance(byhour, integer_types): 

633 byhour = (byhour,) 

634 

635 if freq == HOURLY: 

636 self._byhour = self.__construct_byset(start=dtstart.hour, 

637 byxxx=byhour, 

638 base=24) 

639 else: 

640 self._byhour = set(byhour) 

641 

642 self._byhour = tuple(sorted(self._byhour)) 

643 self._original_rule['byhour'] = self._byhour 

644 

645 # byminute 

646 if byminute is None: 

647 if freq < MINUTELY: 

648 self._byminute = {dtstart.minute} 

649 else: 

650 self._byminute = None 

651 else: 

652 if isinstance(byminute, integer_types): 

653 byminute = (byminute,) 

654 

655 if freq == MINUTELY: 

656 self._byminute = self.__construct_byset(start=dtstart.minute, 

657 byxxx=byminute, 

658 base=60) 

659 else: 

660 self._byminute = set(byminute) 

661 

662 self._byminute = tuple(sorted(self._byminute)) 

663 self._original_rule['byminute'] = self._byminute 

664 

665 # bysecond 

666 if bysecond is None: 

667 if freq < SECONDLY: 

668 self._bysecond = ((dtstart.second,)) 

669 else: 

670 self._bysecond = None 

671 else: 

672 if isinstance(bysecond, integer_types): 

673 bysecond = (bysecond,) 

674 

675 self._bysecond = set(bysecond) 

676 

677 if freq == SECONDLY: 

678 self._bysecond = self.__construct_byset(start=dtstart.second, 

679 byxxx=bysecond, 

680 base=60) 

681 else: 

682 self._bysecond = set(bysecond) 

683 

684 self._bysecond = tuple(sorted(self._bysecond)) 

685 self._original_rule['bysecond'] = self._bysecond 

686 

687 if self._freq >= HOURLY: 

688 self._timeset = None 

689 else: 

690 self._timeset = [] 

691 for hour in self._byhour: 

692 for minute in self._byminute: 

693 for second in self._bysecond: 

694 self._timeset.append( 

695 datetime.time(hour, minute, second, 

696 tzinfo=self._tzinfo)) 

697 self._timeset.sort() 

698 self._timeset = tuple(self._timeset) 

699 

700 def __str__(self): 

701 """ 

702 Output a string that would generate this RRULE if passed to rrulestr. 

703 This is mostly compatible with RFC5545, except for the 

704 dateutil-specific extension BYEASTER. 

705 """ 

706 

707 output = [] 

708 h, m, s = [None] * 3 

709 if self._dtstart: 

710 output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S')) 

711 h, m, s = self._dtstart.timetuple()[3:6] 

712 

713 parts = ['FREQ=' + FREQNAMES[self._freq]] 

714 if self._interval != 1: 

715 parts.append('INTERVAL=' + str(self._interval)) 

716 

717 if self._wkst: 

718 parts.append('WKST=' + repr(weekday(self._wkst))[0:2]) 

719 

720 if self._count is not None: 

721 parts.append('COUNT=' + str(self._count)) 

722 

723 if self._until: 

724 parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S')) 

725 

726 if self._original_rule.get('byweekday') is not None: 

727 # The str() method on weekday objects doesn't generate 

728 # RFC5545-compliant strings, so we should modify that. 

729 original_rule = dict(self._original_rule) 

730 wday_strings = [] 

731 for wday in original_rule['byweekday']: 

732 if wday.n: 

733 wday_strings.append('{n:+d}{wday}'.format( 

734 n=wday.n, 

735 wday=repr(wday)[0:2])) 

736 else: 

737 wday_strings.append(repr(wday)) 

738 

739 original_rule['byweekday'] = wday_strings 

740 else: 

741 original_rule = self._original_rule 

742 

743 partfmt = '{name}={vals}' 

744 for name, key in [('BYSETPOS', 'bysetpos'), 

745 ('BYMONTH', 'bymonth'), 

746 ('BYMONTHDAY', 'bymonthday'), 

747 ('BYYEARDAY', 'byyearday'), 

748 ('BYWEEKNO', 'byweekno'), 

749 ('BYDAY', 'byweekday'), 

750 ('BYHOUR', 'byhour'), 

751 ('BYMINUTE', 'byminute'), 

752 ('BYSECOND', 'bysecond'), 

753 ('BYEASTER', 'byeaster')]: 

754 value = original_rule.get(key) 

755 if value: 

756 parts.append(partfmt.format(name=name, vals=(','.join(str(v) 

757 for v in value)))) 

758 

759 output.append('RRULE:' + ';'.join(parts)) 

760 return '\n'.join(output) 

761 

762 def replace(self, **kwargs): 

763 """Return new rrule with same attributes except for those attributes given new 

764 values by whichever keyword arguments are specified.""" 

765 new_kwargs = {"interval": self._interval, 

766 "count": self._count, 

767 "dtstart": self._dtstart, 

768 "freq": self._freq, 

769 "until": self._until, 

770 "wkst": self._wkst, 

771 "cache": False if self._cache is None else True } 

772 new_kwargs.update(self._original_rule) 

773 new_kwargs.update(kwargs) 

774 return rrule(**new_kwargs) 

775 

776 def _iter(self): 

777 year, month, day, hour, minute, second, weekday, yearday, _ = \ 

778 self._dtstart.timetuple() 

779 

780 # Some local variables to speed things up a bit 

781 freq = self._freq 

782 interval = self._interval 

783 wkst = self._wkst 

784 until = self._until 

785 bymonth = self._bymonth 

786 byweekno = self._byweekno 

787 byyearday = self._byyearday 

788 byweekday = self._byweekday 

789 byeaster = self._byeaster 

790 bymonthday = self._bymonthday 

791 bynmonthday = self._bynmonthday 

792 bysetpos = self._bysetpos 

793 byhour = self._byhour 

794 byminute = self._byminute 

795 bysecond = self._bysecond 

796 

797 ii = _iterinfo(self) 

798 ii.rebuild(year, month) 

799 

800 getdayset = {YEARLY: ii.ydayset, 

801 MONTHLY: ii.mdayset, 

802 WEEKLY: ii.wdayset, 

803 DAILY: ii.ddayset, 

804 HOURLY: ii.ddayset, 

805 MINUTELY: ii.ddayset, 

806 SECONDLY: ii.ddayset}[freq] 

807 

808 if freq < HOURLY: 

809 timeset = self._timeset 

810 else: 

811 gettimeset = {HOURLY: ii.htimeset, 

812 MINUTELY: ii.mtimeset, 

813 SECONDLY: ii.stimeset}[freq] 

814 if ((freq >= HOURLY and 

815 self._byhour and hour not in self._byhour) or 

816 (freq >= MINUTELY and 

817 self._byminute and minute not in self._byminute) or 

818 (freq >= SECONDLY and 

819 self._bysecond and second not in self._bysecond)): 

820 timeset = () 

821 else: 

822 timeset = gettimeset(hour, minute, second) 

823 

824 total = 0 

825 count = self._count 

826 while True: 

827 # Get dayset with the right frequency 

828 dayset, start, end = getdayset(year, month, day) 

829 

830 # Do the "hard" work ;-) 

831 filtered = False 

832 for i in dayset[start:end]: 

833 if ((bymonth and ii.mmask[i] not in bymonth) or 

834 (byweekno and not ii.wnomask[i]) or 

835 (byweekday and ii.wdaymask[i] not in byweekday) or 

836 (ii.nwdaymask and not ii.nwdaymask[i]) or 

837 (byeaster and not ii.eastermask[i]) or 

838 ((bymonthday or bynmonthday) and 

839 ii.mdaymask[i] not in bymonthday and 

840 ii.nmdaymask[i] not in bynmonthday) or 

841 (byyearday and 

842 ((i < ii.yearlen and i+1 not in byyearday and 

843 -ii.yearlen+i not in byyearday) or 

844 (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and 

845 -ii.nextyearlen+i-ii.yearlen not in byyearday)))): 

846 dayset[i] = None 

847 filtered = True 

848 

849 # Output results 

850 if bysetpos and timeset: 

851 poslist = [] 

852 for pos in bysetpos: 

853 if pos < 0: 

854 daypos, timepos = divmod(pos, len(timeset)) 

855 else: 

856 daypos, timepos = divmod(pos-1, len(timeset)) 

857 try: 

858 i = [x for x in dayset[start:end] 

859 if x is not None][daypos] 

860 time = timeset[timepos] 

861 except IndexError: 

862 pass 

863 else: 

864 date = datetime.date.fromordinal(ii.yearordinal+i) 

865 res = datetime.datetime.combine(date, time) 

866 if res not in poslist: 

867 poslist.append(res) 

868 poslist.sort() 

869 for res in poslist: 

870 if until and res > until: 

871 self._len = total 

872 return 

873 elif res >= self._dtstart: 

874 if count is not None: 

875 count -= 1 

876 if count < 0: 

877 self._len = total 

878 return 

879 total += 1 

880 yield res 

881 else: 

882 for i in dayset[start:end]: 

883 if i is not None: 

884 date = datetime.date.fromordinal(ii.yearordinal + i) 

885 for time in timeset: 

886 res = datetime.datetime.combine(date, time) 

887 if until and res > until: 

888 self._len = total 

889 return 

890 elif res >= self._dtstart: 

891 if count is not None: 

892 count -= 1 

893 if count < 0: 

894 self._len = total 

895 return 

896 

897 total += 1 

898 yield res 

899 

900 # Handle frequency and interval 

901 fixday = False 

902 if freq == YEARLY: 

903 year += interval 

904 if year > datetime.MAXYEAR: 

905 self._len = total 

906 return 

907 ii.rebuild(year, month) 

908 elif freq == MONTHLY: 

909 month += interval 

910 if month > 12: 

911 div, mod = divmod(month, 12) 

912 month = mod 

913 year += div 

914 if month == 0: 

915 month = 12 

916 year -= 1 

917 if year > datetime.MAXYEAR: 

918 self._len = total 

919 return 

920 ii.rebuild(year, month) 

921 elif freq == WEEKLY: 

922 if wkst > weekday: 

923 day += -(weekday+1+(6-wkst))+self._interval*7 

924 else: 

925 day += -(weekday-wkst)+self._interval*7 

926 weekday = wkst 

927 fixday = True 

928 elif freq == DAILY: 

929 day += interval 

930 fixday = True 

931 elif freq == HOURLY: 

932 if filtered: 

933 # Jump to one iteration before next day 

934 hour += ((23-hour)//interval)*interval 

935 

936 if byhour: 

937 ndays, hour = self.__mod_distance(value=hour, 

938 byxxx=self._byhour, 

939 base=24) 

940 else: 

941 ndays, hour = divmod(hour+interval, 24) 

942 

943 if ndays: 

944 day += ndays 

945 fixday = True 

946 

947 timeset = gettimeset(hour, minute, second) 

948 elif freq == MINUTELY: 

949 if filtered: 

950 # Jump to one iteration before next day 

951 minute += ((1439-(hour*60+minute))//interval)*interval 

952 

953 valid = False 

954 rep_rate = (24*60) 

955 for j in range(rep_rate // gcd(interval, rep_rate)): 

956 if byminute: 

957 nhours, minute = \ 

958 self.__mod_distance(value=minute, 

959 byxxx=self._byminute, 

960 base=60) 

961 else: 

962 nhours, minute = divmod(minute+interval, 60) 

963 

964 div, hour = divmod(hour+nhours, 24) 

965 if div: 

966 day += div 

967 fixday = True 

968 filtered = False 

969 

970 if not byhour or hour in byhour: 

971 valid = True 

972 break 

973 

974 if not valid: 

975 raise ValueError('Invalid combination of interval and ' + 

976 'byhour resulting in empty rule.') 

977 

978 timeset = gettimeset(hour, minute, second) 

979 elif freq == SECONDLY: 

980 if filtered: 

981 # Jump to one iteration before next day 

982 second += (((86399 - (hour * 3600 + minute * 60 + second)) 

983 // interval) * interval) 

984 

985 rep_rate = (24 * 3600) 

986 valid = False 

987 for j in range(0, rep_rate // gcd(interval, rep_rate)): 

988 if bysecond: 

989 nminutes, second = \ 

990 self.__mod_distance(value=second, 

991 byxxx=self._bysecond, 

992 base=60) 

993 else: 

994 nminutes, second = divmod(second+interval, 60) 

995 

996 div, minute = divmod(minute+nminutes, 60) 

997 if div: 

998 hour += div 

999 div, hour = divmod(hour, 24) 

1000 if div: 

1001 day += div 

1002 fixday = True 

1003 

1004 if ((not byhour or hour in byhour) and 

1005 (not byminute or minute in byminute) and 

1006 (not bysecond or second in bysecond)): 

1007 valid = True 

1008 break 

1009 

1010 if not valid: 

1011 raise ValueError('Invalid combination of interval, ' + 

1012 'byhour and byminute resulting in empty' + 

1013 ' rule.') 

1014 

1015 timeset = gettimeset(hour, minute, second) 

1016 

1017 if fixday and day > 28: 

1018 daysinmonth = calendar.monthrange(year, month)[1] 

1019 if day > daysinmonth: 

1020 while day > daysinmonth: 

1021 day -= daysinmonth 

1022 month += 1 

1023 if month == 13: 

1024 month = 1 

1025 year += 1 

1026 if year > datetime.MAXYEAR: 

1027 self._len = total 

1028 return 

1029 daysinmonth = calendar.monthrange(year, month)[1] 

1030 ii.rebuild(year, month) 

1031 

1032 def __construct_byset(self, start, byxxx, base): 

1033 """ 

1034 If a `BYXXX` sequence is passed to the constructor at the same level as 

1035 `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some 

1036 specifications which cannot be reached given some starting conditions. 

1037 

1038 This occurs whenever the interval is not coprime with the base of a 

1039 given unit and the difference between the starting position and the 

1040 ending position is not coprime with the greatest common denominator 

1041 between the interval and the base. For example, with a FREQ of hourly 

1042 starting at 17:00 and an interval of 4, the only valid values for 

1043 BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not 

1044 coprime. 

1045 

1046 :param start: 

1047 Specifies the starting position. 

1048 :param byxxx: 

1049 An iterable containing the list of allowed values. 

1050 :param base: 

1051 The largest allowable value for the specified frequency (e.g. 

1052 24 hours, 60 minutes). 

1053 

1054 This does not preserve the type of the iterable, returning a set, since 

1055 the values should be unique and the order is irrelevant, this will 

1056 speed up later lookups. 

1057 

1058 In the event of an empty set, raises a :exception:`ValueError`, as this 

1059 results in an empty rrule. 

1060 """ 

1061 

1062 cset = set() 

1063 

1064 # Support a single byxxx value. 

1065 if isinstance(byxxx, integer_types): 

1066 byxxx = (byxxx, ) 

1067 

1068 for num in byxxx: 

1069 i_gcd = gcd(self._interval, base) 

1070 # Use divmod rather than % because we need to wrap negative nums. 

1071 if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0: 

1072 cset.add(num) 

1073 

1074 if len(cset) == 0: 

1075 raise ValueError("Invalid rrule byxxx generates an empty set.") 

1076 

1077 return cset 

1078 

1079 def __mod_distance(self, value, byxxx, base): 

1080 """ 

1081 Calculates the next value in a sequence where the `FREQ` parameter is 

1082 specified along with a `BYXXX` parameter at the same "level" 

1083 (e.g. `HOURLY` specified with `BYHOUR`). 

1084 

1085 :param value: 

1086 The old value of the component. 

1087 :param byxxx: 

1088 The `BYXXX` set, which should have been generated by 

1089 `rrule._construct_byset`, or something else which checks that a 

1090 valid rule is present. 

1091 :param base: 

1092 The largest allowable value for the specified frequency (e.g. 

1093 24 hours, 60 minutes). 

1094 

1095 If a valid value is not found after `base` iterations (the maximum 

1096 number before the sequence would start to repeat), this raises a 

1097 :exception:`ValueError`, as no valid values were found. 

1098 

1099 This returns a tuple of `divmod(n*interval, base)`, where `n` is the 

1100 smallest number of `interval` repetitions until the next specified 

1101 value in `byxxx` is found. 

1102 """ 

1103 accumulator = 0 

1104 for ii in range(1, base + 1): 

1105 # Using divmod() over % to account for negative intervals 

1106 div, value = divmod(value + self._interval, base) 

1107 accumulator += div 

1108 if value in byxxx: 

1109 return (accumulator, value) 

1110 

1111 

1112class _iterinfo(object): 

1113 __slots__ = ["rrule", "lastyear", "lastmonth", 

1114 "yearlen", "nextyearlen", "yearordinal", "yearweekday", 

1115 "mmask", "mrange", "mdaymask", "nmdaymask", 

1116 "wdaymask", "wnomask", "nwdaymask", "eastermask"] 

1117 

1118 def __init__(self, rrule): 

1119 for attr in self.__slots__: 

1120 setattr(self, attr, None) 

1121 self.rrule = rrule 

1122 

1123 def rebuild(self, year, month): 

1124 # Every mask is 7 days longer to handle cross-year weekly periods. 

1125 rr = self.rrule 

1126 if year != self.lastyear: 

1127 self.yearlen = 365 + calendar.isleap(year) 

1128 self.nextyearlen = 365 + calendar.isleap(year + 1) 

1129 firstyday = datetime.date(year, 1, 1) 

1130 self.yearordinal = firstyday.toordinal() 

1131 self.yearweekday = firstyday.weekday() 

1132 

1133 wday = datetime.date(year, 1, 1).weekday() 

1134 if self.yearlen == 365: 

1135 self.mmask = M365MASK 

1136 self.mdaymask = MDAY365MASK 

1137 self.nmdaymask = NMDAY365MASK 

1138 self.wdaymask = WDAYMASK[wday:] 

1139 self.mrange = M365RANGE 

1140 else: 

1141 self.mmask = M366MASK 

1142 self.mdaymask = MDAY366MASK 

1143 self.nmdaymask = NMDAY366MASK 

1144 self.wdaymask = WDAYMASK[wday:] 

1145 self.mrange = M366RANGE 

1146 

1147 if not rr._byweekno: 

1148 self.wnomask = None 

1149 else: 

1150 self.wnomask = [0]*(self.yearlen+7) 

1151 # no1wkst = firstwkst = self.wdaymask.index(rr._wkst) 

1152 no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7 

1153 if no1wkst >= 4: 

1154 no1wkst = 0 

1155 # Number of days in the year, plus the days we got 

1156 # from last year. 

1157 wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7 

1158 else: 

1159 # Number of days in the year, minus the days we 

1160 # left in last year. 

1161 wyearlen = self.yearlen-no1wkst 

1162 div, mod = divmod(wyearlen, 7) 

1163 numweeks = div+mod//4 

1164 for n in rr._byweekno: 

1165 if n < 0: 

1166 n += numweeks+1 

1167 if not (0 < n <= numweeks): 

1168 continue 

1169 if n > 1: 

1170 i = no1wkst+(n-1)*7 

1171 if no1wkst != firstwkst: 

1172 i -= 7-firstwkst 

1173 else: 

1174 i = no1wkst 

1175 for j in range(7): 

1176 self.wnomask[i] = 1 

1177 i += 1 

1178 if self.wdaymask[i] == rr._wkst: 

1179 break 

1180 if 1 in rr._byweekno: 

1181 # Check week number 1 of next year as well 

1182 # TODO: Check -numweeks for next year. 

1183 i = no1wkst+numweeks*7 

1184 if no1wkst != firstwkst: 

1185 i -= 7-firstwkst 

1186 if i < self.yearlen: 

1187 # If week starts in next year, we 

1188 # don't care about it. 

1189 for j in range(7): 

1190 self.wnomask[i] = 1 

1191 i += 1 

1192 if self.wdaymask[i] == rr._wkst: 

1193 break 

1194 if no1wkst: 

1195 # Check last week number of last year as 

1196 # well. If no1wkst is 0, either the year 

1197 # started on week start, or week number 1 

1198 # got days from last year, so there are no 

1199 # days from last year's last week number in 

1200 # this year. 

1201 if -1 not in rr._byweekno: 

1202 lyearweekday = datetime.date(year-1, 1, 1).weekday() 

1203 lno1wkst = (7-lyearweekday+rr._wkst) % 7 

1204 lyearlen = 365+calendar.isleap(year-1) 

1205 if lno1wkst >= 4: 

1206 lno1wkst = 0 

1207 lnumweeks = 52+(lyearlen + 

1208 (lyearweekday-rr._wkst) % 7) % 7//4 

1209 else: 

1210 lnumweeks = 52+(self.yearlen-no1wkst) % 7//4 

1211 else: 

1212 lnumweeks = -1 

1213 if lnumweeks in rr._byweekno: 

1214 for i in range(no1wkst): 

1215 self.wnomask[i] = 1 

1216 

1217 if (rr._bynweekday and (month != self.lastmonth or 

1218 year != self.lastyear)): 

1219 ranges = [] 

1220 if rr._freq == YEARLY: 

1221 if rr._bymonth: 

1222 for month in rr._bymonth: 

1223 ranges.append(self.mrange[month-1:month+1]) 

1224 else: 

1225 ranges = [(0, self.yearlen)] 

1226 elif rr._freq == MONTHLY: 

1227 ranges = [self.mrange[month-1:month+1]] 

1228 if ranges: 

1229 # Weekly frequency won't get here, so we may not 

1230 # care about cross-year weekly periods. 

1231 self.nwdaymask = [0]*self.yearlen 

1232 for first, last in ranges: 

1233 last -= 1 

1234 for wday, n in rr._bynweekday: 

1235 if n < 0: 

1236 i = last+(n+1)*7 

1237 i -= (self.wdaymask[i]-wday) % 7 

1238 else: 

1239 i = first+(n-1)*7 

1240 i += (7-self.wdaymask[i]+wday) % 7 

1241 if first <= i <= last: 

1242 self.nwdaymask[i] = 1 

1243 

1244 if rr._byeaster: 

1245 self.eastermask = [0]*(self.yearlen+7) 

1246 eyday = easter.easter(year).toordinal()-self.yearordinal 

1247 for offset in rr._byeaster: 

1248 self.eastermask[eyday+offset] = 1 

1249 

1250 self.lastyear = year 

1251 self.lastmonth = month 

1252 

1253 def ydayset(self, year, month, day): 

1254 return list(range(self.yearlen)), 0, self.yearlen 

1255 

1256 def mdayset(self, year, month, day): 

1257 dset = [None]*self.yearlen 

1258 start, end = self.mrange[month-1:month+1] 

1259 for i in range(start, end): 

1260 dset[i] = i 

1261 return dset, start, end 

1262 

1263 def wdayset(self, year, month, day): 

1264 # We need to handle cross-year weeks here. 

1265 dset = [None]*(self.yearlen+7) 

1266 i = datetime.date(year, month, day).toordinal()-self.yearordinal 

1267 start = i 

1268 for j in range(7): 

1269 dset[i] = i 

1270 i += 1 

1271 # if (not (0 <= i < self.yearlen) or 

1272 # self.wdaymask[i] == self.rrule._wkst): 

1273 # This will cross the year boundary, if necessary. 

1274 if self.wdaymask[i] == self.rrule._wkst: 

1275 break 

1276 return dset, start, i 

1277 

1278 def ddayset(self, year, month, day): 

1279 dset = [None] * self.yearlen 

1280 i = datetime.date(year, month, day).toordinal() - self.yearordinal 

1281 dset[i] = i 

1282 return dset, i, i + 1 

1283 

1284 def htimeset(self, hour, minute, second): 

1285 tset = [] 

1286 rr = self.rrule 

1287 for minute in rr._byminute: 

1288 for second in rr._bysecond: 

1289 tset.append(datetime.time(hour, minute, second, 

1290 tzinfo=rr._tzinfo)) 

1291 tset.sort() 

1292 return tset 

1293 

1294 def mtimeset(self, hour, minute, second): 

1295 tset = [] 

1296 rr = self.rrule 

1297 for second in rr._bysecond: 

1298 tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo)) 

1299 tset.sort() 

1300 return tset 

1301 

1302 def stimeset(self, hour, minute, second): 

1303 return (datetime.time(hour, minute, second, 

1304 tzinfo=self.rrule._tzinfo),) 

1305 

1306 

1307class rruleset(rrulebase): 

1308 """ The rruleset type allows more complex recurrence setups, mixing 

1309 multiple rules, dates, exclusion rules, and exclusion dates. The type 

1310 constructor takes the following keyword arguments: 

1311 

1312 :param cache: If True, caching of results will be enabled, improving 

1313 performance of multiple queries considerably. """ 

1314 

1315 class _genitem(object): 

1316 def __init__(self, genlist, gen): 

1317 try: 

1318 self.dt = advance_iterator(gen) 

1319 genlist.append(self) 

1320 except StopIteration: 

1321 pass 

1322 self.genlist = genlist 

1323 self.gen = gen 

1324 

1325 def __next__(self): 

1326 try: 

1327 self.dt = advance_iterator(self.gen) 

1328 except StopIteration: 

1329 if self.genlist[0] is self: 

1330 heapq.heappop(self.genlist) 

1331 else: 

1332 self.genlist.remove(self) 

1333 heapq.heapify(self.genlist) 

1334 

1335 next = __next__ 

1336 

1337 def __lt__(self, other): 

1338 return self.dt < other.dt 

1339 

1340 def __gt__(self, other): 

1341 return self.dt > other.dt 

1342 

1343 def __eq__(self, other): 

1344 return self.dt == other.dt 

1345 

1346 def __ne__(self, other): 

1347 return self.dt != other.dt 

1348 

1349 def __init__(self, cache=False): 

1350 super(rruleset, self).__init__(cache) 

1351 self._rrule = [] 

1352 self._rdate = [] 

1353 self._exrule = [] 

1354 self._exdate = [] 

1355 

1356 @_invalidates_cache 

1357 def rrule(self, rrule): 

1358 """ Include the given :py:class:`rrule` instance in the recurrence set 

1359 generation. """ 

1360 self._rrule.append(rrule) 

1361 

1362 @_invalidates_cache 

1363 def rdate(self, rdate): 

1364 """ Include the given :py:class:`datetime` instance in the recurrence 

1365 set generation. """ 

1366 self._rdate.append(rdate) 

1367 

1368 @_invalidates_cache 

1369 def exrule(self, exrule): 

1370 """ Include the given rrule instance in the recurrence set exclusion 

1371 list. Dates which are part of the given recurrence rules will not 

1372 be generated, even if some inclusive rrule or rdate matches them. 

1373 """ 

1374 self._exrule.append(exrule) 

1375 

1376 @_invalidates_cache 

1377 def exdate(self, exdate): 

1378 """ Include the given datetime instance in the recurrence set 

1379 exclusion list. Dates included that way will not be generated, 

1380 even if some inclusive rrule or rdate matches them. """ 

1381 self._exdate.append(exdate) 

1382 

1383 def _iter(self): 

1384 rlist = [] 

1385 self._rdate.sort() 

1386 self._genitem(rlist, iter(self._rdate)) 

1387 for gen in [iter(x) for x in self._rrule]: 

1388 self._genitem(rlist, gen) 

1389 exlist = [] 

1390 self._exdate.sort() 

1391 self._genitem(exlist, iter(self._exdate)) 

1392 for gen in [iter(x) for x in self._exrule]: 

1393 self._genitem(exlist, gen) 

1394 lastdt = None 

1395 total = 0 

1396 heapq.heapify(rlist) 

1397 heapq.heapify(exlist) 

1398 while rlist: 

1399 ritem = rlist[0] 

1400 if not lastdt or lastdt != ritem.dt: 

1401 while exlist and exlist[0] < ritem: 

1402 exitem = exlist[0] 

1403 advance_iterator(exitem) 

1404 if exlist and exlist[0] is exitem: 

1405 heapq.heapreplace(exlist, exitem) 

1406 if not exlist or ritem != exlist[0]: 

1407 total += 1 

1408 yield ritem.dt 

1409 lastdt = ritem.dt 

1410 advance_iterator(ritem) 

1411 if rlist and rlist[0] is ritem: 

1412 heapq.heapreplace(rlist, ritem) 

1413 self._len = total 

1414 

1415 

1416 

1417 

1418class _rrulestr(object): 

1419 """ Parses a string representation of a recurrence rule or set of 

1420 recurrence rules. 

1421 

1422 :param s: 

1423 Required, a string defining one or more recurrence rules. 

1424 

1425 :param dtstart: 

1426 If given, used as the default recurrence start if not specified in the 

1427 rule string. 

1428 

1429 :param cache: 

1430 If set ``True`` caching of results will be enabled, improving 

1431 performance of multiple queries considerably. 

1432 

1433 :param unfold: 

1434 If set ``True`` indicates that a rule string is split over more 

1435 than one line and should be joined before processing. 

1436 

1437 :param forceset: 

1438 If set ``True`` forces a :class:`dateutil.rrule.rruleset` to 

1439 be returned. 

1440 

1441 :param compatible: 

1442 If set ``True`` forces ``unfold`` and ``forceset`` to be ``True``. 

1443 

1444 :param ignoretz: 

1445 If set ``True``, time zones in parsed strings are ignored and a naive 

1446 :class:`datetime.datetime` object is returned. 

1447 

1448 :param tzids: 

1449 If given, a callable or mapping used to retrieve a 

1450 :class:`datetime.tzinfo` from a string representation. 

1451 Defaults to :func:`dateutil.tz.gettz`. 

1452 

1453 :param tzinfos: 

1454 Additional time zone names / aliases which may be present in a string 

1455 representation. See :func:`dateutil.parser.parse` for more 

1456 information. 

1457 

1458 :return: 

1459 Returns a :class:`dateutil.rrule.rruleset` or 

1460 :class:`dateutil.rrule.rrule` 

1461 """ 

1462 

1463 _freq_map = {"YEARLY": YEARLY, 

1464 "MONTHLY": MONTHLY, 

1465 "WEEKLY": WEEKLY, 

1466 "DAILY": DAILY, 

1467 "HOURLY": HOURLY, 

1468 "MINUTELY": MINUTELY, 

1469 "SECONDLY": SECONDLY} 

1470 

1471 _weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3, 

1472 "FR": 4, "SA": 5, "SU": 6} 

1473 

1474 def _handle_int(self, rrkwargs, name, value, **kwargs): 

1475 rrkwargs[name.lower()] = int(value) 

1476 

1477 def _handle_int_list(self, rrkwargs, name, value, **kwargs): 

1478 rrkwargs[name.lower()] = [int(x) for x in value.split(',')] 

1479 

1480 _handle_INTERVAL = _handle_int 

1481 _handle_COUNT = _handle_int 

1482 _handle_BYSETPOS = _handle_int_list 

1483 _handle_BYMONTH = _handle_int_list 

1484 _handle_BYMONTHDAY = _handle_int_list 

1485 _handle_BYYEARDAY = _handle_int_list 

1486 _handle_BYEASTER = _handle_int_list 

1487 _handle_BYWEEKNO = _handle_int_list 

1488 _handle_BYHOUR = _handle_int_list 

1489 _handle_BYMINUTE = _handle_int_list 

1490 _handle_BYSECOND = _handle_int_list 

1491 

1492 def _handle_FREQ(self, rrkwargs, name, value, **kwargs): 

1493 rrkwargs["freq"] = self._freq_map[value] 

1494 

1495 def _handle_UNTIL(self, rrkwargs, name, value, **kwargs): 

1496 global parser 

1497 if not parser: 

1498 from dateutil import parser 

1499 try: 

1500 rrkwargs["until"] = parser.parse(value, 

1501 ignoretz=kwargs.get("ignoretz"), 

1502 tzinfos=kwargs.get("tzinfos")) 

1503 except ValueError: 

1504 raise ValueError("invalid until date") 

1505 

1506 def _handle_WKST(self, rrkwargs, name, value, **kwargs): 

1507 rrkwargs["wkst"] = self._weekday_map[value] 

1508 

1509 def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs): 

1510 """ 

1511 Two ways to specify this: +1MO or MO(+1) 

1512 """ 

1513 l = [] 

1514 for wday in value.split(','): 

1515 if '(' in wday: 

1516 # If it's of the form TH(+1), etc. 

1517 splt = wday.split('(') 

1518 w = splt[0] 

1519 n = int(splt[1][:-1]) 

1520 elif len(wday): 

1521 # If it's of the form +1MO 

1522 for i in range(len(wday)): 

1523 if wday[i] not in '+-0123456789': 

1524 break 

1525 n = wday[:i] or None 

1526 w = wday[i:] 

1527 if n: 

1528 n = int(n) 

1529 else: 

1530 raise ValueError("Invalid (empty) BYDAY specification.") 

1531 

1532 l.append(weekdays[self._weekday_map[w]](n)) 

1533 rrkwargs["byweekday"] = l 

1534 

1535 _handle_BYDAY = _handle_BYWEEKDAY 

1536 

1537 def _parse_rfc_rrule(self, line, 

1538 dtstart=None, 

1539 cache=False, 

1540 ignoretz=False, 

1541 tzinfos=None): 

1542 if line.find(':') != -1: 

1543 name, value = line.split(':') 

1544 if name != "RRULE": 

1545 raise ValueError("unknown parameter name") 

1546 else: 

1547 value = line 

1548 rrkwargs = {} 

1549 for pair in value.split(';'): 

1550 name, value = pair.split('=') 

1551 name = name.upper() 

1552 value = value.upper() 

1553 try: 

1554 getattr(self, "_handle_"+name)(rrkwargs, name, value, 

1555 ignoretz=ignoretz, 

1556 tzinfos=tzinfos) 

1557 except AttributeError: 

1558 raise ValueError("unknown parameter '%s'" % name) 

1559 except (KeyError, ValueError): 

1560 raise ValueError("invalid '%s': %s" % (name, value)) 

1561 return rrule(dtstart=dtstart, cache=cache, **rrkwargs) 

1562 

1563 def _parse_date_value(self, date_value, parms, rule_tzids, 

1564 ignoretz, tzids, tzinfos): 

1565 global parser 

1566 if not parser: 

1567 from dateutil import parser 

1568 

1569 datevals = [] 

1570 value_found = False 

1571 TZID = None 

1572 

1573 for parm in parms: 

1574 if parm.startswith("TZID="): 

1575 try: 

1576 tzkey = rule_tzids[parm.split('TZID=')[-1]] 

1577 except KeyError: 

1578 continue 

1579 if tzids is None: 

1580 from . import tz 

1581 tzlookup = tz.gettz 

1582 elif callable(tzids): 

1583 tzlookup = tzids 

1584 else: 

1585 tzlookup = getattr(tzids, 'get', None) 

1586 if tzlookup is None: 

1587 msg = ('tzids must be a callable, mapping, or None, ' 

1588 'not %s' % tzids) 

1589 raise ValueError(msg) 

1590 

1591 TZID = tzlookup(tzkey) 

1592 continue 

1593 

1594 # RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found 

1595 # only once. 

1596 if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}: 

1597 raise ValueError("unsupported parm: " + parm) 

1598 else: 

1599 if value_found: 

1600 msg = ("Duplicate value parameter found in: " + parm) 

1601 raise ValueError(msg) 

1602 value_found = True 

1603 

1604 for datestr in date_value.split(','): 

1605 date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos) 

1606 if TZID is not None: 

1607 if date.tzinfo is None: 

1608 date = date.replace(tzinfo=TZID) 

1609 else: 

1610 raise ValueError('DTSTART/EXDATE specifies multiple timezone') 

1611 datevals.append(date) 

1612 

1613 return datevals 

1614 

1615 def _parse_rfc(self, s, 

1616 dtstart=None, 

1617 cache=False, 

1618 unfold=False, 

1619 forceset=False, 

1620 compatible=False, 

1621 ignoretz=False, 

1622 tzids=None, 

1623 tzinfos=None): 

1624 global parser 

1625 if compatible: 

1626 forceset = True 

1627 unfold = True 

1628 

1629 TZID_NAMES = dict(map( 

1630 lambda x: (x.upper(), x), 

1631 re.findall('TZID=(?P<name>[^:]+):', s) 

1632 )) 

1633 s = s.upper() 

1634 if not s.strip(): 

1635 raise ValueError("empty string") 

1636 if unfold: 

1637 lines = s.splitlines() 

1638 i = 0 

1639 while i < len(lines): 

1640 line = lines[i].rstrip() 

1641 if not line: 

1642 del lines[i] 

1643 elif i > 0 and line[0] == " ": 

1644 lines[i-1] += line[1:] 

1645 del lines[i] 

1646 else: 

1647 i += 1 

1648 else: 

1649 lines = s.split() 

1650 if (not forceset and len(lines) == 1 and (s.find(':') == -1 or 

1651 s.startswith('RRULE:'))): 

1652 return self._parse_rfc_rrule(lines[0], cache=cache, 

1653 dtstart=dtstart, ignoretz=ignoretz, 

1654 tzinfos=tzinfos) 

1655 else: 

1656 rrulevals = [] 

1657 rdatevals = [] 

1658 exrulevals = [] 

1659 exdatevals = [] 

1660 for line in lines: 

1661 if not line: 

1662 continue 

1663 if line.find(':') == -1: 

1664 name = "RRULE" 

1665 value = line 

1666 else: 

1667 name, value = line.split(':', 1) 

1668 parms = name.split(';') 

1669 if not parms: 

1670 raise ValueError("empty property name") 

1671 name = parms[0] 

1672 parms = parms[1:] 

1673 if name == "RRULE": 

1674 for parm in parms: 

1675 raise ValueError("unsupported RRULE parm: "+parm) 

1676 rrulevals.append(value) 

1677 elif name == "RDATE": 

1678 for parm in parms: 

1679 if parm != "VALUE=DATE-TIME": 

1680 raise ValueError("unsupported RDATE parm: "+parm) 

1681 rdatevals.append(value) 

1682 elif name == "EXRULE": 

1683 for parm in parms: 

1684 raise ValueError("unsupported EXRULE parm: "+parm) 

1685 exrulevals.append(value) 

1686 elif name == "EXDATE": 

1687 exdatevals.extend( 

1688 self._parse_date_value(value, parms, 

1689 TZID_NAMES, ignoretz, 

1690 tzids, tzinfos) 

1691 ) 

1692 elif name == "DTSTART": 

1693 dtvals = self._parse_date_value(value, parms, TZID_NAMES, 

1694 ignoretz, tzids, tzinfos) 

1695 if len(dtvals) != 1: 

1696 raise ValueError("Multiple DTSTART values specified:" + 

1697 value) 

1698 dtstart = dtvals[0] 

1699 else: 

1700 raise ValueError("unsupported property: "+name) 

1701 if (forceset or len(rrulevals) > 1 or rdatevals 

1702 or exrulevals or exdatevals): 

1703 if not parser and (rdatevals or exdatevals): 

1704 from dateutil import parser 

1705 rset = rruleset(cache=cache) 

1706 for value in rrulevals: 

1707 rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart, 

1708 ignoretz=ignoretz, 

1709 tzinfos=tzinfos)) 

1710 for value in rdatevals: 

1711 for datestr in value.split(','): 

1712 rset.rdate(parser.parse(datestr, 

1713 ignoretz=ignoretz, 

1714 tzinfos=tzinfos)) 

1715 for value in exrulevals: 

1716 rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart, 

1717 ignoretz=ignoretz, 

1718 tzinfos=tzinfos)) 

1719 for value in exdatevals: 

1720 rset.exdate(value) 

1721 if compatible and dtstart: 

1722 rset.rdate(dtstart) 

1723 return rset 

1724 else: 

1725 return self._parse_rfc_rrule(rrulevals[0], 

1726 dtstart=dtstart, 

1727 cache=cache, 

1728 ignoretz=ignoretz, 

1729 tzinfos=tzinfos) 

1730 

1731 def __call__(self, s, **kwargs): 

1732 return self._parse_rfc(s, **kwargs) 

1733 

1734 

1735rrulestr = _rrulestr() 

1736 

1737# vim:ts=4:sw=4:et