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