Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/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

1005 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 __reduce_ex__(self, protocol): 

106 rv = super(rrulebase, self).__reduce_ex__(protocol) 

107 state, slots = rv[2], None 

108 if isinstance(state, tuple): 

109 state, slots = state 

110 if self._cache is not None: 

111 state = dict(state) 

112 state.pop("_cache_lock", None) 

113 state.pop("_cache_gen", None) 

114 state["_cache"] = [] 

115 if slots is not None: 

116 state = (state, slots) 

117 return rv[:2] + (state,) + rv[3:] 

118 

119 def __setstate__(self, state): 

120 if isinstance(state, tuple): 

121 state, slots = state 

122 for name, value in slots.items(): 

123 setattr(self, name, value) 

124 self.__dict__.update(state) 

125 if self._cache is not None: 

126 self._cache_lock = _thread.allocate_lock() 

127 self._invalidate_cache() 

128 

129 def __iter__(self): 

130 if self._cache_complete: 

131 return iter(self._cache) 

132 elif self._cache is None: 

133 return self._iter() 

134 else: 

135 return self._iter_cached() 

136 

137 def _invalidate_cache(self): 

138 if self._cache is not None: 

139 self._cache = [] 

140 self._cache_complete = False 

141 self._cache_gen = self._iter() 

142 

143 if self._cache_lock.locked(): 

144 self._cache_lock.release() 

145 

146 self._len = None 

147 

148 def _iter_cached(self): 

149 i = 0 

150 gen = self._cache_gen 

151 cache = self._cache 

152 acquire = self._cache_lock.acquire 

153 release = self._cache_lock.release 

154 while gen: 

155 if i == len(cache): 

156 acquire() 

157 if self._cache_complete: 

158 break 

159 try: 

160 for j in range(10): 

161 cache.append(advance_iterator(gen)) 

162 except StopIteration: 

163 self._cache_gen = gen = None 

164 self._cache_complete = True 

165 break 

166 release() 

167 yield cache[i] 

168 i += 1 

169 while i < self._len: 

170 yield cache[i] 

171 i += 1 

172 

173 def __getitem__(self, item): 

174 if self._cache_complete: 

175 return self._cache[item] 

176 elif isinstance(item, slice): 

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

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

179 else: 

180 return list(itertools.islice(self, 

181 item.start or 0, 

182 item.stop or sys.maxsize, 

183 item.step or 1)) 

184 elif item >= 0: 

185 gen = iter(self) 

186 try: 

187 for i in range(item+1): 

188 res = advance_iterator(gen) 

189 except StopIteration: 

190 raise IndexError 

191 return res 

192 else: 

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

194 

195 def __contains__(self, item): 

196 if self._cache_complete: 

197 return item in self._cache 

198 else: 

199 for i in self: 

200 if i == item: 

201 return True 

202 elif i > item: 

203 return False 

204 return False 

205 

206 # __len__() introduces a large performance penalty. 

207 def count(self): 

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

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

210 if self._len is None: 

211 for x in self: 

212 pass 

213 return self._len 

214 

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

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

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

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

219 if self._cache_complete: 

220 gen = self._cache 

221 else: 

222 gen = self 

223 last = None 

224 if inc: 

225 for i in gen: 

226 if i > dt: 

227 break 

228 last = i 

229 else: 

230 for i in gen: 

231 if i >= dt: 

232 break 

233 last = i 

234 return last 

235 

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

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

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

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

240 if self._cache_complete: 

241 gen = self._cache 

242 else: 

243 gen = self 

244 if inc: 

245 for i in gen: 

246 if i >= dt: 

247 return i 

248 else: 

249 for i in gen: 

250 if i > dt: 

251 return i 

252 return None 

253 

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

255 """ 

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

257 datetime instance, equivalent to `after`. 

258 

259 :param dt: 

260 The datetime at which to start generating recurrences. 

261 

262 :param count: 

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

264 dates are generated until the recurrence rule is exhausted. 

265 

266 :param inc: 

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

268 included in the output. 

269 

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

271 """ 

272 

273 if self._cache_complete: 

274 gen = self._cache 

275 else: 

276 gen = self 

277 

278 # Select the comparison function 

279 if inc: 

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

281 else: 

282 comp = lambda dc, dtc: dc > dtc 

283 

284 # Generate dates 

285 n = 0 

286 for d in gen: 

287 if comp(d, dt): 

288 if count is not None: 

289 n += 1 

290 if n > count: 

291 break 

292 

293 yield d 

294 

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

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

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

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

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

300 if self._cache_complete: 

301 gen = self._cache 

302 else: 

303 gen = self 

304 started = False 

305 l = [] 

306 if inc: 

307 for i in gen: 

308 if i > before: 

309 break 

310 elif not started: 

311 if i >= after: 

312 started = True 

313 l.append(i) 

314 else: 

315 l.append(i) 

316 else: 

317 for i in gen: 

