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

Event(**data: Any)
264    def __init__(self, **data: Any) -> None:
265        """Initialize a Calendar Event.
266
267        This method accepts keyword args with field names on the Calendar such as `summary`,
268        `start`, `end`, `description`, etc.
269        """
270        if "start" in data:
271            data["dtstart"] = data.pop("start")
272        if "end" in data:
273            data["dtend"] = data.pop("end")
274        super().__init__(**data)

Initialize a Calendar Event.

This method accepts keyword args with field names on the Calendar such as summary, start, end, description, etc.

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

Specifies the date and time the event was created.

uid: str

A globally unique identifier for the event.

dtstart: Annotated[Union[datetime.date, datetime.datetime, NoneType], BeforeValidator(func=<function parse_date_and_datetime at 0x7f166a6b8180>, json_schema_input_type=PydanticUndefined)]

The start time or start day of the event.

dtend: Annotated[Union[datetime.date, datetime.datetime, NoneType], BeforeValidator(func=<function parse_date_and_datetime at 0x7f166a6b8180>, json_schema_input_type=PydanticUndefined)]

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: Optional[datetime.timedelta]

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

summary: Optional[str]

Defines a short summary or subject for the event.

attendees: list[ical.types.CalAddress]

Specifies participants in a group-scheduled calendar.

categories: list[str]

Defines the categories for an event.

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

classification: Optional[ical.types.Classification]

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]

Specifies a comment to the calendar user.

contacts: list[str]

Contact information associated with the event.

created: Optional[datetime.datetime]

The date and time the event information was created.

description: Optional[str]

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

geo: Optional[ical.types.Geo]

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

last_modified: Optional[datetime.datetime]
location: Optional[str]

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

organizer: Optional[ical.types.CalAddress]

The organizer of a group-scheduled calendar entity.

priority: Optional[ical.types.Priority]

Defines the relative priority of the calendar event.

recurrence_id: Optional[ical.types.RecurrenceId]

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]

Used to represent a relationship or reference between events.

related: list[str]

Unused and will be deleted in a future release

resources: list[str]

Defines the equipment or resources anticipated for the calendar event.

rrule: Optional[ical.types.Recur]

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[Union[datetime.date, datetime.datetime]], BeforeValidator(func=<function parse_date_and_datetime_list at 0x7f166a6b8220>, json_schema_input_type=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[Union[datetime.date, datetime.datetime]], BeforeValidator(func=<function parse_date_and_datetime_list at 0x7f166a6b8220>, json_schema_input_type=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: Optional[ical.types.RequestStatus]
sequence: Optional[int]

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: Optional[EventStatus]

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: Optional[str]

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

url: Optional[ical.types.Uri]

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]
alarm: list[ical.alarm.Alarm]

A grouping of reminder alarms for the event.

start: datetime.datetime | datetime.date
276    @property
277    def start(self) -> datetime.datetime | datetime.date:
278        """Return the start time for the event."""
279        assert self.dtstart is not None
280        return self.dtstart

Return the start time for the event.

end: datetime.datetime | datetime.date
282    @property
283    def end(self) -> datetime.datetime | datetime.date:
284        """Return the end time for the event."""
285        if self.duration:
286            return self.start + self.duration
287        if self.dtend:
288            return self.dtend
289
290        if isinstance(self.start, datetime.datetime):
291            return self.start
292        return self.start + datetime.timedelta(days=1)

Return the end time for the event.

start_datetime: datetime.datetime
294    @property
295    def start_datetime(self) -> datetime.datetime:
296        """Return the events start as a datetime in UTC"""
297        return normalize_datetime(self.start).astimezone(datetime.timezone.utc)

Return the events start as a datetime in UTC

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

Return the events end as a datetime in UTC.

computed_duration: datetime.timedelta
304    @property
305    def computed_duration(self) -> datetime.timedelta:
306        """Return the event duration."""
307        if self.duration is not None:
308            return self.duration
309        return self.end - self.start

Return the event duration.

timespan: ical.timespan.Timespan
311    @property
312    def timespan(self) -> Timespan:
313        """Return a timespan representing the event start and end."""
314        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:
316    def timespan_of(self, tzinfo: datetime.tzinfo) -> Timespan:
317        """Return a timespan representing the event start and end."""
318        return Timespan.of(
319            normalize_datetime(self.start, tzinfo), normalize_datetime(self.end, tzinfo)
320        )

Return a timespan representing the event start and end.

def starts_within(self, other: Event) -> bool:
322    def starts_within(self, other: "Event") -> bool:
323        """Return True if this event starts while the other event is active."""
324        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:
326    def ends_within(self, other: "Event") -> bool:
327        """Return True if this event ends while the other event is active."""
328        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:
330    def intersects(self, other: "Event") -> bool:
331        """Return True if this event overlaps with the other event."""
332        return self.timespan.intersects(other.timespan)

Return True if this event overlaps with the other event.

def includes(self, other: Event) -> bool:
334    def includes(self, other: "Event") -> bool:
335        """Return True if the other event starts and ends within this event."""
336        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:
338    def is_included_in(self, other: "Event") -> bool:
339        """Return True if this event starts and ends within the other event."""
340        return self.timespan.is_included_in(other.timespan)

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

recurring: bool
362    @property
363    def recurring(self) -> bool:
364        """Return true if this event is recurring.
365
366        A recurring event is typically evaluated specially on the timeline. The
367        data model has a single event, but the timeline evaluates the recurrence
368        to expand and copy the event to multiple places on the timeline
369        using `as_rrule`.
370        """
371        if self.rrule or self.rdate:
372            return True
373        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:
375    def as_rrule(self) -> Iterable[datetime.datetime | datetime.date] | None:
376        """Return an iterable containing the occurrences of a recurring event.
377
378        A recurring event is typically evaluated specially on the timeline. The
379        data model has a single event, but the timeline evaluates the recurrence
380        to expand and copy the event to multiple places on the timeline.
381
382        This is only valid for events where `recurring` is True.
383        """
384        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
model_config = {'validate_assignment': True, 'populate_by_name': True, 'arbitrary_types_allowed': True, 'validate_by_alias': True, 'validate_by_name': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].