ical.event

A grouping of component properties that describe a calendar event.

An event can be an activity (e.g. a meeting from 8am to 9am tomorrow) grouping of properties such as a summary or a description. An event will take up time on a calendar as an opaque time interval, but can alternatively have transparency set to transparent to prevent blocking of time as busy.

An event start and end time may either be a date and time or just a day alone. Events may also span more than one day. Alternatively, an event can have a start and a duration.

  1"""A grouping of component properties that describe a calendar event.
  2
  3An event can be an activity (e.g. a meeting from 8am to 9am tomorrow)
  4grouping of properties such as a summary or a description. An event will
  5take up time on a calendar as an opaque time interval, but can alternatively
  6have transparency set to transparent to prevent blocking of time as busy.
  7
  8An event start and end time may either be a date and time or just a day
  9alone. Events may also span more than one day. Alternatively, an event
 10can have a start and a duration.
 11"""
 12
 13# pylint: disable=unnecessary-lambda
 14
 15from __future__ import annotations
 16
 17import datetime
 18import enum
 19import logging
 20from collections.abc import Iterable
 21from typing import Annotated, Any, Optional, Self, Union
 22
 23from pydantic import BeforeValidator, Field, field_serializer, model_validator
 24
 25from ical.compat import same_day_dtend_compat
 26from ical.types.data_types import serialize_field
 27
 28from .alarm import Alarm
 29from .component import (
 30    ComponentModel,
 31    validate_duration_unit,
 32    validate_until_dtstart,
 33    validate_recurrence_dates,
 34)
 35from .iter import RulesetIterable, as_rrule
 36from .timespan import Timespan
 37from .types import (
 38    CalAddress,
 39    Classification,
 40    ExtraProperty,
 41    Geo,
 42    Priority,
 43    Recur,
 44    RecurrenceId,
 45    RequestStatus,
 46    Uri,
 47    RelatedTo,
 48)
 49from .util import (
 50    dtstamp_factory,
 51    normalize_datetime,
 52    parse_date_and_datetime,
 53    parse_date_and_datetime_list,
 54    uid_factory,
 55)
 56
 57_LOGGER = logging.getLogger(__name__)
 58
 59
 60class EventStatus(str, enum.Enum):
 61    """Status or confirmation of the event set by the organizer."""
 62
 63    CONFIRMED = "CONFIRMED"
 64    """Indicates event is definite."""
 65
 66    TENTATIVE = "TENTATIVE"
 67    """Indicates event is tentative."""
 68
 69    CANCELLED = "CANCELLED"
 70    """Indicates event was cancelled."""
 71
 72
 73class Event(ComponentModel):
 74    """A single event on a calendar.
 75
 76    Can either be for a specific day, or with a start time and duration/end time.
 77
 78    The dtstamp and uid functions have factory methods invoked with a lambda to facilitate
 79    mocking in unit tests.
 80
 81
 82    Example:
 83    ```python
 84    import datetime
 85    from ical.event import Event
 86
 87    event = Event(
 88        dtstart=datetime.datetime(2022, 8, 31, 7, 00, 00),
 89        dtend=datetime.datetime(2022, 8, 31, 7, 30, 00),
 90        summary="Morning exercise",
 91    )
 92    print("The event duration is: ", event.computed_duration)
 93    ```
 94
 95    An Event is a pydantic model, so all properties of a pydantic model apply here to such as
 96    the constructor arguments, properties to return the model as a dictionary or json, as well
 97    as other parsing methods.
 98    """
 99