318 if i >= before: 

319 break 

320 elif not started: 

321 if i > after: 

322 started = True 

323 l.append(i) 

324 else: 

325 l.append(i) 

326 return l 

327 

328 

329class rrule(rrulebase): 

330 """ 

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

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

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

334 

335 rrule(freq) 

336 

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

338 or SECONDLY. 

339 

340 .. note:: 

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

342 and times are ignored rather than coerced: 

343 

344 Recurrence rules may generate recurrence instances with an invalid 

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

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

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

348 counted as part of the recurrence set. 

349 

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

351 start date occurs at the end of the month: 

352 

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

354 >>> from datetime import datetime 

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

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

357 ... # doctest: +NORMALIZE_WHITESPACE 

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

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

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

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

362 

363 Additionally, it supports the following keyword arguments: 

364 

365 :param dtstart: 

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

367 missing parameters in the final recurrence instances will also be 

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

369 instead. 

370 :param interval: 

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

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

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

374 :param wkst: 

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

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

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

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

379 calendar.setfirstweekday(). 

380 :param count: 

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

382 

383 .. note:: 

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

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

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

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

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

389 :param until: 

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

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

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

393 ``until`` parameter. 

394 

395 .. note:: 

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

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

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

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

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

401 :param bysetpos: 

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

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

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

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

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

407 result in the last work day of every month. 

408 :param bymonth: 

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

410 meaning the months to apply the recurrence to. 

411 :param bymonthday: 

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

413 meaning the month days to apply the recurrence to. 

414 :param byyearday: 

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

416 meaning the year days to apply the recurrence to. 

417 :param byeaster: 

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

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

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

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

422 :param byweekno: 

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

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

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

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

427 :param byweekday: 

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

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

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

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

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

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

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

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

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

437 avoid the ambiguity of that keyword. 

438 :param byhour: 

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

440 meaning the hours to apply the recurrence to. 

441 :param byminute: 

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

443 meaning the minutes to apply the recurrence to. 

444 :param bysecond: 

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

446 meaning the seconds to apply the recurrence to. 

447 :param cache: 

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

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

450 times, enabling caching will improve the performance considerably. 

451 """ 

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

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

454 bymonth=None, bymonthday=None, byyearday=None, byeaster=None, 

455 byweekno=None, byweekday=None, 

456 byhour=None, byminute=None, bysecond=None, 

457 cache=False): 

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

459 global easter 

460 if not dtstart: 

461 if until and until.tzinfo: 

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

463 else: 

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

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

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

467 else: 

468 dtstart = dtstart.replace(microsecond=0) 

469 self._dtstart = dtstart 

470 self._tzinfo = dtstart.tzinfo 

471 self._freq = freq 

472 self._interval = interval 

473 self._count = count 

474 

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

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

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

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

479 self._original_rule = {} 

480 

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

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

483 self._until = until 

484 

485 if self._dtstart and self._until: 

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

487 # According to RFC5545 Section 3.3.10: 

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

489 # 

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

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

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

493 # > UTC time. 

494 raise ValueError( 

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

496 'is timezone-aware' 

497 ) 

498 

499 if count is not None and until: 

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

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

502 "raise an error.", DeprecationWarning) 

503 

504 if wkst is None: 

505 self._wkst = calendar.firstweekday() 

506 elif isinstance(wkst, integer_types): 

507 self._wkst = wkst 

508 else: 

509 self._wkst = wkst.weekday 

510 

511 if bysetpos is None: 

512 self._bysetpos = None 

513 elif isinstance(bysetpos, integer_types): 

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

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

516 "or between -366 and -1") 

517 self._bysetpos = (bysetpos,) 

518 else: 

519 self._bysetpos = tuple(bysetpos) 

520 for pos in self._bysetpos: 

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

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

523 "or between -366 and -1") 

524 

525 if self._bysetpos: 

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

527 

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

529 byweekday is None and byeaster is None): 

530 if freq == YEARLY: 

531 if bymonth is None: 

532 bymonth = dtstart.month 

533 self._original_rule['bymonth'] = None 

534 bymonthday = dtstart.day 

535 self._original_rule['bymonthday'] = None 

536 elif freq == MONTHLY: 

537 bymonthday = dtstart.day 

538 self._original_rule['bymonthday'] = None 

539 elif freq == WEEKLY: 

540 byweekday = dtstart.weekday() 

541 self._original_rule['byweekday'] = None 

542 

543 # bymonth 

544 if bymonth is None: 

545 self._bymonth = None 

546 else: 

547 if isinstance(bymonth, integer_types): 

548 bymonth = (bymonth,) 

549 

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

551 

552 if 'bymonth' not in self._original_rule: 

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

554 

555 # byyearday 

556 if byyearday is None: 

557 self._byyearday = None 

558 else: 

559 if isinstance(byyearday, integer_types): 

560 byyearday = (byyearday,) 

561 

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

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

564 

