ical.store
Library for managing the lifecycle of components in a calendar.
A store is like a manager for events within a Calendar, updating the necessary properties such as modification times, sequence numbers, and ids. This higher level API is a more convenient API than working with the lower level objects directly.
1"""Library for managing the lifecycle of components in a calendar. 2 3A store is like a manager for events within a Calendar, updating the necessary 4properties such as modification times, sequence numbers, and ids. This higher 5level API is a more convenient API than working with the lower level objects 6directly. 7""" 8 9# pylint: disable=unnecessary-lambda 10 11from __future__ import annotations 12 13import datetime 14import logging 15from collections.abc import Callable, Iterable, Generator 16from typing import Any, TypeVar, Generic, cast 17 18from .calendar import Calendar 19from .component import validate_recurrence_dates 20from .event import Event 21from .exceptions import StoreError, TodoStoreError, EventStoreError 22from .iter import RulesetIterable, as_rrule 23from .list import todo_list_view 24from .timezone import Timezone 25from .todo import Todo, TodoStatus 26from .types import Range, Recur, RecurrenceId, RelationshipType, Period 27from .tzif.timezoneinfo import TimezoneInfoError 28from .util import dtstamp_factory, local_timezone, normalize_datetime 29 30 31_LOGGER = logging.getLogger(__name__) 32 33 34__all__ = [ 35 "EventStore", 36 "EventStoreError", 37 "TodoStore", 38 "TodoStoreError", 39 "StoreError", 40] 41 42_T = TypeVar("_T", bound="Event | Todo") 43# We won't be able to edit dates more than 100 years in the future, but this 44# should be sufficient for most use cases. 45_MAX_SCAN_DATE = datetime.date.today() + datetime.timedelta(days=365 * 100) 46 47 48def _ensure_timezone( 49 dtvalue: datetime.datetime | datetime.date | None, timezones: list[Timezone] 50) -> Timezone | None: 51 """Create a timezone object for the specified date if it does not already exist.""" 52 if ( 53 not isinstance(dtvalue, datetime.datetime) 54 or not dtvalue.utcoffset() 55 or not dtvalue.tzinfo 56 ): 57 return None 58 59 # Verify this timezone does not already exist. The number of timezones 60 # in a calendar is typically very small so iterate over the whole thing 61 # to avoid any synchronization/cache issues. 62 key = str(dtvalue.tzinfo) 63 for timezone in timezones: 64 if timezone.tz_id == key: 65 return None 66 67 try: 68 return Timezone.from_tzif(key) 69 except TimezoneInfoError as err: 70 raise EventStoreError( 71 f"No timezone information available for event: {key}" 72 ) from err 73 74 75def _match_item(item: _T, uid: str, recurrence_id: str | None) -> bool: 76 """Return True if the item is an instance of a recurring event.""" 77 if item.uid != uid: 78 return False 79 if recurrence_id is None: 80 # Match all items with the specified uids 81 return True 82 # Match a single item with the specified recurrence_id. If the item is an 83 # edited instance match return it 84 if item.recurrence_id == recurrence_id: 85 _LOGGER.debug("Matched exact recurrence_id: %s", item) 86 return True 87 # Otherwise, determine if this instance is in the series 88 _LOGGER.debug( 89 "Expanding item %s %s to look for match of %s", uid, item.dtstart, recurrence_id 90 ) 91 dtstart = RecurrenceId.to_value(recurrence_id) 92 if isinstance(dtstart, datetime.datetime) and isinstance( 93 item.dtstart, datetime.datetime 94 ): 95 if dtstart.tzinfo is not None: 96 # The recurrence_id carries an explicit TZID: the datetime is already 97 # tz-aware, so no further adjustment is needed for comparison. 98 pass 99 elif item.dtstart.tzinfo is not None: 100 # No explicit TZID was supplied; fall back to the item's own timezone 101 # so the wall-clock time comparison is correct. 102 dtstart = dtstart.replace(tzinfo=item.dtstart.tzinfo) 103 for dt in cast(Any, item).as_rrule() or (): 104 if isinstance(dt, datetime.datetime): 105 if dt.date() > _MAX_SCAN_DATE: 106 _LOGGER.debug("Aborting scan, date %s is beyond max scan date", dt) 107 break 108 elif dt > _MAX_SCAN_DATE: 109 _LOGGER.debug("Aborting scan, date %s is beyond max scan date", dt) 110 break 111 if dt == dtstart: 112 _LOGGER.debug("Found expanded recurrence_id: %s", dt) 113 return True 114 return False 115 116 117def _match_items( 118 items: list[_T], uid: str, recurrence_id: str | None 119) -> Generator[tuple[int, _T], None, None]: 120 """Return items from the list that match the uid and recurrence_id.""" 121 for index, item in enumerate(items): 122 if _match_item(item, uid, recurrence_id): 123 yield index, item 124 125 126def _prepare_update( 127 store_item: Event | Todo, 128 item: Event | Todo, 129 recurrence_id: str | None = None, 130 recurrence_range: Range = Range.NONE, 131) -> dict[str, Any]: 132 """Prepare an update to an existing event or todo.""" 133 partial_update = item.model_dump( 134 exclude_unset=True, 135 exclude={"dtstamp", "uid", "sequence", "created", "last_modified"}, 136 ) 137 _LOGGER.debug("Preparing update update=%s", item) 138 update = { 139 "created": store_item.dtstamp, 140 "sequence": (store_item.sequence + 1) if store_item.sequence else 1, 141 "last_modified": item.dtstamp, 142 **partial_update, 143 "dtstamp": item.dtstamp, 144 } 145 if ( 146 isinstance(item, Todo) 147 and isinstance(store_item, Todo) 148 and item.status 149 and not item.completed 150 ): 151 if ( 152 store_item.status != TodoStatus.COMPLETED 153 and item.status == TodoStatus.COMPLETED 154 ): 155 update["completed"] = item.dtstamp 156 if store_item.completed and item.status != TodoStatus.COMPLETED: 157 update["completed"] = None 158 if rrule := update.get("rrule"): 159 update["rrule"] = Recur.model_validate(rrule) 160 if recurrence_id and store_item.rrule: 161 # Forking a new event off the old event preserves the original uid and 162 # recurrence_id. 163 update.update( 164 { 165 "uid": store_item.uid, 166 "recurrence_id": recurrence_id, 167 } 168 ) 169 if recurrence_range == Range.NONE: 170 # The new event copied from the original is a single instance, 171 # which is not recurring. 172 update["rrule"] = None 173 else: 174 # Overwriting with a new recurring event 175 update["created"] = item.dtstamp 176 177 # Adjust start and end time of the event 178 dtstart: datetime.datetime | datetime.date = RecurrenceId.to_value( 179 recurrence_id 180 ) 181 if item.dtstart: 182 dtstart = item.dtstart 183 update["dtstart"] = dtstart 184 # Event either has a duration (which should already be set) or has 185 # an explicit end which needs to be realigned to new start time. 186 if isinstance(store_item, Event) and store_item.dtend: 187 update["dtend"] = dtstart + store_item.computed_duration 188 return update 189 190 191def _prune_invalid_exdates( 192 exdate: list[datetime.date | datetime.datetime], 193 rrule: Recur | None, 194 rdate: list[datetime.date | datetime.datetime | Period], 195 dtstart: datetime.date | datetime.datetime | None, 196) -> list[datetime.date | datetime.datetime]: 197 """Remove EXDATE values that are no longer valid occurrences of the recurrence rule.""" 198 if not dtstart or not (occurrences_iter := as_rrule(rrule, rdate, [], dtstart)): 199 return exdate 200 201 tzinfo = dtstart.tzinfo if isinstance(dtstart, datetime.datetime) else None 202 exdate_normalized = {ex: normalize_datetime(ex, tzinfo=tzinfo) for ex in exdate} 203 max_exdate_normalized = max(exdate_normalized.values()) 204 allowed_occurrences = set() 205 for occ in occurrences_iter: 206 occ_normalized = normalize_datetime(occ, tzinfo=tzinfo) 207 if occ_normalized > max_exdate_normalized: 208 break 209 allowed_occurrences.add(occ_normalized) 210 211 return [ex for ex in exdate if exdate_normalized[ex] in allowed_occurrences] 212 213 214class GenericStore(Generic[_T]): 215 """A store manages the lifecycle of items on a Calendar.""" 216 217 def __init__( 218 self, 219 items: list[_T], 220 timezones: list[Timezone], 221 exc: type[StoreError], 222 dtstamp_fn: Callable[[], datetime.datetime] = lambda: dtstamp_factory(), 223 tzinfo: datetime.tzinfo | None = None, 224 ): 225 """Initialize the EventStore.""" 226 self._items = items 227 self._timezones = timezones 228 self._exc = exc 229 self._dtstamp_fn = dtstamp_fn 230 self._tzinfo = tzinfo or local_timezone() 231 232 def add(self, item: _T) -> _T: 233 """Add the specified item to the calendar. 234 235 This will handle assigning modification dates, sequence numbers, etc 236 if those fields are unset. 237 238 The store will ensure the `ical.calendar.Calendar` has the necessary 239 `ical.timezone.Timezone` needed to fully specify the time information 240 when encoded. 241 """ 242 update: dict[str, Any] = {} 243 if not item.created: 244 update["created"] = item.dtstamp 245 if item.sequence is None: 246 update["sequence"] = 0 247 if isinstance(item, Todo) and not item.dtstart: 248 if item.due: 249 update["dtstart"] = item.due - datetime.timedelta(days=1) 250 else: 251 update["dtstart"] = datetime.datetime.now(tz=self._tzinfo) 252 if ( 253 isinstance(item, Todo) 254 and not item.completed 255 and item.status == TodoStatus.COMPLETED 256 ): 257 update["completed"] = item.dtstamp 258 new_item = cast(_T, item.copy_and_validate(update=update)) 259 260 # The store can only manage cascading deletes for some relationship types 261 for relation in new_item.related_to or (): 262 if relation.reltype != RelationshipType.PARENT: 263 raise self._exc(f"Unsupported relationship type {relation.reltype}") 264 265 _LOGGER.debug("Adding item: %s", new_item) 266 self._ensure_timezone(item.dtstart) 267 if isinstance(item, Event) and item.dtend: 268 self._ensure_timezone(item.dtend) 269 self._items.append(new_item) 270 return new_item 271 272 def delete( 273 self, 274 uid: str, 275 recurrence_id: str | None = None, 276 recurrence_range: Range = Range.NONE, 277 ) -> None: 278 """Delete the item from the calendar. 279 280 This method is used to delete an existing item. For a recurring item 281 either the whole item or instances of an item may be deleted. To 282 delete the complete range of a recurring item, the `uid` property 283 for the item must be specified and the `recurrence_id` should not 284 be specified. To delete an individual instance of the item the 285 `recurrence_id` must be specified. 286 287 When deleting individual instances, the range property may specify 288 if deletion of just a specific instance, or a range of instances. 289 """ 290 items_to_delete: list[_T] = [ 291 item for _, item in _match_items(self._items, uid, recurrence_id) 292 ] 293 if not items_to_delete: 294 raise self._exc( 295 f"No existing item with uid/recurrence_id: {uid}/{recurrence_id}" 296 ) 297 298 for store_item in items_to_delete: 299 self._apply_delete(store_item, recurrence_id, recurrence_range) 300 301 def _apply_delete( 302 self, 303 store_item: _T, 304 recurrence_id: str | None = None, 305 recurrence_range: Range = Range.NONE, 306 ) -> None: 307 if ( 308 recurrence_id 309 and recurrence_range == Range.THIS_AND_FUTURE 310 and RecurrenceId.to_value(recurrence_id) == store_item.dtstart 311 ): 312 # Editing the first instance and all forward is the same as editing the 313 # entire series so don't bother forking a new event 314 recurrence_id = None 315 316 children = [] 317 for event in self._items: 318 for relation in event.related_to or (): 319 if ( 320 relation.reltype == RelationshipType.PARENT 321 and relation.uid == store_item.uid 322 ): 323 children.append(event) 324 for child in children: 325 self._items.remove(child) 326 327 # Deleting all instances in the series 328 if not recurrence_id or not store_item.rrule: 329 self._items.remove(store_item) 330 return 331 332 exdate = RecurrenceId.to_value(recurrence_id) 333 if recurrence_range == Range.NONE: 334 # A single recurrence instance is removed. Add an exclusion to 335 # to the event. 336 if ( 337 isinstance(exdate, datetime.datetime) 338 and isinstance(store_item.dtstart, datetime.datetime) 339 and exdate.tzinfo is None 340 and store_item.dtstart.tzinfo is not None 341 ): 342 # No explicit TZID was carried by the recurrence_id, so align 343 # the exdate to the item's timezone for a correct comparison. 344 exdate = exdate.replace(tzinfo=store_item.dtstart.tzinfo) 345 store_item.exdate.append(exdate) 346 return 347 348 # Assumes any recurrence deletion is valid, and that overwriting 349 # the "until" value will not produce more instances. UNTIL is 350 # inclusive so it can't include the specified exdate. FREQ=DAILY 351 # is the lowest frequency supported so subtracting one day is 352 # safe and works for both dates and datetimes. 353 store_item.rrule.count = None 354 if ( 355 isinstance(exdate, datetime.datetime) 356 and isinstance(store_item.dtstart, datetime.datetime) 357 and store_item.dtstart.tzinfo 358 ): 359 exdate = exdate.astimezone(datetime.timezone.utc) 360 store_item.rrule.until = exdate - datetime.timedelta(days=1) 361 now = self._dtstamp_fn() 362 store_item.dtstamp = now 363 store_item.last_modified = now 364 365 def edit( 366 self, 367 uid: str, 368 item: _T, 369 recurrence_id: str | None = None, 370 recurrence_range: Range = Range.NONE, 371 ) -> None: 372 """Update the item with the specified uid. 373 374 The specified item should be created with minimal fields, just 375 including the fields that should be updated. The default fields such 376 as `uid` and `dtstamp` may be used to set the uid for a new created item 377 when updating a recurring item, or for any modification times. 378 379 For a recurring item, either the whole item or individual instances 380 of the item may be edited. To edit the complete range of a recurring 381 item the `uid` property must be specified and the `recurrence_id` should 382 not be specified. To edit an individual instances of the item the 383 `recurrence_id` must be specified. The `recurrence_range` determines if 384 just that individual instance is updated or all items following as well. 385 386 The store will ensure the `ical.calendar.Calendar` has the necessary 387 `ical.timezone.Timezone` needed to fully specify the item time information 388 when encoded. 389 """ 390 items_to_edit: list[tuple[int, _T]] = [ 391 (store_idx, store_itm) 392 for store_idx, store_itm in _match_items(self._items, uid, recurrence_id) 393 ] 394 if not items_to_edit: 395 raise self._exc( 396 f"No existing item with uid/recurrence_id: {uid}/{recurrence_id}" 397 ) 398 399 for store_index, store_item in items_to_edit: 400 self._apply_edit( 401 store_index, store_item, item, recurrence_id, recurrence_range 402 ) 403 404 def _apply_edit( 405 self, 406 store_index: int, 407 store_item: _T, 408 item: _T, 409 recurrence_id: str | None = None, 410 recurrence_range: Range = Range.NONE, 411 ) -> None: 412 if ( 413 recurrence_id 414 and recurrence_range == Range.THIS_AND_FUTURE 415 and RecurrenceId.to_value(recurrence_id) == store_item.dtstart 416 ): 417 # Editing the first instance and all forward is the same as editing the 418 # entire series so don't bother forking a new item 419 recurrence_id = None 420 421 update = _prepare_update(store_item, item, recurrence_id, recurrence_range) 422 if recurrence_range == Range.NONE: 423 # Changing the recurrence rule of a single item in the middle of the series 424 # is not allowed. It is allowed to convert a single instance item to recurring. 425 if item.rrule and store_item.rrule: 426 if item.rrule.as_rrule_str() != store_item.rrule.as_rrule_str(): 427 raise self._exc( 428 f"Can't update single instance with rrule (rrule={item.rrule})" 429 ) 430 item.rrule = None 431 432 # Make a deep copy since deletion may update this objects recurrence rules 433 new_item = cast(_T, store_item.copy_and_validate(update=update)) 434 if ( 435 recurrence_id 436 and new_item.rrule 437 and new_item.rrule.count 438 and store_item.dtstart 439 ): 440 # The recurring item count needs to skip any items that 441 # come before the start of the new item. Use a RulesetIterable 442 # to handle workarounds for dateutil.rrule limitations. 443 dtstart: datetime.date | datetime.datetime = update["dtstart"] 444 ruleset = RulesetIterable( 445 store_item.dtstart, 446 [new_item.rrule.as_rrule(store_item.dtstart)], 447 [], 448 [], 449 ) 450 for dtvalue in ruleset: 451 if dtvalue >= dtstart: 452 break 453 new_item.rrule.count = new_item.rrule.count - 1 454 455 # The store can only manage cascading deletes for some relationship types 456 for relation in new_item.related_to or (): 457 if relation.reltype != RelationshipType.PARENT: 458 raise self._exc(f"Unsupported relationship type {relation.reltype}") 459 460 self._ensure_timezone(new_item.dtstart) 461 if isinstance(new_item, Event) and new_item.dtend: 462 self._ensure_timezone(new_item.dtend) 463 464 # Clean up EXDATE entries that are no longer valid occurrences of the recurrence rule 465 if new_item.exdate and (new_item.rrule or new_item.rdate): 466 new_item.exdate = _prune_invalid_exdates( 467 new_item.exdate, new_item.rrule, new_item.rdate, new_item.dtstart 468 ) 469 470 # Editing a single instance of a recurring item is like deleting that instance 471 # then adding a new instance on the specified date. If recurrence id is not 472 # specified then the entire item is replaced. 473 self.delete( 474 store_item.uid, 475 recurrence_id=recurrence_id, 476 recurrence_range=recurrence_range, 477 ) 478 self._items.insert(store_index, new_item) 479 480 def _ensure_timezone( 481 self, dtvalue: datetime.datetime | datetime.date | None 482 ) -> None: 483 if (new_timezone := _ensure_timezone(dtvalue, self._timezones)) is not None: 484 self._timezones.append(new_timezone) 485 486 487class EventStore(GenericStore[Event]): 488 """An event store manages the lifecycle of events on a Calendar. 489 490 An `ical.calendar.Calendar` is a lower level object that can be directly 491 manipulated to add/remove an `ical.event.Event`. That is, it does not 492 handle updating timestamps, incrementing sequence numbers, or managing 493 lifecycle of a recurring event during an update. 494 495 496 Here is an example for setting up an `EventStore`: 497 498 ```python 499 import datetime 500 from ical.calendar import Calendar 501 from ical.event import Event 502 from ical.store import EventStore 503 from ical.types import Recur 504 505 calendar = Calendar() 506 store = EventStore(calendar) 507 508 event = Event( 509 summary="Event summary", 510 start="2022-07-03", 511 end="2022-07-04", 512 rrule=Recur.from_rrule("FREQ=WEEKLY;COUNT=3"), 513 ) 514 store.add(event) 515 ``` 516 517 This will add events to the calendar: 518 ```python3 519 for event in calendar.timeline: 520 print(event.summary, event.uid, event.recurrence_id, event.dtstart) 521 ``` 522 With output like this: 523 ``` 524 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220703 2022-07-03 525 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220710 2022-07-10 526 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220717 2022-07-17 527 ``` 528 529 You may also delete an event, or a specific instance of a recurring event: 530 ```python 531 # Delete a single instance of the recurring event 532 store.delete(uid=event.uid, recurrence_id="20220710") 533 ``` 534 535 Then viewing the store using the `print` example removes the individual 536 instance in the event: 537 ``` 538 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220703 2022-07-03 539 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220717 2022-07-17 540 ``` 541 542 Editing an event is also supported: 543 ```python 544 store.edit("event-uid-1", Event(summary="New Summary")) 545 ``` 546 """ 547 548 def __init__( 549 self, 550 calendar: Calendar, 551 dtstamp_fn: Callable[[], datetime.datetime] = lambda: dtstamp_factory(), 552 ): 553 """Initialize the EventStore.""" 554 super().__init__( 555 calendar.events, 556 calendar.timezones, 557 EventStoreError, 558 dtstamp_fn, 559 tzinfo=None, 560 ) 561 562 563class TodoStore(GenericStore[Todo]): 564 """A To-do store manages the lifecycle of to-dos on a Calendar.""" 565 566 def __init__( 567 self, 568 calendar: Calendar, 569 tzinfo: datetime.tzinfo | None = None, 570 dtstamp_fn: Callable[[], datetime.datetime] = lambda: dtstamp_factory(), 571 ): 572 """Initialize the TodoStore.""" 573 super().__init__( 574 calendar.todos, 575 calendar.timezones, 576 TodoStoreError, 577 dtstamp_fn, 578 tzinfo=tzinfo, 579 ) 580 self._calendar = calendar 581 582 def todo_list(self, dtstart: datetime.datetime | None = None) -> Iterable[Todo]: 583 """Return a list of all todos on the calendar. 584 585 This view accounts for recurring todos. 586 """ 587 return todo_list_view(self._calendar.todos, dtstart)
488class EventStore(GenericStore[Event]): 489 """An event store manages the lifecycle of events on a Calendar. 490 491 An `ical.calendar.Calendar` is a lower level object that can be directly 492 manipulated to add/remove an `ical.event.Event`. That is, it does not 493 handle updating timestamps, incrementing sequence numbers, or managing 494 lifecycle of a recurring event during an update. 495 496 497 Here is an example for setting up an `EventStore`: 498 499 ```python 500 import datetime 501 from ical.calendar import Calendar 502 from ical.event import Event 503 from ical.store import EventStore 504 from ical.types import Recur 505 506 calendar = Calendar() 507 store = EventStore(calendar) 508 509 event = Event( 510 summary="Event summary", 511 start="2022-07-03", 512 end="2022-07-04", 513 rrule=Recur.from_rrule("FREQ=WEEKLY;COUNT=3"), 514 ) 515 store.add(event) 516 ``` 517 518 This will add events to the calendar: 519 ```python3 520 for event in calendar.timeline: 521 print(event.summary, event.uid, event.recurrence_id, event.dtstart) 522 ``` 523 With output like this: 524 ``` 525 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220703 2022-07-03 526 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220710 2022-07-10 527 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220717 2022-07-17 528 ``` 529 530 You may also delete an event, or a specific instance of a recurring event: 531 ```python 532 # Delete a single instance of the recurring event 533 store.delete(uid=event.uid, recurrence_id="20220710") 534 ``` 535 536 Then viewing the store using the `print` example removes the individual 537 instance in the event: 538 ``` 539 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220703 2022-07-03 540 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220717 2022-07-17 541 ``` 542 543 Editing an event is also supported: 544 ```python 545 store.edit("event-uid-1", Event(summary="New Summary")) 546 ``` 547 """ 548 549 def __init__( 550 self, 551 calendar: Calendar, 552 dtstamp_fn: Callable[[], datetime.datetime] = lambda: dtstamp_factory(), 553 ): 554 """Initialize the EventStore.""" 555 super().__init__( 556 calendar.events, 557 calendar.timezones, 558 EventStoreError, 559 dtstamp_fn, 560 tzinfo=None, 561 )
An event store manages the lifecycle of events on a Calendar.
An ical.calendar.Calendar is a lower level object that can be directly
manipulated to add/remove an ical.event.Event. That is, it does not
handle updating timestamps, incrementing sequence numbers, or managing
lifecycle of a recurring event during an update.
Here is an example for setting up an EventStore:
import datetime
from ical.calendar import Calendar
from ical.event import Event
from ical.store import EventStore
from ical.types import Recur
calendar = Calendar()
store = EventStore(calendar)
event = Event(
summary="Event summary",
start="2022-07-03",
end="2022-07-04",
rrule=Recur.from_rrule("FREQ=WEEKLY;COUNT=3"),
)
store.add(event)
This will add events to the calendar:
for event in calendar.timeline:
print(event.summary, event.uid, event.recurrence_id, event.dtstart)
With output like this:
Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220703 2022-07-03
Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220710 2022-07-10
Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220717 2022-07-17
You may also delete an event, or a specific instance of a recurring event:
# Delete a single instance of the recurring event
store.delete(uid=event.uid, recurrence_id="20220710")
Then viewing the store using the print example removes the individual
instance in the event:
Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220703 2022-07-03
Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220717 2022-07-17
Editing an event is also supported:
store.edit("event-uid-1", Event(summary="New Summary"))
549 def __init__( 550 self, 551 calendar: Calendar, 552 dtstamp_fn: Callable[[], datetime.datetime] = lambda: dtstamp_factory(), 553 ): 554 """Initialize the EventStore.""" 555 super().__init__( 556 calendar.events, 557 calendar.timezones, 558 EventStoreError, 559 dtstamp_fn, 560 tzinfo=None, 561 )
Initialize the EventStore.
Inherited Members
85class EventStoreError(StoreError): 86 """Exception raised by EventStore operations. 87 88 Raised by :class:`ical.store.EventStore` when an event operation fails. 89 """
Exception raised by EventStore operations.
Raised by ical.store.EventStore when an event operation fails.
564class TodoStore(GenericStore[Todo]): 565 """A To-do store manages the lifecycle of to-dos on a Calendar.""" 566 567 def __init__( 568 self, 569 calendar: Calendar, 570 tzinfo: datetime.tzinfo | None = None, 571 dtstamp_fn: Callable[[], datetime.datetime] = lambda: dtstamp_factory(), 572 ): 573 """Initialize the TodoStore.""" 574 super().__init__( 575 calendar.todos, 576 calendar.timezones, 577 TodoStoreError, 578 dtstamp_fn, 579 tzinfo=tzinfo, 580 ) 581 self._calendar = calendar 582 583 def todo_list(self, dtstart: datetime.datetime | None = None) -> Iterable[Todo]: 584 """Return a list of all todos on the calendar. 585 586 This view accounts for recurring todos. 587 """ 588 return todo_list_view(self._calendar.todos, dtstart)
A To-do store manages the lifecycle of to-dos on a Calendar.
567 def __init__( 568 self, 569 calendar: Calendar, 570 tzinfo: datetime.tzinfo | None = None, 571 dtstamp_fn: Callable[[], datetime.datetime] = lambda: dtstamp_factory(), 572 ): 573 """Initialize the TodoStore.""" 574 super().__init__( 575 calendar.todos, 576 calendar.timezones, 577 TodoStoreError, 578 dtstamp_fn, 579 tzinfo=tzinfo, 580 ) 581 self._calendar = calendar
Initialize the TodoStore.
583 def todo_list(self, dtstart: datetime.datetime | None = None) -> Iterable[Todo]: 584 """Return a list of all todos on the calendar. 585 586 This view accounts for recurring todos. 587 """ 588 return todo_list_view(self._calendar.todos, dtstart)
Return a list of all todos on the calendar.
This view accounts for recurring todos.
Inherited Members
92class TodoStoreError(StoreError): 93 """Exception raised by TodoStore operations. 94 95 Raised by :class:`ical.store.TodoStore` when a todo operation fails. 96 """
Exception raised by TodoStore operations.
Raised by ical.store.TodoStore when a todo operation fails.
76class StoreError(CalendarError): 77 """Exception raised by store operations. 78 79 Raised when a store operation fails, for example when trying to edit 80 or delete an event that does not exist, or when timezone information 81 cannot be resolved for a datetime value being added to the calendar. 82 """
Exception raised by store operations.
Raised when a store operation fails, for example when trying to edit or delete an event that does not exist, or when timezone information cannot be resolved for a datetime value being added to the calendar.