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