565 # byeaster 

566 if byeaster is not None: 

567 if not easter: 

568 from dateutil import easter 

569 if isinstance(byeaster, integer_types): 

570 self._byeaster = (byeaster,) 

571 else: 

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

573 

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

575 else: 

576 self._byeaster = None 

577 

578 # bymonthday 

579 if bymonthday is None: 

580 self._bymonthday = () 

581 self._bynmonthday = () 

582 else: 

583 if isinstance(bymonthday, integer_types): 

584 bymonthday = (bymonthday,) 

585 

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

587 

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

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

590 

591 # Storing positive numbers first, then negative numbers 

592 if 'bymonthday' not in self._original_rule: 

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

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

595 

596 # byweekno 

597 if byweekno is None: 

598 self._byweekno = None 

599 else: 

600 if isinstance(byweekno, integer_types): 

601 byweekno = (byweekno,) 

602 

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

604 

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

606 

607 # byweekday / bynweekday 

608 if byweekday is None: 

609 self._byweekday = None 

610 self._bynweekday = None 

611 else: 

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

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

614 # byweekday set. 

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

616 byweekday = (byweekday,) 

617 

618 self._byweekday = set() 

619 self._bynweekday = set() 

620 for wday in byweekday: 

621 if isinstance(wday, integer_types): 

622 self._byweekday.add(wday) 

623 elif not wday.n or freq > MONTHLY: 

624 self._byweekday.add(wday.weekday) 

625 else: 

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

627 

628 if not self._byweekday: 

629 self._byweekday = None 

630 elif not self._bynweekday: 

631 self._bynweekday = None 

632 

633 if self._byweekday is not None: 

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

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

636 else: 

637 orig_byweekday = () 

638 

639 if self._bynweekday is not None: 

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

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

642 else: 

643 orig_bynweekday = () 

644 

645 if 'byweekday' not in self._original_rule: 

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

647 orig_byweekday, orig_bynweekday)) 

648 

649 # byhour 

650 if byhour is None: 

651 if freq < HOURLY: 

652 self._byhour = {dtstart.hour} 

653 else: 

654 self._byhour = None 

655 else: 

656 if isinstance(byhour, integer_types): 

657 byhour = (byhour,) 

658 

659 if freq == HOURLY: 

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

661 byxxx=byhour, 

662 base=24) 

663 else: 

664 self._byhour = set(byhour) 

665 

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

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

668 

669 # byminute 

670 if byminute is None: 

671 if freq < MINUTELY: 

672 self._byminute = {dtstart.minute} 

673 else: 

674 self._byminute = None 

675 else: 

676 if isinstance(byminute, integer_types): 

677 byminute = (byminute,) 

678 

679 if freq == MINUTELY: 

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

681 byxxx=byminute, 

682 base=60) 

683 else: 

684 self._byminute = set(byminute) 

685 

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

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

688 

689 # bysecond 

690 if bysecond is None: 

691 if freq < SECONDLY: 

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

693 else: 

694 self._bysecond = None 

695 else: 

696 if isinstance(bysecond, integer_types): 

697 bysecond = (bysecond,) 

698 

699 self._bysecond = set(bysecond) 

700 

701 if freq == SECONDLY: 

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

703 byxxx=bysecond, 

704 base=60) 

705 else: 

706 self._bysecond = set(bysecond) 

707 

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

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

710 

711 if self._freq >= HOURLY: 

712 self._timeset = None 

713 else: 

714 self._timeset = [] 

715 for hour in self._byhour: 

716 for minute in self._byminute: 

717 for second in self._bysecond: 

718 self._timeset.append( 

719 datetime.time(hour, minute, second, 

720 tzinfo=self._tzinfo)) 

721 self._timeset.sort() 

722 self._timeset = tuple(self._timeset) 

723 

724 def __str__(self): 

725 """ 

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

727 This is mostly compatible with RFC5545, except for the 

728 dateutil-specific extension BYEASTER. 

729 """ 

730 

731 output = [] 

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

733 if self._dtstart: 

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

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

736 

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

738 if self._interval != 1: 

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

740 

741 if self._wkst: 

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

743 

744 if self._count is not None: 

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

746 

747 if self._until: 

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

749 

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

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

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

753 original_rule = dict(self._original_rule) 

754 wday_strings = [] 

755 for wday in original_rule['byweekday']: 

756 if wday.n: 

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

758 n=wday.n, 

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

760 else: 

761 wday_strings.append(repr(wday)) 

762 

763 original_rule['byweekday'] = wday_strings 

764 else: 

765 original_rule = self._original_rule 

766 

767 partfmt = '{name}={vals}' 

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

769 ('BYMONTH', 'bymonth'), 

770 ('BYMONTHDAY', 'bymonthday'), 

771 ('BYYEARDAY', 'byyearday'), 

772 ('BYWEEKNO', 'byweekno'), 

773 ('BYDAY', 'byweekday'), 

