Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/cal/alarm.py: 37%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1""":rfc:`5545` VALARM component."""
3from __future__ import annotations
5from datetime import date, datetime, timedelta
6from typing import TYPE_CHECKING, NamedTuple
8from icalendar.attr import (
9 ATTACHMENTS_TYPE_SETTER,
10 ATTENDEE_TYPE_SETTER,
11 CONCEPTS_TYPE_SETTER,
12 LINKS_TYPE_SETTER,
13 RELATED_TO_TYPE_SETTER,
14 _set_attachments,
15 attachments_property,
16 attendees_property,
17 create_single_property,
18 description_property,
19 property_del_duration,
20 property_get_duration,
21 property_set_duration,
22 repeat_property,
23 single_int_property,
24 single_string_property,
25 single_utc_property,
26 summary_property,
27 uid_property,
28)
29from icalendar.cal.component import Component
30from icalendar.cal.examples import get_example
31from icalendar.error import InvalidCalendar
32from icalendar.prop import vCalAddress
34if TYPE_CHECKING:
35 import uuid
37 from icalendar.compatibility import Self
38 from icalendar.prop import vBinary, vUri
41class Alarm(Component):
42 """
43 A "VALARM" calendar component is a grouping of component
44 properties that defines an alarm or reminder for an event or a
45 to-do. For example, it may be used to define a reminder for a
46 pending event or an overdue to-do.
48 Example:
50 The following example creates an alarm which uses an audio file
51 from an FTP server.
53 .. code-block:: pycon
55 >>> from icalendar import Alarm
56 >>> alarm = Alarm.example()
57 >>> print(alarm.to_ical().decode())
58 BEGIN:VALARM
59 ACTION:AUDIO
60 ATTACH;FMTTYPE=audio/basic:ftp://example.com/pub/sounds/bell-01.aud
61 DURATION:PT15M
62 REPEAT:4
63 TRIGGER;VALUE=DATE-TIME:19970317T133000Z
64 END:VALARM
65 """
67 name = "VALARM"
68 # some properties MAY/MUST/MUST NOT appear depending on ACTION value
69 required = (
70 "ACTION",
71 "TRIGGER",
72 )
73 singletons = (
74 "ACTION",
75 "DESCRIPTION",
76 "SUMMARY",
77 "TRIGGER",
78 "DURATION",
79 "REPEAT",
80 "UID",
81 "PROXIMITY",
82 "ACKNOWLEDGED",
83 )
84 inclusive = (
85 (
86 "DURATION",
87 "REPEAT",
88 ),
89 (
90 "SUMMARY",
91 "ATTENDEE",
92 ),
93 )
94 multiple = ("ATTENDEE", "ATTACH", "RELATED-TO")
96 REPEAT = single_int_property(
97 "REPEAT",
98 0,
99 """The number of additional times the alarm is triggered after the initial trigger.
101 Defaults to ``0``, meaning the alarm fires once. To repeat the alarm,
102 set both :attr:`REPEAT` and :attr:`DURATION`. The :attr:`DURATION`
103 sets the gap between repetitions. :attr:`REPEAT` is the count of *additional*
104 triggers, so a :attr:`REPEAT` of ``2`` produces three alarms in total
105 (the initial trigger plus two repeats).
107 Conforming with :rfc:`5545#section-3.8.6.2`, this property can appear
108 once in an :class:`~icalendar.cal.alarm.Alarm` component and must be
109 paired with :attr:`DURATION`.
111 Example:
112 Build an alarm that fires once and then repeats twice at
113 five-minute intervals.
115 .. code-block:: pycon
117 >>> from datetime import timedelta
118 >>> from icalendar import Alarm
119 >>> alarm = Alarm()
120 >>> alarm.TRIGGER = timedelta(minutes=-15)
121 >>> alarm.DURATION = timedelta(minutes=5)
122 >>> alarm.REPEAT = 2
123 >>> alarm.REPEAT
124 2
126 Raises:
127 TypeError: If the value is not an ``int``. Booleans are rejected, too,
128 even though ``bool`` subclasses ``int``.
130 ~icalendar.error.InvalidCalendar: If the value is negative.
132 .. versionchanged:: 7.3.0
133 Negative values are no longer accepted.
134 """,
135 min_value=0,
136 )
138 DURATION = property(
139 property_get_duration,
140 property_set_duration,
141 property_del_duration,
142 """The delay between repeated triggers of a repeating alarm.
144 Returns a :class:`datetime.timedelta` or ``None`` when the alarm
145 has no :attr:`DURATION` set. Setting this attribute accepts a
146 :class:`~datetime.timedelta`; deleting it removes the property
147 from the component.
149 :attr:`DURATION` is meaningful only for repeating alarms and must
150 be paired with :attr:`REPEAT`. The two together produce
151 :attr:`REPEAT` additional triggers, each spaced by :attr:`DURATION` after
152 the initial trigger.
154 Conforming with :rfc:`5545#section-3.8.2.5`, the :attr:`DURATION` property
155 can appear once in an :class:`~icalendar.cal.alarm.Alarm` component.
157 Example:
158 Pair :attr:`DURATION` with :attr:`REPEAT` to produce three
159 triggers spaced ten minutes apart.
161 .. code-block:: pycon
163 >>> from datetime import timedelta
164 >>> from icalendar import Alarm
165 >>> alarm = Alarm()
166 >>> alarm.TRIGGER = timedelta(minutes=-30)
167 >>> alarm.DURATION = timedelta(minutes=10)
168 >>> alarm.REPEAT = 2
169 >>> alarm.DURATION
170 datetime.timedelta(seconds=600)
171 """,
172 )
174 ACKNOWLEDGED = single_utc_property(
175 "ACKNOWLEDGED",
176 """This property is the UTC datetime at which this alarm was last sent or acknowledged as defined in :rfc:`9074`.
178 Setting this property allows calendar clients to
179 dismiss or suppress an alarm across multiple devices. Once set to a value
180 greater than or equal to the alarm's computed trigger time, conforming clients
181 will not refire the alarm.
183 Returns ``None`` when no acknowledgment has been recorded.
185 Example:
186 Mark an alarm as acknowledged. Note that the example uses an arbitrary time
187 for the purpose of passing doctests. In actual practice, clients should
188 use the current time in UTC, such as ``datetime.now(UTC)``.
190 .. code-block:: pycon
192 >>> from datetime import timezone, datetime
193 >>> from icalendar import Alarm
194 >>> UTC = timezone.utc
195 >>> alarm = Alarm()
196 >>> alarm.ACKNOWLEDGED = datetime(2024, 1, 15, 10, 0, tzinfo=UTC)
197 >>> alarm.ACKNOWLEDGED
198 datetime.datetime(2024, 1, 15, 10, 0, tzinfo=ZoneInfo(key='UTC'))
200 See also:
201 :attr:`TRIGGER`, the time at which the alarm fires.
202 """,
203 )
205 TRIGGER = create_single_property(
206 "TRIGGER",
207 "dt",
208 (datetime, timedelta),
209 timedelta | datetime | None,
210 """The time at which this alarm fires, per :rfc:`5545#section-3.8.6.3`.
212 The value is either a :class:`~datetime.timedelta` (relative trigger) or a
213 UTC :class:`~datetime.datetime` (absolute trigger).
215 A negative :class:`~datetime.timedelta` fires *before* the related
216 component boundary (start or end); a positive one fires *after* it.
217 Use :attr:`TRIGGER_RELATED` to choose whether the offset is measured from
218 the start or the end of the parent event or to-do.
219 An absolute trigger fires at an exact UTC point in time regardless of the
220 parent component's dates.
222 Examples:
223 Set an alarm to fire 15 minutes before the start of an event.
225 .. code-block:: pycon
227 >>> from datetime import datetime, timedelta, timezone
228 >>> from icalendar import Alarm, Event
229 >>> UTC = timezone.utc
230 >>> event = Event()
231 >>> event.start = datetime(2024, 1, 15, 10, 0, tzinfo=UTC)
232 >>> alarm = Alarm()
233 >>> alarm.TRIGGER = timedelta(minutes=-15)
234 >>> event.add_component(alarm)
235 >>> event.alarms.times[0].trigger
236 datetime.datetime(2024, 1, 15, 9, 45, tzinfo=datetime.timezone.utc)
238 Set an absolute trigger to fire at a specific UTC time.
240 .. code-block:: pycon
242 >>> absolute_alarm = Alarm()
243 >>> absolute_alarm.TRIGGER = datetime(2024, 1, 15, 9, 45, tzinfo=UTC)
244 >>> absolute_alarm.TRIGGER
245 datetime.datetime(2024, 1, 15, 9, 45, tzinfo=datetime.timezone.utc)
247 See also:
248 :attr:`TRIGGER_RELATED`, :attr:`DURATION`, :attr:`REPEAT`
249 """,
250 )
252 @property
253 def TRIGGER_RELATED(self) -> str:
254 """The RELATED parameter of the TRIGGER property.
256 Values are either "START" (default) or "END".
258 A value of START will set the alarm to trigger off the
259 start of the associated event or to-do. A value of END will set
260 the alarm to trigger off the end of the associated event or to-do.
262 In this example, we create an alarm that triggers two hours after the
263 end of its parent component.
265 >>> from icalendar import Alarm
266 >>> from datetime import timedelta
267 >>> alarm = Alarm()
268 >>> alarm.TRIGGER = timedelta(hours=2)
269 >>> alarm.TRIGGER_RELATED = "END"
270 """
271 trigger = self.get("TRIGGER")
272 if trigger is None:
273 return "START"
274 return trigger.params.get("RELATED", "START")
276 @TRIGGER_RELATED.setter
277 def TRIGGER_RELATED(self, value: str):
278 """Set "START" or "END"."""
279 trigger = self.get("TRIGGER")
280 if trigger is None:
281 raise ValueError(
282 "You must set a TRIGGER before setting the RELATED parameter."
283 )
284 trigger.params["RELATED"] = value
286 class Triggers(NamedTuple):
287 """The computed times of alarm triggers.
289 start - triggers relative to the start of the Event or Todo (timedelta)
291 end - triggers relative to the end of the Event or Todo (timedelta)
293 absolute - triggers at a datetime in UTC
294 """
296 start: tuple[timedelta]
297 end: tuple[timedelta]
298 absolute: tuple[datetime]
300 @property
301 def triggers(self):
302 """The computed triggers of an Alarm.
304 This takes the TRIGGER, DURATION and REPEAT properties into account.
306 Here, we create an alarm that triggers 3 times before the start of the
307 parent component.
309 >>> from icalendar import Alarm
310 >>> from datetime import timedelta
311 >>> alarm = Alarm()
312 >>> alarm.TRIGGER = timedelta(hours=-4) # trigger 4 hours before START
313 >>> alarm.DURATION = timedelta(hours=1) # after 1 hour trigger again
314 >>> alarm.REPEAT = 2 # trigger 2 more times
315 >>> alarm.triggers.start == (timedelta(hours=-4), timedelta(hours=-3), timedelta(hours=-2))
316 True
317 >>> alarm.triggers.end
318 ()
319 >>> alarm.triggers.absolute
320 ()
321 """
322 start = []
323 end = []
324 absolute = []
325 trigger = self.TRIGGER
326 if trigger is not None:
327 if isinstance(trigger, date):
328 absolute.append(trigger)
329 add = absolute
330 elif self.TRIGGER_RELATED == "START":
331 start.append(trigger)
332 add = start
333 else:
334 end.append(trigger)
335 add = end
336 duration = self.DURATION
337 if duration is not None:
338 for _ in range(self.repeat):
339 add.append(add[-1] + duration)
340 return self.Triggers(
341 start=tuple(start), end=tuple(end), absolute=tuple(absolute)
342 )
344 repeat = repeat_property
346 attachments = attachments_property
348 @attachments.setter
349 def attachments(self, value: ATTACHMENTS_TYPE_SETTER) -> None:
350 if value is not None and self.ACTION == "AUDIO":
351 count = len(value) if isinstance(value, list) else 1
352 if count > 1:
353 raise InvalidCalendar(
354 "An AUDIO alarm must not contain more than one attachment.\n"
355 f"Alarm has {count} attachments."
356 )
357 _set_attachments(self, value)
359 ACTION = single_string_property(
360 "ACTION",
361 """The action invoked when the alarm triggers.
363 Typical values defined by :rfc:`5545#section-3.8.6.1` are
364 ``AUDIO``, ``DISPLAY``, and ``EMAIL``. The empty string is
365 returned when no ``ACTION`` property is present.
366 """,
367 )
369 @ACTION.setter
370 def ACTION(self, value: str | None) -> None:
371 if value == "AUDIO" and len(self.attachments) > 1:
372 raise InvalidCalendar(
373 "An AUDIO alarm must not contain more than one attachment.\n"
374 f"Alarm has {len(self.attachments)} attachments."
375 )
376 self.pop("ACTION", None)
377 if value is not None:
378 self.add("ACTION", value)
380 uid = single_string_property(
381 "UID",
382 uid_property.__doc__,
383 ["X-ALARMUID", "X-EVOLUTION-ALARM-UID"],
384 )
385 summary = summary_property
386 description = description_property
387 attendees = attendees_property
389 @classmethod
390 def new(
391 cls,
392 /,
393 action: str | None = None,
394 trigger: timedelta | datetime | None = None,
395 attachments: ATTACHMENTS_TYPE_SETTER = None,
396 attendees: ATTENDEE_TYPE_SETTER = None,
397 concepts: CONCEPTS_TYPE_SETTER = None,
398 description: str | None = None,
399 links: LINKS_TYPE_SETTER = None,
400 refids: list[str] | str | None = None,
401 related_to: RELATED_TO_TYPE_SETTER = None,
402 summary: str | None = None,
403 uid: str | uuid.UUID | None = None,
404 ) -> Self:
405 """Create a new alarm with all required properties.
407 This creates a new Alarm in accordance with :rfc:`5545`.
409 Parameters:
410 action: The :attr:`ACTION` of the alarm. Typical values are
411 ``"AUDIO"``, ``"DISPLAY"``, and ``"EMAIL"``. When you set
412 ``"AUDIO"``, the alarm accepts at most one attachment.
413 trigger: The required :attr:`TRIGGER` of the alarm.
414 attachments: The :attr:`attachments` of the alarm.
415 attendees: The :attr:`attendees` of the alarm.
416 concepts: The :attr:`~icalendar.cal.component.Component.concepts` of the alarm.
417 description: The :attr:`description` of the alarm.
418 links: The :attr:`~icalendar.cal.component.Component.links` of the alarm.
419 refids: :attr:`~icalendar.cal.component.Component.refids` of the alarm.
420 related_to: :attr:`~icalendar.cal.component.Component.related_to` of the alarm.
421 summary: The :attr:`summary` of the alarm.
422 uid: The :attr:`uid` of the alarm.
424 Returns:
425 :class:`Alarm`
427 Raises:
428 ~icalendar.error.InvalidCalendar: If the content is not valid
429 according to :rfc:`5545`.
431 .. warning:: As time progresses, we will be stricter with the validation.
432 """
433 if cls._validate_new:
434 if not action:
435 raise InvalidCalendar("Alarm requires an ACTION")
436 if trigger is None:
437 raise InvalidCalendar("Alarm requires a TRIGGER")
438 alarm: Self = super().new(
439 links=links,
440 related_to=related_to,
441 refids=refids,
442 concepts=concepts,
443 )
444 if action is not None:
445 alarm.ACTION = action
446 if trigger is not None:
447 alarm.TRIGGER = trigger
448 alarm.attachments = attachments
449 alarm.summary = summary
450 alarm.description = description
451 alarm.uid = uid
452 alarm.attendees = attendees
453 return alarm
455 def _apply_duration_repeat(
456 self,
457 duration: timedelta | None,
458 repeat: int | None,
459 ) -> None:
460 if duration is not None or repeat is not None:
461 if duration is None or repeat is None:
462 raise InvalidCalendar(
463 "DURATION and REPEAT must be set together or not at all"
464 )
465 self.DURATION = duration
466 self.repeat = repeat
468 @classmethod
469 def new_display(
470 cls,
471 description: str,
472 trigger: timedelta | datetime,
473 duration: timedelta | None = None,
474 repeat: int | None = None,
475 uid: str | uuid.UUID | None = None,
476 links: LINKS_TYPE_SETTER = None,
477 related_to: RELATED_TO_TYPE_SETTER = None,
478 refids: list[str] | str | None = None,
479 concepts: CONCEPTS_TYPE_SETTER = None,
480 ) -> Alarm:
481 """Create a new DISPLAY alarm that shows a text reminder.
483 A DISPLAY alarm pops up a text notification at the trigger time.
484 This is the most common alarm type used by calendar clients.
486 Conforms to :rfc:`5545#section-3.6.6`.
488 Parameters:
489 description: Required. The text to display when the alarm fires.
490 Corresponds to the :attr:`description` property.
491 trigger: Required. When the alarm fires, as a :class:`~datetime.timedelta`
492 relative to the event start (negative means before) or as an
493 absolute :class:`~datetime.datetime` (recommend UTC-aware).
494 concepts: The :attr:`~icalendar.cal.component.Component.concepts` of the alarm.
495 duration: Gap between repeated triggers. Must be paired with
496 ``repeat``. Corresponds to the :attr:`DURATION` property.
497 links: The :attr:`~icalendar.cal.component.Component.links` of the alarm.
498 refids: The :attr:`~icalendar.cal.component.Component.refids` of the alarm.
499 related_to: The :attr:`~icalendar.cal.component.Component.related_to` of the alarm.
500 repeat: Number of *additional* times to fire after the initial
501 trigger. Must be paired with ``duration``.
502 Corresponds to the :attr:`REPEAT` property.
503 uid: Unique identifier for the alarm or ``None``.
505 Returns:
506 :class:`Alarm` with ``ACTION:DISPLAY`` set.
508 Raises:
509 ~icalendar.error.InvalidCalendar: If required fields are missing
510 or ``duration`` and ``repeat`` are not both provided together.
512 Example:
513 Create a display alarm that fires 15 minutes before the event:
515 .. code-block:: pycon
517 >>> from datetime import timedelta
518 >>> from icalendar import Alarm
519 >>> alarm = Alarm.new_display(
520 ... description="Team meeting in 15 minutes",
521 ... trigger=timedelta(minutes=-15),
522 ... )
523 >>> print(alarm.to_ical().decode())
524 BEGIN:VALARM
525 ACTION:DISPLAY
526 DESCRIPTION:Team meeting in 15 minutes
527 TRIGGER:-PT15M
528 END:VALARM
530 Attach the alarm to an event:
532 .. code-block:: python
534 from datetime import datetime, timedelta, timezone
535 from icalendar import Alarm, Event
537 event = Event.new(
538 summary="Team meeting",
539 start=datetime(2025, 6, 1, 10, 0, tzinfo=timezone.utc),
540 end=datetime(2025, 6, 1, 11, 0, tzinfo=timezone.utc),
541 )
542 event.add_component(Alarm.new_display(
543 description="Team meeting in 15 minutes",
544 trigger=timedelta(minutes=-15),
545 ))
546 """
547 if not description:
548 raise InvalidCalendar("DISPLAY alarm requires a description")
549 if trigger is None:
550 raise InvalidCalendar("DISPLAY alarm requires a trigger")
551 alarm: Alarm = cls.new(
552 action="DISPLAY",
553 trigger=trigger,
554 description=description,
555 uid=uid,
556 links=links,
557 related_to=related_to,
558 refids=refids,
559 concepts=concepts,
560 )
561 alarm._apply_duration_repeat(duration, repeat)
562 return alarm
564 @classmethod
565 def new_audio(
566 cls,
567 trigger: timedelta | datetime,
568 attachments: str | bytes | vUri | vBinary | None = None,
569 duration: timedelta | None = None,
570 repeat: int | None = None,
571 uid: str | uuid.UUID | None = None,
572 links: LINKS_TYPE_SETTER = None,
573 related_to: RELATED_TO_TYPE_SETTER = None,
574 refids: list[str] | str | None = None,
575 concepts: CONCEPTS_TYPE_SETTER = None,
576 ) -> Alarm:
577 """Create a new AUDIO alarm that plays a sound.
579 An AUDIO alarm plays a sound at the trigger time. An optional
580 ``attachments`` URI points to the audio file to play; when omitted,
581 the client uses its default alert sound.
583 Conforms to :rfc:`5545#section-3.6.6`.
585 Parameters:
586 trigger: Required. When the alarm fires, as a :class:`~datetime.timedelta`
587 relative to the event start (negative means before) or as an
588 absolute :class:`~datetime.datetime` (recommend UTC-aware).
589 attachments: Optional audio attachment. Accepts a URI as a
590 :class:`str` or :class:`~icalendar.prop.uri.vUri`, or
591 inline binary audio as :class:`bytes` or
592 :class:`~icalendar.prop.binary.vBinary`. When ``None``,
593 the client uses its default sound.
594 duration: Gap between repeated triggers. Must be paired with
595 ``repeat``. Corresponds to the :attr:`DURATION` property.
596 links: The :attr:`~icalendar.cal.component.Component.links` of the alarm.
597 refids: The :attr:`~icalendar.cal.component.Component.refids` of the alarm.
598 related_to: The :attr:`~icalendar.cal.component.Component.related_to` of the alarm.
599 repeat: Number of *additional* times to fire after the initial
600 trigger. Must be paired with ``duration``.
601 Corresponds to the :attr:`REPEAT` property.
602 uid: Unique identifier for the alarm or ``None``.
604 Returns:
605 :class:`Alarm` with ``ACTION:AUDIO`` set.
607 Raises:
608 ~icalendar.error.InvalidCalendar: If required fields are missing
609 or ``duration`` and ``repeat`` are not both provided together.
611 Example:
612 Create an audio alarm using a custom sound file:
614 .. code-block:: pycon
616 >>> from datetime import timedelta
617 >>> from icalendar import Alarm
618 >>> alarm = Alarm.new_audio(
619 ... trigger=timedelta(minutes=-5),
620 ... attachments="ftp://example.com/pub/sounds/bell-01.aud",
621 ... )
622 >>> print(alarm.to_ical().decode())
623 BEGIN:VALARM
624 ACTION:AUDIO
625 ATTACH:ftp://example.com/pub/sounds/bell-01.aud
626 TRIGGER:-PT5M
627 END:VALARM
628 """
629 if trigger is None:
630 raise InvalidCalendar("AUDIO alarm requires a trigger")
631 alarm: Alarm = cls.new(
632 action="AUDIO",
633 trigger=trigger,
634 attachments=attachments,
635 uid=uid,
636 links=links,
637 related_to=related_to,
638 refids=refids,
639 concepts=concepts,
640 )
641 alarm._apply_duration_repeat(duration, repeat)
642 return alarm
644 @classmethod
645 def new_email(
646 cls,
647 summary: str,
648 description: str,
649 trigger: timedelta | datetime,
650 attendees: ATTENDEE_TYPE_SETTER,
651 attachments: ATTACHMENTS_TYPE_SETTER = None,
652 duration: timedelta | None = None,
653 repeat: int | None = None,
654 uid: str | uuid.UUID | None = None,
655 links: LINKS_TYPE_SETTER = None,
656 related_to: RELATED_TO_TYPE_SETTER = None,
657 refids: list[str] | str | None = None,
658 concepts: CONCEPTS_TYPE_SETTER = None,
659 ) -> Alarm:
660 """Create a new EMAIL alarm that sends an email notification.
662 An EMAIL alarm sends an email to each address in ``attendees`` when
663 the alarm fires.
665 Conforms to :rfc:`5545#section-3.6.6`.
667 Parameters:
668 attendees: Required. One or more recipient addresses as email strings or
669 :class:`~icalendar.prop.cal_address.vCalAddress` instances. A
670 single address or a sequence of addresses. At least one is
671 required.
672 description: Required. Body of the email.
673 Corresponds to the :attr:`description` property.
674 summary: Required. Subject line of the email.
675 Corresponds to the :attr:`summary` property.
676 trigger: Required. When the alarm fires, as a :class:`~datetime.timedelta`
677 relative to the event start (negative means before) or as an
678 absolute :class:`~datetime.datetime` (recommend UTC-aware).
679 attachments: The :attr:`attachments` of the alarm. A single value
680 or a sequence of them. Both URIs and binary data are accepted.
681 concepts: The :attr:`~icalendar.cal.component.Component.concepts` of the alarm.
682 duration: Gap between repeated triggers. Must be paired with
683 ``repeat``. Corresponds to the :attr:`DURATION` property.
684 links: The :attr:`~icalendar.cal.component.Component.links` of the alarm.
685 refids: The :attr:`~icalendar.cal.component.Component.refids` of the alarm.
686 related_to: The :attr:`~icalendar.cal.component.Component.related_to` of the alarm.
687 repeat: Number of *additional* times to fire after the initial
688 trigger. Must be paired with ``duration``.
689 Corresponds to the :attr:`REPEAT` property.
690 uid: Unique identifier for the alarm or ``None``.
692 Returns:
693 :class:`Alarm` with ``ACTION:EMAIL`` set.
695 Raises:
696 ~icalendar.error.InvalidCalendar: If required fields are missing,
697 ``attendees`` is empty, or ``duration`` and ``repeat`` are not
698 both provided together.
700 Example:
701 Create an email alarm sent to two recipients. Plain email strings
702 and ``mailto:``-prefixed strings are both accepted and normalized
703 to :class:`~icalendar.prop.cal_address.vCalAddress`:
705 .. code-block:: pycon
707 >>> from datetime import timedelta
708 >>> from icalendar import Alarm
709 >>> alarm = Alarm.new_email(
710 ... summary="Meeting reminder",
711 ... description="Your meeting starts in 30 minutes.",
712 ... trigger=timedelta(minutes=-30),
713 ... attendees=["user@example.com", "mailto:boss@example.com"],
714 ... )
715 >>> print(alarm.to_ical().decode())
716 BEGIN:VALARM
717 ACTION:EMAIL
718 ATTENDEE:mailto:user@example.com
719 ATTENDEE:mailto:boss@example.com
720 DESCRIPTION:Your meeting starts in 30 minutes.
721 SUMMARY:Meeting reminder
722 TRIGGER:-PT30M
723 END:VALARM
724 """
725 if isinstance(attendees, (str, vCalAddress)):
726 attendees = [attendees]
727 elif attendees is None:
728 attendees = []
729 cleaned_attendees = [
730 a
731 for a in attendees
732 if a is not None and (not isinstance(a, str) or a.strip())
733 ]
734 if not summary:
735 raise InvalidCalendar("EMAIL alarm requires a summary")
736 if not description:
737 raise InvalidCalendar("EMAIL alarm requires a description")
738 if trigger is None:
739 raise InvalidCalendar("EMAIL alarm requires a trigger")
740 if not cleaned_attendees:
741 raise InvalidCalendar("EMAIL alarm requires at least one valid attendee")
742 alarm: Alarm = cls.new(
743 action="EMAIL",
744 trigger=trigger,
745 attachments=attachments,
746 summary=summary,
747 description=description,
748 uid=uid,
749 attendees=cleaned_attendees,
750 links=links,
751 related_to=related_to,
752 refids=refids,
753 concepts=concepts,
754 )
755 alarm._apply_duration_repeat(duration, repeat)
756 return alarm
758 @classmethod
759 def example(cls, name: str = "example") -> Alarm:
760 """Return the alarm example with the given name."""
761 return cls.from_ical(get_example("alarms", name))
764__all__ = ["Alarm"]