100    dtstamp: Annotated[
101        Union[datetime.date, datetime.datetime],
102        BeforeValidator(parse_date_and_datetime),
103    ] = Field(default_factory=lambda: dtstamp_factory())
104    """Specifies the date and time the event was created."""
105
106    uid: str = Field(default_factory=lambda: uid_factory())
107    """A globally unique identifier for the event."""
108
109    # Has an alias of 'start'
110    dtstart: Annotated[
111        Union[datetime.date, datetime.datetime, None],
112        BeforeValidator(parse_date_and_datetime),
113    ] = Field(default=None)
114    """The start time or start day of the event."""
115
116    # Has an alias of 'end'
117    dtend: Annotated[
118        Union[datetime.date, datetime.datetime, None],
119        BeforeValidator(parse_date_and_datetime),
120    ] = None
121    """The end time or end day of the event.
122
123    This may be specified as an explicit date. Alternatively, a duration
124    can be used instead.
125    """
126
127    duration: Optional[datetime.timedelta] = None
128    """The duration of the event as an alternative to an explicit end date/time."""
129
130    summary: Optional[str] = None
131    """Defines a short summary or subject for the event."""
132
133    attendees: list[CalAddress] = Field(alias="attendee", default_factory=list)
134    """Specifies participants in a group-scheduled calendar."""
135
136    categories: list[str] = Field(default_factory=list)
137    """Defines the categories for an event.
138
139    Specifies a category or subtype. Can be useful for searching for a particular
140    type of event.
141    """
142
143    classification: Optional[Classification] = Field(alias="class", default=None)
144    """An access classification for a calendar event.
145
146    This provides a method of capturing the scope of access of a calendar, in
147    conjunction with an access control system.
148    """
149
150    comment: list[str] = Field(default_factory=list)
151    """Specifies a comment to the calendar user."""
152
153    contacts: list[str] = Field(alias="contact", default_factory=list)
154    """Contact information associated with the event."""
155
156    created: Optional[datetime.datetime] = None
157    """The date and time the event information was created."""
158
159    description: Optional[str] = None
160    """A more complete description of the event than provided by the summary."""
161
162    geo: Optional[Geo] = None
163    """Specifies a latitude and longitude global position for the event activity."""
164
165    last_modified: Optional[datetime.datetime] = Field(
166        alias="last-modified", default=None
167    )
168
169    location: Optional[str] = None
170    """Defines the intended venue for the activity defined by this event."""
171
172    organizer: Optional[CalAddress] = None
173    """The organizer of a group-scheduled calendar entity."""
174
175    priority: Optional[Priority] = None
176    """Defines the relative priority of the calendar event."""
177
178    recurrence_id: Optional[RecurrenceId] = Field(alias="recurrence-id", default=None)
179    """Defines a specific instance of a recurring event.
180
181    The full range of calendar events specified by a recurrence set is referenced
182    by referring to just the uid. The `recurrence_id` allows reference of an individual
183    instance within the recurrence set.
184    """
185
186    related_to: list[RelatedTo] = Field(alias="related-to", default_factory=list)
187    """Used to represent a relationship or reference between events."""
188
189    related: list[str] = Field(default_factory=list)
190    """Unused and will be deleted in a future release"""
191
192    resources: list[str] = Field(default_factory=list)
193    """Defines the equipment or resources anticipated for the calendar event."""
194
195    rrule: Optional[Recur] = None
196    """A recurrence rule specification.
197
198    Defines a rule for specifying a repeated event. The recurrence set is the complete
199    set of recurrence instances for a calendar component (based on rrule, rdate, exdate).
200    The recurrence set is generated by gathering the rrule and rdate properties then
201    excluding any times specified by exdate. The recurrence is generated with the dtstart
202    property defining the first instance of the recurrence set.
203
204    Typically a dtstart should be specified with a date local time and timezone to make
205    sure all instances have the same start time regardless of time zone changing.
206    """
207
208    rdate: Annotated[
209        list[Union[datetime.date, datetime.datetime]],
210        BeforeValidator(parse_date_and_datetime_list),
211    ] = Field(default_factory=list)
212    """Defines the list of date/time values for recurring events.
213
214    Can appear along with the rrule property to define a set of repeating occurrences of the
215    event. The recurrence set is the complete set of recurrence instances for a calendar component
216    (based on rrule, rdate, exdate). The recurrence set is generated by gathering the rrule
217    and rdate properties then excluding any times specified by exdate.
218    """
219
220    exdate: Annotated[
221        list[Union[datetime.date, datetime.datetime]],
222        BeforeValidator(parse_date_and_datetime_list),
223    ] = Field(default_factory=list)
224    """Defines the list of exceptions for recurring events.
225
226    The exception dates are used in computing the recurrence set. The recurrence set is
227    the complete set of recurrence instances for a calendar component (based on rrule, rdate,
228    exdate). The recurrence set is generated by gathering the rrule and rdate properties
229    then excluding any times specified by exdate.
230    """
231
232    request_status: Optional[RequestStatus] = Field(
233        default=None,
234        alias="request-status",
235    )
236
237    sequence: Optional[int] = None
238    """The revision sequence number in the calendar component.
239
240    When an event is created, its sequence number is 0. It is monotonically incremented
241    by the organizer's calendar user agent every time a significant revision is made to
242    the calendar event.
243    """
244
245    status: Optional[EventStatus] = None
246    """Defines the overall status or confirmation of the event.
247
248    In a group-scheduled calendar, used by the organizer to provide a confirmation
249    of the event to attendees.
250    """
251
252    transparency: Optional[str] = Field(alias="transp", default=None)
253    """Defines whether or not an event is transparent to busy time searches."""
254
255    url: Optional[Uri] = None
256    """Defines a url associated with the event.
257
258    May convey a location where a more dynamic rendition of the calendar event
259    information associated with the event can be found.
260    """
261
262    # Unknown or unsupported properties
263    extras: list[ExtraProperty] = Field(default_factory=list)
264
265    alarm: list[Alarm] = Field(alias="valarm", default_factory=list)
266    """A grouping of reminder alarms for the event."""
267
268    def __init__(self, **data: Any) -> None:
269        """Initialize a Calendar Event.
270
271        This method accepts keyword args with field names on the Calendar such as `summary`,
272        `start`, `end`, `description`, etc.
273        """
274        if "start" in data:
275            data["dtstart"] = data.pop("start")
276        if "end" in data:
277            data["dtend"] = data.pop("end")
278        super().__init__(**data)
279
280    @property
281    def start(self) -> datetime.datetime | datetime.date:
282        """Return the start time for the event."""
283        assert self.dtstart is not None
284        return self.dtstart
285
286    @property
287    def end(self) -> datetime.datetime | datetime.date:
288        """Return the end time for the event."""
289        if self.duration:
290            return self.start + self.duration
291        if self.dtend:
292            return self.dtend
293
294        if isinstance(self.start, datetime.datetime):
295            return self.start
296        return self.start + datetime.timedelta(days=1)
297
298    @property
299    def start_datetime(self) -> datetime.datetime:
300        """Return the events start as a datetime in UTC"""
301        return normalize_datetime(self.start).astimezone(datetime.timezone.utc)
302
303    @property
304    def end_datetime(self) -> datetime.datetime:
305        """Return the events end as a datetime in UTC."""
306        return normalize_datetime(self.end).astimezone(datetime.timezone.utc)
307
308    @property
309    def computed_duration(self) -> datetime.timedelta:
310        """Return the event duration."""
311        if self.duration is not None:
312            return self.duration
313        return self.end - self.start
314
315    @property
316    def timespan(self) -> Timespan:
317        """Return a timespan representing the event start and end."""
318        return Timespan.of(self.start, self.end)
319
320    def timespan_of(self, tzinfo: datetime.tzinfo) -> Timespan:
321        """Return a timespan representing the event start and end."""
322        return Timespan.of(
323            normalize_datetime(self.start, tzinfo), normalize_datetime(self.end, tzinfo)
324        )
325
326    def starts_within(self, other: "Event") -> bool:
327        """Return True if this event starts while the other event is active."""
328        return self.timespan.starts_within(other.timespan)
329
330    def ends_within(self, other: "Event") -> bool:
331        """Return True if this event ends while the other event is active."""
332        return self.timespan.ends_within(other.timespan)
333
334    def intersects(self, other: "Event") -> bool:
335        """Return True if this event overlaps with the other event."""
336        return self.timespan.intersects(other.timespan)
337
338    def includes(self, other: "Event") -> bool:
339        """Return True if the other event starts and ends within this event."""
340        return self.timespan.includes(other.timespan)
341
342    def is_included_in(self, other: "Event") -> bool:
343        """Return True if this event starts and ends within the other event."""
344        return self.timespan.is_included_in(other.timespan)
345
346    def __lt__(self, other: Any) -> bool:
347        if not isinstance(other, Event):
348            return NotImplemented
349        return self.timespan < other.timespan
350
351    def __gt__(self, other: Any) -> bool:
352        if not isinstance(other, Event):
353            return NotImplemented
354        return self.timespan > other.timespan
355
356    def __le__(self, other: Any) -> bool:
357        if not isinstance(other, Event):
358            return NotImplemented
359        return self.timespan <= other.timespan
360
361    def __ge__(self, other: Any) -> bool:
362        if not isinstance(other, Event):
363            return NotImplemented
364        return self.timespan >= other.timespan
365
366    @property
367    def recurring(self) -> bool:
368        """Return true if this event is recurring.
369
370        A recurring event is typically evaluated specially on the timeline. The
371        data model has a single event, but the timeline evaluates the recurrence
372        to expand and copy the event to multiple places on the timeline
373        using `as_rrule`.
374        """
375        if self.rrule or self.rdate:
376            return True
377        return False
378
379    def as_rrule(self) -> Iterable[datetime.datetime | datetime.date] | None:
380        """Return an iterable containing the occurrences of a recurring event.
381
382        A recurring event is typically evaluated specially on the timeline. The
383        data model has a single event, but the timeline evaluates the recurrence
384        to expand and copy the event to multiple places on the timeline.
385
386        This is only valid for events where `recurring` is True.
387        """
388        return as_rrule(self.rrule, self.rdate, self.exdate, self.dtstart)
389
390    @model_validator(mode="before")
391    @classmethod
392    def _inspect_date_types(cls, values: dict[str, Any]) -> dict[str, Any]:
393        """Debug the date and date/time values of the event."""
394        dtstart = values.get("dtstart")
395        dtend = values.get("dtend")
396        if not dtstart or not dtend:
397            return values
398        _LOGGER.debug("Found initial values dtstart=%s, dtend=%s", dtstart, dtend)
399        return values
400
401    _validate_until_dtstart = model_validator(mode="after")(validate_until_dtstart)
402    _validate_recurrence_dates = model_validator(mode="after")(
403        validate_recurrence_dates
404    )
405
406    @model_validator(mode="after")
407    def _validate_date_types(self) -> Self:
408        """Validate that start and end values are the same date or datetime type."""
409        dtstart = self.dtstart
410        dtend = self.dtend
411
412        if not dtstart or not dtend:
413            return self
414        if isinstance(dtstart, datetime.datetime):
415            if not isinstance(dtend, datetime.datetime):
416                _LOGGER.debug("Unexpected data types for values: %s", self)
417                raise ValueError(
418                    f"Unexpected dtstart value '{dtstart}' was datetime but "
419                    f"dtend value '{dtend}' was not datetime"
420                )
421        elif isinstance(dtstart, datetime.date):
422            if isinstance(dtend, datetime.datetime):
423                raise ValueError(
424                    f"Unexpected dtstart value '{dtstart}' was date but "
425                    f"dtend value '{dtend}' was datetime"
426                )
427        return self
428
429    @model_validator(mode="after")
430    def _validate_datetime_timezone(self) -> Self:
431        """Validate that start and end values have the same timezone information."""
432        if (
433            not (dtstart := self.dtstart)
434            or not (dtend := self.dtend)
435            or not isinstance(dtstart, datetime.datetime)
436            or not isinstance(dtend, datetime.datetime)
437        ):
438            return self
439        if dtstart.tzinfo is None and dtend.tzinfo is not None:
440            raise ValueError(
441                f"Expected end datetime value in localtime but was {dtend}"
442            )
443        if dtstart.tzinfo is not None and dtend.tzinfo is None:
444            raise ValueError(f"Expected end datetime with timezone but was {dtend}")
445        return self
446
447    @model_validator(mode="after")
448    def _validate_one_end_or_duration(self) -> Self:
449        """Validate that only one of duration or end date may be set."""
450        if self.dtend and self.duration:
451            raise ValueError("Only one of dtend or duration may be set.")
452        return self
453
454    @model_validator(mode="after")
455    def _validate_same_day_dtend(self) -> Self:
456        """Fix same-day DTEND for all-day events when compat mode is enabled."""
457        if same_day_dtend_compat.is_same_day_dtend_compat_enabled():
458            if isinstance(self.dtstart, datetime.date) and not isinstance(self.dtstart, datetime.datetime):
459                if self.dtend and self.dtend == self.dtstart:
460                    self.dtend = self.dtstart + datetime.timedelta(days=1)
461        return self
462
463    _validate_duration_unit = model_validator(mode="after")(validate_duration_unit)
464
465    serialize_fields = field_serializer("*")(serialize_field)  # type: ignore[pydantic-field]
class EventStatus(builtins.str, enum.Enum):
61class EventStatus(str, enum.Enum):
62    """Status or confirmation of the event set by the organizer."""
63
64    CONFIRMED = "CONFIRMED"
65    """Indicates event is definite."""
66
67    TENTATIVE = "TENTATIVE"
68    """Indicates event is tentative."""
69
70    CANCELLED = "CANCELLED"
71    """Indicates event was cancelled."""

