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

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):
 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[ParsedProperty] = 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    _validate_duration_unit = model_validator(mode="after")(validate_duration_unit)
455
456    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 0x7fa48559d4e0>, 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 0x7fa48559d4e0>, 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 0x7fa48559d4e0>, 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 0x7fa48559d640>, 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 0x7fa48559d640>, 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.parsing.property.ParsedProperty] = PydanticUndefined
alarm: list[ical.alarm.Alarm] = PydanticUndefined

A grouping of reminder alarms for the event.

start: datetime.datetime | datetime.date
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

Return the start time for the event.

end: datetime.datetime | datetime.date
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)

Return the end time for the event.

start_datetime: datetime.datetime
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)

Return the events start as a datetime in UTC

end_datetime: datetime.datetime
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)

Return the events end as a datetime in UTC.

computed_duration: datetime.timedelta
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

Return the event duration.

timespan: ical.timespan.Timespan
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)

Return a timespan representing the event start and end.

def timespan_of(self, tzinfo: datetime.tzinfo) -> ical.timespan.Timespan:
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        )

Return a timespan representing the event start and end.

def starts_within(self, other: Event) -> bool:
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)

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

def ends_within(self, other: Event) -> bool:
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)

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

def intersects(self, other: Event) -> bool:
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)

Return True if this event overlaps with the other event.

def includes(self, other: Event) -> bool:
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)

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

def is_included_in(self, other: Event) -> bool:
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)

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

recurring: bool
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

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

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:
171def serialize_field(self: BaseModel, value: Any, info: SerializationInfo) -> Any:
172    if not info.context or not info.context.get("ics"):
173        return value
174    if isinstance(value, list):
175        res = []
176        for val in value:
177            for base in val.__class__.__mro__[:-1]:
178                if (func := DATA_TYPE.encode_property_json.get(base)) is not None:
179                    res.append(func(val))
180                    break
181            else:
182                res.append(val)
183        return res
184
185    for base in value.__class__.__mro__[:-1]:
186        if (func := DATA_TYPE.encode_property_json.get(base)) is not None:
187            return func(value)
188    return value

The type of the None singleton.