"""RECUR property type from :rfc:`5545`."""
from typing import Any, ClassVar
from icalendar.caselessdict import CaselessDict
from icalendar.compatibility import Self
from icalendar.error import JCalParsingError
from icalendar.parser import Parameters
from icalendar.parser_tools import DEFAULT_ENCODING, SEQUENCE_TYPES
from icalendar.prop.dt import vDDDTypes
from icalendar.prop.integer import vInt
from icalendar.prop.recur.frequency import vFrequency
from icalendar.prop.recur.month import vMonth
from icalendar.prop.recur.skip import vSkip
from icalendar.prop.recur.weekday import vWeekday
from icalendar.prop.text import vText
[docs]
class vRecur(CaselessDict):
"""Recurrence definition.
Property Name:
RRULE
Purpose:
This property defines a rule or repeating pattern for recurring events, to-dos,
journal entries, or time zone definitions.
Value Type:
RECUR
Property Parameters:
IANA and non-standard property parameters can be specified on this property.
Conformance:
This property can be specified in recurring "VEVENT", "VTODO", and "VJOURNAL"
calendar components as well as in the "STANDARD" and "DAYLIGHT" sub-components
of the "VTIMEZONE" calendar component, but it SHOULD NOT be specified more than once.
The recurrence set generated with multiple "RRULE" properties is undefined.
Description:
The recurrence rule, if specified, is used in computing the recurrence set.
The recurrence set is the complete set of recurrence instances for a calendar component.
The recurrence set is generated by considering the initial "DTSTART" property along
with the "RRULE", "RDATE", and "EXDATE" properties contained within the
recurring component. The "DTSTART" property defines the first instance in the
recurrence set. The "DTSTART" property value SHOULD be synchronized with the
recurrence rule, if specified. The recurrence set generated with a "DTSTART" property
value not synchronized with the recurrence rule is undefined.
The final recurrence set is generated by gathering all of the start DATE-TIME
values generated by any of the specified "RRULE" and "RDATE" properties, and then
excluding any start DATE-TIME values specified by "EXDATE" properties.
This implies that start DATE- TIME values specified by "EXDATE" properties take
precedence over those specified by inclusion properties (i.e., "RDATE" and "RRULE").
Where duplicate instances are generated by the "RRULE" and "RDATE" properties,
only one recurrence is considered. Duplicate instances are ignored.
The "DTSTART" property specified within the iCalendar object defines the first
instance of the recurrence. In most cases, a "DTSTART" property of DATE-TIME value
type used with a recurrence rule, should be specified as a date with local time
and time zone reference to make sure all the recurrence instances start at the
same local time regardless of time zone changes.
If the duration of the recurring component is specified with the "DTEND" or
"DUE" property, then the same exact duration will apply to all the members of the
generated recurrence set. Else, if the duration of the recurring component is
specified with the "DURATION" property, then the same nominal duration will apply
to all the members of the generated recurrence set and the exact duration of each
recurrence instance will depend on its specific start time. For example, recurrence
instances of a nominal duration of one day will have an exact duration of more or less
than 24 hours on a day where a time zone shift occurs. The duration of a specific
recurrence may be modified in an exception component or simply by using an
"RDATE" property of PERIOD value type.
Examples:
The following RRULE specifies daily events for 10 occurrences.
.. code-block:: text
RRULE:FREQ=DAILY;COUNT=10
Below, we parse the RRULE ical string.
.. code-block:: pycon
>>> from icalendar.prop import vRecur
>>> rrule = vRecur.from_ical('FREQ=DAILY;COUNT=10')
>>> rrule
vRecur({'FREQ': ['DAILY'], 'COUNT': [10]})
You can choose to add an rrule to an :class:`icalendar.cal.Event` or
:class:`icalendar.cal.Todo`.
.. code-block:: pycon
>>> from icalendar import Event
>>> event = Event()
>>> event.add('RRULE', 'FREQ=DAILY;COUNT=10')
>>> event.rrules
[vRecur({'FREQ': ['DAILY'], 'COUNT': [10]})]
"""
default_value: ClassVar[str] = "RECUR"
params: Parameters
frequencies = [
"SECONDLY",
"MINUTELY",
"HOURLY",
"DAILY",
"WEEKLY",
"MONTHLY",
"YEARLY",
]
# Mac iCal ignores RRULEs where FREQ is not the first rule part.
# Sorts parts according to the order listed in RFC 5545, section 3.3.10.
canonical_order = (
"RSCALE",
"FREQ",
"UNTIL",
"COUNT",
"INTERVAL",
"BYSECOND",
"BYMINUTE",
"BYHOUR",
"BYDAY",
"BYWEEKDAY",
"BYMONTHDAY",
"BYYEARDAY",
"BYWEEKNO",
"BYMONTH",
"BYSETPOS",
"WKST",
"SKIP",
)
types = CaselessDict(
{
"COUNT": vInt,
"INTERVAL": vInt,
"BYSECOND": vInt,
"BYMINUTE": vInt,
"BYHOUR": vInt,
"BYWEEKNO": vInt,
"BYMONTHDAY": vInt,
"BYYEARDAY": vInt,
"BYMONTH": vMonth,
"UNTIL": vDDDTypes,
"BYSETPOS": vInt,
"WKST": vWeekday,
"BYDAY": vWeekday,
"FREQ": vFrequency,
"BYWEEKDAY": vWeekday,
"SKIP": vSkip, # RFC 7529
"RSCALE": vText, # RFC 7529
}
)
# for reproducible serialization:
# RULE: if and only if it can be a list it will be a list
# look up in RFC
jcal_not_a_list = {"FREQ", "UNTIL", "COUNT", "INTERVAL", "WKST", "SKIP", "RSCALE"}
def __init__(self, *args, params: dict[str, Any] | None = None, **kwargs):
if args and isinstance(args[0], str):
# we have a string as an argument.
args = (self.from_ical(args[0]),) + args[1:]
for k, v in kwargs.items():
if not isinstance(v, SEQUENCE_TYPES):
kwargs[k] = [v]
super().__init__(*args, **kwargs)
self.params = Parameters(params)
[docs]
def to_ical(self):
result = []
for key, vals in self.sorted_items():
typ = self.types.get(key, vText)
if not isinstance(vals, SEQUENCE_TYPES):
vals = [vals]
param_vals = b",".join(typ(val).to_ical() for val in vals)
# CaselessDict keys are always unicode
param_key = key.encode(DEFAULT_ENCODING)
result.append(param_key + b"=" + param_vals)
return b";".join(result)
[docs]
@classmethod
def parse_type(cls, key, values):
# integers
parser = cls.types.get(key, vText)
return [parser.from_ical(v) for v in values.split(",")]
[docs]
@classmethod
def from_ical(cls, ical: str):
if isinstance(ical, cls):
return ical
try:
recur = cls()
for pairs in ical.split(";"):
try:
key, vals = pairs.split("=")
except ValueError:
# E.g. incorrect trailing semicolon, like (issue #157):
# FREQ=YEARLY;BYMONTH=11;BYDAY=1SU;
continue
recur[key] = cls.parse_type(key, vals)
return cls(recur)
except ValueError:
raise
except Exception as e:
raise ValueError(f"Error in recurrence rule: {ical}") from e
[docs]
@classmethod
def examples(cls) -> list[Self]:
"""Examples of vRecur."""
return [cls.from_ical("FREQ=DAILY;COUNT=10")]
from icalendar.param import VALUE
[docs]
def to_jcal(self, name: str) -> list:
"""The jCal representation of this property according to :rfc:`7265`."""
recur = {}
for k, v in self.items():
key = k.lower()
if key.upper() in self.jcal_not_a_list:
value = v[0] if isinstance(v, list) and len(v) == 1 else v
elif not isinstance(v, list):
value = [v]
else:
value = v
recur[key] = value
if "until" in recur:
until = recur["until"]
until_jcal = vDDDTypes(until).to_jcal("until")
recur["until"] = until_jcal[-1]
return [name, self.params.to_jcal(), self.VALUE.lower(), recur]
[docs]
@classmethod
def from_jcal(cls, jcal_property: list) -> Self:
"""Parse jCal from :rfc:`7265`.
Parameters:
jcal_property: The jCal property to parse.
Raises:
~error.JCalParsingError: If the provided jCal is invalid.
"""
JCalParsingError.validate_property(jcal_property, cls)
params = Parameters.from_jcal_property(jcal_property)
if not isinstance(jcal_property[3], dict) or not all(
isinstance(k, str) for k in jcal_property[3]
):
raise JCalParsingError(
"The recurrence rule must be a mapping with string keys.",
cls,
3,
value=jcal_property[3],
)
recur = {}
for key, value in jcal_property[3].items():
value_type = cls.types.get(key, vText)
with JCalParsingError.reraise_with_path_added(3, key):
if isinstance(value, list):
recur[key.lower()] = values = []
for i, v in enumerate(value):
with JCalParsingError.reraise_with_path_added(i):
values.append(value_type.parse_jcal_value(v))
else:
recur[key] = value_type.parse_jcal_value(value)
until = recur.get("until")
if until is not None and not isinstance(until, list):
recur["until"] = [until]
return cls(recur, params=params)
def __eq__(self, other: object) -> bool:
"""self == other"""
if not isinstance(other, vRecur):
return super().__eq__(other)
if self.keys() != other.keys():
return False
for key in self.keys():
v1 = self[key]
v2 = other[key]
if not isinstance(v1, SEQUENCE_TYPES):
v1 = [v1]
if not isinstance(v2, SEQUENCE_TYPES):
v2 = [v2]
if v1 != v2:
return False
return True
__hash__ = None
__all__ = ["vRecur"]