|
from datetime import datetime, date
|
|
|
|
from tzlocal import get_localzone_name
|
|
|
|
from .util.date_time_util import ensure_localisation
|
|
|
|
|
|
class Duration:
|
|
"""Represents properties that contain a duration of time."""
|
|
|
|
def __init__(self, w=None, d=None, h=None, m=None, s=None):
|
|
"""
|
|
:param w: weeks
|
|
:param d: days
|
|
:param h: hours
|
|
:param m: minutes
|
|
:param s: seconds
|
|
"""
|
|
|
|
self.w = w
|
|
self.d = d
|
|
self.h = h
|
|
self.m = m
|
|
self.s = s
|
|
|
|
def __str__(self):
|
|
res = 'P'
|
|
if self.w:
|
|
res += '{}W'.format(self.w)
|
|
if self.d:
|
|
res += '{}D'.format(self.d)
|
|
if self.h or self.m or self.s:
|
|
res += 'T'
|
|
if self.h:
|
|
res += '{}H'.format(self.h)
|
|
if self.m:
|
|
res += '{}M'.format(self.m)
|
|
if self.s:
|
|
res += '{}S'.format(self.s)
|
|
|
|
return res
|
|
|
|
|
|
class _DayOfTheWeek:
|
|
"""Weekday representation. Optionally includes positive or negative integer
|
|
value that indicates the nth occurrence of a specific day within the "MONTHLY"
|
|
or "YEARLY" recurrence rules.
|
|
|
|
>>> str(SU)
|
|
'SU'
|
|
|
|
>>> str(FR)
|
|
'FR'
|
|
|
|
>>> str(SU(4))
|
|
'4SU'
|
|
|
|
>>> str(SU(-1))
|
|
'-1SU'
|
|
"""
|
|
|
|
def __init__(self, short, n=None):
|
|
self.short = short
|
|
self.n = n
|
|
|
|
def __call__(self, n):
|
|
return _DayOfTheWeek(self.short, n)
|
|
|
|
def __str__(self):
|
|
if self.n is None:
|
|
return self.short
|
|
else:
|
|
return str(self.n) + self.short
|
|
|
|
|
|
SU = SUNDAY = _DayOfTheWeek('SU')
|
|
MO = MONDAY = _DayOfTheWeek('MO')
|
|
TU = TUESDAY = _DayOfTheWeek('TU')
|
|
WE = WEDNESDAY = _DayOfTheWeek('WE')
|
|
TH = THURSDAY = _DayOfTheWeek('TH')
|
|
FR = FRIDAY = _DayOfTheWeek('FR')
|
|
SA = SATURDAY = _DayOfTheWeek('SA')
|
|
|
|
DEFAULT_WEEK_START = SUNDAY
|
|
|
|
SECONDLY = 'SECONDLY'
|
|
MINUTELY = 'MINUTELY'
|
|
HOURLY = 'HOURLY'
|
|
|
|
DAILY = 'DAILY'
|
|
WEEKLY = 'WEEKLY'
|
|
MONTHLY = 'MONTHLY'
|
|
YEARLY = 'YEARLY'
|
|
|
|
|
|
class Recurrence:
|
|
|
|
@staticmethod
|
|
def rule(
|
|
freq=DAILY,
|
|
interval=None,
|
|
count=None,
|
|
until=None,
|
|
by_second=None,
|
|
by_minute=None,
|
|
by_hour=None,
|
|
by_week_day=None,
|
|
by_month_day=None,
|
|
by_year_day=None,
|
|
by_week=None,
|
|
by_month=None,
|
|
by_set_pos=None,
|
|
week_start=DEFAULT_WEEK_START
|
|
):
|
|
"""This property defines a rule or repeating pattern for recurring events.
|
|
|
|
:param freq:
|
|
Identifies the type of recurrence rule. Possible values are SECONDLY, HOURLY,
|
|
MINUTELY, DAILY, WEEKLY, MONTHLY, YEARLY. Default: DAILY
|
|
:param interval:
|
|
Positive integer representing how often the recurrence rule repeats
|
|
:param count:
|
|
Number of occurrences at which to range-bound the recurrence
|
|
:param until:
|
|
End date of recurrence
|
|
:param by_second:
|
|
Second or list of seconds within a minute. Valid values are 0 to 60
|
|
:param by_minute:
|
|
Minute or list of minutes within a hour. Valid values are 0 to 59
|
|
:param by_hour:
|
|
Hour or list of hours of the day. Valid values are 0 to 23
|
|
:param by_week_day:
|
|
Day or list of days of the week.
|
|
Possible values: :py:obj:`~SUNDAY`, :py:obj:`~MONDAY`, :py:obj:`~TUESDAY`, :py:obj:`~WEDNESDAY`,
|
|
:py:obj:`~THURSDAY`, :py:obj:`~FRIDAY`, :py:obj:`~SATURDAY`
|
|
:param by_month_day:
|
|
Day or list of days of the month. Valid values are 1 to 31 or -31 to -1.
|
|
For example, -10 represents the tenth to the last day of the month.
|
|
:param by_year_day:
|
|
Day or list of days of the year. Valid values are 1 to 366 or -366 to -1.
|
|
For example, -1 represents the last day of the year.
|
|
:param by_week:
|
|
Ordinal or list of ordinals specifying weeks of the year. Valid values are 1 to 53 or -53 to -1.
|
|
:param by_month:
|
|
Month or list of months of the year. Valid values are 1 to 12.
|
|
:param by_set_pos:
|
|
Value or list of values which corresponds to the nth occurrence within the set of events
|
|
specified by the rule. Valid values are 1 to 366 or -366 to -1.
|
|
It can only be used in conjunction with another by_xxx parameter.
|
|
:param week_start:
|
|
The day on which the workweek starts.
|
|
Possible values: :py:obj:`~SUNDAY`, :py:obj:`~MONDAY`, :py:obj:`~TUESDAY`, :py:obj:`~WEDNESDAY`,
|
|
:py:obj:`~THURSDAY`, :py:obj:`~FRIDAY`, :py:obj:`~SATURDAY`
|
|
|
|
:return:
|
|
String representing specified recurrence rule in `RRULE format`_.
|
|
|
|
.. note:: If none of the by_day, by_month_day, or by_year_day are specified, the day is gotten from start date.
|
|
|
|
|
|
.. _`RRULE format`: https://tools.ietf.org/html/rfc5545#section-3.8.5
|
|
"""
|
|
return 'RRULE:' + Recurrence._rule(freq, interval, count, until, by_second, by_minute, by_hour, by_week_day,
|
|
by_month_day, by_year_day, by_week, by_month, by_set_pos, week_start)
|
|
|
|
@staticmethod
|
|
def exclude_rule(
|
|
freq=DAILY,
|
|
interval=None,
|
|
count=None,
|
|
until=None,
|
|
by_second=None,
|
|
by_minute=None,
|
|
by_hour=None,
|
|
by_week_day=None,
|
|
by_month_day=None,
|
|
by_year_day=None,
|
|
by_week=None,
|
|
by_month=None,
|
|
by_set_pos=None,
|
|
week_start=DEFAULT_WEEK_START
|
|
):
|
|
"""This property defines an exclusion rule or repeating pattern for recurring events.
|
|
|
|
:param freq:
|
|
Identifies the type of recurrence rule. Possible values are SECONDLY, HOURLY,
|
|
MINUTELY, DAILY, WEEKLY, MONTHLY, YEARLY. Default: DAILY
|
|
:param interval:
|
|
Positive integer representing how often the recurrence rule repeats
|
|
:param count:
|
|
Number of occurrences at which to range-bound the recurrence
|
|
:param until:
|
|
End date of recurrence
|
|
:param by_second:
|
|
Second or list of seconds within a minute. Valid values are 0 to 60
|
|
:param by_minute:
|
|
Minute or list of minutes within a hour. Valid values are 0 to 59
|
|
:param by_hour:
|
|
Hour or list of hours of the day. Valid values are 0 to 23
|
|
:param by_week_day:
|
|
Day or list of days of the week.
|
|
Possible values: :py:obj:`~SUNDAY`, :py:obj:`~MONDAY`, :py:obj:`~TUESDAY`, :py:obj:`~WEDNESDAY`,
|
|
:py:obj:`~THURSDAY`, :py:obj:`~FRIDAY`, :py:obj:`~SATURDAY`
|
|
:param by_month_day:
|
|
Day or list of days of the month. Valid values are 1 to 31 or -31 to -1.
|
|
For example, -10 represents the tenth to the last day of the month.
|
|
:param by_year_day:
|
|
Day or list of days of the year. Valid values are 1 to 366 or -366 to -1.
|
|
For example, -1 represents the last day of the year.
|
|
:param by_week:
|
|
Ordinal or list of ordinals specifying weeks of the year. Valid values are 1 to 53 or -53 to -1.
|
|
:param by_month:
|
|
Month or list of months of the year. Valid values are 1 to 12.
|
|
:param by_set_pos:
|
|
Value or list of values which corresponds to the nth occurrence within the set of events
|
|
specified by the rule. Valid values are 1 to 366 or -366 to -1.
|
|
It can only be used in conjunction with another by_xxx parameter.
|
|
:param week_start:
|
|
The day on which the workweek starts.
|
|
Possible values: :py:obj:`~SUNDAY`, :py:obj:`~MONDAY`, :py:obj:`~TUESDAY`, :py:obj:`~WEDNESDAY`,
|
|
:py:obj:`~THURSDAY`, :py:obj:`~FRIDAY`, :py:obj:`~SATURDAY`
|
|
|
|
:return:
|
|
String representing specified recurrence rule in `RRULE format`_.
|
|
|
|
.. note:: If none of the by_day, by_month_day, or by_year_day are specified, the day is gotten from start date.
|
|
|
|
|
|
.. _`RRULE format`: https://tools.ietf.org/html/rfc5545#section-3.8.5
|
|
"""
|
|
return 'EXRULE:' + Recurrence._rule(freq, interval, count, until, by_second, by_minute, by_hour, by_week_day,
|
|
by_month_day, by_year_day, by_week, by_month, by_set_pos, week_start)
|
|
|
|
@staticmethod
|
|
def dates(ds):
|
|
"""Converts date(s) set to RDATE format.
|
|
|
|
:param ds:
|
|
date/datetime object or list of date/datetime objects
|
|
|
|
:return:
|
|
RDATE string of dates.
|
|
"""
|
|
return 'RDATE;' + Recurrence._dates(ds)
|
|
|
|
@staticmethod
|
|
def times(dts, timezone=get_localzone_name()):
|
|
"""Converts datetime(s) set to RDATE format.
|
|
|
|
:param dts:
|
|
datetime object or list of datetime objects
|
|
:param timezone:
|
|
Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
|
|
the computers local timezone is used if it is configured. UTC is used otherwise.
|
|
|
|
:return:
|
|
RDATE string of datetimes with specified timezone.
|
|
"""
|
|
return 'RDATE;' + Recurrence._times(dts, timezone)
|
|
|
|
@staticmethod
|
|
def periods(ps, timezone=get_localzone_name()):
|
|
"""Converts date period(s) to RDATE format.
|
|
|
|
Period is defined as tuple of starting date/datetime and ending date/datetime or duration as Duration object:
|
|
(date/datetime, date/datetime/Duration)
|
|
|
|
:param ps:
|
|
Period or list of periods.
|
|
:param timezone:
|
|
Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
|
|
the computers local timezone is used if it is configured. UTC is used otherwise.
|
|
|
|
:return:
|
|
RDATE string of periods.
|
|
"""
|
|
return 'RDATE;' + Recurrence._periods(ps, timezone)
|
|
|
|
@staticmethod
|
|
def exclude_dates(ds):
|
|
"""Converts date(s) set to EXDATE format.
|
|
|
|
:param ds:
|
|
date/datetime object or list of date/datetime objects
|
|
|
|
:return:
|
|
EXDATE string of dates.
|
|
"""
|
|
return 'EXDATE;' + Recurrence._dates(ds)
|
|
|
|
@staticmethod
|
|
def exclude_times(dts, timezone=get_localzone_name()):
|
|
"""Converts datetime(s) set to EXDATE format.
|
|
|
|
:param dts:
|
|
datetime object or list of datetime objects
|
|
:param timezone:
|
|
Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
|
|
the computers local timezone is used if it is configured. UTC is used otherwise.
|
|
|
|
:return:
|
|
EXDATE string of datetimes with specified timezone.
|
|
"""
|
|
return 'EXDATE;' + Recurrence._times(dts, timezone)
|
|
|
|
@staticmethod
|
|
def exclude_periods(ps, timezone=get_localzone_name()):
|
|
"""Converts date period(s) to EXDATE format.
|
|
|
|
Period is defined as tuple of starting date/datetime and ending date/datetime or duration as Duration object:
|
|
(date/datetime, date/datetime/Duration)
|
|
|
|
:param ps:
|
|
Period or list of periods.
|
|
:param timezone:
|
|
Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
|
|
the computers local timezone is used if it is configured. UTC is used otherwise.
|
|
|
|
:return:
|
|
EXDATE string of periods.
|
|
"""
|
|
return 'EXDATE;' + Recurrence._periods(ps, timezone)
|
|
|
|
@staticmethod
|
|
def _times(dts, timezone=get_localzone_name()):
|
|
"""Converts datetime(s) set to RDATE format.
|
|
|
|
:param dts:
|
|
datetime object or list of datetime objects
|
|
:param timezone:
|
|
Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
|
|
the computers local timezone is used if it is configured. UTC is used otherwise.
|
|
|
|
:return:
|
|
RDATE string of datetimes with specified timezone.
|
|
"""
|
|
|
|
if not isinstance(dts, list):
|
|
dts = [dts]
|
|
|
|
localized_datetimes = []
|
|
for dt in dts:
|
|
if not isinstance(dt, (date, datetime)):
|
|
msg = 'The dts object(s) must be date or datetime, not {!r}.'.format(dt.__class__.__name__)
|
|
raise TypeError(msg)
|
|
localized_datetimes.append(ensure_localisation(dt, timezone))
|
|
|
|
return 'TZID={}:{}'.format(timezone, ','.join(d.strftime('%Y%m%dT%H%M%S') for d in localized_datetimes))
|
|
|
|
@staticmethod
|
|
def _dates(ds):
|
|
"""Converts date(s) set to RDATE format.
|
|
|
|
:param ds:
|
|
date/datetime object or list of date/datetime objects
|
|
|
|
:return:
|
|
RDATE string of dates.
|
|
"""
|
|
if not isinstance(ds, list):
|
|
ds = [ds]
|
|
|
|
for d in ds:
|
|
if not (isinstance(d, (date, datetime))):
|
|
msg = 'The dates object(s) must be date or datetime, not {!r}.'.format(d.__class__.__name__)
|
|
raise TypeError(msg)
|
|
|
|
return 'VALUE=DATE:' + ','.join(d.strftime('%Y%m%d') for d in ds)
|
|
|
|
@staticmethod
|
|
def _periods(ps, timezone=get_localzone_name()):
|
|
"""Converts date period(s) to RDATE format.
|
|
|
|
Period is defined as tuple of starting date/datetime and ending date/datetime or duration as Duration object:
|
|
(date/datetime, date/datetime/Duration)
|
|
|
|
:param ps:
|
|
Period or list of periods.
|
|
:param timezone:
|
|
Timezone formatted as an IANA Time Zone Database name, e.g. "Europe/Zurich". By default,
|
|
the computers local timezone is used if it is configured. UTC is used otherwise.
|
|
|
|
:return:
|
|
RDATE string of periods.
|
|
"""
|
|
if not isinstance(ps, list):
|
|
ps = [ps]
|
|
|
|
period_strings = []
|
|
for start, end in ps:
|
|
if not isinstance(start, (date, datetime)):
|
|
msg = 'The start object(s) must be a date or datetime, not {!r}.'.format(end.__class__.__name__)
|
|
raise TypeError(msg)
|
|
|
|
start = ensure_localisation(start, timezone)
|
|
if isinstance(end, (date, datetime)):
|
|
end = ensure_localisation(end, timezone)
|
|
pstr = '{}/{}'.format(start.strftime('%Y%m%dT%H%M%SZ'), end.strftime('%Y%m%dT%H%M%SZ'))
|
|
elif isinstance(end, Duration):
|
|
pstr = '{}/{}'.format(start.strftime('%Y%m%dT%H%M%SZ'), end)
|
|
else:
|
|
msg = 'The end object(s) must be a date, datetime or Duration, not {!r}.'.format(end.__class__.__name__)
|
|
raise TypeError(msg)
|
|
period_strings.append(pstr)
|
|
|
|
return 'VALUE=PERIOD:' + ','.join(period_strings)
|
|
|
|
@staticmethod
|
|
def _rule(
|
|
freq=DAILY,
|
|
interval=None,
|
|
count=None,
|
|
until=None,
|
|
by_second=None,
|
|
by_minute=None,
|
|
by_hour=None,
|
|
by_week_day=None,
|
|
by_month_day=None,
|
|
by_year_day=None,
|
|
by_week=None,
|
|
by_month=None,
|
|
by_set_pos=None,
|
|
week_start=DEFAULT_WEEK_START
|
|
):
|
|
"""This property defines a rule or repeating pattern for recurring events.
|
|
|
|
:param freq:
|
|
Identifies the type of recurrence rule. Possible values are SECONDLY, HOURLY,
|
|
MINUTELY, DAILY, WEEKLY, MONTHLY, YEARLY. Default: DAILY
|
|
:param interval:
|
|
Positive integer representing how often the recurrence rule repeats
|
|
:param count:
|
|
Number of occurrences at which to range-bound the recurrence
|
|
:param until:
|
|
End date of recurrence
|
|
:param by_second:
|
|
Second or list of seconds within a minute. Valid values are 0 to 60
|
|
:param by_minute:
|
|
Minute or list of minutes within a hour. Valid values are 0 to 59
|
|
:param by_hour:
|
|
Hour or list of hours of the day. Valid values are 0 to 23
|
|
:param by_week_day:
|
|
Day or list of days of the week.
|
|
Possible values: :py:obj:`~SUNDAY`, :py:obj:`~MONDAY`, :py:obj:`~TUESDAY`, :py:obj:`~WEDNESDAY`,
|
|
:py:obj:`~THURSDAY`, :py:obj:`~FRIDAY`, :py:obj:`~SATURDAY`
|
|
:param by_month_day:
|
|
Day or list of days of the month. Valid values are 1 to 31 or -31 to -1.
|
|
For example, -10 represents the tenth to the last day of the month.
|
|
:param by_year_day:
|
|
Day or list of days of the year. Valid values are 1 to 366 or -366 to -1.
|
|
For example, -1 represents the last day of the year.
|
|
:param by_week:
|
|
Ordinal or list of ordinals specifying weeks of the year. Valid values are 1 to 53 or -53 to -1.
|
|
:param by_month:
|
|
Month or list of months of the year. Valid values are 1 to 12.
|
|
:param by_set_pos:
|
|
Value or list of values which corresponds to the nth occurrence within the set of events
|
|
specified by the rule. Valid values are 1 to 366 or -366 to -1.
|
|
It can only be used in conjunction with another by_xxx parameter.
|
|
:param week_start:
|
|
The day on which the workweek starts.
|
|
Possible values: :py:obj:`~SUNDAY`, :py:obj:`~MONDAY`, :py:obj:`~TUESDAY`, :py:obj:`~WEDNESDAY`,
|
|
:py:obj:`~THURSDAY`, :py:obj:`~FRIDAY`, :py:obj:`~SATURDAY`
|
|
|
|
:return:
|
|
String representing specified recurrence rule in `RRULE format`_.
|
|
|
|
.. note:: If none of the by_day, by_month_day, or by_year_day are specified, the day is gotten from start date.
|
|
|
|
|
|
.. _`RRULE format`: https://tools.ietf.org/html/rfc5545#section-3.8.5
|
|
"""
|
|
|
|
def ensure_iterable(it):
|
|
return it if isinstance(it, (list, tuple, set)) else [it] if it is not None else []
|
|
|
|
def check_all_type(it, type_, name):
|
|
if any(not isinstance(o, type_) for o in it):
|
|
raise TypeError('"{}" parameter must be a {} or list of {}s.'
|
|
.format(name, type_.__name__, type_.__name__))
|
|
|
|
def check_all_type_and_range(it, type_, range_, name, nonzero=False):
|
|
check_all_type(it, type_, name)
|
|
low, high = range_
|
|
if any(not (low <= o <= high) for o in it):
|
|
raise ValueError('"{}" parameter must be in range {}-{}.'
|
|
.format(name, low, high))
|
|
if nonzero and any(o == 0 for o in it):
|
|
raise ValueError('"{}" parameter must be in range {}-{} and nonzero.'
|
|
.format(name, low, high))
|
|
|
|
def to_string(values):
|
|
return ','.join(map(str, values)) if values else None
|
|
|
|
if freq not in (SECONDLY, MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY):
|
|
raise ValueError('"freq" parameter must be one of SECONDLY, HOURLY, MINUTELY, DAILY, '
|
|
'WEEKLY, MONTHLY or YEARLY. {} was provided'.format(freq))
|
|
if interval is not None and (not isinstance(interval, int) or interval < 1):
|
|
raise ValueError('"interval" parameter must be a positive int. '
|
|
'{} was provided'.format(interval))
|
|
if count is not None and (not isinstance(count, int) or count < 1):
|
|
raise ValueError('"count" parameter must be a positive int. '
|
|
'{} was provided'.format(count))
|
|
if until is not None:
|
|
if not isinstance(until, (date, datetime)):
|
|
raise TypeError('The until object must be a date or datetime, '
|
|
'not {!r}.'.format(until.__class__.__name__))
|
|
else:
|
|
until = until.strftime("%Y%m%dT%H%M%SZ")
|
|
if count is not None and until is not None:
|
|
raise ValueError('"count" and "until" may not appear in one recurrence rule.')
|
|
|
|
by_second = ensure_iterable(by_second)
|
|
check_all_type_and_range(by_second, int, (0, 60), "by_second")
|
|
|
|
by_minute = ensure_iterable(by_minute)
|
|
check_all_type_and_range(by_minute, int, (0, 59), "by_minute")
|
|
|
|
by_hour = ensure_iterable(by_hour)
|
|
check_all_type_and_range(by_hour, int, (0, 23), "by_hour")
|
|
|
|
by_week_day = ensure_iterable(by_week_day)
|
|
check_all_type(by_week_day, _DayOfTheWeek, "by_week_day")
|
|
|
|
by_month_day = ensure_iterable(by_month_day)
|
|
check_all_type_and_range(by_month_day, int, (-31, 31), "by_month_day", nonzero=True)
|
|
|
|
by_year_day = ensure_iterable(by_year_day)
|
|
check_all_type_and_range(by_year_day, int, (-366, 366), "by_year_day", nonzero=True)
|
|
|
|
by_week = ensure_iterable(by_week)
|
|
check_all_type_and_range(by_week, int, (-53, 53), "by_week", nonzero=True)
|
|
|
|
by_month = ensure_iterable(by_month)
|
|
check_all_type_and_range(by_month, int, (1, 12), "by_month")
|
|
|
|
by_set_pos = ensure_iterable(by_set_pos)
|
|
check_all_type_and_range(by_set_pos, int, (-366, 366), "by_set_pos", nonzero=True)
|
|
if by_set_pos and all(not v for v in (by_second, by_minute, by_hour,
|
|
by_week_day, by_month_day, by_year_day,
|
|
by_week, by_month)):
|
|
raise ValueError('"by_set_pos" parameter can only be used in conjunction with another by_xxx parameter.')
|
|
|
|
if not isinstance(week_start, _DayOfTheWeek):
|
|
raise ValueError('"week_start" parameter must be one of SUNDAY, MONDAY, etc. '
|
|
'{} was provided'.format(week_start))
|
|
|
|
rrule = 'FREQ={}'.format(freq)
|
|
|
|
rule_properties = (
|
|
('INTERVAL', interval),
|
|
('COUNT', count),
|
|
('UNTIL', until),
|
|
('BYSECOND', to_string(by_second)),
|
|
('BYMINUTE', to_string(by_minute)),
|
|
('BYHOUR', to_string(by_hour)),
|
|
('BYDAY', to_string(by_week_day)),
|
|
('BYMONTHDAY', to_string(by_month_day)),
|
|
('BYYEARDAY', to_string(by_year_day)),
|
|
('BYWEEKNO', to_string(by_week)),
|
|
('BYMONTH', to_string(by_month)),
|
|
('BYSETPOS', to_string(by_set_pos)),
|
|
('WKST', week_start)
|
|
)
|
|
|
|
for key, value in rule_properties:
|
|
if value:
|
|
rrule += ';{}={}'.format(key, value)
|
|
|
|
return rrule
|
|
|