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

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

406 

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

408 

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. 

423 

424 Returns: 

425 :class:`Alarm` 

426 

427 Raises: 

428 ~icalendar.error.InvalidCalendar: If the content is not valid 

429 according to :rfc:`5545`. 

430 

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 

454 

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 

467 

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. 

482 

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. 

485 

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

487 

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

504 

505 Returns: 

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

507 

508 Raises: 

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

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

511 

512 Example: 

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

514 

515 .. code-block:: pycon 

516 

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 

529 

530 Attach the alarm to an event: 

531 

532 .. code-block:: python 

533 

534 from datetime import datetime, timedelta, timezone 

535 from icalendar import Alarm, Event 

536 

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 

563 

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. 

578 

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. 

582 

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

584 

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

603 

604 Returns: 

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

606 

607 Raises: 

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

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

610 

611 Example: 

612 Create an audio alarm using a custom sound file: 

613 

614 .. code-block:: pycon 

615 

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 

643 

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. 

661 

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

663 the alarm fires. 

664 

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

666 

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

691 

692 Returns: 

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

694 

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. 

699 

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

704 

705 .. code-block:: pycon 

706 

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 

757 

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

762 

763 

764__all__ = ["Alarm"]