Status or confirmation of the event set by the organizer.

CONFIRMED = <EventStatus.CONFIRMED: 'CONFIRMED'>

Indicates event is definite.

TENTATIVE = <EventStatus.TENTATIVE: 'TENTATIVE'>

Indicates event is tentative.

CANCELLED = <EventStatus.CANCELLED: 'CANCELLED'>

Indicates event was cancelled.

class Event(ical.component.ComponentModel):
 74class Event(ComponentModel):
 75    """A single event on a calendar.
 76
 77    Can either be for a specific day, or with a start time and duration/end time.
 78
 79    The dtstamp and uid functions have factory methods invoked with a lambda to facilitate
 80    mocking in unit tests.
 81
 82
 83    Example:
 84    ```python
 85    import datetime
 86    from ical.event import Event
 87
 88    event = Event(
 89        dtstart=datetime.datetime(2022, 8, 31, 7, 00, 00),
 90        dtend=datetime.datetime(2022, 8, 31, 7, 30, 00),
 91        summary="Morning exercise",
 92    )
 93    print("The event duration is: ", event.computed_duration)
 94    ```
 95
 96    An Event is a pydantic model, so all properties of a pydantic model apply here to such as
 97    the constructor arguments, properties to return the model as a dictionary or json, as well
 98    as other parsing methods.
 99    """
