1""":rfc:`5545` VEVENT component."""
2
3from __future__ import annotations
4
5import uuid
6from datetime import date, datetime, timedelta
7from typing import TYPE_CHECKING, Literal, Sequence
8
9from icalendar.attr import (
10 X_MOZ_LASTACK_property,
11 X_MOZ_SNOOZE_TIME_property,
12 attendees_property,
13 categories_property,
14 class_property,
15 color_property,
16 contacts_property,
17 create_single_property,
18 description_property,
19 exdates_property,
20 location_property,
21 organizer_property,
22 priority_property,
23 property_del_duration,
24 property_doc_duration_template,
25 property_get_duration,
26 property_set_duration,
27 rdates_property,
28 rrules_property,
29 sequence_property,
30 status_property,
31 summary_property,
32 transparency_property,
33 uid_property,
34 url_property,
35)
36from icalendar.cal.component import Component
37from icalendar.cal.examples import get_example
38from icalendar.error import IncompleteComponent, InvalidCalendar
39from icalendar.tools import is_date
40
41if TYPE_CHECKING:
42 from icalendar.alarms import Alarms
43 from icalendar.enums import CLASS, STATUS, TRANSP
44 from icalendar.prop import vCalAddress
45
46
47class Event(Component):
48 """A grouping of component properties that describe an event.
49
50 Description:
51 A "VEVENT" calendar component is a grouping of
52 component properties, possibly including "VALARM" calendar
53 components, that represents a scheduled amount of time on a
54 calendar. For example, it can be an activity; such as a one-hour
55 long, department meeting from 8:00 AM to 9:00 AM, tomorrow.
56 Generally, an event will take up time on an individual calendar.
57 Hence, the event will appear as an opaque interval in a search for
58 busy time. Alternately, the event can have its Time Transparency
59 set to "TRANSPARENT" in order to prevent blocking of the event in
60 searches for busy time.
61
62 The "VEVENT" is also the calendar component used to specify an
63 anniversary or daily reminder within a calendar. These events
64 have a DATE value type for the "DTSTART" property instead of the
65 default value type of DATE-TIME. If such a "VEVENT" has a "DTEND"
66 property, it MUST be specified as a DATE value also. The
67 anniversary type of "VEVENT" can span more than one date (i.e.,
68 "DTEND" property value is set to a calendar date after the
69 "DTSTART" property value). If such a "VEVENT" has a "DURATION"
70 property, it MUST be specified as a "dur-day" or "dur-week" value.
71
72 The "DTSTART" property for a "VEVENT" specifies the inclusive
73 start of the event. For recurring events, it also specifies the
74 very first instance in the recurrence set. The "DTEND" property
75 for a "VEVENT" calendar component specifies the non-inclusive end
76 of the event. For cases where a "VEVENT" calendar component
77 specifies a "DTSTART" property with a DATE value type but no
78 "DTEND" nor "DURATION" property, the event's duration is taken to
79 be one day. For cases where a "VEVENT" calendar component
80 specifies a "DTSTART" property with a DATE-TIME value type but no
81 "DTEND" property, the event ends on the same calendar date and
82 time of day specified by the "DTSTART" property.
83
84 The "VEVENT" calendar component cannot be nested within another
85 calendar component. However, "VEVENT" calendar components can be
86 related to each other or to a "VTODO" or to a "VJOURNAL" calendar
87 component with the "RELATED-TO" property.
88
89 Examples:
90 The following is an example of the "VEVENT" calendar
91 component used to represent a meeting that will also be opaque to
92 searches for busy time:
93
94 .. code-block:: text
95
96 BEGIN:VEVENT
97 UID:19970901T130000Z-123401@example.com
98 DTSTAMP:19970901T130000Z
99 DTSTART:19970903T163000Z
100 DTEND:19970903T190000Z
101 SUMMARY:Annual Employee Review
102 CLASS:PRIVATE
103 CATEGORIES:BUSINESS,HUMAN RESOURCES
104 END:VEVENT
105
106 The following is an example of the "VEVENT" calendar component
107 used to represent a reminder that will not be opaque, but rather
108 transparent, to searches for busy time:
109
110 .. code-block:: text
111
112 BEGIN:VEVENT
113 UID:19970901T130000Z-123402@example.com
114 DTSTAMP:19970901T130000Z
115 DTSTART:19970401T163000Z
116 DTEND:19970402T010000Z
117 SUMMARY:Laurel is in sensitivity awareness class.
118 CLASS:PUBLIC
119 CATEGORIES:BUSINESS,HUMAN RESOURCES
120 TRANSP:TRANSPARENT
121 END:VEVENT
122
123 The following is an example of the "VEVENT" calendar component
124 used to represent an anniversary that will occur annually:
125
126 .. code-block:: text
127
128 BEGIN:VEVENT
129 UID:19970901T130000Z-123403@example.com
130 DTSTAMP:19970901T130000Z
131 DTSTART;VALUE=DATE:19971102
132 SUMMARY:Our Blissful Anniversary
133 TRANSP:TRANSPARENT
134 CLASS:CONFIDENTIAL
135 CATEGORIES:ANNIVERSARY,PERSONAL,SPECIAL OCCASION
136 RRULE:FREQ=YEARLY
137 END:VEVENT
138
139 The following is an example of the "VEVENT" calendar component
140 used to represent a multi-day event scheduled from June 28th, 2007
141 to July 8th, 2007 inclusively. Note that the "DTEND" property is
142 set to July 9th, 2007, since the "DTEND" property specifies the
143 non-inclusive end of the event.
144
145 .. code-block:: text
146
147 BEGIN:VEVENT
148 UID:20070423T123432Z-541111@example.com
149 DTSTAMP:20070423T123432Z
150 DTSTART;VALUE=DATE:20070628
151 DTEND;VALUE=DATE:20070709
152 SUMMARY:Festival International de Jazz de Montreal
153 TRANSP:TRANSPARENT
154 END:VEVENT
155
156 Create a new Event:
157
158 .. code-block:: python
159
160 >>> from icalendar import Event
161 >>> from datetime import datetime
162 >>> event = Event.new(start=datetime(2021, 1, 1, 12, 30, 0))
163 >>> print(event.to_ical())
164 BEGIN:VEVENT
165 DTSTART:20210101T123000
166 DTSTAMP:20250517T080612Z
167 UID:d755cef5-2311-46ed-a0e1-6733c9e15c63
168 END:VEVENT
169
170 """
171
172 name = "VEVENT"
173
174 canonical_order = (
175 "SUMMARY",
176 "DTSTART",
177 "DTEND",
178 "DURATION",
179 "DTSTAMP",
180 "UID",
181 "RECURRENCE-ID",
182 "SEQUENCE",
183 "RRULE",
184 "RDATE",
185 "EXDATE",
186 )
187
188 required = (
189 "UID",
190 "DTSTAMP",
191 )
192 singletons = (
193 "CLASS",
194 "CREATED",
195 "COLOR",
196 "DESCRIPTION",
197 "DTSTART",
198 "GEO",
199 "LAST-MODIFIED",
200 "LOCATION",
201 "ORGANIZER",
202 "PRIORITY",
203 "DTSTAMP",
204 "SEQUENCE",
205 "STATUS",
206 "SUMMARY",
207 "TRANSP",
208 "URL",
209 "RECURRENCE-ID",
210 "DTEND",
211 "DURATION",
212 "UID",
213 )
214 exclusive = (
215 "DTEND",
216 "DURATION",
217 )
218 multiple = (
219 "ATTACH",
220 "ATTENDEE",
221 "CATEGORIES",
222 "COMMENT",
223 "CONTACT",
224 "EXDATE",
225 "RSTATUS",
226 "RELATED",
227 "RESOURCES",
228 "RDATE",
229 "RRULE",
230 )
231 ignore_exceptions = True
232
233 @property
234 def alarms(self) -> Alarms:
235 """Compute the alarm times for this component.
236
237 >>> from icalendar import Event
238 >>> event = Event.example("rfc_9074_example_1")
239 >>> len(event.alarms.times)
240 1
241 >>> alarm_time = event.alarms.times[0]
242 >>> alarm_time.trigger # The time when the alarm pops up
243 datetime.datetime(2021, 3, 2, 10, 15, tzinfo=ZoneInfo(key='America/New_York'))
244 >>> alarm_time.is_active() # This alarm has not been acknowledged
245 True
246
247 Note that this only uses DTSTART and DTEND, but ignores
248 RDATE, EXDATE, and RRULE properties.
249 """
250 from icalendar.alarms import Alarms
251
252 return Alarms(self)
253
254 @classmethod
255 def example(cls, name: str = "rfc_9074_example_3") -> Event:
256 """Return the calendar example with the given name."""
257 return cls.from_ical(get_example("events", name))
258
259 DTSTART = create_single_property(
260 "DTSTART",
261 "dt",
262 (datetime, date),
263 date,
264 'The "DTSTART" property for a "VEVENT" specifies the inclusive start of the event.', # noqa: E501
265 )
266 DTEND = create_single_property(
267 "DTEND",
268 "dt",
269 (datetime, date),
270 date,
271 'The "DTEND" property for a "VEVENT" calendar component specifies the non-inclusive end of the event.', # noqa: E501
272 )
273
274 def _get_start_end_duration(self):
275 """Verify the calendar validity and return the right attributes."""
276 start = self.DTSTART
277 end = self.DTEND
278 duration = self.DURATION
279 if duration is not None and end is not None:
280 raise InvalidCalendar(
281 "Only one of DTEND and DURATION may be in a VEVENT, not both."
282 )
283 if (
284 start is not None
285 and is_date(start)
286 and duration is not None
287 and duration.seconds != 0
288 ):
289 raise InvalidCalendar(
290 "When DTSTART is a date, DURATION must be of days or weeks."
291 )
292 if start is not None and end is not None and is_date(start) != is_date(end):
293 raise InvalidCalendar(
294 "DTSTART and DTEND must be of the same type, either date or datetime."
295 )
296 return start, end, duration
297
298 DURATION = property(
299 property_get_duration,
300 property_set_duration,
301 property_del_duration,
302 property_doc_duration_template.format(component="VEVENT"),
303 )
304
305 @property
306 def duration(self) -> timedelta:
307 """The duration of the VEVENT.
308
309 Returns the DURATION property if set, otherwise calculated from start and end.
310 When setting duration, the end time is automatically calculated from start +
311 duration.
312 You can set the duration to automatically adjust the end time while keeping
313 start locked.
314
315 Setting the duration will:
316 1. Keep the start time locked (unchanged)
317 2. Adjust the end time to start + duration
318 3. Remove any existing DTEND property
319 4. Set the DURATION property
320 """
321 # First check if DURATION property is explicitly set
322 if "DURATION" in self:
323 return self["DURATION"].dt
324
325 # Fall back to calculated duration from start and end
326 return self.end - self.start
327
328 @duration.setter
329 def duration(self, value: timedelta):
330 if not isinstance(value, timedelta):
331 raise TypeError(f"Use timedelta, not {type(value).__name__}.")
332
333 # Use the set_duration method with default start-locked behavior
334 self.set_duration(value, locked="start")
335
336 @property
337 def start(self) -> date | datetime:
338 """The start of the event.
339
340 Invalid values raise an InvalidCalendar.
341 If there is no start, we also raise an IncompleteComponent error.
342
343 You can get the start, end and duration of an event as follows:
344
345 >>> from datetime import datetime
346 >>> from icalendar import Event
347 >>> event = Event()
348 >>> event.start = datetime(2021, 1, 1, 12)
349 >>> event.end = datetime(2021, 1, 1, 12, 30) # 30 minutes
350 >>> event.duration # 1800 seconds == 30 minutes
351 datetime.timedelta(seconds=1800)
352 >>> print(event.to_ical())
353 BEGIN:VEVENT
354 DTSTART:20210101T120000
355 DTEND:20210101T123000
356 END:VEVENT
357 """
358 start = self._get_start_end_duration()[0]
359 if start is None:
360 raise IncompleteComponent("No DTSTART given.")
361 return start
362
363 @start.setter
364 def start(self, start: date | datetime | None):
365 """Set the start."""
366 self.DTSTART = start
367
368 @property
369 def end(self) -> date | datetime:
370 """The end of the event.
371
372 Invalid values raise an InvalidCalendar error.
373 If there is no end, we also raise an IncompleteComponent error.
374 """
375 start, end, duration = self._get_start_end_duration()
376 if end is None and duration is None:
377 if start is None:
378 raise IncompleteComponent("No DTEND or DURATION+DTSTART given.")
379 if is_date(start):
380 return start + timedelta(days=1)
381 return start
382 if duration is not None:
383 if start is not None:
384 return start + duration
385 raise IncompleteComponent("No DTEND or DURATION+DTSTART given.")
386 return end
387
388 @end.setter
389 def end(self, end: date | datetime | None):
390 """Set the end."""
391 self.DTEND = end
392
393 def set_duration(
394 self, duration: timedelta | None, locked: Literal["start", "end"] = "start"
395 ):
396 """Set the duration of the event relative to either start or end.
397
398 Args:
399 duration: The duration to set, or None to convert to DURATION property
400 locked: Which property to keep unchanged ('start' or 'end')
401 """
402 from icalendar.attr import set_duration_with_locking
403
404 set_duration_with_locking(self, duration, locked, "DTEND")
405
406 def set_start(
407 self, start: date | datetime, locked: Literal["duration", "end"] | None = None
408 ):
409 """Set the start and keep the duration or end of the event.
410
411 Args:
412 start: The start time to set
413 locked: Which property to keep unchanged ('duration', 'end', or None
414 for auto-detect)
415 """
416 if locked is None:
417 # Auto-detect based on existing properties
418 if "DURATION" in self:
419 locked = "duration"
420 elif "DTEND" in self:
421 locked = "end"
422 else:
423 # Default to duration if no existing properties
424 locked = "duration"
425
426 if locked == "duration":
427 # Keep duration locked, adjust end
428 current_duration = (
429 self.duration if "DURATION" in self or "DTEND" in self else None
430 )
431 self.DTSTART = start
432 if current_duration is not None:
433 self.DURATION = current_duration
434 elif locked == "end":
435 # Keep end locked, adjust duration
436 current_end = self.end
437 self.DTSTART = start
438 self.pop("DURATION", None)
439 self.DTEND = current_end
440 else:
441 raise ValueError(
442 f"locked must be 'duration', 'end', or None, not {locked!r}"
443 )
444
445 def set_end(
446 self, end: date | datetime, locked: Literal["start", "duration"] = "start"
447 ):
448 """Set the end of the component, keeping either the start or the duration same.
449
450 Args:
451 end: The end time to set
452 locked: Which property to keep unchanged ('start' or 'duration')
453 """
454 if locked == "start":
455 # Keep start locked, adjust duration
456 self.pop("DURATION", None)
457 self.DTEND = end
458 elif locked == "duration":
459 # Keep duration locked, adjust start
460 current_duration = self.duration
461 self.DTSTART = end - current_duration
462 self.DURATION = current_duration
463 else:
464 raise ValueError(f"locked must be 'start' or 'duration', not {locked!r}")
465
466 X_MOZ_SNOOZE_TIME = X_MOZ_SNOOZE_TIME_property
467 X_MOZ_LASTACK = X_MOZ_LASTACK_property
468 color = color_property
469 sequence = sequence_property
470 categories = categories_property
471 rdates = rdates_property
472 exdates = exdates_property
473 rrules = rrules_property
474 uid = uid_property
475 summary = summary_property
476 description = description_property
477 classification = class_property
478 url = url_property
479 organizer = organizer_property
480 location = location_property
481 priority = priority_property
482 contacts = contacts_property
483 transparency = transparency_property
484 status = status_property
485 attendees = attendees_property
486
487 @classmethod
488 def new(
489 cls,
490 /,
491 attendees: list[vCalAddress] | None = None,
492 categories: Sequence[str] = (),
493 classification: CLASS | None = None,
494 color: str | None = None,
495 comments: list[str] | str | None = None,
496 contacts: list[str] | str | None = None,
497 created: date | None = None,
498 description: str | None = None,
499 end: date | datetime | None = None,
500 last_modified: date | None = None,
501 location: str | None = None,
502 organizer: vCalAddress | str | None = None,
503 priority: int | None = None,
504 sequence: int | None = None,
505 stamp: date | None = None,
506 start: date | datetime | None = None,
507 status: STATUS | None = None,
508 transparency: TRANSP | None = None,
509 summary: str | None = None,
510 uid: str | uuid.UUID | None = None,
511 url: str | None = None,
512 ):
513 """Create a new event with all required properties.
514
515 This creates a new Event in accordance with :rfc:`5545`.
516
517 Arguments:
518 attendees: The :attr:`attendees` of the event.
519 categories: The :attr:`categories` of the event.
520 classification: The :attr:`classification` of the event.
521 color: The :attr:`color` of the event.
522 comments: The :attr:`Component.comments` of the event.
523 created: The :attr:`Component.created` of the event.
524 description: The :attr:`description` of the event.
525 end: The :attr:`end` of the event.
526 last_modified: The :attr:`Component.last_modified` of the event.
527 location: The :attr:`location` of the event.
528 organizer: The :attr:`organizer` of the event.
529 priority: The :attr:`priority` of the event.
530 sequence: The :attr:`sequence` of the event.
531 stamp: The :attr:`Component.stamp` of the event.
532 If None, this is set to the current time.
533 start: The :attr:`start` of the event.
534 status: The :attr:`status` of the event.
535 summary: The :attr:`summary` of the event.
536 transparency: The :attr:`transparency` of the event.
537 uid: The :attr:`uid` of the event.
538 If None, this is set to a new :func:`uuid.uuid4`.
539 url: The :attr:`url` of the event.
540
541 Returns:
542 :class:`Event`
543
544 Raises:
545 InvalidCalendar: If the content is not valid according to :rfc:`5545`.
546
547 .. warning:: As time progresses, we will be stricter with the validation.
548 """
549 event = super().new(
550 stamp=stamp if stamp is not None else cls._utc_now(),
551 created=created,
552 last_modified=last_modified,
553 comments=comments,
554 )
555 event.summary = summary
556 event.description = description
557 event.uid = uid if uid is not None else uuid.uuid4()
558 event.start = start
559 event.end = end
560 event.color = color
561 event.categories = categories
562 event.sequence = sequence
563 event.classification = classification
564 event.url = url
565 event.organizer = organizer
566 event.location = location
567 event.priority = priority
568 event.transparency = transparency
569 event.contacts = contacts
570 event.status = status
571 event.attendees = attendees
572 if cls._validate_new:
573 cls._validate_start_and_end(start, end)
574 return event
575
576
577__all__ = ["Event"]