""":rfc:`5545` VALARM component."""
from __future__ import annotations
from datetime import date, datetime, timedelta
from typing import TYPE_CHECKING, NamedTuple
from icalendar.attr import (
CONCEPTS_TYPE_SETTER,
LINKS_TYPE_SETTER,
RELATED_TO_TYPE_SETTER,
attendees_property,
create_single_property,
description_property,
property_del_duration,
property_get_duration,
property_set_duration,
single_int_property,
single_string_property,
single_utc_property,
summary_property,
uid_property,
)
from icalendar.cal.component import Component
from icalendar.cal.examples import get_example
if TYPE_CHECKING:
import uuid
from icalendar.prop import vCalAddress
[docs]
class Alarm(Component):
"""
A "VALARM" calendar component is a grouping of component
properties that defines an alarm or reminder for an event or a
to-do. For example, it may be used to define a reminder for a
pending event or an overdue to-do.
Example:
The following example creates an alarm which uses an audio file
from an FTP server.
.. code-block:: pycon
>>> from icalendar import Alarm
>>> alarm = Alarm.example()
>>> print(alarm.to_ical().decode())
BEGIN:VALARM
ACTION:AUDIO
ATTACH;FMTTYPE=audio/basic:ftp://example.com/pub/sounds/bell-01.aud
DURATION:PT15M
REPEAT:4
TRIGGER;VALUE=DATE-TIME:19970317T133000Z
END:VALARM
"""
name = "VALARM"
# some properties MAY/MUST/MUST NOT appear depending on ACTION value
required = (
"ACTION",
"TRIGGER",
)
singletons = (
"ATTACH",
"ACTION",
"DESCRIPTION",
"SUMMARY",
"TRIGGER",
"DURATION",
"REPEAT",
"UID",
"PROXIMITY",
"ACKNOWLEDGED",
)
inclusive = (
(
"DURATION",
"REPEAT",
),
(
"SUMMARY",
"ATTENDEE",
),
)
multiple = ("ATTENDEE", "ATTACH", "RELATED-TO")
REPEAT = single_int_property(
"REPEAT",
0,
"""The number of additional times the alarm is triggered after the initial trigger.
Defaults to ``0``, meaning the alarm fires once. To repeat the alarm,
set both :attr:`REPEAT` and :attr:`DURATION`. The :attr:`DURATION`
sets the gap between repetitions. ``REPEAT`` is the count of *additional*
triggers, so a ``REPEAT`` of ``2`` produces three alarms in total
(the initial trigger plus two repeats).
Conforming with :rfc:`5545#section-3.8.6.2`, this property can appear
once in an :class:`~icalendar.cal.alarm.Alarm` component and must be
paired with :attr:`DURATION`.
Example:
Build an alarm that fires once and then repeats twice at
five-minute intervals.
.. code-block:: pycon
>>> from datetime import timedelta
>>> from icalendar import Alarm
>>> alarm = Alarm()
>>> alarm.TRIGGER = timedelta(minutes=-15)
>>> alarm.DURATION = timedelta(minutes=5)
>>> alarm.REPEAT = 2
>>> alarm.REPEAT
2
""",
)
DURATION = property(
property_get_duration,
property_set_duration,
property_del_duration,
"""The delay between repeated triggers of a repeating alarm.
Returns a :class:`datetime.timedelta` or ``None`` when the alarm
has no :attr:`DURATION` set. Setting this attribute accepts a
:class:`~datetime.timedelta`; deleting it removes the property
from the component.
:attr:`DURATION` is meaningful only for repeating alarms and must
be paired with :attr:`REPEAT`. The two together produce
``REPEAT`` additional triggers, each spaced by ``DURATION`` after
the initial trigger.
Conforming with :rfc:`5545#section-3.8.2.5`, the ``DURATION`` property
can appear once in an :class:`~icalendar.cal.alarm.Alarm` component.
Example:
Pair :attr:`DURATION` with :attr:`REPEAT` to produce three
triggers spaced ten minutes apart.
.. code-block:: pycon
>>> from datetime import timedelta
>>> from icalendar import Alarm
>>> alarm = Alarm()
>>> alarm.TRIGGER = timedelta(minutes=-30)
>>> alarm.DURATION = timedelta(minutes=10)
>>> alarm.REPEAT = 2
>>> alarm.DURATION
datetime.timedelta(seconds=600)
""",
)
ACKNOWLEDGED = single_utc_property(
"ACKNOWLEDGED",
"""The UTC datetime at which this alarm was last sent or acknowledged.
Defined in :rfc:`9074`. Setting this property allows calendar clients to
dismiss or suppress an alarm across multiple devices. Once set to a value
greater than or equal to the alarm's computed trigger time, conforming clients
will not re-fire the alarm.
Returns ``None`` when no acknowledgment has been recorded.
Example:
Mark an alarm as acknowledged at the current time.
.. code-block:: pycon
>>> from datetime import timezone, datetime
>>> from icalendar import Alarm
>>> alarm = Alarm()
>>> alarm.ACKNOWLEDGED = datetime(2024, 1, 15, 10, 0, 0, tzinfo=timezone.utc)
>>> alarm.ACKNOWLEDGED # doctest: +ELLIPSIS
datetime.datetime(2024, 1, 15, 10, 0, tzinfo=...)
See also:
:attr:`TRIGGER` — the time at which the alarm fires.
""",
)
TRIGGER = create_single_property(
"TRIGGER",
"dt",
(datetime, timedelta),
timedelta | datetime | None,
"""The time at which this alarm fires, per :rfc:`5545#section-3.8.6.3`.
The value is either a :class:`~datetime.timedelta` (relative trigger) or a
UTC :class:`~datetime.datetime` (absolute trigger).
A negative :class:`~datetime.timedelta` fires *before* the related
component boundary (start or end); a positive one fires *after* it.
Use :attr:`TRIGGER_RELATED` to choose whether the offset is measured from
the start or the end of the parent event or to-do.
An absolute trigger fires at an exact UTC point in time regardless of the
parent component's dates.
Example:
Set an alarm to fire 15 minutes before the start of an event.
.. code-block:: pycon
>>> from datetime import timedelta
>>> from icalendar import Alarm
>>> alarm = Alarm()
>>> alarm.TRIGGER = timedelta(minutes=-15)
>>> alarm.TRIGGER # doctest: +ELLIPSIS
datetime.timedelta(...)
See also:
:attr:`TRIGGER_RELATED`, :attr:`DURATION`, :attr:`REPEAT`
""",
)
@property
def TRIGGER_RELATED(self) -> str:
"""The RELATED parameter of the TRIGGER property.
Values are either "START" (default) or "END".
A value of START will set the alarm to trigger off the
start of the associated event or to-do. A value of END will set
the alarm to trigger off the end of the associated event or to-do.
In this example, we create an alarm that triggers two hours after the
end of its parent component:
>>> from icalendar import Alarm
>>> from datetime import timedelta
>>> alarm = Alarm()
>>> alarm.TRIGGER = timedelta(hours=2)
>>> alarm.TRIGGER_RELATED = "END"
"""
trigger = self.get("TRIGGER")
if trigger is None:
return "START"
return trigger.params.get("RELATED", "START")
@TRIGGER_RELATED.setter
def TRIGGER_RELATED(self, value: str):
"""Set "START" or "END"."""
trigger = self.get("TRIGGER")
if trigger is None:
raise ValueError(
"You must set a TRIGGER before setting the RELATED parameter."
)
trigger.params["RELATED"] = value
[docs]
class Triggers(NamedTuple):
"""The computed times of alarm triggers.
start - triggers relative to the start of the Event or Todo (timedelta)
end - triggers relative to the end of the Event or Todo (timedelta)
absolute - triggers at a datetime in UTC
"""
start: tuple[timedelta]
end: tuple[timedelta]
absolute: tuple[datetime]
@property
def triggers(self):
"""The computed triggers of an Alarm.
This takes the TRIGGER, DURATION and REPEAT properties into account.
Here, we create an alarm that triggers 3 times before the start of the
parent component:
>>> from icalendar import Alarm
>>> from datetime import timedelta
>>> alarm = Alarm()
>>> alarm.TRIGGER = timedelta(hours=-4) # trigger 4 hours before START
>>> alarm.DURATION = timedelta(hours=1) # after 1 hour trigger again
>>> alarm.REPEAT = 2 # trigger 2 more times
>>> alarm.triggers.start == (timedelta(hours=-4), timedelta(hours=-3), timedelta(hours=-2))
True
>>> alarm.triggers.end
()
>>> alarm.triggers.absolute
()
"""
start = []
end = []
absolute = []
trigger = self.TRIGGER
if trigger is not None:
if isinstance(trigger, date):
absolute.append(trigger)
add = absolute
elif self.TRIGGER_RELATED == "START":
start.append(trigger)
add = start
else:
end.append(trigger)
add = end
duration = self.DURATION
if duration is not None:
for _ in range(self.REPEAT):
add.append(add[-1] + duration)
return self.Triggers(
start=tuple(start), end=tuple(end), absolute=tuple(absolute)
)
uid = single_string_property(
"UID",
uid_property.__doc__,
"X-ALARMUID",
)
summary = summary_property
description = description_property
attendees = attendees_property
[docs]
@classmethod
def new(
cls,
/,
attendees: list[vCalAddress] | None = None,
concepts: CONCEPTS_TYPE_SETTER = None,
description: str | None = None,
links: LINKS_TYPE_SETTER = None,
refids: list[str] | str | None = None,
related_to: RELATED_TO_TYPE_SETTER = None,
summary: str | None = None,
uid: str | uuid.UUID | None = None,
):
"""Create a new alarm with all required properties.
This creates a new Alarm in accordance with :rfc:`5545`.
Parameters:
attendees: The :attr:`attendees` of the alarm.
concepts: The :attr:`~icalendar.Component.concepts` of the alarm.
description: The :attr:`description` of the alarm.
links: The :attr:`~icalendar.Component.links` of the alarm.
refids: :attr:`~icalendar.Component.refids` of the alarm.
related_to: :attr:`~icalendar.Component.related_to` of the alarm.
summary: The :attr:`summary` of the alarm.
uid: The :attr:`uid` of the alarm.
Returns:
:class:`Alarm`
Raises:
~error.InvalidCalendar: If the content is not valid
according to :rfc:`5545`.
.. warning:: As time progresses, we will be stricter with the validation.
"""
alarm: Alarm = super().new(
links=links,
related_to=related_to,
refids=refids,
concepts=concepts,
)
alarm.summary = summary
alarm.description = description
alarm.uid = uid
alarm.attendees = attendees
return alarm
[docs]
@classmethod
def example(cls, name: str = "example") -> Alarm:
"""Return the alarm example with the given name."""
return cls.from_ical(get_example("alarms", name))
__all__ = ["Alarm"]