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