100
101    dtstamp: Annotated[
102        Union[datetime.date, datetime.datetime],
103        BeforeValidator(parse_date_and_datetime),
104    ] = Field(default_factory=lambda: dtstamp_factory())
105    """Specifies the date and time the event was created."""
106
107    uid: str = Field(default_factory=lambda: uid_factory())
108    """A globally unique identifier for the event."""
109
110    # Has an alias of 'start'
111    dtstart: Annotated[
112        Union[datetime.date, datetime.datetime, None],
113        BeforeValidator(parse_date_and_datetime),
114    ] = Field(default=None)
115    """The start time or start day of the event."""
116
117    # Has an alias of 'end'
118    dtend: Annotated[
119        Union[datetime.date, datetime.datetime, None],
120        BeforeValidator(parse_date_and_datetime),
121    ] = None
122    """The end time or end day of the event.
123
124    This may be specified as an explicit date. Alternatively, a duration
125    can be used instead.
126    """
127
128    duration: Optional[datetime.timedelta] = None
129    """The duration of the event as an alternative to an explicit end date/time."""
130
131    summary: Optional[str] = None
132    """Defines a short summary or subject for the event."""
133
134    attendees: list[CalAddress] = Field(alias="attendee", default_factory=list)
135    """Specifies participants in a group-scheduled calendar."""
136
137    categories: list[str] = Field(default_factory=list)
138    """Defines the categories for an event.
139
140    Specifies a category or subtype. Can be useful for searching for a particular
141    type of event.
142    """
143
144    classification: Optional[Classification] = Field(alias="class", default=None)
145    """An access classification for a calendar event.
146
147    This provides a method of capturing the scope of access of a calendar, in
148    conjunction with an access control system.
149    """
150
151    comment: list[str] = Field(default_factory=list)
152    """Specifies a comment to the calendar user."""
153
154    contacts: list[str] = Field(alias="contact", default_factory=list)
155    """Contact information associated with the event."""
156
157    created: Optional[datetime.datetime] = None
158    """The date and time the event information was created."""
159
160    description: Optional[str] = None
161    """A more complete description of the event than provided by the summary."""
162
163    geo: Optional[Geo] = None
164    """Specifies a latitude and longitude global position for the event activity."""
165
166    last_modified: Optional[datetime.datetime] = Field(
167        alias="last-modified", default=None
168    )
169
170    location: Optional[str] = None
171    """Defines the intended venue for the activity defined by this event."""
172
173    organizer: Optional[CalAddress] = None
174    """The organizer of a group-scheduled calendar entity."""
175
176    priority: Optional[Priority] = None
177    """Defines the relative priority of the calendar event."""
178
179    recurrence_id: Optional[RecurrenceId] = Field(alias="recurrence-id", default=None)
180    """Defines a specific instance of a recurring event.
181
182    The full range of calendar events specified by a recurrence set is referenced
183    by referring to just the uid. The `recurrence_id` allows reference of an individual
184    instance within the recurrence set.
185    """
186
187    related_to: list[RelatedTo] = Field(alias="related-to", default_factory=list)
188    """Used to represent a relationship or reference between events."""
189
190    related: list[str] = Field(default_factory=list)
191    """Unused and will be deleted in a future release"""
192
193    resources: list[str] = Field(default_factory=list)
194    """Defines the equipment or resources anticipated for the calendar event."""
195
196    rrule: Optional[Recur] = None
197    """A recurrence rule specification.
198
199    Defines a rule for specifying a repeated event. The recurrence set is the complete
200    set of recurrence instances for a calendar component (based on rrule, rdate, exdate).
201    The recurrence set is generated by gathering the rrule and rdate properties then
202    excluding any times specified by exdate. The recurrence is generated with the dtstart
203    property defining the first instance of the recurrence set.
204
205    Typically a dtstart should be specified with a date local time and timezone to make
206    sure all instances have the same start time regardless of time zone changing.
207    """
208
209    rdate: Annotated[
210        list[Union[datetime.date, datetime.datetime]],
211        BeforeValidator(parse_date_and_datetime_list),
212    ] = Field(default_factory=list)
213    """Defines the list of date/time values for recurring events.
214
215    Can appear along with the rrule property to define a set of repeating occurrences of the
216    event. The recurrence set is the complete set of recurrence instances for a calendar component
217    (based on rrule, rdate, exdate). The recurrence set is generated by gathering the rrule
218    and rdate properties then excluding any times specified by exdate.
219    """
220
221    exdate: Annotated[
222        list[Union[datetime.date, datetime.datetime]],
223        BeforeValidator(parse_date_and_datetime_list),
224    ] = Field(default_factory=list)
225    """Defines the list of exceptions for recurring events.
226
227    The exception dates are used in computing the recurrence set. The recurrence set is
228    the complete set of recurrence instances for a calendar component (based on rrule, rdate,
229    exdate). The recurrence set is generated by gathering the rrule and rdate properties
230    then excluding any times specified by exdate.
231    """
232
233    request_status: Optional[RequestStatus] = Field(
234        default=None,
235        alias="request-status",
236    )
237
238    sequence: Optional[int] = None
239    """The revision sequence number in the calendar component.
240
241    When an event is created, its sequence number is 0. It is monotonically incremented
242    by the organizer's calendar user agent every time a significant revision is made to
243    the calendar event.
244    """
245
246    status: Optional[EventStatus] = None
247    """Defines the overall status or confirmation of the event.
248
249    In a group-scheduled calendar, used by the organizer to provide a confirmation
250    of the event to attendees.
251    """
252
253    transparency: Optional[str] = Field(alias="transp", default=None)
254    """Defines whether or not an event is transparent to busy time searches."""
255
256    url: Optional[Uri] = None
257    """Defines a url associated with the event.
258
259    May convey a location where a more dynamic rendition of the calendar event
260    information associated with the event can be found.
261    """
262
263    # Unknown or unsupported properties
264    extras: list[ExtraProperty] = Field(default_factory=list)
265
266    alarm: list[Alarm] = Field(alias="valarm", default_factory=list)
267    """A grouping of reminder alarms for the event."""
268
269    def __init__(self, **data: Any) -> None:
270        """Initialize a Calendar Event.
271
272        This method accepts keyword args with field names on the Calendar such as `summary`,
273        `start`, `end`, `description`, etc.
274        """
275        if "start" in data:
276            data["dtstart"] = data.pop("start")
277        if "end" in data:
278            data["dtend"] = data.pop("end")
279        super().__init__(**data)
280
281    @property
282    def start(self) -> datetime.datetime | datetime.date:
283        """Return the start time for the event."""
284        assert self.dtstart is not None
285        return self.dtstart
286
287    @property
288    def end(self) -> datetime.datetime | datetime.date:
289        """Return the end time for the event."""
290        if self.duration:
291            return self.start + self.duration
292        if self.dtend:
293            return self.dtend
294
295        if isinstance(self.start, datetime.datetime):
296            return self.start
297        return self.start + datetime.timedelta(days=1)
298
299    @property
300    def start_datetime(self) -> datetime.datetime:
301        """Return the events start as a datetime in UTC"""
302        return normalize_datetime(self.start).astimezone(datetime.timezone.utc)
303
304    @property
305    def end_datetime(self) -> datetime.datetime:
306        """Return the events end as a datetime in UTC."""
307        return normalize_datetime(self.end).astimezone(datetime.timezone.utc)
308
309    @property
310    def computed_duration(self) -> datetime.timedelta:
311        """Return the event duration."""
312        if self.duration is not None:
313            return self.duration
314        return self.end - self.start
315
316    @property
317    def timespan(self) -> Timespan:
318        """Return a timespan representing the event start and end."""
319        return Timespan.of(self.start, self.end)
320
321    def timespan_of(self, tzinfo: datetime.tzinfo) -> Timespan:
322        """Return a timespan representing the event start and end."""
323        return Timespan.of(
324            normalize_datetime(self.start, tzinfo), normalize_datetime(self.end, tzinfo)
325        )
326
327    def starts_within(self, other: "Event") -> bool:
328        """Return True if this event starts while the other event is active."""
329        return self.timespan.starts_within(other.timespan)
330
331    def ends_within(self, other: "Event") -> bool:
332        """Return True if this event ends while the other event is active."""
333        return self.timespan.ends_within(other.timespan)
334
335    def intersects(self, other: "Event") -> bool:
336        """Return True if this event overlaps with the other event."""
337        return self.timespan.intersects(other.timespan)
338
339    def includes(self, other: "Event") -> bool:
340        """Return True if the other event starts and ends within this event."""
341        return self.timespan.includes(other.timespan)
342
343    def is_included_in(self, other: "Event") -> bool:
344        """Return True if this event starts and ends within the other event."""
345        return self.timespan.is_included_in(other.timespan)
346
347    def __lt__(self, other: Any) -> bool:
348        if not isinstance(other, Event):
349            return NotImplemented
350        return self.timespan < other.timespan
351
352    def __gt__(self, other: Any) -> bool:
353        if not isinstance(other, Event):
354            return NotImplemented
355        return self.timespan > other.timespan
356
357    def __le__(self, other: Any) -> bool:
358        if not isinstance(other, Event):
359            return NotImplemented
360        return self.timespan <= other.timespan
361
362    def __ge__(self, other: Any) -> bool:
363        if not isinstance(other, Event):
364            return NotImplemented
365        return self.timespan >= other.timespan
366
367    @property
368    def recurring(self) -> bool:
369        """Return true if this event is recurring.
370
371        A recurring event is typically evaluated specially on the timeline. The
372        data model has a single event, but the timeline evaluates the recurrence
373        to expand and copy the event to multiple places on the timeline
374        using `as_rrule`.
375        """
376        if self.rrule or self.rdate:
377            return True
378        return False
379
380    def as_rrule(self) -> Iterable[datetime.datetime | datetime.date] | None:
381        """Return an iterable containing the occurrences of a recurring event.
382
383        A recurring event is typically evaluated specially on the timeline. The
384        data model has a single event, but the timeline evaluates the recurrence
385        to expand and copy the event to multiple places on the timeline.
386
387        This is only valid for events where `recurring` is True.
388        """
389        return as_rrule(self.rrule, self.rdate, self.exdate, self.dtstart)
390
391    @model_validator(mode="before")
392    @classmethod
393    def _inspect_date_types(cls, values: dict[str, Any]) -> dict[str, Any]:
394        """Debug the date and date/time values of the event."""
395        dtstart = values.get("dtstart")
396        dtend = values.get("dtend")
397        if not dtstart or not dtend:
398            return values
399        _LOGGER.debug("Found initial values dtstart=%s, dtend=%s", dtstart, dtend)
400        return values
401
402    _validate_until_dtstart = model_validator(mode="after")(validate_until_dtstart)
403    _validate_recurrence_dates = model_validator(mode="after")(
404        validate_recurrence_dates
405    )
406
407    @model_validator(mode="after")
408    def _validate_date_types(self) -> Self:
409        """Validate that start and end values are the same date or datetime type."""
410        dtstart = self.dtstart
411        dtend = self.dtend
412
413        if not dtstart or not dtend:
414            return self
415        if isinstance(dtstart, datetime.datetime):
416            if not isinstance(dtend, datetime.datetime):
417                _LOGGER.debug("Unexpected data types for values: %s", self)
418                raise ValueError(
419                    f"Unexpected dtstart value '{dtstart}' was datetime but "
420                    f"dtend value '{dtend}' was not datetime"
421                )
422        elif isinstance(dtstart, datetime.date):
423            if isinstance(dtend, datetime.datetime):
424                raise ValueError(
425                    f"Unexpected dtstart value '{dtstart}' was date but "
426                    f"dtend value '{dtend}' was datetime"
427                )
428        return self
429
430    @model_validator(mode="after")
431    def _validate_datetime_timezone(self) -> Self:
432        """Validate that start and end values have the same timezone information."""
433        if (
434            not (dtstart := self.dtstart)
435            or not (dtend := self.dtend)
436            or not isinstance(dtstart, datetime.datetime)
437            or not isinstance(dtend, datetime.datetime)
438        ):
439            return self
440        if dtstart.tzinfo is None and dtend.tzinfo is not None:
441            raise ValueError(
442                f"Expected end datetime value in localtime but was {dtend}"
443            )
444        if dtstart.tzinfo is not None and dtend.tzinfo is None:
445            raise ValueError(f"Expected end datetime with timezone but was {dtend}")
446        return self
447
448    @model_validator(mode="after")
449    def _validate_one_end_or_duration(self) -> Self:
450        """Validate that only one of duration or end date may be set."""
451        if self.dtend and self.duration:
452            raise ValueError("Only one of dtend or duration may be set.")
453        return self
454
455    @model_validator(mode="after")
456    def _validate_same_day_dtend(self) -> Self:
457        """Fix same-day DTEND for all-day events when compat mode is enabled."""
458        if same_day_dtend_compat.is_same_day_dtend_compat_enabled():
459            if isinstance(self.dtstart, datetime.date) and not isinstance(self.dtstart, datetime.datetime):
460                if self.dtend and self.dtend == self.dtstart:
461                    self.dtend = self.dtstart + datetime.timedelta(days=1)
462        return self
463
464    _validate_duration_unit = model_validator(mode="after")(validate_duration_unit)
465
466    serialize_fields = field_serializer("*")(serialize_field)  # type: ignore[pydantic-field]

