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