774 ('BYHOUR', 'byhour'), 

775 ('BYMINUTE', 'byminute'), 

776 ('BYSECOND', 'bysecond'), 

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

778 value = original_rule.get(key) 

779 if value: 

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

781 for v in value)))) 

782 

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

784 return '\n'.join(output) 

785 

786 def replace(self, **kwargs): 

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

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

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

790 "count": self._count, 

791 "dtstart": self._dtstart, 

792 "freq": self._freq, 

793 "until": self._until, 

794 "wkst": self._wkst, 

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

796 new_kwargs.update(self._original_rule) 

797 new_kwargs.update(kwargs) 

798 return rrule(**new_kwargs) 

799 

800 def _iter(self): 

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

802 self._dtstart.timetuple() 

803 

804 # Some local variables to speed things up a bit 

805 freq = self._freq 

806 interval = self._interval 

807 wkst = self._wkst 

808 until = self._until 

809 bymonth = self._bymonth 

810 byweekno = self._byweekno 

811 byyearday = self._byyearday 

812 byweekday = self._byweekday 

813 byeaster = self._byeaster 

814 bymonthday = self._bymonthday 

815 bynmonthday = self._bynmonthday 

816 bysetpos = self._bysetpos 

817 byhour = self._byhour 

818 byminute = self._byminute 

819 bysecond = self._bysecond 

820 

821 ii = _iterinfo(self) 

822 ii.rebuild(year, month) 

823 

824 getdayset = {YEARLY: ii.ydayset, 

825 MONTHLY: ii.mdayset, 

826 WEEKLY: ii.wdayset, 

827 DAILY: ii.ddayset, 

828 HOURLY: ii.ddayset, 

829 MINUTELY: ii.ddayset, 

830 SECONDLY: ii.ddayset}[freq] 

831 

832 if freq < HOURLY: 

833 timeset = self._timeset 

834 else: 

835 gettimeset = {HOURLY: ii.htimeset, 

836 MINUTELY: ii.mtimeset, 

837 SECONDLY: ii.stimeset}[freq] 

