""":rfc:`5545` iCalendar component."""
from __future__ import annotations
from datetime import timedelta
from typing import TYPE_CHECKING, Sequence
from icalendar.attr import (
categories_property,
images_property,
multi_language_text_property,
single_string_property,
source_property,
uid_property,
url_property,
)
from icalendar.cal.component import Component
from icalendar.cal.examples import get_example
from icalendar.cal.timezone import Timezone
from icalendar.error import IncompleteComponent
from icalendar.version import __version__
if TYPE_CHECKING:
import uuid
from datetime import date, datetime
from icalendar.cal.availability import Availability
from icalendar.cal.event import Event
from icalendar.cal.free_busy import FreeBusy
from icalendar.cal.todo import Todo
[docs]
class Calendar(Component):
"""
The "VCALENDAR" object is a collection of calendar information.
This information can include a variety of components, such as
"VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VTIMEZONE", or any
other type of calendar component.
Examples:
Create a new Calendar:
>>> from icalendar import Calendar
>>> calendar = Calendar.new(name="My Calendar")
>>> print(calendar.calendar_name)
My Calendar
"""
name = "VCALENDAR"
canonical_order = (
"VERSION",
"PRODID",
"CALSCALE",
"METHOD",
"DESCRIPTION",
"X-WR-CALDESC",
"NAME",
"X-WR-CALNAME",
)
required = (
"PRODID",
"VERSION",
)
singletons = (
"PRODID",
"VERSION",
"CALSCALE",
"METHOD",
"COLOR", # RFC 7986
)
multiple = (
"CATEGORIES", # RFC 7986
"DESCRIPTION", # RFC 7986
"NAME", # RFC 7986
)
[docs]
@classmethod
def example(cls, name: str = "example") -> Calendar:
"""Return the calendar example with the given name."""
return cls.from_ical(get_example("calendars", name))
[docs]
@classmethod
def from_ical(cls, st, multiple=False):
comps = Component.from_ical(st, multiple=True)
all_timezones_so_far = True
for comp in comps:
for component in comp.subcomponents:
if component.name == "VTIMEZONE":
if not all_timezones_so_far:
# If a preceding component refers to a VTIMEZONE defined
# later in the source st
# (forward references are allowed by RFC 5545), then the
# earlier component may have
# the wrong timezone attached.
# However, during computation of comps, all VTIMEZONEs
# observed do end up in
# the timezone cache. So simply re-running from_ical will
# rely on the cache
# for those forward references to produce the correct result.
# See test_create_america_new_york_forward_reference.
return Component.from_ical(st, multiple)
else:
all_timezones_so_far = False
# No potentially forward VTIMEZONEs to worry about
if multiple:
return comps
if len(comps) > 1:
raise ValueError(
cls._format_error(
"Found multiple components where only one is allowed", st
)
)
if len(comps) < 1:
raise ValueError(
cls._format_error(
"Found no components where exactly one is required", st
)
)
return comps[0]
@property
def events(self) -> list[Event]:
"""All event components in the calendar.
This is a shortcut to get all events.
Modifications do not change the calendar.
Use :py:meth:`Component.add_component`.
>>> from icalendar import Calendar
>>> calendar = Calendar.example()
>>> event = calendar.events[0]
>>> event.start
datetime.date(2022, 1, 1)
>>> print(event["SUMMARY"])
New Year's Day
"""
return self.walk("VEVENT")
@property
def todos(self) -> list[Todo]:
"""All todo components in the calendar.
This is a shortcut to get all todos.
Modifications do not change the calendar.
Use :py:meth:`Component.add_component`.
"""
return self.walk("VTODO")
@property
def availabilities(self) -> list[Availability]:
"""All :class:`Availability` components in the calendar.
This is a shortcut to get all availabilities.
Modifications do not change the calendar.
Use :py:meth:`Component.add_component`.
"""
return self.walk("VAVAILABILITY")
@property
def freebusy(self) -> list[FreeBusy]:
"""All FreeBusy components in the calendar.
This is a shortcut to get all FreeBusy.
Modifications do not change the calendar.
Use :py:meth:`Component.add_component`.
"""
return self.walk("VFREEBUSY")
[docs]
def get_used_tzids(self) -> set[str]:
"""The set of TZIDs in use.
This goes through the whole calendar to find all occurrences of
timezone information like the TZID parameter in all attributes.
>>> from icalendar import Calendar
>>> calendar = Calendar.example("timezone_rdate")
>>> calendar.get_used_tzids()
{'posix/Europe/Vaduz'}
Even if you use UTC, this will not show up.
"""
result = set()
for _name, value in self.property_items(sorted=False):
if hasattr(value, "params"):
result.add(value.params.get("TZID"))
return result - {None}
[docs]
def get_missing_tzids(self) -> set[str]:
"""The set of missing timezone component tzids.
To create a :rfc:`5545` compatible calendar,
all of these timezones should be added.
"""
tzids = self.get_used_tzids()
for timezone in self.timezones:
tzids.remove(timezone.tz_name)
return tzids
@property
def timezones(self) -> list[Timezone]:
"""Return the timezones components in this calendar.
>>> from icalendar import Calendar
>>> calendar = Calendar.example("pacific_fiji")
>>> [timezone.tz_name for timezone in calendar.timezones]
['custom_Pacific/Fiji']
.. note::
This is a read-only property.
"""
return self.walk("VTIMEZONE")
[docs]
def add_missing_timezones(
self,
first_date: date = Timezone.DEFAULT_FIRST_DATE,
last_date: date = Timezone.DEFAULT_LAST_DATE,
):
"""Add all missing VTIMEZONE components.
This adds all the timezone components that are required.
VTIMEZONE components are inserted at the beginning of the calendar
to ensure they appear before other components that reference them.
.. note::
Timezones that are not known will not be added.
:param first_date: earlier than anything that happens in the calendar
:param last_date: later than anything happening in the calendar
>>> from icalendar import Calendar, Event
>>> from datetime import datetime
>>> from zoneinfo import ZoneInfo
>>> calendar = Calendar()
>>> event = Event()
>>> calendar.add_component(event)
>>> event.start = datetime(1990, 10, 11, 12, tzinfo=ZoneInfo("Europe/Berlin"))
>>> calendar.timezones
[]
>>> calendar.add_missing_timezones()
>>> calendar.timezones[0].tz_name
'Europe/Berlin'
>>> calendar.get_missing_tzids() # check that all are added
set()
"""
missing_tzids = self.get_missing_tzids()
if not missing_tzids:
return
existing_timezone_count = len(self.timezones)
for tzid in missing_tzids:
try:
timezone = Timezone.from_tzid(
tzid, first_date=first_date, last_date=last_date
)
except ValueError:
continue
self.subcomponents.insert(existing_timezone_count, timezone)
existing_timezone_count += 1
calendar_name = multi_language_text_property(
"NAME",
"X-WR-CALNAME",
"""This property specifies the name of the calendar.
This implements :rfc:`7986` ``NAME`` and ``X-WR-CALNAME``.
Property Parameters:
IANA, non-standard, alternate text
representation, and language property parameters can be specified
on this property.
Conformance:
This property can be specified multiple times in an
iCalendar object. However, each property MUST represent the name
of the calendar in a different language.
Description:
This property is used to specify a name of the
iCalendar object that can be used by calendar user agents when
presenting the calendar data to a user. Whilst a calendar only
has a single name, multiple language variants can be specified by
including this property multiple times with different "LANGUAGE"
parameter values on each.
Example:
Below, we set the name of the calendar.
.. code-block:: pycon
>>> from icalendar import Calendar
>>> calendar = Calendar()
>>> calendar.calendar_name = "My Calendar"
>>> print(calendar.to_ical())
BEGIN:VCALENDAR
NAME:My Calendar
END:VCALENDAR
""",
)
description = multi_language_text_property(
"DESCRIPTION",
"X-WR-CALDESC",
"""This property specifies the description of the calendar.
This implements :rfc:`7986` ``DESCRIPTION`` and ``X-WR-CALDESC``.
Conformance:
This property can be specified multiple times in an
iCalendar object. However, each property MUST represent the
description of the calendar in a different language.
Description:
This property is used to specify a lengthy textual
description of the iCalendar object that can be used by calendar
user agents when describing the nature of the calendar data to a
user. Whilst a calendar only has a single description, multiple
language variants can be specified by including this property
multiple times with different "LANGUAGE" parameter values on each.
Example:
Below, we add a description to a calendar.
.. code-block:: pycon
>>> from icalendar import Calendar
>>> calendar = Calendar()
>>> calendar.description = "This is a calendar"
>>> print(calendar.to_ical())
BEGIN:VCALENDAR
DESCRIPTION:This is a calendar
END:VCALENDAR
""",
)
color = single_string_property(
"COLOR",
"""This property specifies a color used for displaying the calendar.
This implements :rfc:`7986` ``COLOR`` and ``X-APPLE-CALENDAR-COLOR``.
Please note that since :rfc:`7986`, subcomponents can have their own color.
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 Calendar
>>> calendar = Calendar()
>>> calendar.color = "black"
>>> print(calendar.to_ical())
BEGIN:VCALENDAR
COLOR:black
END:VCALENDAR
""",
"X-APPLE-CALENDAR-COLOR",
)
categories = categories_property
uid = uid_property
prodid = single_string_property(
"PRODID",
"""PRODID specifies the identifier for the product that created the iCalendar object.
Conformance:
The property MUST be specified once in an iCalendar object.
Description:
The vendor of the implementation SHOULD assure that
this is a globally unique identifier; using some technique such as
an FPI value, as defined in [ISO.9070.1991].
This property SHOULD NOT be used to alter the interpretation of an
iCalendar object beyond the semantics specified in this memo. For
example, it is not to be used to further the understanding of non-
standard properties.
Example:
The following is an example of this property. It does not
imply that English is the default language.
.. code-block:: text
-//ABC Corporation//NONSGML My Product//EN
""", # noqa: E501
)
version = single_string_property(
"VERSION",
"""VERSION of the calendar specification.
The default is ``"2.0"`` for :rfc:`5545`.
Purpose:
This property specifies the identifier corresponding to the
highest version number or the minimum and maximum range of the
iCalendar specification that is required in order to interpret the
iCalendar object.
""",
)
calscale = single_string_property(
"CALSCALE",
"""CALSCALE defines the calendar scale used for the calendar information specified in the iCalendar object.
Compatibility:
:rfc:`7529` makes the case that GREGORIAN stays the default and other calendar scales
are implemented on the RRULE.
Conformance:
This property can be specified once in an iCalendar
object. The default value is "GREGORIAN".
Description:
This memo is based on the Gregorian calendar scale.
The Gregorian calendar scale is assumed if this property is not
specified in the iCalendar object. It is expected that other
calendar scales will be defined in other specifications or by
future versions of this memo.
""", # noqa: E501
default="GREGORIAN",
)
method = single_string_property(
"METHOD",
"""METHOD defines the iCalendar object method associated with the calendar object.
Description:
When used in a MIME message entity, the value of this
property MUST be the same as the Content-Type "method" parameter
value. If either the "METHOD" property or the Content-Type
"method" parameter is specified, then the other MUST also be
specified.
No methods are defined by this specification. This is the subject
of other specifications, such as the iCalendar Transport-
independent Interoperability Protocol (iTIP) defined by :rfc:`5546`.
If this property is not present in the iCalendar object, then a
scheduling transaction MUST NOT be assumed. In such cases, the
iCalendar object is merely being used to transport a snapshot of
some calendar information; without the intention of conveying a
scheduling semantic.
""", # noqa: E501
)
url = url_property
source = source_property
@property
def refresh_interval(self) -> timedelta | None:
"""REFRESH-INTERVAL specifies a suggested minimum interval for
polling for changes of the calendar data from the original source
of that data.
Conformance:
This property can be specified once in an iCalendar
object, consisting of a positive duration of time.
Description:
This property specifies a positive duration that gives
a suggested minimum polling interval for checking for updates to
the calendar data. The value of this property SHOULD be used by
calendar user agents to limit the polling interval for calendar
data updates to the minimum interval specified.
Raises:
ValueError: When setting a negative duration.
"""
refresh_interval = self.get("REFRESH-INTERVAL")
return refresh_interval.dt if refresh_interval else None
@refresh_interval.setter
def refresh_interval(self, value: timedelta | None):
"""Set the REFRESH-INTERVAL."""
if not isinstance(value, timedelta) and value is not None:
raise TypeError(
"REFRESH-INTERVAL must be either a positive timedelta,"
" or None to delete it."
)
if value is not None and value.total_seconds() <= 0:
raise ValueError("REFRESH-INTERVAL must be a positive timedelta.")
if value is not None:
del self.refresh_interval
self.add("REFRESH-INTERVAL", value)
else:
del self.refresh_interval
@refresh_interval.deleter
def refresh_interval(self):
"""Delete REFRESH-INTERVAL."""
self.pop("REFRESH-INTERVAL")
images = images_property
[docs]
@classmethod
def new(
cls,
/,
calscale: str | None = None,
categories: Sequence[str] = (),
color: str | None = None,
description: str | None = None,
language: str | None = None,
last_modified: date | datetime | None = None,
method: str | None = None,
name: str | None = None,
organization: str | None = None,
prodid: str | None = None,
refresh_interval: timedelta | None = None,
source: str | None = None,
uid: str | uuid.UUID | None = None,
url: str | None = None,
version: str = "2.0",
):
"""Create a new Calendar with all required properties.
This creates a new Calendar in accordance with :rfc:`5545` and :rfc:`7986`.
Arguments:
calscale: The :attr:`calscale` of the calendar.
categories: The :attr:`categories` of the calendar.
color: The :attr:`color` of the calendar.
description: The :attr:`description` of the calendar.
language: The language for the calendar. Used to generate localized `prodid`.
last_modified: The :attr:`Component.last_modified` of the calendar.
method: The :attr:`method` of the calendar.
name: The :attr:`calendar_name` of the calendar.
organization: The organization name. Used to generate `prodid` if not provided.
prodid: The :attr:`prodid` of the component. If None and organization is provided,
generates a `prodid` in format "-//organization//name//language".
refresh_interval: The :attr:`refresh_interval` of the calendar.
source: The :attr:`source` of the calendar.
uid: The :attr:`uid` of the calendar.
If None, this is set to a new :func:`uuid.uuid4`.
url: The :attr:`url` of the calendar.
version: The :attr:`version` of the calendar.
Returns:
:class:`Calendar`
Raises:
InvalidCalendar: If the content is not valid according to :rfc:`5545`.
.. warning:: As time progresses, we will be stricter with the validation.
""" # noqa: E501
calendar = cls()
# Generate prodid if not provided but organization is given
if prodid is None and organization:
app_name = name or "Calendar"
lang = language.upper() if language else "EN"
prodid = f"-//{organization}//{app_name}//{lang}"
elif prodid is None:
prodid = f"-//collective//icalendar//{__version__}//EN"
calendar.prodid = prodid
calendar.version = version
calendar.calendar_name = name
calendar.color = color
calendar.description = description
calendar.method = method
calendar.calscale = calscale
calendar.categories = categories
calendar.uid = uid
calendar.url = url
calendar.refresh_interval = refresh_interval
calendar.source = source
calendar.last_modified = last_modified
return calendar
[docs]
def validate(self):
"""Validate that the calendar has required properties and components.
This method can be called explicitly to validate a calendar before output.
Raises:
IncompleteComponent: If the calendar lacks required properties or
components.
"""
if not self.get("PRODID"):
raise IncompleteComponent("Calendar must have a PRODID")
if not self.get("VERSION"):
raise IncompleteComponent("Calendar must have a VERSION")
if not self.subcomponents:
raise IncompleteComponent(
"Calendar must contain at least one component (event, todo, etc.)"
)
__all__ = ["Calendar"]