Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
wiseplat
GitHub Repository: wiseplat/python-code
Path: blob/master/ invest-robot-contest_TinkoffBotTwitch-main/venv/lib/python3.8/site-packages/dateutil/rrule.py
7763 views
1
# -*- coding: utf-8 -*-
2
"""
3
The rrule module offers a small, complete, and very fast, implementation of
4
the recurrence rules documented in the
5
`iCalendar RFC <https://tools.ietf.org/html/rfc5545>`_,
6
including support for caching of results.
7
"""
8
import calendar
9
import datetime
10
import heapq
11
import itertools
12
import re
13
import sys
14
from functools import wraps
15
# For warning about deprecation of until and count
16
from warnings import warn
17
18
from six import advance_iterator, integer_types
19
20
from six.moves import _thread, range
21
22
from ._common import weekday as weekdaybase
23
24
try:
25
from math import gcd
26
except 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.
35
M366MASK = 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)
37
M365MASK = list(M366MASK)
38
M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32))
39
MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
40
MDAY365MASK = list(MDAY366MASK)
41
M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0))
42
NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
43
NMDAY365MASK = list(NMDAY366MASK)
44
M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366)
45
M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365)
46
WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55
47
del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31]
48
MDAY365MASK = tuple(MDAY365MASK)
49
M365MASK = tuple(M365MASK)
50
51
FREQNAMES = ['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.
62
easter = None
63
parser = None
64
65
66
class 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
77
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
78
79
80
def _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
94
class 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
trough 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
305
class 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
1112
class _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
1307
class 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
1418
class _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
1735
rrulestr = _rrulestr()
1736
1737
# vim:ts=4:sw=4:et
1738
1739