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 23from .list import todo_list_view 24from .timezone import Timezone 25from .todo import Todo, TodoStatus 26from .types import Range, Recur, RecurrenceId, RelationshipType 27from .tzif.timezoneinfo import TimezoneInfoError 28from .util import dtstamp_factory, local_timezone 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 # The recurrence_id does not support timezone information, so put it in the 96 # same timezone as the item to compare. 97 if item.dtstart.tzinfo is not None: 98 dtstart = dtstart.replace(tzinfo=item.dtstart.tzinfo) 99 for dt in item.as_rrule() or (): 100 if isinstance(dt, datetime.datetime): 101 if dt.date() > _MAX_SCAN_DATE: 102 _LOGGER.debug("Aborting scan, date %s is beyond max scan date", dt) 103 break 104 elif dt > _MAX_SCAN_DATE: 105 _LOGGER.debug("Aborting scan, date %s is beyond max scan date", dt) 106 break 107 if dt == dtstart: 108 _LOGGER.debug("Found expanded recurrence_id: %s", dt) 109 return True 110 return False 111 112 113def _match_items( 114 items: list[_T], uid: str, recurrence_id: str | None 115) -> Generator[tuple[int, _T], None, None]: 116 """Return items from the list that match the uid and recurrence_id.""" 117 for index, item in enumerate(items): 118 if _match_item(item, uid, recurrence_id): 119 yield index, item 120 121 122def _prepare_update( 123 store_item: Event | Todo, 124 item: Event | Todo, 125 recurrence_id: str | None = None, 126 recurrence_range: Range = Range.NONE, 127) -> dict[str, Any]: 128 """Prepare an update to an existing event.""" 129 partial_update = item.model_dump( 130 exclude_unset=True, 131 exclude={"dtstamp", "uid", "sequence", "created", "last_modified"}, 132 ) 133 _LOGGER.debug("Preparing update update=%s", item) 134 update = { 135 "created": store_item.dtstamp, 136 "sequence": (store_item.sequence + 1) if store_item.sequence else 1, 137 "last_modified": item.dtstamp, 138 **partial_update, 139 "dtstamp": item.dtstamp, 140 } 141 if ( 142 isinstance(item, Todo) 143 and isinstance(store_item, Todo) 144 and item.status 145 and not item.completed 146 ): 147 if ( 148 store_item.status != TodoStatus.COMPLETED 149 and item.status == TodoStatus.COMPLETED 150 ): 151 update["completed"] = item.dtstamp 152 if store_item.completed and item.status != TodoStatus.COMPLETED: 153 update["completed"] = None 154 if rrule := update.get("rrule"): 155 update["rrule"] = Recur.model_validate(rrule) 156 if recurrence_id and store_item.rrule: 157 # Forking a new event off the old event preserves the original uid and 158 # recurrence_id. 159 update.update( 160 { 161 "uid": store_item.uid, 162 "recurrence_id": recurrence_id, 163 } 164 ) 165 if recurrence_range == Range.NONE: 166 # The new event copied from the original is a single instance, 167 # which is not recurring. 168 update["rrule"] = None 169 else: 170 # Overwriting with a new recurring event 171 update["created"] = item.dtstamp 172 173 # Adjust start and end time of the event 174 dtstart: datetime.datetime | datetime.date = RecurrenceId.to_value( 175 recurrence_id 176 ) 177 if item.dtstart: 178 dtstart = item.dtstart 179 update["dtstart"] = dtstart 180 # Event either has a duration (which should already be set) or has 181 # an explicit end which needs to be realigned to new start time. 182 if isinstance(store_item, Event) and store_item.dtend: 183 update["dtend"] = dtstart + store_item.computed_duration 184 return update 185 186 187class GenericStore(Generic[_T]): 188 """A a store manages the lifecycle of items on a Calendar.""" 189 190 def __init__( 191 self, 192 items: list[_T], 193 timezones: list[Timezone], 194 exc: type[StoreError], 195 dtstamp_fn: Callable[[], datetime.datetime] = lambda: dtstamp_factory(), 196 tzinfo: datetime.tzinfo | None = None, 197 ): 198 """Initialize the EventStore.""" 199 self._items = items 200 self._timezones = timezones 201 self._exc = exc 202 self._dtstamp_fn = dtstamp_fn 203 self._tzinfo = tzinfo or local_timezone() 204 205 def add(self, item: _T) -> _T: 206 """Add the specified item to the calendar. 207 208 This will handle assigning modification dates, sequence numbers, etc 209 if those fields are unset. 210 211 The store will ensure the `ical.calendar.Calendar` has the necessary 212 `ical.timezone.Timezone` needed to fully specify the time information 213 when encoded. 214 """ 215 update: dict[str, Any] = {} 216 if not item.created: 217 update["created"] = item.dtstamp 218 if item.sequence is None: 219 update["sequence"] = 0 220 if isinstance(item, Todo) and not item.dtstart: 221 if item.due: 222 update["dtstart"] = item.due - datetime.timedelta(days=1) 223 else: 224 update["dtstart"] = datetime.datetime.now(tz=self._tzinfo) 225 if ( 226 isinstance(item, Todo) 227 and not item.completed 228 and item.status == TodoStatus.COMPLETED 229 ): 230 update["completed"] = item.dtstamp 231 new_item = cast(_T, item.copy_and_validate(update=update)) 232 233 # The store can only manage cascading deletes for some relationship types 234 for relation in new_item.related_to or (): 235 if relation.reltype != RelationshipType.PARENT: 236 raise self._exc(f"Unsupported relationship type {relation.reltype}") 237 238 _LOGGER.debug("Adding item: %s", new_item) 239 self._ensure_timezone(item.dtstart) 240 if isinstance(item, Event) and item.dtend: 241 self._ensure_timezone(item.dtend) 242 self._items.append(new_item) 243 return new_item 244 245 def delete( 246 self, 247 uid: str, 248 recurrence_id: str | None = None, 249 recurrence_range: Range = Range.NONE, 250 ) -> None: 251 """Delete the item from the calendar. 252 253 This method is used to delete an existing item. For a recurring item 254 either the whole item or instances of an item may be deleted. To 255 delete the complete range of a recurring item, the `uid` property 256 for the item must be specified and the `recurrence_id` should not 257 be specified. To delete an individual instance of the item the 258 `recurrence_id` must be specified. 259 260 When deleting individual instances, the range property may specify 261 if deletion of just a specific instance, or a range of instances. 262 """ 263 items_to_delete: list[_T] = [ 264 item for _, item in _match_items(self._items, uid, recurrence_id) 265 ] 266 if not items_to_delete: 267 raise self._exc( 268 f"No existing item with uid/recurrence_id: {uid}/{recurrence_id}" 269 ) 270 271 for store_item in items_to_delete: 272 self._apply_delete(store_item, recurrence_id, recurrence_range) 273 274 def _apply_delete( 275 self, 276 store_item: _T, 277 recurrence_id: str | None = None, 278 recurrence_range: Range = Range.NONE, 279 ) -> None: 280 if ( 281 recurrence_id 282 and recurrence_range == Range.THIS_AND_FUTURE 283 and RecurrenceId.to_value(recurrence_id) == store_item.dtstart 284 ): 285 # Editing the first instance and all forward is the same as editing the 286 # entire series so don't bother forking a new event 287 recurrence_id = None 288 289 children = [] 290 for event in self._items: 291 for relation in event.related_to or (): 292 if ( 293 relation.reltype == RelationshipType.PARENT 294 and relation.uid == store_item.uid 295 ): 296 children.append(event) 297 for child in children: 298 self._items.remove(child) 299 300 # Deleting all instances in the series 301 if not recurrence_id or not store_item.rrule: 302 self._items.remove(store_item) 303 return 304 305 exdate = RecurrenceId.to_value(recurrence_id) 306 if recurrence_range == Range.NONE: 307 # A single recurrence instance is removed. Add an exclusion to 308 # to the event. 309 # RecurrenceId does not support timezone information. The exclusion 310 # must have the same timezone as the item to compare. 311 if ( 312 isinstance(exdate, datetime.datetime) 313 and isinstance(store_item.dtstart, datetime.datetime) 314 and store_item.dtstart.tzinfo 315 ): 316 exdate = exdate.replace(tzinfo=store_item.dtstart.tzinfo) 317 store_item.exdate.append(exdate) 318 return 319 320 # Assumes any recurrence deletion is valid, and that overwriting 321 # the "until" value will not produce more instances. UNTIL is 322 # inclusive so it can't include the specified exdate. FREQ=DAILY 323 # is the lowest frequency supported so subtracting one day is 324 # safe and works for both dates and datetimes. 325 store_item.rrule.count = None 326 if ( 327 isinstance(exdate, datetime.datetime) 328 and isinstance(store_item.dtstart, datetime.datetime) 329 and store_item.dtstart.tzinfo 330 ): 331 exdate = exdate.astimezone(datetime.timezone.utc) 332 store_item.rrule.until = exdate - datetime.timedelta(days=1) 333 now = self._dtstamp_fn() 334 store_item.dtstamp = now 335 store_item.last_modified = now 336 337 def edit( 338 self, 339 uid: str, 340 item: _T, 341 recurrence_id: str | None = None, 342 recurrence_range: Range = Range.NONE, 343 ) -> None: 344 """Update the item with the specified uid. 345 346 The specified item should be created with minimal fields, just 347 including the fields that should be updated. The default fields such 348 as `uid` and `dtstamp` may be used to set the uid for a new created item 349 when updating a recurring item, or for any modification times. 350 351 For a recurring item, either the whole item or individual instances 352 of the item may be edited. To edit the complete range of a recurring 353 item the `uid` property must be specified and the `recurrence_id` should 354 not be specified. To edit an individual instances of the item the 355 `recurrence_id` must be specified. The `recurrence_range` determines if 356 just that individual instance is updated or all items following as well. 357 358 The store will ensure the `ical.calendar.Calendar` has the necessary 359 `ical.timezone.Timezone` needed to fully specify the item time information 360 when encoded. 361 """ 362 items_to_edit: list[tuple[int, _T]] = [ 363 (index, item) 364 for index, item in _match_items(self._items, uid, recurrence_id) 365 ] 366 if not items_to_edit: 367 raise self._exc( 368 f"No existing item with uid/recurrence_id: {uid}/{recurrence_id}" 369 ) 370 371 for store_index, store_item in items_to_edit: 372 self._apply_edit( 373 store_index, store_item, item, recurrence_id, recurrence_range 374 ) 375 376 def _apply_edit( 377 self, 378 store_index: int, 379 store_item: _T, 380 item: _T, 381 recurrence_id: str | None = None, 382 recurrence_range: Range = Range.NONE, 383 ) -> None: 384 if ( 385 recurrence_id 386 and recurrence_range == Range.THIS_AND_FUTURE 387 and RecurrenceId.to_value(recurrence_id) == store_item.dtstart 388 ): 389 # Editing the first instance and all forward is the same as editing the 390 # entire series so don't bother forking a new item 391 recurrence_id = None 392 393 update = _prepare_update(store_item, item, recurrence_id, recurrence_range) 394 if recurrence_range == Range.NONE: 395 # Changing the recurrence rule of a single item in the middle of the series 396 # is not allowed. It is allowed to convert a single instance item to recurring. 397 if item.rrule and store_item.rrule: 398 if item.rrule.as_rrule_str() != store_item.rrule.as_rrule_str(): 399 raise self._exc( 400 f"Can't update single instance with rrule (rrule={item.rrule})" 401 ) 402 item.rrule = None 403 404 # Make a deep copy since deletion may update this objects recurrence rules 405 new_item = cast(_T, store_item.copy_and_validate(update=update)) 406 if ( 407 recurrence_id 408 and new_item.rrule 409 and new_item.rrule.count 410 and store_item.dtstart 411 ): 412 # The recurring item count needs to skip any items that 413 # come before the start of the new item. Use a RulesetIterable 414 # to handle workarounds for dateutil.rrule limitations. 415 dtstart: datetime.date | datetime.datetime = update["dtstart"] 416 ruleset = RulesetIterable( 417 store_item.dtstart, 418 [new_item.rrule.as_rrule(store_item.dtstart)], 419 [], 420 [], 421 ) 422 for dtvalue in ruleset: 423 if dtvalue >= dtstart: 424 break 425 new_item.rrule.count = new_item.rrule.count - 1 426 427 # The store can only manage cascading deletes for some relationship types 428 for relation in new_item.related_to or (): 429 if relation.reltype != RelationshipType.PARENT: 430 raise self._exc(f"Unsupported relationship type {relation.reltype}") 431 432 self._ensure_timezone(new_item.dtstart) 433 if isinstance(new_item, Event) and new_item.dtend: 434 self._ensure_timezone(new_item.dtend) 435 436 # Editing a single instance of a recurring item is like deleting that instance 437 # then adding a new instance on the specified date. If recurrence id is not 438 # specified then the entire item is replaced. 439 self.delete( 440 store_item.uid, 441 recurrence_id=recurrence_id, 442 recurrence_range=recurrence_range, 443 ) 444 self._items.insert(store_index, new_item) 445 446 def _ensure_timezone( 447 self, dtvalue: datetime.datetime | datetime.date | None 448 ) -> None: 449 if (new_timezone := _ensure_timezone(dtvalue, self._timezones)) is not None: 450 self._timezones.append(new_timezone) 451 452 453class EventStore(GenericStore[Event]): 454 """An event store manages the lifecycle of events on a Calendar. 455 456 An `ical.calendar.Calendar` is a lower level object that can be directly 457 manipulated to add/remove an `ical.event.Event`. That is, it does not 458 handle updating timestamps, incrementing sequence numbers, or managing 459 lifecycle of a recurring event during an update. 460 461 462 Here is an example for setting up an `EventStore`: 463 464 ```python 465 import datetime 466 from ical.calendar import Calendar 467 from ical.event import Event 468 from ical.store import EventStore 469 from ical.types import Recur 470 471 calendar = Calendar() 472 store = EventStore(calendar) 473 474 event = Event( 475 summary="Event summary", 476 start="2022-07-03", 477 end="2022-07-04", 478 rrule=Recur.from_rrule("FREQ=WEEKLY;COUNT=3"), 479 ) 480 store.add(event) 481 ``` 482 483 This will add events to the calendar: 484 ```python3 485 for event in calendar.timeline: 486 print(event.summary, event.uid, event.recurrence_id, event.dtstart) 487 ``` 488 With output like this: 489 ``` 490 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220703 2022-07-03 491 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220710 2022-07-10 492 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220717 2022-07-17 493 ``` 494 495 You may also delete an event, or a specific instance of a recurring event: 496 ```python 497 # Delete a single instance of the recurring event 498 store.delete(uid=event.uid, recurrence_id="20220710") 499 ``` 500 501 Then viewing the store using the `print` example removes the individual 502 instance in the event: 503 ``` 504 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220703 2022-07-03 505 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220717 2022-07-17 506 ``` 507 508 Editing an event is also supported: 509 ```python 510 store.edit("event-uid-1", Event(summary="New Summary")) 511 ``` 512 """ 513 514 def __init__( 515 self, 516 calendar: Calendar, 517 dtstamp_fn: Callable[[], datetime.datetime] = lambda: dtstamp_factory(), 518 ): 519 """Initialize the EventStore.""" 520 super().__init__( 521 calendar.events, 522 calendar.timezones, 523 EventStoreError, 524 dtstamp_fn, 525 tzinfo=None, 526 ) 527 528 529class TodoStore(GenericStore[Todo]): 530 """A To-do store manages the lifecycle of to-dos on a Calendar.""" 531 532 def __init__( 533 self, 534 calendar: Calendar, 535 tzinfo: datetime.tzinfo | None = None, 536 dtstamp_fn: Callable[[], datetime.datetime] = lambda: dtstamp_factory(), 537 ): 538 """Initialize the TodoStore.""" 539 super().__init__( 540 calendar.todos, 541 calendar.timezones, 542 TodoStoreError, 543 dtstamp_fn, 544 tzinfo=tzinfo, 545 ) 546 self._calendar = calendar 547 548 def todo_list(self, dtstart: datetime.datetime | None = None) -> Iterable[Todo]: 549 """Return a list of all todos on the calendar. 550 551 This view accounts for recurring todos. 552 """ 553 return todo_list_view(self._calendar.todos, dtstart)
454class EventStore(GenericStore[Event]): 455 """An event store manages the lifecycle of events on a Calendar. 456 457 An `ical.calendar.Calendar` is a lower level object that can be directly 458 manipulated to add/remove an `ical.event.Event`. That is, it does not 459 handle updating timestamps, incrementing sequence numbers, or managing 460 lifecycle of a recurring event during an update. 461 462 463 Here is an example for setting up an `EventStore`: 464 465 ```python 466 import datetime 467 from ical.calendar import Calendar 468 from ical.event import Event 469 from ical.store import EventStore 470 from ical.types import Recur 471 472 calendar = Calendar() 473 store = EventStore(calendar) 474 475 event = Event( 476 summary="Event summary", 477 start="2022-07-03", 478 end="2022-07-04", 479 rrule=Recur.from_rrule("FREQ=WEEKLY;COUNT=3"), 480 ) 481 store.add(event) 482 ``` 483 484 This will add events to the calendar: 485 ```python3 486 for event in calendar.timeline: 487 print(event.summary, event.uid, event.recurrence_id, event.dtstart) 488 ``` 489 With output like this: 490 ``` 491 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220703 2022-07-03 492 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220710 2022-07-10 493 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220717 2022-07-17 494 ``` 495 496 You may also delete an event, or a specific instance of a recurring event: 497 ```python 498 # Delete a single instance of the recurring event 499 store.delete(uid=event.uid, recurrence_id="20220710") 500 ``` 501 502 Then viewing the store using the `print` example removes the individual 503 instance in the event: 504 ``` 505 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220703 2022-07-03 506 Event summary a521cf45-2c02-11ed-9e5c-066a07ffbaf5 20220717 2022-07-17 507 ``` 508 509 Editing an event is also supported: 510 ```python 511 store.edit("event-uid-1", Event(summary="New Summary")) 512 ``` 513 """ 514 515 def __init__( 516 self, 517 calendar: Calendar, 518 dtstamp_fn: Callable[[], datetime.datetime] = lambda: dtstamp_factory(), 519 ): 520 """Initialize the EventStore.""" 521 super().__init__( 522 calendar.events, 523 calendar.timezones, 524 EventStoreError, 525 dtstamp_fn, 526 tzinfo=None, 527 )
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"))
515 def __init__( 516 self, 517 calendar: Calendar, 518 dtstamp_fn: Callable[[], datetime.datetime] = lambda: dtstamp_factory(), 519 ): 520 """Initialize the EventStore.""" 521 super().__init__( 522 calendar.events, 523 calendar.timezones, 524 EventStoreError, 525 dtstamp_fn, 526 tzinfo=None, 527 )
Initialize the EventStore.
Inherited Members
Exception thrown by the EventStore.
530class TodoStore(GenericStore[Todo]): 531 """A To-do store manages the lifecycle of to-dos on a Calendar.""" 532 533 def __init__( 534 self, 535 calendar: Calendar, 536 tzinfo: datetime.tzinfo | None = None, 537 dtstamp_fn: Callable[[], datetime.datetime] = lambda: dtstamp_factory(), 538 ): 539 """Initialize the TodoStore.""" 540 super().__init__( 541 calendar.todos, 542 calendar.timezones, 543 TodoStoreError, 544 dtstamp_fn, 545 tzinfo=tzinfo, 546 ) 547 self._calendar = calendar 548 549 def todo_list(self, dtstart: datetime.datetime | None = None) -> Iterable[Todo]: 550 """Return a list of all todos on the calendar. 551 552 This view accounts for recurring todos. 553 """ 554 return todo_list_view(self._calendar.todos, dtstart)
A To-do store manages the lifecycle of to-dos on a Calendar.
533 def __init__( 534 self, 535 calendar: Calendar, 536 tzinfo: datetime.tzinfo | None = None, 537 dtstamp_fn: Callable[[], datetime.datetime] = lambda: dtstamp_factory(), 538 ): 539 """Initialize the TodoStore.""" 540 super().__init__( 541 calendar.todos, 542 calendar.timezones, 543 TodoStoreError, 544 dtstamp_fn, 545 tzinfo=tzinfo, 546 ) 547 self._calendar = calendar
Initialize the TodoStore.
549 def todo_list(self, dtstart: datetime.datetime | None = None) -> Iterable[Todo]: 550 """Return a list of all todos on the calendar. 551 552 This view accounts for recurring todos. 553 """ 554 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
Exception thrown by the TodoStore.
Exception thrown by a Store.