A single event on a calendar.

Can either be for a specific day, or with a start time and duration/end time.

The dtstamp and uid functions have factory methods invoked with a lambda to facilitate mocking in unit tests.

Example:

import datetime
from ical.event import Event

event = Event(
    dtstart=datetime.datetime(2022, 8, 31, 7, 00, 00),
    dtend=datetime.datetime(2022, 8, 31, 7, 30, 00),
    summary="Morning exercise",
)
print("The event duration is: ", event.computed_duration)

An Event is a pydantic model, so all properties of a pydantic model apply here to such as the constructor arguments, properties to return the model as a dictionary or json, as well as other parsing methods.

dtstamp: Annotated[datetime.date | datetime.datetime, BeforeValidator(func=<function parse_date_and_datetime at 0x7f8df9eba820>, json_schema_input_type=PydanticUndefined)] = PydanticUndefined

Specifies the date and time the event was created.

uid: str = PydanticUndefined

A globally unique identifier for the event.

dtstart: Annotated[datetime.date | datetime.datetime | None, BeforeValidator(func=<function parse_date_and_datetime at 0x7f8df9eba820>, json_schema_input_type=PydanticUndefined)] = None

The start time or start day of the event.

dtend: Annotated[datetime.date | datetime.datetime | None, BeforeValidator(func=<function parse_date_and_datetime at 0x7f8df9eba820>, json_schema_input_type=PydanticUndefined)] = None

