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