838 if ((freq >= HOURLY and 

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

840 (freq >= MINUTELY and 

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

842 (freq >= SECONDLY and 

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

844 timeset = () 

845 else: 

846 timeset = gettimeset(hour, minute, second) 

847 

848 total = 0 

849 count = self._count 

850 while True: 

851 # Get dayset with the right frequency 

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

853 

854 # Do the "hard" work ;-) 

855 filtered = False 

856 for i in dayset[start:end]: 

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

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

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

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

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

862 ((bymonthday or bynmonthday) and 

863 ii.mdaymask[i] not in bymonthday and 

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

865 (byyearday and 

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

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

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

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

870 dayset[i] = None 

871 filtered = True 

872 

873 # Output results 

874 if bysetpos and timeset: 

875 poslist = [] 

876 for pos in bysetpos: 

877 if pos < 0: 

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

879 else: 

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

881 try: 

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

883 if x is not None][daypos] 

884 time = timeset[timepos] 

885 except IndexError: 

886 pass 

887 else: 

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

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

890 if res not in poslist: 

891 poslist.append(res) 

892 poslist.sort() 

893 for res in poslist: 

894 if until and res > until: 

895 self._len = total 

896 return 

897 elif res >= self._dtstart: 

898 if count is not None: 

899 count -= 1 

900 if count < 0: 

901 self._len = total 

902 return 

903 total += 1 

904 yield res 

905 else: 

906 for i in dayset[start:end]: 

907 if i is not None: 

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

909 for time in timeset: 

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

911 if until and res > until: 

912 self._len = total 

913 return 

914 elif res >= self._dtstart: 

915 if count is not None: 

916 count -= 1 

917 if count < 0: 

918 self._len = total 

919 return 

920 

921 total += 1 

922 yield res 

923 

924 # Handle frequency and interval 

925 fixday = False 

926 if freq == YEARLY: 

927 year += interval 

928 if year > datetime.MAXYEAR: 

929 self._len = total 

930 return 

931 ii.rebuild(year, month) 

932 elif freq == MONTHLY: 

933 month += interval 

934 if month > 12: 

935 div, mod = divmod(month, 12) 

936 month = mod 

937 year += div 

938 if month == 0: 

939 month = 12 

940 year -= 1 

941 if year > datetime.MAXYEAR: 

942 self._len = total 

943 return 

944 ii.rebuild(year, month) 

945 elif freq == WEEKLY: 

946 if wkst > weekday: 

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

948 else: 

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

950 weekday = wkst 

951 fixday = True 

952 elif freq == DAILY: 

953 day += interval 

954 fixday = True 

955 elif freq == HOURLY: 

956 if filtered: 

957 # Jump to one iteration before next day 

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

959 

960 if byhour: 

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

962 byxxx=self._byhour, 

963 base=24) 

964 else: 

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

966 

967 if ndays: 

968 day += ndays 

969 fixday = True 

970 

971 timeset = gettimeset(hour, minute, second) 

972 elif freq == MINUTELY: 

973 if filtered: 

974 # Jump to one iteration before next day 

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

976 

977 valid = False 

978 rep_rate = (24*60) 

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

980 if byminute: 

981 nhours, minute = \ 

982 self.__mod_distance(value=minute, 

983 byxxx=self._byminute, 

984 base=60) 

985 else: 

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

987 

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

989 if div: 

990 day += div 

991 fixday = True 

992 filtered = False 

993 

994 if not byhour or hour in byhour: 

995 valid = True 

996 break 

997 

998 if not valid: 

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

1000 'byhour resulting in empty rule.') 

1001 

1002 timeset = gettimeset(hour, minute, second) 

1003 elif freq == SECONDLY: 

1004 if filtered: 

1005 # Jump to one iteration before next day 

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

1007 // interval) * interval) 

1008 

1009 rep_rate = (24 * 3600) 

1010 valid = False 

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

1012 if bysecond: 

1013 nminutes, second = \ 

1014 self.__mod_distance(value=second, 

1015 byxxx=self._bysecond, 

1016 base=60) 

1017 else: 

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

1019 

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

1021 if div: 

1022 hour += div 

1023 div, hour = divmod(hour, 24) 

1024 if div: 

1025 day += div 

1026 fixday = True 

1027 

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

1029 (not byminute or minute in byminute) and 

1030 (not bysecond or second in bysecond)): 

1031 valid = True 

1032 break 

1033 

1034 if not valid: 

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

1036 'byhour and byminute resulting in empty' + 

1037 ' rule.') 

1038 

1039 timeset = gettimeset(hour, minute, second) 

1040 

1041 if fixday and day > 28: 

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

1043 if day > daysinmonth: 

1044 while day > daysinmonth: 

1045 day -= daysinmonth 

1046 month += 1 

1047 if month == 13: 

1048 month = 1 

1049 year += 1 

1050 if year > datetime.MAXYEAR: 

1051 self._len = total 

1052 return 

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

1054 ii.rebuild(year, month) 

1055 

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

1057 """ 

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

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

1060 specifications which cannot be reached given some starting conditions. 

1061 

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

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

1064 ending position is not coprime with the greatest common denominator 

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

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

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

1068 coprime. 

1069 

1070 :param start: 

1071 Specifies the starting position. 

1072 :param byxxx: 

1073 An iterable containing the list of allowed values. 

1074 :param base: 

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

1076 24 hours, 60 minutes). 

1077 

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

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

1080 speed up later lookups. 

1081 

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

1083 results in an empty rrule. 

1084 """ 

1085 

1086 cset = set() 

1087 

1088 # Support a single byxxx value. 

1089 if isinstance(byxxx, integer_types): 

1090 byxxx = (byxxx, ) 

1091 

1092 for num in byxxx: 

1093 i_gcd = gcd(self._interval, base) 

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

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

1096 cset.add(num) 

1097 

1098 if len(cset) == 0: 

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

1100 

1101 return cset 

1102 

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

1104 """ 

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

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

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

1108 

1109 :param value: 

1110 The old value of the component. 

1111 :param byxxx: 

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

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

1114 valid rule is present. 

1115 :param base: 

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

1117 24 hours, 60 minutes). 

1118 

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

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

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

1122 

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

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

1125 value in `byxxx` is found. 

1126 """ 

1127 accumulator = 0 

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

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

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

1131 accumulator += div 

1132 if value in byxxx: 

1133 return (accumulator, value) 

1134 

1135 

1136class _iterinfo(object): 

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

1138 "yearlen", "nextyearlen", "yearordinal", "yearweekday", 

1139 "mmask", "mrange", "mdaymask", "nmdaymask", 

1140 "wdaymask", "wnomask", "nwdaymask", "eastermask"] 

1141 

1142 def __init__(self, rrule): 

1143 for attr in self.__slots__: 

1144 setattr(self, attr, None) 

1145 self.rrule = rrule 

1146 

1147 def rebuild(self, year, month): 

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

1149 rr = self.rrule 

1150 if year != self.lastyear: 

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

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

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

1154 self.yearordinal = firstyday.toordinal() 

1155 self.yearweekday = firstyday.weekday() 

1156 

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

1158 if self.yearlen == 365: 

1159 self.mmask = M365MASK 

1160 self.mdaymask = MDAY365MASK 

1161 self.nmdaymask = NMDAY365MASK 

1162 self.wdaymask = WDAYMASK[wday:] 

1163 self.mrange = M365RANGE 

1164 else: 

1165 self.mmask = M366MASK 

1166 self.mdaymask = MDAY366MASK 

1167 self.nmdaymask = NMDAY366MASK 

1168 self.wdaymask = WDAYMASK[wday:] 

1169 self.mrange = M366RANGE 

