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

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)
247    def __init__(self, **data: Any) -> None:
248        """Initialize a Calendar Event.
249
250        This method accepts keyword args with field names on the Calendar such as `summary`,
251        `start`, `end`, `description`, etc.
252        """
253        if "start" in data:
254            data["dtstart"] = data.pop("start")
255        if "end" in data:
256            data["dtend"] = data.pop("end")
257        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: Union[datetime.datetime, datetime.date]

Specifies the date and time the event was created.

uid: str

A globally unique identifier for the event.

dtstart: Union[datetime.datetime, datetime.date]

The start time or start day of the event.

dtend: Union[datetime.datetime, datetime.date, NoneType]

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: 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 priorirty 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: list[typing.Union[datetime.datetime, datetime.date]]

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: list[typing.Union[datetime.datetime, datetime.date]]

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 organizers 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 transparenty 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
259    @property
260    def start(self) -> datetime.datetime | datetime.date:
261        """Return the start time for the event."""
262        return self.dtstart

Return the start time for the event.

end: datetime.datetime | datetime.date
264    @property
265    def end(self) -> datetime.datetime | datetime.date:
266        """Return the end time for the event."""
267        if self.duration:
268            return self.dtstart + self.duration
269        if self.dtend:
270            return self.dtend
271
272        if isinstance(self.dtstart, datetime.datetime):
273            return self.dtstart
274        return self.dtstart + datetime.timedelta(days=1)

Return the end time for the event.

start_datetime: datetime.datetime
276    @property
277    def start_datetime(self) -> datetime.datetime:
278        """Return the events start as a datetime in UTC"""
279        return normalize_datetime(self.start).astimezone(datetime.timezone.utc)

Return the events start as a datetime in UTC

end_datetime: datetime.datetime
281    @property
282    def end_datetime(self) -> datetime.datetime:
283        """Return the events end as a datetime in UTC."""
284        return normalize_datetime(self.end).astimezone(datetime.timezone.utc)

Return the events end as a datetime in UTC.

computed_duration: datetime.timedelta
286    @property
287    def computed_duration(self) -> datetime.timedelta:
288        """Return the event duration."""
289        if self.duration is not None:
290            return self.duration
291        return self.end - self.start

Return the event duration.

timespan: ical.timespan.Timespan
293    @property
294    def timespan(self) -> Timespan:
295        """Return a timespan representing the event start and end."""
296        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:
298    def timespan_of(self, tzinfo: datetime.tzinfo) -> Timespan:
299        """Return a timespan representing the event start and end."""
300        return Timespan.of(
301            normalize_datetime(self.start, tzinfo), normalize_datetime(self.end, tzinfo)
302        )

Return a timespan representing the event start and end.

def starts_within(self, other: Event) -> bool:
304    def starts_within(self, other: "Event") -> bool:
305        """Return True if this event starts while the other event is active."""
306        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:
308    def ends_within(self, other: "Event") -> bool:
309        """Return True if this event ends while the other event is active."""
310        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:
312    def intersects(self, other: "Event") -> bool:
313        """Return True if this event overlaps with the other event."""
314        return self.timespan.intersects(other.timespan)

Return True if this event overlaps with the other event.

def includes(self, other: Event) -> bool:
316    def includes(self, other: "Event") -> bool:
317        """Return True if the other event starts and ends within this event."""
318        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:
320    def is_included_in(self, other: "Event") -> bool:
321        """Return True if this event starts and ends within the other event."""
322        return self.timespan.is_included_in(other.timespan)

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

recurring: bool
344    @property
345    def recurring(self) -> bool:
346        """Return true if this event is recurring.
347
348        A recurring event is typically evaluated specially on the timeline. The
349        data model has a single event, but the timeline evaluates the recurrence
350        to expand and copy the the event to multiple places on the timeline
351        using `as_rrule`.
352        """
353        if self.rrule or self.rdate:
354            return True
355        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 the event to multiple places on the timeline using as_rrule.

def as_rrule(self) -> Iterable[datetime.datetime | datetime.date] | None:
357    def as_rrule(self) -> Iterable[datetime.datetime | datetime.date] | None:
358        """Return an iterable containing the occurrences of a recurring event.
359
360        A recurring event is typically evaluated specially on the timeline. The
361        data model has a single event, but the timeline evaluates the recurrence
362        to expand and copy the the event to multiple places on the timeline.
363
364        This is only valid for events where `recurring` is True.
365        """
366        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 the event to multiple places on the timeline.

This is only valid for events where recurring is True.