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