1170 

1171 if not rr._byweekno: 

1172 self.wnomask = None 

1173 else: 

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

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

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

1177 if no1wkst >= 4: 

1178 no1wkst = 0 

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

1180 # from last year. 

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

1182 else: 

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

1184 # left in last year. 

1185 wyearlen = self.yearlen-no1wkst 

1186 div, mod = divmod(wyearlen, 7) 

1187 numweeks = div+mod//4 

1188 for n in rr._byweekno: 

1189 if n < 0: 

1190 n += numweeks+1 

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

1192 continue 

1193 if n > 1: 

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

1195 if no1wkst != firstwkst: 

1196 i -= 7-firstwkst 

1197 else: 

1198 i = no1wkst 

1199 for j in range(7): 

1200 self.wnomask[i] = 1 

1201 i += 1 

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

1203 break 

1204 if 1 in rr._byweekno: 

1205 # Check week number 1 of next year as well 

1206 # TODO: Check -numweeks for next year. 

1207 i = no1wkst+numweeks*7 

1208 if no1wkst != firstwkst: 

1209 i -= 7-firstwkst 

1210 if i < self.yearlen: 

1211 # If week starts in next year, we 

1212 # don't care about it. 

1213 for j in range(7): 

1214 self.wnomask[i] = 1 

1215 i += 1 

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

1217 break 

1218 if no1wkst: 

1219 # Check last week number of last year as 

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

1221 # started on week start, or week number 1 

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

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

1224 # this year. 

1225 if -1 not in rr._byweekno: 

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

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

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

1229 if lno1wkst >= 4: 

1230 lno1wkst = 0 

1231 lnumweeks = 52+(lyearlen + 

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

1233 else: 

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

1235 else: 

1236 lnumweeks = -1 

1237 if lnumweeks in rr._byweekno: 

1238 for i in range(no1wkst): 

1239 self.wnomask[i] = 1 

1240 

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

1242 year != self.lastyear)): 

1243 ranges = [] 

1244 if rr._freq == YEARLY: 

1245 if rr._bymonth: 

1246 for month in rr._bymonth: 

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

1248 else: 

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

1250 elif rr._freq == MONTHLY: 

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

1252 if ranges: 

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

1254 # care about cross-year weekly periods. 

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

1256 for first, last in ranges: 

1257 last -= 1 

1258 for wday, n in rr._bynweekday: 

1259 if n < 0: 

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

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

1262 else: 

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

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

1265 if first <= i <= last: 

1266 self.nwdaymask[i] = 1 

1267 

1268 if rr._byeaster: 

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

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

1271 for offset in rr._byeaster: 

1272 self.eastermask[eyday+offset] = 1 

1273 

1274 self.lastyear = year 

1275 self.lastmonth = month 

1276 

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

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

1279 

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

1281 dset = [None]*self.yearlen 

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

1283 for i in range(start, end): 

1284 dset[i] = i 

1285 return dset, start, end 

1286 

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

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

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

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

1291 start = i 

1292 for j in range(7): 

1293 dset[i] = i 

1294 i += 1 

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

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

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

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

1299 break 

1300 return dset, start, i 

1301 

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

1303 dset = [None] * self.yearlen 

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

1305 dset[i] = i 

1306 return dset, i, i + 1 

1307 

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

1309 tset = [] 

1310 rr = self.rrule 

1311 for minute in rr._byminute: 

1312 for second in rr._bysecond: 

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

1314 tzinfo=rr._tzinfo)) 

1315 tset.sort() 

1316 return tset 

1317 

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

1319 tset = [] 

1320 rr = self.rrule 

1321 for second in rr._bysecond: 

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

1323 tset.sort() 

1324 return tset 

1325 

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

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

1328 tzinfo=self.rrule._tzinfo),) 

1329 

1330 

1331class rruleset(rrulebase): 

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

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

1334 constructor takes the following keyword arguments: 

1335 

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

1337 performance of multiple queries considerably. """ 

1338 

1339 class _genitem(object): 

1340 def __init__(self, genlist, gen): 

1341 try: 

1342 self.dt = advance_iterator(gen) 

1343 genlist.append(self) 

1344 except StopIteration: 

1345 pass 

1346 self.genlist = genlist 

1347 self.gen = gen 

1348 

1349 def __next__(self): 

1350 try: 

1351 self.dt = advance_iterator(self.gen) 

1352 except StopIteration: 

1353 if self.genlist[0] is self: 

1354 heapq.heappop(self.genlist) 

1355 else: 

1356 self.genlist.remove(self) 

1357 heapq.heapify(self.genlist) 

1358 

1359 next = __next__ 

1360 

1361 def __lt__(self, other): 

1362 return self.dt < other.dt 

1363 

1364 def __gt__(self, other): 

1365 return self.dt > other.dt 

1366 

1367 def __eq__(self, other): 

1368 return self.dt == other.dt 

1369 

1370 def __ne__(self, other): 

1371 return self.dt != other.dt 

1372 

1373 def __init__(self, cache=False): 

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

1375 self._rrule = [] 

1376 self._rdate = [] 

1377 self._exrule = [] 

1378 self._exdate = [] 

1379 

1380 @_invalidates_cache 

1381 def rrule(self, rrule): 

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

1383 generation. """ 

