Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/cal/alarm.py: 38%

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

138 statements  

1""":rfc:`5545` VALARM component.""" 

2 

3from __future__ import annotations 

4 

5from datetime import date, datetime, timedelta 

6from typing import TYPE_CHECKING, NamedTuple 

7 

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 

33 

34if TYPE_CHECKING: 

35 import uuid 

36 

37 from icalendar.compatibility import Self 

38 from icalendar.prop import vBinary, vUri 

39 

40 

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. 

47 

48 Example: 

49 

50 The following example creates an alarm which uses an audio file 

51 from an FTP server. 

52 

53 .. code-block:: pycon 

54 

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 """ 

66 

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") 

95 

96 REPEAT = single_int_property( 

97 "REPEAT", 

98 0, 

99 """The number of additional times the alarm is triggered after the initial trigger. 

100 

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). 

106 

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`. 

110 

111 Example: 

112 Build an alarm that fires once and then repeats twice at 

113 five-minute intervals. 

114 

115 .. code-block:: pycon 

116 

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 

125 

126 Raises: 

127 TypeError: If the value is not an ``int``. Booleans are rejected, too, 

128 even though ``bool`` subclasses ``int``. 

129 

130 ~icalendar.error.InvalidCalendar: If the value is negative. 

131 

132 .. versionchanged:: 7.3.0 

133 Negative values are no longer accepted. 

134 """, 

135 min_value=0, 

136 ) 

137 

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. 

143 

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. 

148 

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. 

153 

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. 

156 

157 Example: 

158 Pair :attr:`DURATION` with :attr:`REPEAT` to produce three 

159 triggers spaced ten minutes apart. 

160 

161 .. code-block:: pycon 

162 

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 ) 

173 

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`. 

177 

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. 

182 

183 Returns ``None`` when no acknowledgment has been recorded. 

184 

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)``. 

189 

190 .. code-block:: pycon 

191 

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')) 

199 

200 See also: 

201 :attr:`TRIGGER`, the time at which the alarm fires. 

202 """, 

203 ) 

204 

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`. 

211 

212 The value is either a :class:`~datetime.timedelta` (relative trigger) or a 

213 UTC :class:`~datetime.datetime` (absolute trigger). 

214 

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. 

221 

222 Examples: 

223 Set an alarm to fire 15 minutes before the start of an event. 

224 

225 .. code-block:: pycon 

226 

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) 

237 

238 Set an absolute trigger to fire at a specific UTC time. 

239 

240 .. code-block:: pycon 

241 

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) 

246 

247 See also: 

248 :attr:`TRIGGER_RELATED`, :attr:`DURATION`, :attr:`REPEAT` 

249 """, 

250 ) 

251 

252 @property 

253 def TRIGGER_RELATED(self) -> str: 

254 """The RELATED parameter of the TRIGGER property. 

255 

256 Values are either "START" (default) or "END". 

257 

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. 

261 

262 In this example, we create an alarm that triggers two hours after the 

263 end of its parent component. 

264 

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") 

275 

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 

285 

286 class Triggers(NamedTuple): 

287 """The computed times of alarm triggers. 

288 

289 start - triggers relative to the start of the Event or Todo (timedelta) 

290 

291 end - triggers relative to the end of the Event or Todo (timedelta) 

292 

293 absolute - triggers at a datetime in UTC 

294 """ 

295 

296 start: tuple[timedelta] 

297 end: tuple[timedelta] 

298 absolute: tuple[datetime] 

299 

300 @property 

301 def triggers(self): 

302 """The computed triggers of an Alarm. 

303 

304 This takes the TRIGGER, DURATION and REPEAT properties into account. 

305 

306 Here, we create an alarm that triggers 3 times before the start of the 

307 parent component. 

308 

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 ) 

343 

344 repeat = repeat_property 

345 

346 attachments = attachments_property 

347 

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) 

358 