The end time or end day of the event.

This may be specified as an explicit date. Alternatively, a duration can be used instead.

duration: datetime.timedelta | None = None

The duration of the event as an alternative to an explicit end date/time.

summary: str | None = None

Defines a short summary or subject for the event.

attendees: list[ical.types.CalAddress] = PydanticUndefined

Specifies participants in a group-scheduled calendar.

categories: list[str] = PydanticUndefined

Defines the categories for an event.

Specifies a category or subtype. Can be useful for searching for a particular type of event.

classification: ical.types.Classification | None = None

An access classification for a calendar event.

This provides a method of capturing the scope of access of a calendar, in conjunction with an access control system.

comment: list[str] = PydanticUndefined

Specifies a comment to the calendar user.

contacts: list[str] = PydanticUndefined

Contact information associated with the event.

created: datetime.datetime | None = None

The date and time the event information was created.

description: str | None = None

A more complete description of the event than provided by the summary.

geo: ical.types.Geo | None = None

Specifies a latitude and longitude global position for the event activity.

last_modified: datetime.datetime | None = None
location: str | None = None

Defines the intended venue for the activity defined by this event.

organizer: ical.types.CalAddress | None = None

The organizer of a group-scheduled calendar entity.

priority: ical.types.Priority | None = None

Defines the relative priority of the calendar event.

