"""Attributes of Components and properties."""
from __future__ import annotations
import itertools
from datetime import date, datetime, timedelta
from typing import TYPE_CHECKING, Literal, Optional, Sequence, Union
from icalendar.enums import BUSYTYPE, CLASS, STATUS, TRANSP, StrEnum
from icalendar.error import IncompleteComponent, InvalidCalendar
from icalendar.parser_tools import SEQUENCE_TYPES
from icalendar.prop import (
vCalAddress,
vCategory,
vDDDTypes,
vDuration,
vRecur,
vText,
)
from icalendar.prop.conference import Conference
from icalendar.prop.image import Image
from icalendar.timezone import tzp
from icalendar.tools import is_date
if TYPE_CHECKING:
from icalendar.cal import Component
def _get_rdates(
self: Component,
) -> list[Union[tuple[date, None], tuple[datetime, None], tuple[datetime, datetime]]]:
"""The RDATE property defines the list of DATE-TIME values for recurring components.
RDATE is defined in :rfc:`5545`.
The return value is a list of tuples ``(start, end)``.
``start`` can be a :class:`datetime.date` or a :class:`datetime.datetime`,
with and without timezone.
``end`` is :obj:`None` if the end is not specified and a :class:`datetime.datetime`
if the end is specified.
Value Type:
The default value type for this property is DATE-TIME.
The value type can be set to DATE or PERIOD.
Property Parameters:
IANA, non-standard, value data type, and time
zone identifier 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.
Description:
This property can appear along with the "RRULE"
property to define an aggregate set of repeating occurrences.
When they both appear in a recurring component, the recurrence
instances are defined by the union of occurrences defined by both
the "RDATE" and "RRULE".
The recurrence dates, if specified, are 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 match the pattern of the recurrence rule, if
specified. The recurrence set generated with a "DTSTART" property
value that doesn't match the pattern of the 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.
Example:
Below, we set one RDATE in a list and get the resulting tuple of start and end.
.. code-block:: pycon
>>> from icalendar import Event
>>> from datetime import datetime
>>> event = Event()
# Add a list of recurrence dates
>>> event.add("RDATE", [datetime(2025, 4, 28, 16, 5)])
>>> event.rdates
[(datetime.datetime(2025, 4, 28, 16, 5), None)]
.. note::
You cannot modify the RDATE value by modifying the result.
Use :func:`icalendar.cal.Component.add` to add values.
If you want to compute recurrences, have a look at :ref:`Related projects`.
"""
result = []
rdates = self.get("RDATE", [])
for rdates in (rdates,) if not isinstance(rdates, list) else rdates:
for dts in rdates.dts:
rdate = dts.dt
if isinstance(rdate, tuple):
# we have a period as rdate
if isinstance(rdate[1], timedelta):
result.append((rdate[0], rdate[0] + rdate[1]))
else:
result.append(rdate)
else:
# we have a date/datetime
result.append((rdate, None))
return result
rdates_property = property(_get_rdates)
def _get_exdates(self: Component) -> list[date | datetime]:
"""EXDATE defines the list of DATE-TIME exceptions for recurring components.
EXDATE is defined in :rfc:`5545`.
Value Type:
The default value type for this property is DATE-TIME.
The value type can be set to DATE.
Property Parameters:
IANA, non-standard, value data type, and time
zone identifier 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.
Description:
The exception dates, if specified, are 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 match the pattern of the recurrence rule, if
specified. The recurrence set generated with a "DTSTART" property
value that doesn't match the pattern of the 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"). When duplicate instances are generated by the "RRULE"
and "RDATE" properties, only one recurrence is considered.
Duplicate instances are ignored.
The "EXDATE" property can be used to exclude the value specified
in "DTSTART". However, in such cases, the original "DTSTART" date
MUST still be maintained by the calendaring and scheduling system
because the original "DTSTART" value has inherent usage
dependencies by other properties such as the "RECURRENCE-ID".
Example:
Below, we add an exdate in a list and get the resulting list of exdates.
.. code-block:: pycon
>>> from icalendar import Event
>>> from datetime import datetime
>>> event = Event()
# Add a list of excluded dates
>>> event.add("EXDATE", [datetime(2025, 4, 28, 16, 5)])
>>> event.exdates
[datetime.datetime(2025, 4, 28, 16, 5)]
.. note::
You cannot modify the EXDATE value by modifying the result.
Use :func:`icalendar.cal.Component.add` to add values.
If you want to compute recurrences, have a look at :ref:`Related projects`.
"""
result = []
exdates = self.get("EXDATE", [])
for exdates in (exdates,) if not isinstance(exdates, list) else exdates:
for dts in exdates.dts:
exdate = dts.dt
# we have a date/datetime
result.append(exdate)
return result
exdates_property = property(_get_exdates)
def _get_rrules(self: Component) -> list[vRecur]:
"""RRULE defines a rule or repeating pattern for recurring components.
RRULE is defined in :rfc:`5545`.
:rfc:`7529` adds the ``SKIP`` parameter :class:`icalendar.prop.vSkip`.
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:
Daily for 10 occurrences:
.. code-block:: pycon
>>> from icalendar import Event
>>> from datetime import datetime
>>> from zoneinfo import ZoneInfo
>>> event = Event()
>>> event.start = datetime(1997, 9, 2, 9, 0, tzinfo=ZoneInfo("America/New_York"))
>>> event.add("RRULE", "FREQ=DAILY;COUNT=10")
>>> print(event.to_ical())
BEGIN:VEVENT
DTSTART;TZID=America/New_York:19970902T090000
RRULE:FREQ=DAILY;COUNT=10
END:VEVENT
>>> event.rrules
[vRecur({'FREQ': ['DAILY'], 'COUNT': [10]})]
Daily until December 24, 1997:
.. code-block:: pycon
>>> from icalendar import Event, vRecur
>>> from datetime import datetime
>>> from zoneinfo import ZoneInfo
>>> event = Event()
>>> event.start = datetime(1997, 9, 2, 9, 0, tzinfo=ZoneInfo("America/New_York"))
>>> event.add("RRULE", vRecur({"FREQ": ["DAILY"]}, until=datetime(1997, 12, 24, tzinfo=ZoneInfo("UTC"))))
>>> print(event.to_ical())
BEGIN:VEVENT
DTSTART;TZID=America/New_York:19970902T090000
RRULE:FREQ=DAILY;UNTIL=19971224T000000Z
END:VEVENT
>>> event.rrules
[vRecur({'FREQ': ['DAILY'], 'UNTIL': [datetime.datetime(1997, 12, 24, 0, 0, tzinfo=ZoneInfo(key='UTC'))]})]
.. note::
You cannot modify the RRULE value by modifying the result.
Use :func:`icalendar.cal.Component.add` to add values.
If you want to compute recurrences, have a look at :ref:`Related projects`.
""" # noqa: E501
rrules = self.get("RRULE", [])
if not isinstance(rrules, list):
return [rrules]
return rrules
rrules_property = property(_get_rrules)
[docs]
def multi_language_text_property(
main_prop: str, compatibility_prop: Optional[str], doc: str
) -> property:
"""This creates a text property.
This property can be defined several times with different ``LANGUAGE`` parameters.
Args:
main_prop (str): The property to set and get, such as ``NAME``
compatibility_prop (str): An old property used before, such as ``X-WR-CALNAME``
doc (str): The documentation string
"""
def fget(self: Component) -> Optional[str]:
"""Get the property"""
result = self.get(main_prop)
if result is None and compatibility_prop is not None:
result = self.get(compatibility_prop)
if isinstance(result, list):
for item in result:
if "LANGUAGE" not in item.params:
return item
return result
def fset(self: Component, value: Optional[str]):
"""Set the property."""
fdel(self)
if value is not None:
self.add(main_prop, value)
def fdel(self: Component):
"""Delete the property."""
self.pop(main_prop, None)
if compatibility_prop is not None:
self.pop(compatibility_prop, None)
return property(fget, fset, fdel, doc)
[docs]
def single_int_property(prop: str, default: int, doc: str) -> property:
"""Create a property for an int value that exists only once.
Args:
prop: The name of the property
default: The default value
doc: The documentation string
"""
def fget(self: Component) -> int:
"""Get the property"""
try:
return int(self.get(prop, default))
except ValueError as e:
raise InvalidCalendar(f"{prop} must be an int") from e
def fset(self: Component, value: Optional[int]):
"""Set the property."""
fdel(self)
if value is not None:
self.add(prop, value)
def fdel(self: Component):
"""Delete the property."""
self.pop(prop, None)
return property(fget, fset, fdel, doc)
[docs]
def single_utc_property(name: str, docs: str) -> property:
"""Create a property to access a value of datetime in UTC timezone.
Args:
name: name of the property
docs: documentation string
"""
docs = (
f"""The {name} property. datetime in UTC
All values will be converted to a datetime in UTC.
"""
+ docs
)
def fget(self: Component) -> Optional[datetime]:
"""Get the value."""
if name not in self:
return None
dt = self.get(name)
if isinstance(dt, vText):
# we might be in an attribute that is not typed
value = vDDDTypes.from_ical(dt)
else:
value = getattr(dt, "dt", dt)
if value is None or not isinstance(value, date):
raise InvalidCalendar(f"{name} must be a datetime in UTC, not {value}")
return tzp.localize_utc(value)
def fset(self: Component, value: Optional[datetime]):
"""Set the value"""
if value is None:
fdel(self)
return
if not isinstance(value, date):
raise TypeError(f"{name} takes a datetime in UTC, not {value}")
fdel(self)
self.add(name, tzp.localize_utc(value))
def fdel(self: Component):
"""Delete the property."""
self.pop(name, None)
return property(fget, fset, fdel, doc=docs)
def single_string_property(
name: str, docs: str, other_name: Optional[str] = None, default: str = ""
) -> property:
"""Create a property to access a single string value."""
def fget(self: Component) -> str:
"""Get the value."""
result = self.get(
name, None if other_name is None else self.get(other_name, None)
)
if result is None or result == []:
return default
if isinstance(result, list):
return result[0]
return result
def fset(self: Component, value: Optional[str]):
"""Set the value.
Setting the value to None will delete it.
"""
fdel(self)
if value is not None:
self.add(name, value)
def fdel(self: Component):
"""Delete the property."""
self.pop(name, None)
if other_name is not None:
self.pop(other_name, None)
return property(fget, fset, fdel, doc=docs)
color_property = single_string_property(
"COLOR",
"""This property specifies a color used for displaying the component.
This implements :rfc:`7986` ``COLOR`` property.
Property Parameters:
IANA and non-standard property parameters can
be specified on this property.
Conformance:
This property can be specified once in an iCalendar
object or in ``VEVENT``, ``VTODO``, or ``VJOURNAL`` calendar components.
Description:
This property specifies a color that clients MAY use
when presenting the relevant data to a user. Typically, this
would appear as the "background" color of events or tasks. The
value is a case-insensitive color name taken from the CSS3 set of
names, defined in Section 4.3 of `W3C.REC-css3-color-20110607 <https://www.w3.org/TR/css-color-3/>`_.
Example:
``"turquoise"``, ``"#ffffff"``
.. code-block:: pycon
>>> from icalendar import Todo
>>> todo = Todo()
>>> todo.color = "green"
>>> print(todo.to_ical())
BEGIN:VTODO
COLOR:green
END:VTODO
""",
)
sequence_property = single_int_property(
"SEQUENCE",
0,
"""This property defines the revision sequence number of the calendar component within a sequence of revisions.
Value Type:
INTEGER
Property Parameters:
IANA and non-standard property parameters can be specified on this property.
Conformance:
The property can be specified in "VEVENT", "VTODO", or
"VJOURNAL" calendar component.
Description:
When a calendar component is created, its sequence
number is 0. It is monotonically incremented by the "Organizer's"
CUA each time the "Organizer" makes a significant revision to the
calendar component.
The "Organizer" includes this property in an iCalendar object that
it sends to an "Attendee" to specify the current version of the
calendar component.
The "Attendee" includes this property in an iCalendar object that
it sends to the "Organizer" to specify the version of the calendar
component to which the "Attendee" is referring.
A change to the sequence number is not the mechanism that an
"Organizer" uses to request a response from the "Attendees". The
"RSVP" parameter on the "ATTENDEE" property is used by the
"Organizer" to indicate that a response from the "Attendees" is
requested.
Recurrence instances of a recurring component MAY have different
sequence numbers.
Examples:
The following is an example of this property for a calendar
component that was just created by the "Organizer":
.. code-block:: pycon
>>> from icalendar import Event
>>> event = Event()
>>> event.sequence
0
The following is an example of this property for a calendar
component that has been revised 10 different times by the
"Organizer":
.. code-block:: pycon
>>> from icalendar import Calendar
>>> calendar = Calendar.example("issue_156_RDATE_with_PERIOD_TZID_khal")
>>> event = calendar.events[0]
>>> event.sequence
10
""", # noqa: E501
)
def _get_categories(component: Component) -> list[str]:
"""Get all the categories."""
categories: Optional[vCategory | list[vCategory]] = component.get("CATEGORIES")
if isinstance(categories, list):
_set_categories(
component,
list(itertools.chain.from_iterable(cat.cats for cat in categories)),
)
return _get_categories(component)
if categories is None:
categories = vCategory([])
component.add("CATEGORIES", categories)
return categories.cats
def _set_categories(component: Component, cats: Optional[Sequence[str]]) -> None:
"""Set the categories."""
if not cats and cats != []:
_del_categories(component)
return
component["CATEGORIES"] = categories = vCategory(cats)
if isinstance(cats, list):
cats.clear()
cats.extend(categories.cats)
categories.cats = cats
def _del_categories(component: Component) -> None:
"""Delete the categories."""
component.pop("CATEGORIES", None)
categories_property = property(
_get_categories,
_set_categories,
_del_categories,
"""This property defines the categories for a component.
Property Parameters:
IANA, non-standard, and language property parameters can be specified on this
property.
Conformance:
The property can be specified within "VEVENT", "VTODO", or "VJOURNAL" calendar
components.
Since :rfc:`7986` it can also be defined on a "VCALENDAR" component.
Description:
This property is used to specify categories or subtypes
of the calendar component. The categories are useful in searching
for a calendar component of a particular type and category.
Within the "VEVENT", "VTODO", or "VJOURNAL" calendar components,
more than one category can be specified as a COMMA-separated list
of categories.
Example:
Below, we add the categories to an event:
.. code-block:: pycon
>>> from icalendar import Event
>>> event = Event()
>>> event.categories = ["Work", "Meeting"]
>>> print(event.to_ical())
BEGIN:VEVENT
CATEGORIES:Work,Meeting
END:VEVENT
>>> event.categories.append("Lecture")
>>> event.categories == ["Work", "Meeting", "Lecture"]
True
.. note::
At present, we do not take the LANGUAGE parameter into account.
""",
)
def _get_attendees(self: Component) -> list[vCalAddress]:
"""Get attendees."""
value = self.get("ATTENDEE")
if value is None:
value = []
self["ATTENDEE"] = value
return value
if isinstance(value, vCalAddress):
return [value]
return value
def _set_attendees(self: Component, value: list[vCalAddress] | vCalAddress | None):
"""Set attendees."""
_del_attendees(self)
if value is None:
return
if not isinstance(value, list):
value = [value]
self["ATTENDEE"] = value
def _del_attendees(self: Component):
"""Delete all attendees."""
self.pop("ATTENDEE", None)
attendees_property = property(
_get_attendees,
_set_attendees,
_del_attendees,
"""ATTENDEE defines one or more "Attendees" within a calendar component.
Conformance:
This property MUST be specified in an iCalendar object
that specifies a group-scheduled calendar entity. This property
MUST NOT be specified in an iCalendar object when publishing the
calendar information (e.g., NOT in an iCalendar object that
specifies the publication of a calendar user's busy time, event,
to-do, or journal). This property is not specified in an
iCalendar object that specifies only a time zone definition or
that defines calendar components that are not group-scheduled
components, but are components only on a single user's calendar.
Description:
This property MUST only be specified within calendar
components to specify participants, non-participants, and the
chair of a group-scheduled calendar entity. The property is
specified within an "EMAIL" category of the "VALARM" calendar
component to specify an email address that is to receive the email
type of iCalendar alarm.
Examples:
Add a new attendee to an existing event.
.. code-block:: pycon
>>> from icalendar import Event, vCalAddress
>>> event = Event()
>>> event.attendees.append(vCalAddress("mailto:me@my-domain.com"))
>>> print(event.to_ical())
BEGIN:VEVENT
ATTENDEE:mailto:me@my-domain.com
END:VEVENT
Create an email alarm with several attendees:
>>> from icalendar import Alarm, vCalAddress
>>> alarm = Alarm.new(attendees = [
... vCalAddress("mailto:me@my-domain.com"),
... vCalAddress("mailto:you@my-domain.com"),
... ], summary = "Email alarm")
>>> print(alarm.to_ical())
BEGIN:VALARM
ATTENDEE:mailto:me@my-domain.com
ATTENDEE:mailto:you@my-domain.com
SUMMARY:Email alarm
END:VALARM
""",
)
uid_property = single_string_property(
"UID",
"""UID specifies the persistent, globally unique identifier for a component.
We recommend using :func:`uuid.uuid4` to generate new values.
Returns:
The value of the UID property as a string or ``""`` if no value is set.
Description:
The "UID" itself MUST be a globally unique identifier.
The generator of the identifier MUST guarantee that the identifier
is unique.
This is the method for correlating scheduling messages with the
referenced "VEVENT", "VTODO", or "VJOURNAL" calendar component.
The full range of calendar components specified by a recurrence
set is referenced by referring to just the "UID" property value
corresponding to the calendar component. The "RECURRENCE-ID"
property allows the reference to an individual instance within the
recurrence set.
This property is an important method for group-scheduling
applications to match requests with later replies, modifications,
or deletion requests. Calendaring and scheduling applications
MUST generate this property in "VEVENT", "VTODO", and "VJOURNAL"
calendar components to assure interoperability with other group-
scheduling applications. This identifier is created by the
calendar system that generates an iCalendar object.
Implementations MUST be able to receive and persist values of at
least 255 octets for this property, but they MUST NOT truncate
values in the middle of a UTF-8 multi-octet sequence.
:rfc:`7986` states that UID can be used, for
example, to identify duplicate calendar streams that a client may
have been given access to. It can be used in conjunction with the
"LAST-MODIFIED" property also specified on the "VCALENDAR" object
to identify the most recent version of a calendar.
Conformance:
:rfc:`5545` states that the "UID" property can be specified on "VEVENT", "VTODO",
and "VJOURNAL" calendar components.
:rfc:`7986` modifies the definition of the "UID" property to
allow it to be defined in an iCalendar object.
:rfc:`9074` adds a "UID" property to "VALARM" components to allow a unique
identifier to be specified. The value of this property can then be used
to refer uniquely to the "VALARM" component.
This property can be specified once only.
Security:
:rfc:`7986` states that UID values MUST NOT include any data that
might identify a user, host, domain, or any other security- or
privacy-sensitive information. It is RECOMMENDED that calendar user
agents now generate "UID" values that are hex-encoded random
Universally Unique Identifier (UUID) values as defined in
Sections 4.4 and 4.5 of :rfc:`4122`.
You can use the :mod:`uuid` module to generate new UUIDs.
Compatibility:
For Alarms, ``X-ALARMUID`` is also considered.
Examples:
The following is an example of such a property value:
``5FC53010-1267-4F8E-BC28-1D7AE55A7C99``.
Set the UID of a calendar:
.. code-block:: pycon
>>> from icalendar import Calendar
>>> from uuid import uuid4
>>> calendar = Calendar()
>>> calendar.uid = uuid4()
>>> print(calendar.to_ical())
BEGIN:VCALENDAR
UID:d755cef5-2311-46ed-a0e1-6733c9e15c63
END:VCALENDAR
""",
)
summary_property = multi_language_text_property(
"SUMMARY",
None,
"""SUMMARY defines a short summary or subject for the calendar component.
Property Parameters:
IANA, non-standard, alternate text
representation, and language property parameters can be specified
on this property.
Conformance:
The property can be specified in "VEVENT", "VTODO",
"VJOURNAL", or "VALARM" calendar components.
Description:
This property is used in the "VEVENT", "VTODO", and
"VJOURNAL" calendar components to capture a short, one-line
summary about the activity or journal entry.
This property is used in the "VALARM" calendar component to
capture the subject of an EMAIL category of alarm.
Examples:
The following is an example of this property:
.. code-block:: pycon
SUMMARY:Department Party
""",
)
description_property = multi_language_text_property(
"DESCRIPTION",
None,
"""DESCRIPTION provides a more complete description of the calendar component than that provided by the "SUMMARY" property.
Property Parameters:
IANA, non-standard, alternate text
representation, and language property parameters can be specified
on this property.
Conformance:
The property can be specified in the "VEVENT", "VTODO",
"VJOURNAL", or "VALARM" calendar components. The property can be
specified multiple times only within a "VJOURNAL" calendar
component.
Description:
This property is used in the "VEVENT" and "VTODO" to
capture lengthy textual descriptions associated with the activity.
This property is used in the "VALARM" calendar component to
capture the display text for a DISPLAY category of alarm, and to
capture the body text for an EMAIL category of alarm.
Examples:
The following is an example of this property with formatted
line breaks in the property value:
.. code-block:: pycon
DESCRIPTION:Meeting to provide technical review for "Phoenix"
design.\\nHappy Face Conference Room. Phoenix design team
MUST attend this meeting.\\nRSVP to team leader.
""", # noqa: E501
)
[docs]
def create_single_property(
prop: str,
value_attr: Optional[str],
value_type: tuple[type],
type_def: type,
doc: str,
vProp: type = vDDDTypes, # noqa: N803
):
"""Create a single property getter and setter.
:param prop: The name of the property.
:param value_attr: The name of the attribute to get the value from.
:param value_type: The type of the value.
:param type_def: The type of the property.
:param doc: The docstring of the property.
:param vProp: The type of the property from :mod:`icalendar.prop`.
"""
def p_get(self: Component):
default = object()
result = self.get(prop, default)
if result is default:
return None
if isinstance(result, list):
raise InvalidCalendar(f"Multiple {prop} defined.")
value = result if value_attr is None else getattr(result, value_attr, result)
if not isinstance(value, value_type):
raise InvalidCalendar(
f"{prop} must be either a "
f"{' or '.join(t.__name__ for t in value_type)},"
f" not {value}."
)
return value
def p_set(self: Component, value) -> None:
if value is None:
p_del(self)
return
if not isinstance(value, value_type):
raise TypeError(
f"Use {' or '.join(t.__name__ for t in value_type)}, "
f"not {type(value).__name__}."
)
self[prop] = vProp(value)
if prop in self.exclusive:
for other_prop in self.exclusive:
if other_prop != prop:
self.pop(other_prop, None)
p_set.__annotations__["value"] = p_get.__annotations__["return"] = Optional[
type_def
]
def p_del(self: Component):
self.pop(prop)
p_doc = f"""The {prop} property.
{doc}
Accepted values: {", ".join(t.__name__ for t in value_type)}.
If the attribute has invalid values, we raise InvalidCalendar.
If the value is absent, we return None.
You can also delete the value with del or by setting it to None.
"""
return property(p_get, p_set, p_del, p_doc)
X_MOZ_SNOOZE_TIME_property = single_utc_property(
"X-MOZ-SNOOZE-TIME", "Thunderbird: Alarms before this time are snoozed."
)
X_MOZ_LASTACK_property = single_utc_property(
"X-MOZ-LASTACK", "Thunderbird: Alarms before this time are acknowledged."
)
[docs]
def property_get_duration(self: Component) -> Optional[timedelta]:
"""Getter for property DURATION."""
default = object()
duration = self.get("duration", default)
if isinstance(duration, vDDDTypes):
return duration.dt
if isinstance(duration, vDuration):
return duration.td
if duration is not default and not isinstance(duration, timedelta):
raise InvalidCalendar(
f"DURATION must be a timedelta, not {type(duration).__name__}."
)
return None
[docs]
def property_set_duration(self: Component, value: Optional[timedelta]):
"""Setter for property DURATION."""
if value is None:
self.pop("duration", None)
return
if not isinstance(value, timedelta):
raise TypeError(f"Use timedelta, not {type(value).__name__}.")
self["duration"] = vDuration(value)
self.pop("DTEND")
self.pop("DUE")
[docs]
def property_del_duration(self: Component):
"""Delete property DURATION."""
self.pop("DURATION")
property_doc_duration_template = """The DURATION property.
The "DTSTART" property for a "{component}" specifies the inclusive
start of the {component}.
The "DURATION" property in conjunction with the DTSTART property
for a "{component}" calendar component specifies the non-inclusive end
of the event.
If you would like to calculate the duration of a {component}, do not use this.
Instead use the duration property (lower case).
"""
[docs]
def duration_property(component: str) -> property:
"""Return the duration property."""
return property(
property_get_duration,
property_set_duration,
property_del_duration,
property_doc_duration_template.format(component=component),
)
def multi_text_property(name: str, docs: str) -> property:
"""Get a property that can occur several times and is text.
Examples: Journal.descriptions, Event.comments
"""
def fget(self: Component) -> list[str]:
"""Get the values."""
descriptions = self.get(name)
if descriptions is None:
return []
if not isinstance(descriptions, SEQUENCE_TYPES):
return [descriptions]
return descriptions
def fset(self: Component, values: Optional[str | Sequence[str]]):
"""Set the values."""
fdel(self)
if values is None:
return
if isinstance(values, str):
self.add(name, values)
else:
for description in values:
self.add(name, description)
def fdel(self: Component):
"""Delete the values."""
self.pop(name)
return property(fget, fset, fdel, docs)
descriptions_property = multi_text_property(
"DESCRIPTION",
"""DESCRIPTION provides a more complete description of the calendar component than that provided by the "SUMMARY" property.
Property Parameters:
IANA, non-standard, alternate text
representation, and language property parameters can be specified
on this property.
Conformance:
The property can be
specified multiple times only within a "VJOURNAL" calendar component.
Description:
This property is used in the "VJOURNAL" calendar component to
capture one or more textual journal entries.
Examples:
The following is an example of this property with formatted
line breaks in the property value:
.. code-block:: pycon
DESCRIPTION:Meeting to provide technical review for "Phoenix"
design.\\nHappy Face Conference Room. Phoenix design team
MUST attend this meeting.\\nRSVP to team leader.
""", # noqa: E501
)
comments_property = multi_text_property(
"COMMENT",
"""COMMENT is used to specify a comment to the calendar user.
Purpose:
This property specifies non-processing information intended
to provide a comment to the calendar user.
Conformance:
In :rfc:`5545`, this property can be specified multiple times in
"VEVENT", "VTODO", "VJOURNAL", and "VFREEBUSY" calendar components
as well as in the "STANDARD" and "DAYLIGHT" sub-components.
In :rfc:`7953`, this property can be specified multiple times in
"VAVAILABILITY" and "VAVAILABLE".
Property Parameters:
IANA, non-standard, alternate text
representation, and language property parameters can be specified
on this property.
""",
)
def _get_organizer(self: Component) -> Optional[vCalAddress]:
"""ORGANIZER defines the organizer for a calendar component.
Property Parameters:
IANA, non-standard, language, common name,
directory entry reference, and sent-by property parameters can be
specified on this property.
Conformance:
This property MUST be specified in an iCalendar object
that specifies a group-scheduled calendar entity. This property
MUST be specified in an iCalendar object that specifies the
publication of a calendar user's busy time. This property MUST
NOT be specified in an iCalendar object that specifies only a time
zone definition or that defines calendar components that are not
group-scheduled components, but are components only on a single
user's calendar.
Description:
This property is specified within the "VEVENT",
"VTODO", and "VJOURNAL" calendar components to specify the
organizer of a group-scheduled calendar entity. The property is
specified within the "VFREEBUSY" calendar component to specify the
calendar user requesting the free or busy time. When publishing a
"VFREEBUSY" calendar component, the property is used to specify
the calendar that the published busy time came from.
The property has the property parameters "CN", for specifying the
common or display name associated with the "Organizer", "DIR", for
specifying a pointer to the directory information associated with
the "Organizer", "SENT-BY", for specifying another calendar user
that is acting on behalf of the "Organizer". The non-standard
parameters may also be specified on this property. If the
"LANGUAGE" property parameter is specified, the identified
language applies to the "CN" parameter value.
"""
return self.get("ORGANIZER")
def _set_organizer(self: Component, value: Optional[vCalAddress | str]):
"""Set the value."""
_del_organizer(self)
if value is not None:
self.add("ORGANIZER", value)
def _del_organizer(self: Component):
"""Delete the value."""
self.pop("ORGANIZER")
organizer_property = property(_get_organizer, _set_organizer, _del_organizer)
def single_string_enum_property(
name: str, enum: type[StrEnum], default: StrEnum, docs: str
) -> property:
"""Create a property to access a single string value and convert it to an enum."""
prop = single_string_property(name, docs, default=default)
def fget(self: Component) -> StrEnum:
"""Get the value."""
value = prop.fget(self)
if value == default:
return default
return enum(str(value))
def fset(self: Component, value: str | StrEnum | None) -> None:
"""Set the value."""
if value == "":
value = None
prop.fset(self, value)
return property(fget, fset, prop.fdel, doc=docs)
busy_type_property = single_string_enum_property(
"BUSYTYPE",
BUSYTYPE,
BUSYTYPE.BUSY_UNAVAILABLE,
"""BUSYTYPE specifies the default busy time type.
Returns:
:class:`icalendar.enums.BUSYTYPE`
Description:
This property is used to specify the default busy time
type. The values correspond to those used by the FBTYPE"
parameter used on a "FREEBUSY" property, with the exception that
the "FREE" value is not used in this property. If not specified
on a component that allows this property, the default is "BUSY-
UNAVAILABLE".
""",
)
priority_property = single_int_property(
"PRIORITY",
0,
"""
Conformance:
This property can be specified in "VEVENT" and "VTODO" calendar components
according to :rfc:`5545`.
:rfc:`7953` adds this property to "VAVAILABILITY".
Description:
This priority is specified as an integer in the range 0
to 9. A value of 0 specifies an undefined priority. A value of 1
is the highest priority. A value of 2 is the second highest
priority. Subsequent numbers specify a decreasing ordinal
priority. A value of 9 is the lowest priority.
A CUA with a three-level priority scheme of "HIGH", "MEDIUM", and
"LOW" is mapped into this property such that a property value in
the range of 1 to 4 specifies "HIGH" priority. A value of 5 is
the normal or "MEDIUM" priority. A value in the range of 6 to 9
is "LOW" priority.
A CUA with a priority schema of "A1", "A2", "A3", "B1", "B2", ...,
"C3" is mapped into this property such that a property value of 1
specifies "A1", a property value of 2 specifies "A2", a property
value of 3 specifies "A3", and so forth up to a property value of
9 specifies "C3".
Other integer values are reserved for future use.
Within a "VEVENT" calendar component, this property specifies a
priority for the event. This property may be useful when more
than one event is scheduled for a given time period.
Within a "VTODO" calendar component, this property specified a
priority for the to-do. This property is useful in prioritizing
multiple action items for a given time period.
""",
)
class_property = single_string_enum_property(
"CLASS",
CLASS,
CLASS.PUBLIC,
"""CLASS specifies the class of the calendar component.
Returns:
:class:`icalendar.enums.CLASS`
Description:
An access classification is only one component of the
general security system within a calendar application. It
provides a method of capturing the scope of the access the
calendar owner intends for information within an individual
calendar entry. The access classification of an individual
iCalendar component is useful when measured along with the other
security components of a calendar system (e.g., calendar user
authentication, authorization, access rights, access role, etc.).
Hence, the semantics of the individual access classifications
cannot be completely defined by this memo alone. Additionally,
due to the "blind" nature of most exchange processes using this
memo, these access classifications cannot serve as an enforcement
statement for a system receiving an iCalendar object. Rather,
they provide a method for capturing the intention of the calendar
owner for the access to the calendar component. If not specified
in a component that allows this property, the default value is
PUBLIC. Applications MUST treat x-name and iana-token values they
don't recognize the same way as they would the PRIVATE value.
""",
)
transparency_property = single_string_enum_property(
"TRANSP",
TRANSP,
TRANSP.OPAQUE,
"""TRANSP defines whether or not an event is transparent to busy time searches.
Returns:
:class:`icalendar.enums.TRANSP`
Description:
Time Transparency is the characteristic of an event
that determines whether it appears to consume time on a calendar.
Events that consume actual time for the individual or resource
associated with the calendar SHOULD be recorded as OPAQUE,
allowing them to be detected by free/busy time searches. Other
events, which do not take up the individual's (or resource's) time
SHOULD be recorded as TRANSPARENT, making them invisible to free/
busy time searches.
""",
)
status_property = single_string_enum_property(
"STATUS",
STATUS,
"",
"""STATUS defines the overall status or confirmation for the calendar component.
Returns:
:class:`icalendar.enums.STATUS`
The default value is ``""``.
Description:
In a group-scheduled calendar component, the property
is used by the "Organizer" to provide a confirmation of the event
to the "Attendees". For example in a "VEVENT" calendar component,
the "Organizer" can indicate that a meeting is tentative,
confirmed, or cancelled. In a "VTODO" calendar component, the
"Organizer" can indicate that an action item needs action, is
completed, is in process or being worked on, or has been
cancelled. In a "VJOURNAL" calendar component, the "Organizer"
can indicate that a journal entry is draft, final, or has been
cancelled or removed.
""",
)
url_property = single_string_property(
"URL",
"""A Uniform Resource Locator (URL) associated with the iCalendar object.
Description:
This property may be used in a calendar component to
convey a location where a more dynamic rendition of the calendar
information associated with the calendar component can be found.
This memo does not attempt to standardize the form of the URI, nor
the format of the resource pointed to by the property value. If
the URL property and Content-Location MIME header are both
specified, they MUST point to the same resource.
Conformance:
This property can be specified once in the "VEVENT",
"VTODO", "VJOURNAL", or "VFREEBUSY" calendar components.
Since :rfc:`7986`, this property can also be defined on a "VCALENDAR".
Example:
The following is an example of this property:
.. code-block:: text
URL:http://example.com/pub/calendars/jsmith/mytime.ics
""",
)
source_property = single_string_property(
"SOURCE",
"""A URI from where calendar data can be refreshed.
Description:
This property identifies a location where a client can
retrieve updated data for the calendar. Clients SHOULD honor any
specified "REFRESH-INTERVAL" value when periodically retrieving
data. Note that this property differs from the "URL" property in
that "URL" is meant to provide an alternative representation of
the calendar data rather than the original location of the data.
Conformance:
This property can be specified once in an iCalendar object.
Example:
The following is an example of this property:
.. code-block:: text
SOURCE;VALUE=URI:https://example.com/holidays.ics
""",
)
location_property = multi_language_text_property(
"LOCATION",
None,
"""The intended venue for the activity defined by a calendar component.
Property Parameters:
IANA, non-standard, alternate text
representation, and language property parameters can be specified
on this property.
Conformance:
Since :rfc:`5545`, this property can be specified in "VEVENT" or "VTODO"
calendar component.
:rfc:`7953` adds this property to "VAVAILABILITY" and "VAVAILABLE".
Description:
Specific venues such as conference or meeting rooms may
be explicitly specified using this property. An alternate
representation may be specified that is a URI that points to
directory information with more structured specification of the
location. For example, the alternate representation may specify
either an LDAP URL :rfc:`4516` pointing to an LDAP server entry or a
CID URL :rfc:`2392` pointing to a MIME body part containing a
Virtual-Information Card (vCard) :rfc:`2426` for the location.
""",
)
contacts_property = multi_text_property(
"CONTACT",
"""Contact information associated with the calendar component.
Purpose:
This property is used to represent contact information or
alternately a reference to contact information associated with the
calendar component.
Property Parameters:
IANA, non-standard, alternate text
representation, and language property parameters can be specified
on this property.
Conformance:
In :rfc:`5545`, this property can be specified in a "VEVENT", "VTODO",
"VJOURNAL", or "VFREEBUSY" calendar component.
In :rfc:`7953`, this property can be specified in a "VAVAILABILITY"
amd "VAVAILABLE" calendar component.
Description:
The property value consists of textual contact
information. An alternative representation for the property value
can also be specified that refers to a URI pointing to an
alternate form, such as a vCard :rfc:`2426`, for the contact
information.
Example:
The following is an example of this property referencing
textual contact information:
.. code-block:: text
CONTACT:Jim Dolittle\\, ABC Industries\\, +1-919-555-1234
The following is an example of this property with an alternate
representation of an LDAP URI to a directory entry containing the
contact information:
.. code-block:: text
CONTACT;ALTREP="ldap://example.com:6666/o=ABC%20Industries\\,
c=US???(cn=Jim%20Dolittle)":Jim Dolittle\\, ABC Industries\\,
+1-919-555-1234
The following is an example of this property with an alternate
representation of a MIME body part containing the contact
information, such as a vCard :rfc:`2426` embedded in a text/
directory media type :rfc:`2425`:
.. code-block:: text
CONTACT;ALTREP="CID:part3.msg970930T083000SILVER@example.com":
Jim Dolittle\\, ABC Industries\\, +1-919-555-1234
The following is an example of this property referencing a network
resource, such as a vCard :rfc:`2426` object containing the contact
information:
.. code-block:: text
CONTACT;ALTREP="http://example.com/pdi/jdoe.vcf":Jim
Dolittle\\, ABC Industries\\, +1-919-555-1234
""",
)
def timezone_datetime_property(name: str, docs: str):
"""Create a property to access the values with a proper timezone."""
return single_utc_property(name, docs)
rfc_7953_dtstart_property = timezone_datetime_property(
"DTSTART",
"""Start of the component.
This is almost the same as :attr:`Event.DTSTART` with one exception:
The values MUST have a timezone and DATE is not allowed.
Description:
:rfc:`7953`: If specified, the "DTSTART" and "DTEND" properties in
"VAVAILABILITY" components and "AVAILABLE" subcomponents MUST be
"DATE-TIME" values specified as either the date with UTC time or
the date with local time and a time zone reference.
""",
)
rfc_7953_dtend_property = timezone_datetime_property(
"DTEND",
"""Start of the component.
This is almost the same as :attr:`Event.DTEND` with one exception:
The values MUST have a timezone and DATE is not allowed.
Description:
:rfc:`7953`: If specified, the "DTSTART" and "DTEND" properties in
"VAVAILABILITY" components and "AVAILABLE" subcomponents MUST be
"DATE-TIME" values specified as either the date with UTC time or
the date with local time and a time zone reference.
""",
)
@property
def rfc_7953_duration_property(self) -> Optional[timedelta]:
"""Compute the duration of this component.
If there is no :attr:`DTEND` or :attr:`DURATION` set, this is None.
Otherwise, the duration is calculated from :attr:`DTSTART` and
:attr:`DTEND`/:attr:`DURATION`.
This is in accordance with :rfc:`7953`:
If "DTEND" or "DURATION" are not present, then the end time is unbounded.
"""
duration = self.DURATION
if duration:
return duration
end = self.DTEND
if end is None:
return None
start = self.DTSTART
if start is None:
raise IncompleteComponent("Cannot compute duration without start.")
return end - start
@property
def rfc_7953_end_property(self) -> Optional[timedelta]:
"""Compute the duration of this component.
If there is no :attr:`DTEND` or :attr:`DURATION` set, this is None.
Otherwise, the duration is calculated from :attr:`DTSTART` and
:attr:`DTEND`/:attr:`DURATION`.
This is in accordance with :rfc:`7953`:
If "DTEND" or "DURATION" are not present, then the end time is unbounded.
"""
duration = self.DURATION
if duration:
start = self.DTSTART
if start is None:
raise IncompleteComponent("Cannot compute end without start.")
return start + duration
end = self.DTEND
if end is None:
return None
return end
@rfc_7953_end_property.setter
def rfc_7953_end_property(self, value: datetime):
self.DTEND = value
@rfc_7953_end_property.deleter
def rfc_7953_end_property(self):
del self.DTEND
[docs]
def get_start_end_duration_with_validation(
component: Component,
start_property: str,
end_property: str,
component_name: str,
) -> tuple[date | datetime | None, date | datetime | None, timedelta | None]:
"""
Validate the component and return start, end, and duration.
This tests validity according to :rfc:`5545` rules
for ``Event`` and ``Todo`` components.
Args:
component: The component to validate, either ``Event`` or ``Todo``.
start_property: The start property name, ``DTSTART``.
end_property: The end property name, either ``DTEND`` for ``Event`` or
``DUE`` for ``Todo``.
component_name: The component name for error messages,
either ``VEVENT`` or ``VTODO``.
Returns:
tuple: (start, end, duration) values from the component.
Raises:
InvalidCalendar: If the component violates RFC 5545 constraints.
"""
start = getattr(component, start_property, None)
end = getattr(component, end_property, None)
duration = component.DURATION
# RFC 5545: Only one of end property and DURATION may be present
if duration is not None and end is not None:
end_name = "DTEND" if end_property == "DTEND" else "DUE"
msg = (
f"Only one of {end_name} and DURATION "
f"may be in a {component_name}, not both."
)
raise InvalidCalendar(msg)
# RFC 5545: When DTSTART is a date, DURATION must be of days or weeks
if (
start is not None
and is_date(start)
and duration is not None
and duration.seconds != 0
):
msg = "When DTSTART is a date, DURATION must be of days or weeks."
raise InvalidCalendar(msg)
# RFC 5545: DTSTART and end property must be of the same type
if start is not None and end is not None and is_date(start) != is_date(end):
end_name = "DTEND" if end_property == "DTEND" else "DUE"
msg = (
f"DTSTART and {end_name} must be of the same type, either date or datetime."
)
raise InvalidCalendar(msg)
return start, end, duration
[docs]
def get_start_property(component: Component) -> date | datetime:
"""
Get the start property with validation.
Args:
component: The component from which to get its start property.
Returns:
The ``DTSTART`` value.
Raises:
IncompleteComponent: If no ``DTSTART`` is present.
"""
# Trigger validation by calling _get_start_end_duration
start, end, duration = component._get_start_end_duration() # noqa: SLF001
if start is None:
msg = "No DTSTART given."
raise IncompleteComponent(msg)
return start
[docs]
def get_end_property(component: Component, end_property: str) -> date | datetime:
"""
Get the end property with fallback logic for ``Event`` and ``Todo`` components.
Args:
component: The component to get end from
end_property: The end property name, either ``DTEND`` for ``Event`` or
``DUE`` for ``Todo``.
Returns:
The computed end value.
Raises:
IncompleteComponent: If the provided information is incomplete
to compute the end property.
"""
# Trigger validation by calling _get_start_end_duration
start, end, duration = component._get_start_end_duration() # noqa: SLF001
if end is None and duration is None:
if start is None:
end_name = "DTEND" if end_property == "DTEND" else "DUE"
msg = f"No {end_name} or DURATION+DTSTART given."
raise IncompleteComponent(msg)
# Default behavior: date gets +1 day, datetime gets same time
if is_date(start):
return start + timedelta(days=1)
return start
if duration is not None:
if start is not None:
return start + duration
end_name = "DTEND" if end_property == "DTEND" else "DUE"
msg = f"No {end_name} or DURATION+DTSTART given."
raise IncompleteComponent(msg)
return end
[docs]
def get_duration_property(component: Component) -> timedelta:
"""
Get the duration property with fallback calculation from start and end.
Args:
component: The component from which to get its duration property.
Returns:
The duration as a timedelta.
"""
# First check if DURATION property is explicitly set
if "DURATION" in component:
return component["DURATION"].dt
# Fall back to calculated duration from start and end
return component.end - component.start
[docs]
def set_duration_with_locking(
component: Component,
duration: timedelta | None,
locked: Literal["start", "end"],
end_property: str,
) -> None:
"""
Set the duration with explicit locking behavior for ``Event`` and ``Todo``.
Args:
component: The component to modify, either ``Event`` or ``Todo``.
duration: The duration to set, or ``None`` to convert to ``DURATION`` property.
locked: Which property to keep unchanged, either ``start`` or ``end``.
end_property: The end property name, either ``DTEND`` for ``Event`` or
``DUE`` for ``Todo``.
"""
# Convert to DURATION property if duration is None
if duration is None:
if "DURATION" in component:
return # Already has DURATION property
current_duration = component.duration
component.DURATION = current_duration
return
if not isinstance(duration, timedelta):
msg = f"Use timedelta, not {type(duration).__name__}."
raise TypeError(msg)
# Validate date/duration compatibility
start = component.DTSTART
if start is not None and is_date(start) and duration.seconds != 0:
msg = "When DTSTART is a date, DURATION must be of days or weeks."
raise InvalidCalendar(msg)
if locked == "start":
# Keep start locked, adjust end
if start is None:
msg = "Cannot set duration without DTSTART. Set start time first."
raise IncompleteComponent(msg)
component.pop(end_property, None) # Remove end property
component.DURATION = duration
elif locked == "end":
# Keep end locked, adjust start
current_end = component.end
component.DTSTART = current_end - duration
component.pop(end_property, None) # Remove end property
component.DURATION = duration
else:
msg = f"locked must be 'start' or 'end', not {locked!r}"
raise ValueError(msg)
[docs]
def set_start_with_locking(
component: Component,
start: date | datetime,
locked: Literal["duration", "end"] | None,
end_property: str,
) -> None:
"""
Set the start with explicit locking behavior for ``Event`` and ``Todo`` components.
Args:
component: The component to modify, either ``Event`` or ``Todo``.
start: The start time to set.
locked: Which property to keep unchanged, either ``duration``, ``end``,
or ``None`` for auto-detect.
end_property: The end property name, either ``DTEND`` for ``Event`` or
``DUE`` for ``Todo``.
"""
if locked is None:
# Auto-detect based on existing properties
if "DURATION" in component:
locked = "duration"
elif end_property in component:
locked = "end"
else:
# Default to duration if no existing properties
locked = "duration"
if locked == "duration":
# Keep duration locked, adjust end
current_duration = (
component.duration
if "DURATION" in component or end_property in component
else None
)
component.DTSTART = start
if current_duration is not None:
component.pop(end_property, None) # Remove end property
component.DURATION = current_duration
elif locked == "end":
# Keep end locked, adjust duration
current_end = component.end
component.DTSTART = start
component.pop("DURATION", None) # Remove duration property
setattr(component, end_property, current_end)
else:
msg = f"locked must be 'duration', 'end', or None, not {locked!r}"
raise ValueError(msg)
[docs]
def set_end_with_locking(
component: Component,
end: date | datetime,
locked: Literal["start", "duration"],
end_property: str,
) -> None:
"""
Set the end with explicit locking behavior for Event and Todo components.
Args:
component: The component to modify, either ``Event`` or ``Todo``.
end: The end time to set.
locked: Which property to keep unchanged, either ``start`` or ``duration``.
end_property: The end property name, either ``DTEND`` for ``Event`` or ``DUE``
for ``Todo``.
"""
if locked == "start":
# Keep start locked, adjust duration
component.pop("DURATION", None) # Remove duration property
setattr(component, end_property, end)
elif locked == "duration":
# Keep duration locked, adjust start
current_duration = component.duration
component.DTSTART = end - current_duration
component.pop(end_property, None) # Remove end property
component.DURATION = current_duration
else:
msg = f"locked must be 'start' or 'duration', not {locked!r}"
raise ValueError(msg)
def _get_images(self: Component) -> list[Image]:
"""IMAGE specifies an image associated with the calendar or a calendar component.
Description:
This property specifies an image for an iCalendar
object or a calendar component via a URI or directly with inline
data that can be used by calendar user agents when presenting the
calendar data to a user. Multiple properties MAY be used to
specify alternative sets of images with, for example, varying
media subtypes, resolutions, or sizes. When multiple properties
are present, calendar user agents SHOULD display only one of them,
picking one that provides the most appropriate image quality, or
display none. The "DISPLAY" parameter is used to indicate the
intended display mode for the image. The "ALTREP" parameter,
defined in :rfc:`5545`, can be used to provide a "clickable" image
where the URI in the parameter value can be "launched" by a click
on the image in the calendar user agent.
Conformance:
This property can be specified multiple times in an
iCalendar object or in "VEVENT", "VTODO", or "VJOURNAL" calendar
components.
.. note::
At the present moment, this property is read-only. If you require a setter,
please open an issue or a pull request.
"""
images = self.get("IMAGE", [])
if not isinstance(images, SEQUENCE_TYPES):
images = [images]
return [Image.from_property_value(img) for img in images]
images_property = property(_get_images)
def _get_conferences(self: Component) -> list[Conference]:
"""Return the CONFERENCE properties as a list.
Purpose:
This property specifies information for accessing a conferencing system.
Conformance:
This property can be specified multiple times in a
"VEVENT" or "VTODO" calendar component.
Description:
This property specifies information for accessing a
conferencing system for attendees of a meeting or task. This
might be for a telephone-based conference number dial-in with
access codes included (such as a tel: URI :rfc:`3966` or a sip: or
sips: URI :rfc:`3261`), for a web-based video chat (such as an http:
or https: URI :rfc:`7230`), or for an instant messaging group chat
room (such as an xmpp: URI :rfc:`5122`). If a specific URI for a
conferencing system is not available, a data: URI :rfc:`2397`
containing a text description can be used.
A conference system can be a bidirectional communication channel
or a uni-directional "broadcast feed".
The "FEATURE" property parameter is used to describe the key
capabilities of the conference system to allow a client to choose
the ones that give the required level of interaction from a set of
multiple properties.
The "LABEL" property parameter is used to convey additional
details on the use of the URI. For example, the URIs or access
codes for the moderator and attendee of a teleconference system
could be different, and the "LABEL" property parameter could be
used to "tag" each "CONFERENCE" property to indicate which is
which.
The "LANGUAGE" property parameter can be used to specify the
language used for text values used with this property (as per
Section 3.2.10 of :rfc:`5545`).
Example:
The following are examples of this property:
.. code-block:: text
CONFERENCE;VALUE=URI;FEATURE=PHONE,MODERATOR;
LABEL=Moderator dial-in:tel:+1-412-555-0123,,,654321
CONFERENCE;VALUE=URI;FEATURE=PHONE;
LABEL=Attendee dial-in:tel:+1-412-555-0123,,,555123
CONFERENCE;VALUE=URI;FEATURE=PHONE;
LABEL=Attendee dial-in:tel:+1-888-555-0456,,,555123
CONFERENCE;VALUE=URI;FEATURE=CHAT;
LABEL=Chat room:xmpp:chat-123@conference.example.com
CONFERENCE;VALUE=URI;FEATURE=AUDIO,VIDEO;
LABEL=Attendee dial-in:https://chat.example.com/audio?id=123456
Get all conferences:
.. code-block:: pycon
>>> from icalendar import Event
>>> event = Event()
>>> event.conferences
[]
Set a conference:
.. code-block:: pycon
>>> from icalendar import Event, Conference
>>> event = Event()
>>> event.conferences = [
... Conference(
... "tel:+1-412-555-0123,,,654321",
... feature="PHONE,MODERATOR",
... label="Moderator dial-in",
... language="EN",
... )
... ]
>>> print(event.to_ical())
BEGIN:VEVENT
CONFERENCE;FEATURE="PHONE,MODERATOR";LABEL=Moderator dial-in;LANGUAGE=EN:t
el:+1-412-555-0123,,,654321
END:VEVENT
"""
conferences = self.get("CONFERENCE", [])
if not isinstance(conferences, SEQUENCE_TYPES):
conferences = [conferences]
return [Conference.from_uri(conference) for conference in conferences]
def _set_conferences(self: Component, conferences: list[Conference] | None):
"""Set the conferences."""
_del_conferences(self)
for conference in conferences or []:
self.add("CONFERENCE", conference.to_uri())
def _del_conferences(self: Component):
"""Delete all conferences."""
self.pop("CONFERENCE")
conferences_property = property(_get_conferences, _set_conferences, _del_conferences)
__all__ = [
"attendees_property",
"busy_type_property",
"categories_property",
"class_property",
"color_property",
"comments_property",
"conferences_property",
"contacts_property",
"create_single_property",
"description_property",
"descriptions_property",
"duration_property",
"exdates_property",
"get_duration_property",
"get_end_property",
"get_start_end_duration_with_validation",
"get_start_property",
"images_property",
"location_property",
"multi_language_text_property",
"organizer_property",
"priority_property",
"property_del_duration",
"property_doc_duration_template",
"property_get_duration",
"property_set_duration",
"rdates_property",
"rfc_7953_dtend_property",
"rfc_7953_dtstart_property",
"rfc_7953_duration_property",
"rfc_7953_end_property",
"rrules_property",
"sequence_property",
"set_duration_with_locking",
"set_end_with_locking",
"set_start_with_locking",
"single_int_property",
"single_utc_property",
"source_property",
"status_property",
"summary_property",
"transparency_property",
"uid_property",
"url_property",
]