1384 self._rrule.append(rrule) 

1385 

1386 @_invalidates_cache 

1387 def rdate(self, rdate): 

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

1389 set generation. """ 

1390 self._rdate.append(rdate) 

1391 

1392 @_invalidates_cache 

1393 def exrule(self, exrule): 

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

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

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

1397 """ 

1398 self._exrule.append(exrule) 

1399 

1400 @_invalidates_cache 

1401 def exdate(self, exdate): 

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

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

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

1405 self._exdate.append(exdate) 

1406 

1407 def _iter(self): 

1408 rlist = [] 

1409 self._rdate.sort() 

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

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

1412 self._genitem(rlist, gen) 

1413 exlist = [] 

1414 self._exdate.sort() 

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

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

1417 self._genitem(exlist, gen) 

1418 lastdt = None 

1419 total = 0 

1420 heapq.heapify(rlist) 

1421 heapq.heapify(exlist) 

1422 while rlist: 

1423 ritem = rlist[0] 

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

1425 while exlist and exlist[0] < ritem: 

1426 exitem = exlist[0] 

1427 advance_iterator(exitem) 

1428 if exlist and exlist[0] is exitem: 

1429 heapq.heapreplace(exlist, exitem) 

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

1431 total += 1 

1432 yield ritem.dt 

1433 lastdt = ritem.dt 

1434 advance_iterator(ritem) 

1435 if rlist and rlist[0] is ritem: 

1436 heapq.heapreplace(rlist, ritem) 

1437 self._len = total 

1438 

1439 

1440 

1441 

1442class _rrulestr(object): 

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

1444 recurrence rules. 

1445 

1446 :param s: 

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

1448 

1449 :param dtstart: 

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

1451 rule string. 

1452 

1453 :param cache: 

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

1455 performance of multiple queries considerably. 

1456 

1457 :param unfold: 

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

1459 than one line and should be joined before processing. 

1460 

1461 :param forceset: 

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

1463 be returned. 

1464 

1465 :param compatible: 

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

1467 

1468 :param ignoretz: 

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

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

1471 

1472 :param tzids: 

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

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

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

1476 

1477 :param tzinfos: 

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

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

1480 information. 

1481 

1482 :return: 

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

1484 :class:`dateutil.rrule.rrule` 

1485 """ 

1486 

1487 _freq_map = {"YEARLY": YEARLY, 

1488 "MONTHLY": MONTHLY, 

1489 "WEEKLY": WEEKLY, 

1490 "DAILY": DAILY, 

1491 "HOURLY": HOURLY, 

1492 "MINUTELY": MINUTELY, 

1493 "SECONDLY": SECONDLY} 

1494 

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

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

1497 

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

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

1500 

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

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

1503 

1504 _handle_INTERVAL = _handle_int 

1505 _handle_COUNT = _handle_int 

1506 _handle_BYSETPOS = _handle_int_list 

1507 _handle_BYMONTH = _handle_int_list 

1508 _handle_BYMONTHDAY = _handle_int_list 

1509 _handle_BYYEARDAY = _handle_int_list 

1510 _handle_BYEASTER = _handle_int_list 

1511 _handle_BYWEEKNO = _handle_int_list 

1512 _handle_BYHOUR = _handle_int_list 

1513 _handle_BYMINUTE = _handle_int_list 

1514 _handle_BYSECOND = _handle_int_list 

1515 

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

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

1518 

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

1520 global parser 

1521 if not parser: 

1522 from dateutil import parser 

1523 try: 

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

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

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

1527 except ValueError: 

1528 raise ValueError("invalid until date") 

1529 

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

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

1532 

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

1534 """ 

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

