Skip to main content
`Ctrl`+`K`

# Source code for icalendar.attr

    """Attributes of Components and properties."""
    from __future__ import annotations

    import itertools
    from datetime import date, datetime, timedelta
    from typing import TYPE_CHECKING, Optional, Union

    from icalendar.error import InvalidCalendar
    from icalendar.prop import vCategory, vDDDTypes, vRecur, vText
    from icalendar.timezone import tzp

    if TYPE_CHECKING:
        from icalendar.cal import Component

    def _get_rdates(self: Component) -> list[
            Union[tuple[date, None],
                  tuple[datetime, None],
                  tuple[datetime, datetime]]]:
        """The RDATE property defines the list of DATE-TIME values for recurring components.

        RDATE is defined in :rfc:`5545`.
        The return value is a list of tuples ``(start, end)``.

        ``start`` can be a :class:`datetime.date` or a :class:`datetime.datetime`,
        with and without timezone.

        ``end`` is :obj:`None` if the end is not specified and a :class:`datetime.datetime`
        if the end is specified.

        Value Type:
            The default value type for this property is DATE-TIME.
            The value type can be set to DATE or PERIOD.

        Property Parameters:
            IANA, non-standard, value data type, and time
            zone identifier property parameters can be specified on this
            property.

        Conformance:
            This property can be specified in recurring "VEVENT",
            "VTODO", and "VJOURNAL" calendar components as well as in the
            "STANDARD" and "DAYLIGHT" sub-components of the "VTIMEZONE"
            calendar component.

        Description:
            This property can appear along with the "RRULE"
            property to define an aggregate set of repeating occurrences.
            When they both appear in a recurring component, the recurrence
            instances are defined by the union of occurrences defined by both
            the "RDATE" and "RRULE".

            The recurrence dates, if specified, are used in computing the
            recurrence set.  The recurrence set is the complete set of
            recurrence instances for a calendar component.  The recurrence set
            is generated by considering the initial "DTSTART" property along
            with the "RRULE", "RDATE", and "EXDATE" properties contained
            within the recurring component.  The "DTSTART" property defines
            the first instance in the recurrence set.  The "DTSTART" property
            value SHOULD match the pattern of the recurrence rule, if
            specified.  The recurrence set generated with a "DTSTART" property
            value that doesn't match the pattern of the rule is undefined.
            The final recurrence set is generated by gathering all of the
            start DATE-TIME values generated by any of the specified "RRULE"
            and "RDATE" properties, and then excluding any start DATE-TIME
            values specified by "EXDATE" properties.  This implies that start
            DATE-TIME values specified by "EXDATE" properties take precedence
            over those specified by inclusion properties (i.e., "RDATE" and
            "RRULE").  Where duplicate instances are generated by the "RRULE"
            and "RDATE" properties, only one recurrence is considered.
            Duplicate instances are ignored.

        Example:
            Below, we set one RDATE in a list and get the resulting tuple of start and end.

            .. code-block:: pycon

                >>> from icalendar import Event
                >>> from datetime import datetime
                >>> event = Event()

                # Add a list of recurrence dates
                >>> event.add("RDATE", [datetime(2025, 4, 28, 16, 5)])
                >>> event.rdates
                [(datetime.datetime(2025, 4, 28, 16, 5), None)]

        .. note::

            You cannot modify the RDATE value by modifying the result.
            Use :func:`icalendar.cal.Component.add` to add values.

            If you want to compute recurrences, have a look at :ref:`Related projects`.

        """
        result = []
        rdates = self.get("RDATE", [])
        for rdates in (rdates,) if not isinstance(rdates, list) else rdates:
            for dts in rdates.dts:
                rdate = dts.dt
                if isinstance(rdate, tuple):
                    # we have a period as rdate
                    if isinstance(rdate[1], timedelta):
                        result.append((rdate[0], rdate[0] + rdate[1]))
                    else:
                        result.append(rdate)
                else:
                    # we have a date/datetime
                    result.append((rdate, None))
        return result

    rdates_property = property(_get_rdates)

    def _get_exdates(self: Component) -> list[date|datetime]:
        """EXDATE defines the list of DATE-TIME exceptions for recurring components.

        EXDATE is defined in :rfc:`5545`.

        Value Type:
            The default value type for this property is DATE-TIME.
            The value type can be set to DATE.

        Property Parameters:
            IANA, non-standard, value data type, and time
            zone identifier property parameters can be specified on this
            property.

        Conformance:
            This property can be specified in recurring "VEVENT",
            "VTODO", and "VJOURNAL" calendar components as well as in the
            "STANDARD" and "DAYLIGHT" sub-components of the "VTIMEZONE"
            calendar component.

        Description:
            The exception dates, if specified, are used in
            computing the recurrence set.  The recurrence set is the complete
            set of recurrence instances for a calendar component.  The
            recurrence set is generated by considering the initial "DTSTART"
            property along with the "RRULE", "RDATE", and "EXDATE" properties
            contained within the recurring component.  The "DTSTART" property
            defines the first instance in the recurrence set.  The "DTSTART"
            property value SHOULD match the pattern of the recurrence rule, if
            specified.  The recurrence set generated with a "DTSTART" property
            value that doesn't match the pattern of the rule is undefined.
            The final recurrence set is generated by gathering all of the
            start DATE-TIME values generated by any of the specified "RRULE"
            and "RDATE" properties, and then excluding any start DATE-TIME
            values specified by "EXDATE" properties.  This implies that start
            DATE-TIME values specified by "EXDATE" properties take precedence
            over those specified by inclusion properties (i.e., "RDATE" and
            "RRULE").  When duplicate instances are generated by the "RRULE"
            and "RDATE" properties, only one recurrence is considered.
            Duplicate instances are ignored.

            The "EXDATE" property can be used to exclude the value specified
            in "DTSTART".  However, in such cases, the original "DTSTART" date
            MUST still be maintained by the calendaring and scheduling system
            because the original "DTSTART" value has inherent usage
            dependencies by other properties such as the "RECURRENCE-ID".

        Example:
            Below, we add an exdate in a list and get the resulting list of exdates.

            .. code-block:: pycon

                >>> from icalendar import Event
                >>> from datetime import datetime
                >>> event = Event()

                # Add a list of excluded dates
                >>> event.add("EXDATE", [datetime(2025, 4, 28, 16, 5)])
                >>> event.exdates
                [datetime.datetime(2025, 4, 28, 16, 5)]

        .. note::

            You cannot modify the EXDATE value by modifying the result.
            Use :func:`icalendar.cal.Component.add` to add values.

            If you want to compute recurrences, have a look at :ref:`Related projects`.

        """
        result = []
        exdates = self.get("EXDATE", [])
        for exdates in (exdates,) if not isinstance(exdates, list) else exdates:
            for dts in exdates.dts:
                exdate = dts.dt
                # we have a date/datetime
                result.append(exdate)
        return result

    exdates_property = property(_get_exdates)

    def _get_rrules(self: Component) -> list[vRecur]:
        """RRULE defines a rule or repeating pattern for recurring components.

        RRULE is defined in :rfc:`5545`.
        :rfc:`7529` adds the ``SKIP`` parameter :class:`icalendar.prop.vSkip`.

        Property Parameters:
            IANA and non-standard property parameters can
            be specified on this property.

        Conformance:
            This property can be specified in recurring "VEVENT",
            "VTODO", and "VJOURNAL" calendar components as well as in the
            "STANDARD" and "DAYLIGHT" sub-components of the "VTIMEZONE"
            calendar component, but it SHOULD NOT be specified more than once.
            The recurrence set generated with multiple "RRULE" properties is
            undefined.

        Description:
            The recurrence rule, if specified, is used in computing
            the recurrence set.  The recurrence set is the complete set of
            recurrence instances for a calendar component.  The recurrence set
            is generated by considering the initial "DTSTART" property along
            with the "RRULE", "RDATE", and "EXDATE" properties contained
            within the recurring component.  The "DTSTART" property defines
            the first instance in the recurrence set.  The "DTSTART" property
            value SHOULD be synchronized with the recurrence rule, if
            specified.  The recurrence set generated with a "DTSTART" property
            value not synchronized with the recurrence rule is undefined.  The
            final recurrence set is generated by gathering all of the start
            DATE-TIME values generated by any of the specified "RRULE" and
            "RDATE" properties, and then excluding any start DATE-TIME values
            specified by "EXDATE" properties.  This implies that start DATE-
            TIME values specified by "EXDATE" properties take precedence over
            those specified by inclusion properties (i.e., "RDATE" and
            "RRULE").  Where duplicate instances are generated by the "RRULE"
            and "RDATE" properties, only one recurrence is considered.
            Duplicate instances are ignored.

            The "DTSTART" property specified within the iCalendar object
            defines the first instance of the recurrence.  In most cases, a
            "DTSTART" property of DATE-TIME value type used with a recurrence
            rule, should be specified as a date with local time and time zone
            reference to make sure all the recurrence instances start at the
            same local time regardless of time zone changes.

            If the duration of the recurring component is specified with the
            "DTEND" or "DUE" property, then the same exact duration will apply
            to all the members of the generated recurrence set.  Else, if the
            duration of the recurring component is specified with the
            "DURATION" property, then the same nominal duration will apply to
            all the members of the generated recurrence set and the exact
            duration of each recurrence instance will depend on its specific
            start time.  For example, recurrence instances of a nominal
            duration of one day will have an exact duration of more or less
            than 24 hours on a day where a time zone shift occurs.  The
            duration of a specific recurrence may be modified in an exception
            component or simply by using an "RDATE" property of PERIOD value
            type.

        Examples:
            Daily for 10 occurrences:

            .. code-block:: pycon

                >>> from icalendar import Event
                >>> from datetime import datetime
                >>> from zoneinfo import ZoneInfo
                >>> event = Event()
                >>> event.start = datetime(1997, 9, 2, 9, 0, tzinfo=ZoneInfo("America/New_York"))
                >>> event.add("RRULE", "FREQ=DAILY;COUNT=10")
                >>> print(event.to_ical())
                BEGIN:VEVENT
                DTSTART;TZID=America/New_York:19970902T090000
                RRULE:FREQ=DAILY;COUNT=10
                END:VEVENT
                >>> event.rrules
                [vRecur({'FREQ': ['DAILY'], 'COUNT': [10]})]

            Daily until December 24, 1997:

            .. code-block:: pycon

                >>> from icalendar import Event, vRecur
                >>> from datetime import datetime
                >>> from zoneinfo import ZoneInfo
                >>> event = Event()
                >>> event.start = datetime(1997, 9, 2, 9, 0, tzinfo=ZoneInfo("America/New_York"))
                >>> event.add("RRULE", vRecur({"FREQ": ["DAILY"]}, until=datetime(1997, 12, 24, tzinfo=ZoneInfo("UTC"))))
                >>> print(event.to_ical())
                BEGIN:VEVENT
                DTSTART;TZID=America/New_York:19970902T090000
                RRULE:FREQ=DAILY;UNTIL=19971224T000000Z
                END:VEVENT
                >>> event.rrules
                [vRecur({'FREQ': ['DAILY'], 'UNTIL': [datetime.datetime(1997, 12, 24, 0, 0, tzinfo=ZoneInfo(key='UTC'))]})]

        .. note::

            You cannot modify the RRULE value by modifying the result.
            Use :func:`icalendar.cal.Component.add` to add values.

            If you want to compute recurrences, have a look at :ref:`Related projects`.

        """
        rrules = self.get("RRULE", [])
        if not isinstance(rrules, list):
            return [rrules]
        return rrules

    rrules_property = property(_get_rrules)

    def multi_language_text_property(main_prop:str, compatibility_prop:str, doc:str) -> property:
        """This creates a text property.

        This property can be defined several times with different ``LANGUAGE`` parameters.

        Args:
            main_prop (str): The property to set and get, such as ``NAME``
            compatibility_prop (str): An old property used before, such as ``X-WR-CALNAME``
            doc (str): The documentation string
        """
        def fget(self: Component) -> Optional[str]:
            """Get the property"""
            result = self.get(main_prop, self.get(compatibility_prop))
            if isinstance(result, list):
                for item in result:
                    if "LANGUAGE" not in item.params:
                        return item
            return result

        def fset(self: Component, value:str):
            """Set the property."""
            fdel(self)
            self.add(main_prop, value)

        def fdel(self: Component):
            """Delete the property."""
            self.pop(main_prop, None)
            self.pop(compatibility_prop, None)

        return property(fget, fset, fdel, doc)

    def single_int_property(prop:str, default:int, doc:str) -> property:
        """Create a property for an int value that exists only once.

        Args:
            prop: The name of the property
            default: The default value
            doc: The documentation string
        """
        def fget(self: Component) -> int:
            """Get the property"""
            try:
                return int(self.get(prop, default))
            except ValueError as e:
                raise InvalidCalendar(f"{prop} must be an int") from e

        def fset(self: Component, value:int):
            """Set the property."""
            fdel(self)
            self.add(prop, value)

        def fdel(self: Component):
            """Delete the property."""
            self.pop(prop, None)

        return property(fget, fset, fdel, doc)

    def single_utc_property(name: str, docs: str) -> property:
        """Create a property to access a value of datetime in UTC timezone.

        Args:
            name: name of the property
            docs: documentation string
        """
        docs = (
            f"""The {name} property. datetime in UTC

        All values will be converted to a datetime in UTC.
        """
            + docs
        )

        def fget(self: Component) -> Optional[datetime]:
            """Get the value."""
            if name not in self:
                return None
            dt = self.get(name)
            if isinstance(dt, vText):
                # we might be in an attribute that is not typed
                value = vDDDTypes.from_ical(dt)
            else:
                value = getattr(dt, "dt", None)
            if value is None or not isinstance(value, date):
                raise InvalidCalendar(f"{name} must be a datetime in UTC, not {value}")
            return tzp.localize_utc(value)

        def fset(self: Component, value: datetime):
            """Set the value"""
            if not isinstance(value, date):
                raise TypeError(f"{name} takes a datetime in UTC, not {value}")
            fdel(self)
            self.add(name, tzp.localize_utc(value))

        def fdel(self: Component):
            """Delete the property."""
            self.pop(name, None)

        return property(fget, fset, fdel, doc=docs)

    def single_string_property(name: str, docs: str, other_name:Optional[str]=None) -> property:
        """Create a property to access a single string value."""

        def fget(self: Component) -> str:
            """Get the value."""
            result = self.get(name, None if other_name is None else self.get(other_name, None))
            if result is None or result == []:
                return ""
            if isinstance(result, list):
                return result[0]
            return result

        def fset(self: Component, value: str):
            """Set the value"""
            fdel(self)
            self.add(name, value)

        def fdel(self: Component):
            """Delete the property."""
            self.pop(name, None)
            if other_name is not None:
                self.pop(other_name, None)

        return property(fget, fset, fdel, doc=docs)

    color_property = single_string_property(
        "COLOR",
        """This property specifies a color used for displaying the component.

        This implements :rfc:`7986` ``COLOR`` property.

        Property Parameters:
            IANA and non-standard property parameters can
            be specified on this property.

        Conformance:
            This property can be specified once in an iCalendar
            object or in ``VEVENT``, ``VTODO``, or ``VJOURNAL`` calendar components.

        Description:
            This property specifies a color that clients MAY use
            when presenting the relevant data to a user.  Typically, this
            would appear as the "background" color of events or tasks.  The
            value is a case-insensitive color name taken from the CSS3 set of
            names, defined in Section 4.3 of `W3C.REC-css3-color-20110607 <https://www.w3.org/TR/css-color-3/>`_.

        Example:
            ``"turquoise"``, ``"#ffffff"``

            .. code-block:: pycon

                >>> from icalendar import Todo
                >>> todo = Todo()
                >>> todo.color = "green"
                >>> print(todo.to_ical())
                BEGIN:VTODO
                COLOR:green
                END:VTODO
        """
    )

    sequence_property = single_int_property(
        "SEQUENCE",
        0,
        """This property defines the revision sequence number of the calendar component within a sequence of revisions.

    Value Type:
        INTEGER

    Property Parameters:
        IANA and non-standard property parameters can be specified on this property.

    Conformance:
        The property can be specified in "VEVENT", "VTODO", or
        "VJOURNAL" calendar component.

    Description:
        When a calendar component is created, its sequence
        number is 0.  It is monotonically incremented by the "Organizer's"
        CUA each time the "Organizer" makes a significant revision to the
        calendar component.

        The "Organizer" includes this property in an iCalendar object that
        it sends to an "Attendee" to specify the current version of the
        calendar component.

        The "Attendee" includes this property in an iCalendar object that
        it sends to the "Organizer" to specify the version of the calendar
        component to which the "Attendee" is referring.

        A change to the sequence number is not the mechanism that an
        "Organizer" uses to request a response from the "Attendees".  The
        "RSVP" parameter on the "ATTENDEE" property is used by the
        "Organizer" to indicate that a response from the "Attendees" is
        requested.

        Recurrence instances of a recurring component MAY have different
        sequence numbers.

    Examples:
        The following is an example of this property for a calendar
        component that was just created by the "Organizer":

        .. code-block:: pycon

            >>> from icalendar import Event
            >>> event = Event()
            >>> event.sequence
            0

        The following is an example of this property for a calendar
        component that has been revised 10 different times by the
        "Organizer":

        .. code-block:: pycon

            >>> from icalendar import Calendar
            >>> calendar = Calendar.example("issue_156_RDATE_with_PERIOD_TZID_khal")
            >>> event = calendar.events[0]
            >>> event.sequence
            10
        """
    )

    def _get_categories(component: Component) -> list[str]:
        """Get all the categories."""
        categories : Optional[vCategory|list[vCategory]] = component.get("CATEGORIES")
        if isinstance(categories, list):
            _set_categories(component, list(itertools.chain.from_iterable(cat.cats for cat in categories)))
            return _get_categories(component)
        if categories is None:
            categories = vCategory([])
            component.add("CATEGORIES", categories)
        return categories.cats

    def _set_categories(component: Component, cats: list[str]) -> None:
        """Set the categories."""
        component["CATEGORIES"] = categories = vCategory(cats)
        cats.clear()
        cats.extend(categories.cats)
        categories.cats = cats

    def _del_categories(component: Component) -> None:
        """Delete the categories."""
        component.pop("CATEGORIES", None)

    categories_property = property(
        _get_categories,
        _set_categories,
        _del_categories,
        """This property defines the categories for a component.

    Property Parameters:
        IANA, non-standard, and language property parameters can be specified on this
        property.

    Conformance:
        The property can be specified within "VEVENT", "VTODO", or "VJOURNAL" calendar
        components.
        Since :rfc:`7986` it can also be defined on a "VCALENDAR" component.

    Description:
        This property is used to specify categories or subtypes
        of the calendar component.  The categories are useful in searching
        for a calendar component of a particular type and category.
        Within the "VEVENT", "VTODO", or "VJOURNAL" calendar components,
        more than one category can be specified as a COMMA-separated list
        of categories.

    Example:
        Below, we add the categories to an event:

        .. code-block:: pycon

            >>> from icalendar import Event
            >>> event = Event()
            >>> event.categories = ["Work", "Meeting"]
            >>> print(event.to_ical())
            BEGIN:VEVENT
            CATEGORIES:Work,Meeting
            END:VEVENT
            >>> event.categories.append("Lecture")
            >>> event.categories == ["Work", "Meeting", "Lecture"]
            True

    .. note::

       At present, we do not take the LANGUAGE parameter into account.
    """
    )

    uid_property = single_string_property(
        "UID", """UID specifies the persistent, globally unique identifier for a component.

    We recommend using :func:`uuid.uuid4` to generate new values.

    Returns:
        The value of the UID property as a string or ``""`` if no value is set.

    Description:
        The "UID" itself MUST be a globally unique identifier.
        The generator of the identifier MUST guarantee that the identifier
        is unique.

        This is the method for correlating scheduling messages with the
        referenced "VEVENT", "VTODO", or "VJOURNAL" calendar component.
        The full range of calendar components specified by a recurrence
        set is referenced by referring to just the "UID" property value
        corresponding to the calendar component.  The "RECURRENCE-ID"
        property allows the reference to an individual instance within the
        recurrence set.

        This property is an important method for group-scheduling
        applications to match requests with later replies, modifications,
        or deletion requests.  Calendaring and scheduling applications
        MUST generate this property in "VEVENT", "VTODO", and "VJOURNAL"
        calendar components to assure interoperability with other group-
        scheduling applications.  This identifier is created by the
        calendar system that generates an iCalendar object.

        Implementations MUST be able to receive and persist values of at
        least 255 octets for this property, but they MUST NOT truncate
        values in the middle of a UTF-8 multi-octet sequence.

        :rfc:`7986` states that UID can be used, for
        example, to identify duplicate calendar streams that a client may
        have been given access to.  It can be used in conjunction with the
        "LAST-MODIFIED" property also specified on the "VCALENDAR" object
        to identify the most recent version of a calendar.

    Conformance:
        :rfc:`5545` states that the "UID" property can be specified on "VEVENT", "VTODO",
        and "VJOURNAL" calendar components.
        :rfc:`7986` modifies the definition of the "UID" property to
        allow it to be defined in an iCalendar object.
        :rfc:`9074`  adds a "UID" property to "VALARM" components to allow a unique
        identifier to be specified. The value of this property can then be used
        to refer uniquely to the "VALARM" component.

        This property can be specified once only.

    Security:
        :rfc:`7986` states that UID values MUST NOT include any data that
        might identify a user, host, domain, or any other security- or
        privacy-sensitive information.  It is RECOMMENDED that calendar user
        agents now generate "UID" values that are hex-encoded random
        Universally Unique Identifier (UUID) values as defined in
        Sections 4.4 and 4.5 of :rfc:`4122`.
        You can use the :mod:`uuid` module to generate new UUIDs.

    Compatibility:
        For Alarms, ``X-ALARMUID`` is also considered.

    Examples:
        The following is an example of such a property value:
        ``5FC53010-1267-4F8E-BC28-1D7AE55A7C99``.

        Set the UID of a calendar:

        .. code-block:: pycon

            >>> from icalendar import Calendar
            >>> from uuid import uuid4
            >>> calendar = Calendar()
            >>> calendar.uid = uuid4()
            >>> print(calendar.to_ical())
            BEGIN:VCALENDAR
            UID:d755cef5-2311-46ed-a0e1-6733c9e15c63
            END:VCALENDAR

    """
    )

    __all__ = [
        "categories_property",
        "color_property",
        "exdates_property",
        "multi_language_text_property",
        "rdates_property",
        "rrules_property",
        "sequence_property",
        "single_int_property",
        "single_utc_property",
        "uid_property",
    ]