Source code for icalendar.cal.todo

""":rfc:`5545` VTODO component."""

from __future__ import annotations

import uuid
from datetime import date, datetime, timedelta
from typing import TYPE_CHECKING, Optional, Sequence

from icalendar.attr import (
    X_MOZ_LASTACK_property,
    X_MOZ_SNOOZE_TIME_property,
    categories_property,
    color_property,
    create_single_property,
    description_property,
    exdates_property,
    property_del_duration,
    property_doc_duration_template,
    property_get_duration,
    property_set_duration,
    rdates_property,
    rrules_property,
    sequence_property,
    summary_property,
    uid_property,
)
from icalendar.cal.component import Component
from icalendar.error import IncompleteComponent, InvalidCalendar
from icalendar.tools import is_date

if TYPE_CHECKING:
    from icalendar.alarms import Alarms


[docs] class Todo(Component): """ A "VTODO" calendar component is a grouping of component properties that represents an action item or assignment. For example, it can be used to represent an item of work assigned to an individual, such as "Prepare for the upcoming conference seminar on Internet Calendaring". Examples: Create a new Todo: >>> from icalendar import Todo >>> todo = Todo.new() >>> print(todo.to_ical()) BEGIN:VTODO DTSTAMP:20250517T080612Z UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 END:VTODO """ name = "VTODO" required = ( "UID", "DTSTAMP", ) singletons = ( "CLASS", "COLOR", "COMPLETED", "CREATED", "DESCRIPTION", "DTSTAMP", "DTSTART", "GEO", "LAST-MODIFIED", "LOCATION", "ORGANIZER", "PERCENT-COMPLETE", "PRIORITY", "RECURRENCE-ID", "SEQUENCE", "STATUS", "SUMMARY", "UID", "URL", "DUE", "DURATION", ) exclusive = ( "DUE", "DURATION", ) multiple = ( "ATTACH", "ATTENDEE", "CATEGORIES", "COMMENT", "CONTACT", "EXDATE", "RSTATUS", "RELATED", "RESOURCES", "RDATE", "RRULE", ) DTSTART = create_single_property( "DTSTART", "dt", (datetime, date), date, 'The "DTSTART" property for a "VTODO" specifies the inclusive start of the Todo.', # noqa: E501 ) DUE = create_single_property( "DUE", "dt", (datetime, date), date, 'The "DUE" property for a "VTODO" calendar component specifies the non-inclusive end of the Todo.', # noqa: E501 ) DURATION = property( property_get_duration, property_set_duration, property_del_duration, property_doc_duration_template.format(component="VTODO"), ) def _get_start_end_duration(self): """Verify the calendar validity and return the right attributes.""" start = self.DTSTART end = self.DUE duration = self.DURATION if duration is not None and end is not None: raise InvalidCalendar( "Only one of DUE and DURATION may be in a VTODO, not both." ) if ( isinstance(start, date) and not isinstance(start, datetime) and duration is not None and duration.seconds != 0 ): raise InvalidCalendar( "When DTSTART is a date, DURATION must be of days or weeks." ) if start is not None and end is not None and is_date(start) != is_date(end): raise InvalidCalendar( "DTSTART and DUE must be of the same type, either date or datetime." ) return start, end, duration @property def start(self) -> date | datetime: """The start of the VTODO. Invalid values raise an InvalidCalendar. If there is no start, we also raise an IncompleteComponent error. You can get the start, end and duration of a Todo as follows: >>> from datetime import datetime >>> from icalendar import Todo >>> todo = Todo() >>> todo.start = datetime(2021, 1, 1, 12) >>> todo.end = datetime(2021, 1, 1, 12, 30) # 30 minutes >>> todo.duration # 1800 seconds == 30 minutes datetime.timedelta(seconds=1800) >>> print(todo.to_ical()) BEGIN:VTODO DTSTART:20210101T120000 DUE:20210101T123000 END:VTODO """ start = self._get_start_end_duration()[0] if start is None: raise IncompleteComponent("No DTSTART given.") return start @start.setter def start(self, start: Optional[date | datetime]): """Set the start.""" self.DTSTART = start @property def end(self) -> date | datetime: """The end of the todo. Invalid values raise an InvalidCalendar error. If there is no end, we also raise an IncompleteComponent error. """ start, end, duration = self._get_start_end_duration() if end is None and duration is None: if start is None: raise IncompleteComponent("No DUE or DURATION+DTSTART given.") if is_date(start): return start + timedelta(days=1) return start if duration is not None: if start is not None: return start + duration raise IncompleteComponent("No DUE or DURATION+DTSTART given.") return end @end.setter def end(self, end: date | datetime | None): """Set the end.""" self.DUE = end @property def duration(self) -> timedelta: """The duration of the VTODO. This duration is calculated from the start and end of the Todo. You cannot set the duration as it is unclear what happens to start and end. """ return self.end - self.start X_MOZ_SNOOZE_TIME = X_MOZ_SNOOZE_TIME_property X_MOZ_LASTACK = X_MOZ_LASTACK_property @property def alarms(self) -> Alarms: """Compute the alarm times for this component. >>> from datetime import datetime >>> from icalendar import Todo >>> todo = Todo() # empty without alarms >>> todo.start = datetime(2024, 10, 26, 10, 21) >>> len(todo.alarms.times) 0 Note that this only uses DTSTART and DUE, but ignores RDATE, EXDATE, and RRULE properties. """ from icalendar.alarms import Alarms return Alarms(self) color = color_property sequence = sequence_property categories = categories_property rdates = rdates_property exdates = exdates_property rrules = rrules_property uid = uid_property summary = summary_property description = description_property
[docs] @classmethod def new( cls, /, categories: Sequence[str] = (), color: Optional[str] = None, description: Optional[str] = None, dtstamp: Optional[date] = None, end: Optional[date | datetime] = None, sequence: Optional[int] = None, start: Optional[date | datetime] = None, summary: Optional[str] = None, uid: Optional[str | uuid.UUID] = None, ): """Create a new TODO with all required properties. This creates a new Todo in accordance with :rfc:`5545`. Arguments: categories: The :attr:`categories` of the todo. color: The :attr:`color` of the todo. description: The :attr:`description` of the todo. dtstamp: The :attr:`DTSTAMP` of the todo. If None, this is set to the current time. end: The :attr:`end` of the todo. sequence: The :attr:`sequence` of the todo. start: The :attr:`start` of the todo. summary: The :attr:`summary` of the todo. uid: The :attr:`uid` of the todo. If None, this is set to a new :func:`uuid.uuid4`. Returns: :class:`Todo` Raises: IncompleteComponent: If the content is not valid according to :rfc:`5545`. .. warning:: As time progresses, we will be stricter with the validation. """ todo = super().new(dtstamp=dtstamp if dtstamp is not None else cls._utc_now()) todo.summary = summary todo.description = description todo.uid = uid if uid is not None else uuid.uuid4() todo.start = start todo.end = end todo.color = color todo.categories = categories todo.sequence = sequence return todo
__all__ = ["Todo"]