1536 """ 

1537 l = [] 

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

1539 if '(' in wday: 

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

1541 splt = wday.split('(') 

1542 w = splt[0] 

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

1544 elif len(wday): 

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

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

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

1548 break 

1549 n = wday[:i] or None 

1550 w = wday[i:] 

1551 if n: 

1552 n = int(n) 

1553 else: 

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

1555 

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

1557 rrkwargs["byweekday"] = l 

1558 

1559 _handle_BYDAY = _handle_BYWEEKDAY 

1560 

1561 def _parse_rfc_rrule(self, line, 

1562 dtstart=None, 

1563 cache=False, 

1564 ignoretz=False, 

1565 tzinfos=None): 

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

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

1568 if name != "RRULE": 

1569 raise ValueError("unknown parameter name") 

1570 else: 

1571 value = line 

1572 rrkwargs = {} 

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

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

1575 name = name.upper() 

1576 value = value.upper() 

1577 try: 

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

1579 ignoretz=ignoretz, 

1580 tzinfos=tzinfos) 

1581 except AttributeError: 

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

1583 except (KeyError, ValueError): 

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

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

1586 

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

1588 ignoretz, tzids, tzinfos): 

1589 global parser 

1590 if not parser: 

1591 from dateutil import parser 

1592 

1593 datevals = [] 

1594 value_found = False 

1595 TZID = None 

1596 

1597 for parm in parms: 

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

1599 try: 

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

1601 except KeyError: 

1602 continue 

1603 if tzids is None: 

1604 from . import tz 

1605 tzlookup = tz.gettz 

1606 elif callable(tzids): 

1607 tzlookup = tzids 

1608 else: 

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

1610 if tzlookup is None: 

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

1612 'not %s' % tzids) 

1613 raise ValueError(msg) 

1614 

1615 TZID = tzlookup(tzkey) 

1616 continue 

1617 

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

1619 # only once. 

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

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

1622 else: 

1623 if value_found: 

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

1625 raise ValueError(msg) 

1626 value_found = True 

1627 

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

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

1630 if TZID is not None: 

1631 if date.tzinfo is None: 

1632 date = date.replace(tzinfo=TZID) 

1633 else: 

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

1635 datevals.append(date) 

1636 

1637 return datevals 

1638 

1639 def _parse_rfc(self, s, 

1640 dtstart=None, 

1641 cache=False, 

1642 unfold=False, 

1643 forceset=False, 

1644 compatible=False, 

1645 ignoretz=False, 

1646 tzids=None, 

1647 tzinfos=None): 

1648 global parser 

1649 if compatible: 

1650 forceset = True 

1651 unfold = True 

1652 

1653 TZID_NAMES = dict(map( 

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

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

1656 )) 

1657 s = s.upper() 

1658 if not s.strip(): 

1659 raise ValueError("empty string") 

1660 if unfold: 

1661 lines = s.splitlines() 

1662 i = 0 

1663 while i < len(lines): 

1664 line = lines[i].rstrip() 

1665 if not line: 

1666 del lines[i] 

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

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

1669 del lines[i] 

1670 else: 

1671 i += 1 

1672 else: 

1673 lines = s.split() 

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

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

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

1677 dtstart=dtstart, ignoretz=ignoretz, 

1678 tzinfos=tzinfos) 

1679 else: 

1680 rrulevals = [] 

1681 rdatevals = [] 

1682 exrulevals = [] 

1683 exdatevals = [] 

1684 for line in lines: 

1685 if not line: 

1686 continue 

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

1688 name = "RRULE" 

1689 value = line 

1690 else: 

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

1692 parms = name.split(';') 

1693 if not parms: 

1694 raise ValueError("empty property name") 

1695 name = parms[0] 

1696 parms = parms[1:] 

1697 if name == "RRULE": 

1698 for parm in parms: 

1699 if parm.startswith("X-"): 

1700 # Experimental random vendor features; safe to ignore 

1701 continue 

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

1703 rrulevals.append(value) 

1704 elif name == "RDATE": 

1705 for parm in parms: 

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

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

1708 rdatevals.append(value) 

1709 elif name == "EXRULE": 

1710 for parm in parms: 

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

1712 exrulevals.append(value) 

1713 elif name == "EXDATE": 

1714 exdatevals.extend( 

1715 self._parse_date_value(value, parms, 

1716 TZID_NAMES, ignoretz, 

1717 tzids, tzinfos) 

1718 ) 

1719 elif name == "DTSTART": 

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

1721 ignoretz, tzids, tzinfos) 

1722 if len(dtvals) != 1: 

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

1724 value) 

1725 dtstart = dtvals[0] 

1726 else: 

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

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

1729 or exrulevals or exdatevals): 

1730 if not parser and (rdatevals or exdatevals): 

1731 from dateutil import parser 

1732 rset = rruleset(cache=cache) 

1733 for value in rrulevals: 

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

1735 ignoretz=ignoretz, 

1736 tzinfos=tzinfos)) 

1737 for value in rdatevals: 

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

1739 rset.rdate(parser.parse(datestr, 

1740 ignoretz=ignoretz, 

1741 tzinfos=tzinfos)) 

1742 for value in exrulevals: 

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

1744 ignoretz=ignoretz, 

1745 tzinfos=tzinfos)) 

1746 for value in exdatevals: 

1747 rset.exdate(value) 

1748 if compatible and dtstart: 

1749 rset.rdate(dtstart) 

1750 return rset 

1751 else: 

1752 return self._parse_rfc_rrule(rrulevals[0], 

1753 dtstart=dtstart, 

1754 cache=cache, 

1755 ignoretz=ignoretz, 

1756 tzinfos=tzinfos) 

1757 

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

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

1760 

1761 

1762rrulestr = _rrulestr() 

1763 

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