recurrence_id: ical.types.RecurrenceId | None = None

Defines a specific instance of a recurring event.

The full range of calendar events specified by a recurrence set is referenced by referring to just the uid. The recurrence_id allows reference of an individual instance within the recurrence set.

related_to: list[ical.types.RelatedTo] = PydanticUndefined

Used to represent a relationship or reference between events.

related: list[str] = PydanticUndefined

Unused and will be deleted in a future release

resources: list[str] = PydanticUndefined

Defines the equipment or resources anticipated for the calendar event.

rrule: ical.types.Recur | None = None

A recurrence rule specification.

Defines a rule for specifying a repeated event. The recurrence set is the complete set of recurrence instances for a calendar component (based on rrule, rdate, exdate). The recurrence set is generated by gathering the rrule and rdate properties then excluding any times specified by exdate. The recurrence is generated with the dtstart property defining the first instance of the recurrence set.

Typically a dtstart should be specified with a date local time and timezone to make sure all instances have the same start time regardless of time zone changing.

rdate: Annotated[list[datetime.date | datetime.datetime], BeforeValidator(func=<function parse_date_and_datetime_list at 0x7f8df9eba980>, json_schema_input_type=PydanticUndefined)] = PydanticUndefined

Defines the list of date/time values for recurring events.

Can appear along with the rrule property to define a set of repeating occurrences of the event. The recurrence set is the complete set of recurrence instances for a calendar component (based on rrule, rdate, exdate). The recurrence set is generated by gathering the rrule and rdate properties then excluding any times specified by exdate.

exdate: Annotated[list[datetime.date | datetime.datetime], BeforeValidator(func=<function parse_date_and_datetime_list at 0x7f8df9eba980>, json_schema_input_type=PydanticUndefined)] = PydanticUndefined

Defines the list of exceptions for recurring events.

The exception dates are used in computing the recurrence set. The recurrence set is the complete set of recurrence instances for a calendar component (based on rrule, rdate, exdate). The recurrence set is generated by gathering the rrule and rdate properties then excluding any times specified by exdate.

request_status: ical.types.RequestStatus | None = None
sequence: int | None = None

The revision sequence number in the calendar component.

When an event is created, its sequence number is 0. It is monotonically incremented by the organizer's calendar user agent every time a significant revision is made to the calendar event.

status: EventStatus | None = None

Defines the overall status or confirmation of the event.

In a group-scheduled calendar, used by the organizer to provide a confirmation of the event to attendees.

transparency: str | None = None

Defines whether or not an event is transparent to busy time searches.

url: ical.types.Uri | None = None

Defines a url associated with the event.

May convey a location where a more dynamic rendition of the calendar event information associated with the event can be found.

extras: list[ical.types.ExtraProperty] = PydanticUndefined
alarm: list[ical.alarm.Alarm] = PydanticUndefined

A grouping of reminder alarms for the event.