359 ACTION = single_string_property( 

360 "ACTION", 

361 """The action invoked when the alarm triggers. 

362 

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 ) 

368 

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) 

379 

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 

388 

389 @classmethod 

390 def new( 

391 cls, 

392 /, 

393 action: str | None = None, 

394 attachments: ATTACHMENTS_TYPE_SETTER = None, 

395 attendees: ATTENDEE_TYPE_SETTER = None, 

396 concepts: CONCEPTS_TYPE_SETTER = None, 

397 description: str | None = None, 

398 links: LINKS_TYPE_SETTER = None, 

399 refids: list[str] | str | None = None, 

400 related_to: RELATED_TO_TYPE_SETTER = None, 

401 summary: str | None = None, 

402 uid: str | uuid.UUID | None = None, 

403 ) -> Self: 

404 """Create a new alarm with all required properties. 

405 

406 This creates a new Alarm in accordance with :rfc:`5545`. 

407 

408 Parameters: 

409 action: The :attr:`ACTION` of the alarm. Typical values are 

410 ``"AUDIO"``, ``"DISPLAY"``, and ``"EMAIL"``. When you set 

411 ``"AUDIO"``, the alarm accepts at most one attachment. 

412 attachments: The :attr:`attachments` of the alarm. 

413 attendees: The :attr:`attendees` of the alarm. 

414 concepts: The :attr:`~icalendar.cal.component.Component.concepts` of the alarm. 

415 description: The :attr:`description` of the alarm. 

416 links: The :attr:`~icalendar.cal.component.Component.links` of the alarm. 

417 refids: :attr:`~icalendar.cal.component.Component.refids` of the alarm. 

418 related_to: :attr:`~icalendar.cal.component.Component.related_to` of the alarm. 

419 summary: The :attr:`summary` of the alarm. 

420 uid: The :attr:`uid` of the alarm. 

421 

422 Returns: 

423 :class:`Alarm` 

424 

425 Raises: 

426 ~error.InvalidCalendar: If the content is not valid 

427 according to :rfc:`5545`. 

428 

429 .. warning:: As time progresses, we will be stricter with the validation. 

430 """ 

431 alarm: Self = super().new( 

432 links=links, 

433 related_to=related_to, 

434 refids=refids, 

435 concepts=concepts, 

436 ) 

437 if action is not None: 

438 alarm.ACTION = action 

439 alarm.attachments = attachments 

440 alarm.summary = summary 

441 alarm.description = description 

442 alarm.uid = uid 

443 alarm.attendees = attendees 

444 return alarm 

445 

446 def _apply_duration_repeat( 

447 self, 

448 duration: timedelta | None, 

449 repeat: int | None, 

450 ) -> None: 

451 if duration is not None or repeat is not None: 

452 if duration is None or repeat is None: 

453 raise InvalidCalendar( 

454 "DURATION and REPEAT must be set together or not at all" 

455 ) 

456 self.DURATION = duration 

457 self.repeat = repeat 

458 

459 @classmethod 

460 def new_display( 

461 cls, 

462 description: str, 

463 trigger: timedelta | datetime, 

464 duration: timedelta | None = None, 

465 repeat: int | None = None, 

466 uid: str | uuid.UUID | None = None, 

467 links: LINKS_TYPE_SETTER = None, 

468 related_to: RELATED_TO_TYPE_SETTER = None, 

469 refids: list[str] | str | None = None, 

470 concepts: CONCEPTS_TYPE_SETTER = None, 

471 ) -> Alarm: 

472 """Create a new DISPLAY alarm that shows a text reminder. 

473 

474 A DISPLAY alarm pops up a text notification at the trigger time. 

475 This is the most common alarm type used by calendar clients. 

476 

477 Conforms to :rfc:`5545#section-3.6.6`. 

478 

479 Parameters: 

480 description: Required. The text to display when the alarm fires. 

481 Corresponds to the :attr:`description` property. 

482 trigger: Required. When the alarm fires, as a :class:`~datetime.timedelta` 

483 relative to the event start (negative means before) or as an 

484 absolute :class:`~datetime.datetime` (recommend UTC-aware). 

485 concepts: The :attr:`~icalendar.cal.component.Component.concepts` of the alarm. 

486 duration: Gap between repeated triggers. Must be paired with 

487 ``repeat``. Corresponds to the :attr:`DURATION` property. 

488 links: The :attr:`~icalendar.cal.component.Component.links` of the alarm. 

489 refids: The :attr:`~icalendar.cal.component.Component.refids` of the alarm. 

490 related_to: The :attr:`~icalendar.cal.component.Component.related_to` of the alarm. 

491 repeat: Number of *additional* times to fire after the initial 

492 trigger. Must be paired with ``duration``. 

493 Corresponds to the :attr:`REPEAT` property. 

494 uid: Unique identifier for the alarm or ``None``. 

495 

496 Returns: 

497 :class:`Alarm` with ``ACTION:DISPLAY`` set. 

498 

499 Raises: 

500 ~icalendar.error.InvalidCalendar: If required fields are missing 

501 or ``duration`` and ``repeat`` are not both provided together. 

502 

503 Example: 

504 Create a display alarm that fires 15 minutes before the event: 

505 

506 .. code-block:: pycon 

507 

508 >>> from datetime import timedelta 

509 >>> from icalendar import Alarm 

510 >>> alarm = Alarm.new_display( 

511 ... description="Team meeting in 15 minutes", 

512 ... trigger=timedelta(minutes=-15), 

513 ... ) 

514 >>> print(alarm.to_ical().decode()) 

515 BEGIN:VALARM 

516 ACTION:DISPLAY 

517 DESCRIPTION:Team meeting in 15 minutes 

518 TRIGGER:-PT15M 

519 END:VALARM 

520 

521 Attach the alarm to an event: 

522 

523 .. code-block:: python 

524 

525 from datetime import datetime, timedelta, timezone 

526 from icalendar import Alarm, Event 

527 

528 event = Event.new( 

529 summary="Team meeting", 

530 start=datetime(2025, 6, 1, 10, 0, tzinfo=timezone.utc), 

531 end=datetime(2025, 6, 1, 11, 0, tzinfo=timezone.utc), 

532 ) 

533 event.add_component(Alarm.new_display( 

534 description="Team meeting in 15 minutes", 

535 trigger=timedelta(minutes=-15), 

536 )) 

537 """ 

538 if not description: 

539 raise InvalidCalendar("DISPLAY alarm requires a description") 

540 if trigger is None: 

541 raise InvalidCalendar("DISPLAY alarm requires a trigger") 

542 alarm: Alarm = cls.new( 

543 action="DISPLAY", 

544 description=description, 

545 uid=uid, 

546 links=links, 

547 related_to=related_to, 

548 refids=refids, 

549 concepts=concepts, 

550 ) 

551 alarm.TRIGGER = trigger 

552 alarm._apply_duration_repeat(duration, repeat) 

553 return alarm 

554 

555 @classmethod 

556 def new_audio( 

557 cls, 

558 trigger: timedelta | datetime, 

559 attachments: str | bytes | vUri | vBinary | None = None, 

560 duration: timedelta | None = None, 

561 repeat: int | None = None, 

562 uid: str | uuid.UUID | None = None, 

563 links: LINKS_TYPE_SETTER = None, 

564 related_to: RELATED_TO_TYPE_SETTER = None, 

565 refids: list[str] | str | None = None, 

566 concepts: CONCEPTS_TYPE_SETTER = None, 

567 ) -> Alarm: 

568 """Create a new AUDIO alarm that plays a sound. 

569 

570 An AUDIO alarm plays a sound at the trigger time. An optional 

571 ``attachments`` URI points to the audio file to play; when omitted, 

572 the client uses its default alert sound. 

573 

574 Conforms to :rfc:`5545#section-3.6.6`. 

575 

576 Parameters: 

577 trigger: Required. When the alarm fires, as a :class:`~datetime.timedelta` 

578 relative to the event start (negative means before) or as an 

579 absolute :class:`~datetime.datetime` (recommend UTC-aware). 

580 attachments: Optional audio attachment. Accepts a URI as a 

581 :class:`str` or :class:`~icalendar.prop.uri.vUri`, or 

582 inline binary audio as :class:`bytes` or 

583 :class:`~icalendar.prop.binary.vBinary`. When ``None``, 

584 the client uses its default sound. 

585 duration: Gap between repeated triggers. Must be paired with 

586 ``repeat``. Corresponds to the :attr:`DURATION` property. 

587 links: The :attr:`~icalendar.cal.component.Component.links` of the alarm. 

588 refids: The :attr:`~icalendar.cal.component.Component.refids` of the alarm. 

589 related_to: The :attr:`~icalendar.cal.component.Component.related_to` of the alarm. 

590 repeat: Number of *additional* times to fire after the initial 

591 trigger. Must be paired with ``duration``. 

592 Corresponds to the :attr:`REPEAT` property. 

593 uid: Unique identifier for the alarm or ``None``. 

594 

595 Returns: 

596 :class:`Alarm` with ``ACTION:AUDIO`` set. 

597 

598 Raises: 

599 ~icalendar.error.InvalidCalendar: If required fields are missing 

600 or ``duration`` and ``repeat`` are not both provided together. 

601 

602 Example: 

603 Create an audio alarm using a custom sound file: 

604 

605 .. code-block:: pycon 

606 

607 >>> from datetime import timedelta 

608 >>> from icalendar import Alarm 

609 >>> alarm = Alarm.new_audio( 

610 ... trigger=timedelta(minutes=-5), 

611 ... attachments="ftp://example.com/pub/sounds/bell-01.aud", 

612 ... ) 

613 >>> print(alarm.to_ical().decode()) 

614 BEGIN:VALARM 

615 ACTION:AUDIO 

616 ATTACH:ftp://example.com/pub/sounds/bell-01.aud 

617 TRIGGER:-PT5M 

618 END:VALARM 

619 """ 

620 if trigger is None: 

621 raise InvalidCalendar("AUDIO alarm requires a trigger") 

622 alarm: Alarm = cls.new( 

623 action="AUDIO", 

624 attachments=attachments, 

625 uid=uid, 

626 links=links, 

627 related_to=related_to, 

628 refids=refids, 

629 concepts=concepts, 

630 ) 

631 alarm.TRIGGER = trigger 

632 alarm._apply_duration_repeat(duration, repeat) 

633 return alarm 

634 

635 @classmethod 

636 def new_email( 

637 cls, 

638 summary: str, 

639 description: str, 

640 trigger: timedelta | datetime, 

641 attendees: ATTENDEE_TYPE_SETTER, 

642 attachments: ATTACHMENTS_TYPE_SETTER = None, 

643 duration: timedelta | None = None, 

644 repeat: int | None = None, 

645 uid: str | uuid.UUID | None = None, 

646 links: LINKS_TYPE_SETTER = None, 

647 related_to: RELATED_TO_TYPE_SETTER = None, 

648 refids: list[str] | str | None = None, 

649 concepts: CONCEPTS_TYPE_SETTER = None, 

650 ) -> Alarm: 

651 """Create a new EMAIL alarm that sends an email notification. 

652 

653 An EMAIL alarm sends an email to each address in ``attendees`` when 

654 the alarm fires. 

655 

656 Conforms to :rfc:`5545#section-3.6.6`. 

657 

658 Parameters: 

659 attendees: Required. One or more recipient addresses as email strings or 

660 :class:`~icalendar.prop.cal_address.vCalAddress` instances. A 

661 single address or a sequence of addresses. At least one is 

662 required. 

663 description: Required. Body of the email. 

664 Corresponds to the :attr:`description` property. 

665 summary: Required. Subject line of the email. 

666 Corresponds to the :attr:`summary` property. 

667 trigger: Required. When the alarm fires, as a :class:`~datetime.timedelta` 

668 relative to the event start (negative means before) or as an 

669 absolute :class:`~datetime.datetime` (recommend UTC-aware). 

670 attachments: The :attr:`attachments` of the alarm. A single value 

671 or a sequence of them. Both URIs and binary data are accepted. 

672 concepts: The :attr:`~icalendar.cal.component.Component.concepts` of the alarm. 

673 duration: Gap between repeated triggers. Must be paired with 

674 ``repeat``. Corresponds to the :attr:`DURATION` property. 

675 links: The :attr:`~icalendar.cal.component.Component.links` of the alarm. 

676 refids: The :attr:`~icalendar.cal.component.Component.refids` of the alarm. 

677 related_to: The :attr:`~icalendar.cal.component.Component.related_to` of the alarm. 

678 repeat: Number of *additional* times to fire after the initial 

679 trigger. Must be paired with ``duration``. 

680 Corresponds to the :attr:`REPEAT` property. 

681 uid: Unique identifier for the alarm or ``None``. 

682 

683 Returns: 

684 :class:`Alarm` with ``ACTION:EMAIL`` set. 

685 

686 Raises: 

687 ~icalendar.error.InvalidCalendar: If required fields are missing, 

688 ``attendees`` is empty, or ``duration`` and ``repeat`` are not 

689 both provided together. 

690 

691 Example: 

692 Create an email alarm sent to two recipients. Plain email strings 

693 and ``mailto:``-prefixed strings are both accepted and normalized 

694 to :class:`~icalendar.prop.cal_address.vCalAddress`: 

695 

696 .. code-block:: pycon 

697 

698 >>> from datetime import timedelta 

699 >>> from icalendar import Alarm 

700 >>> alarm = Alarm.new_email( 

701 ... summary="Meeting reminder", 

702 ... description="Your meeting starts in 30 minutes.", 

703 ... trigger=timedelta(minutes=-30), 

704 ... attendees=["user@example.com", "mailto:boss@example.com"], 

705 ... ) 

706 >>> print(alarm.to_ical().decode()) 

707 BEGIN:VALARM 

708 ACTION:EMAIL 

709 ATTENDEE:mailto:user@example.com 

710 ATTENDEE:mailto:boss@example.com 

711 DESCRIPTION:Your meeting starts in 30 minutes. 

712 SUMMARY:Meeting reminder 

713 TRIGGER:-PT30M 

714 END:VALARM 

715 """ 

716 if isinstance(attendees, (str, vCalAddress)): 

717 attendees = [attendees] 

718 elif attendees is None: 

719 attendees = [] 

720 cleaned_attendees = [ 

721 a 

722 for a in attendees 

723 if a is not None and (not isinstance(a, str) or a.strip()) 

724 ] 

725 if not summary: 

726 raise InvalidCalendar("EMAIL alarm requires a summary") 

727 if not description: 

728 raise InvalidCalendar("EMAIL alarm requires a description") 

729 if trigger is None: 

730 raise InvalidCalendar("EMAIL alarm requires a trigger") 

731 if not cleaned_attendees: 

732 raise InvalidCalendar("EMAIL alarm requires at least one valid attendee") 

733 alarm: Alarm = cls.new( 

734 action="EMAIL", 

735 attachments=attachments, 

736 summary=summary, 

737 description=description, 

738 uid=uid, 

739 attendees=cleaned_attendees, 

740 links=links, 

741 related_to=related_to, 

742 refids=refids, 

743 concepts=concepts, 

744 ) 

745 alarm.TRIGGER = trigger 

746 alarm._apply_duration_repeat(duration, repeat) 

747 return alarm 

748 

749 @classmethod 

750 def example(cls, name: str = "example") -> Alarm: 

751 """Return the alarm example with the given name.""" 

752 return cls.from_ical(get_example("alarms", name)) 

753 

754 

755__all__ = ["Alarm"]