start: datetime.datetime | datetime.date
281    @property
282    def start(self) -> datetime.datetime | datetime.date:
283        """Return the start time for the event."""
284        assert self.dtstart is not None
285        return self.dtstart

Return the start time for the event.

end: datetime.datetime | datetime.date
287    @property
288    def end(self) -> datetime.datetime | datetime.date:
289        """Return the end time for the event."""
290        if self.duration:
291            return self.start + self.duration
292        if self.dtend:
293            return self.dtend
294
295        if isinstance(self.start, datetime.datetime):
296            return self.start
297        return self.start + datetime.timedelta(days=1)

Return the end time for the event.

start_datetime: datetime.datetime
299    @property
300    def start_datetime(self) -> datetime.datetime:
301        """Return the events start as a datetime in UTC"""
302        return normalize_datetime(self.start).astimezone(datetime.timezone.utc)

Return the events start as a datetime in UTC

end_datetime: datetime.datetime
304    @property
305    def end_datetime(self) -> datetime.datetime:
306        """Return the events end as a datetime in UTC."""
307        return normalize_datetime(self.end).astimezone(datetime.timezone.utc)

Return the events end as a datetime in UTC.

computed_duration: datetime.timedelta
309    @property
310    def computed_duration(self) -> datetime.timedelta:
311        """Return the event duration."""
312        if self.duration is not None:
313            return self.duration
314        return self.end - self.start

Return the event duration.

timespan: ical.timespan.Timespan
316    @property
317    def timespan(self) -> Timespan:
318        """Return a timespan representing the event start and end."""
319        return Timespan.of(self.start, self.end)

Return a timespan representing the event start and end.

def timespan_of(self, tzinfo: datetime.tzinfo) -> ical.timespan.Timespan:
321    def timespan_of(self, tzinfo: datetime.tzinfo) -> Timespan:
322        """Return a timespan representing the event start and end."""
323        return Timespan.of(
324            normalize_datetime(self.start, tzinfo), normalize_datetime(self.end, tzinfo)
325        )

Return a timespan representing the event start and end.

def starts_within(self, other: Event) -> bool:
327    def starts_within(self, other: "Event") -> bool:
328        """Return True if this event starts while the other event is active."""
329        return self.timespan.starts_within(other.timespan)

Return True if this event starts while the other event is active.

def ends_within(self, other: Event) -> bool:
331    def ends_within(self, other: "Event") -> bool:
332        """Return True if this event ends while the other event is active."""
333        return self.timespan.ends_within(other.timespan)

Return True if this event ends while the other event is active.

def intersects(self, other: Event) -> bool:
335    def intersects(self, other: "Event") -> bool:
336        """Return True if this event overlaps with the other event."""
337        return self.timespan.intersects(other.timespan)

Return True if this event overlaps with the other event.

def includes(self, other: Event) -> bool:
339    def includes(self, other: "Event") -> bool:
340        """Return True if the other event starts and ends within this event."""
341        return self.timespan.includes(other.timespan)

Return True if the other event starts and ends within this event.

def is_included_in(self, other: Event) -> bool:
343    def is_included_in(self, other: "Event") -> bool:
344        """Return True if this event starts and ends within the other event."""
345        return self.timespan.is_included_in(other.timespan)

Return True if this event starts and ends within the other event.

recurring: bool
367    @property
368    def recurring(self) -> bool:
369        """Return true if this event is recurring.
370
371        A recurring event is typically evaluated specially on the timeline. The
372        data model has a single event, but the timeline evaluates the recurrence
373        to expand and copy the event to multiple places on the timeline
374        using `as_rrule`.
375        """
376        if self.rrule or self.rdate:
377            return True
378        return False

Return true if this event is recurring.

A recurring event is typically evaluated specially on the timeline. The data model has a single event, but the timeline evaluates the recurrence to expand and copy the event to multiple places on the timeline using as_rrule.

def as_rrule(self) -> Iterable[datetime.datetime | datetime.date] | None:
380    def as_rrule(self) -> Iterable[datetime.datetime | datetime.date] | None:
381        """Return an iterable containing the occurrences of a recurring event.
382
383        A recurring event is typically evaluated specially on the timeline. The
384        data model has a single event, but the timeline evaluates the recurrence
385        to expand and copy the event to multiple places on the timeline.
386
387        This is only valid for events where `recurring` is True.
388        """
389        return as_rrule(self.rrule, self.rdate, self.exdate, self.dtstart)

Return an iterable containing the occurrences of a recurring event.

A recurring event is typically evaluated specially on the timeline. The data model has a single event, but the timeline evaluates the recurrence to expand and copy the event to multiple places on the timeline.

This is only valid for events where recurring is True.

def serialize_fields( self: pydantic.main.BaseModel, value: Any, info: pydantic_core.core_schema.SerializationInfo) -> Any:
287def serialize_field(self: BaseModel, value: Any, info: SerializationInfo) -> Any:
288    if not info.context or not info.context.get("ics"):
289        return value
290    if isinstance(value, list):
291        res = []
292        for val in value:
293            for base in val.__class__.__mro__[:-1]:
294                if (func := DATA_TYPE.encode_property_json.get(base)) is not None:
295                    res.append(func(val))
296                    break
297            else:
298                res.append(val)
299        return res
300
301    for base in value.__class__.__mro__[:-1]:
302        if (func := DATA_TYPE.encode_property_json.get(base)) is not None:
303            return func(value)
304    return value

The type of the None singleton.