1""":rfc:`5545` iCalendar component."""
2
3from __future__ import annotations
4
5import uuid
6from datetime import timedelta
7from typing import TYPE_CHECKING
8
9from icalendar.attr import (
10 CONCEPTS_TYPE_SETTER,
11 LINKS_TYPE_SETTER,
12 RELATED_TO_TYPE_SETTER,
13 categories_property,
14 images_property,
15 multi_language_text_property,
16 single_string_property,
17 source_property,
18 uid_property,
19 url_property,
20)
21from icalendar.cal.component import Component
22from icalendar.cal.examples import get_example
23from icalendar.cal.timezone import Timezone
24from icalendar.error import IncompleteComponent
25from icalendar.parser.ical.calendar import CalendarIcalParser
26from icalendar.version import __version__
27
28if TYPE_CHECKING:
29 from collections.abc import Iterable, Sequence
30 from datetime import date, datetime
31
32 from icalendar.cal.availability import Availability
33 from icalendar.cal.event import Event
34 from icalendar.cal.free_busy import FreeBusy
35 from icalendar.cal.journal import Journal
36 from icalendar.cal.todo import Todo
37 from icalendar.parser.ical.component import ComponentIcalParser
38
39
40DEFAULT_PRODID = f"-//collective//icalendar//{__version__}//EN"
41
42
43class Calendar(Component):
44 """
45 The "VCALENDAR" object is a collection of calendar information.
46 This information can include a variety of components, such as
47 "VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VTIMEZONE", or any
48 other type of calendar component.
49
50 Examples:
51 Create a new Calendar:
52
53 >>> from icalendar import Calendar
54 >>> calendar = Calendar.new(name="My Calendar")
55 >>> print(calendar.calendar_name)
56 My Calendar
57
58 """
59
60 name = "VCALENDAR"
61 canonical_order = (
62 "VERSION",
63 "PRODID",
64 "CALSCALE",
65 "METHOD",
66 "DESCRIPTION",
67 "X-WR-CALDESC",
68 "NAME",
69 "X-WR-CALNAME",
70 )
71 required = (
72 "PRODID",
73 "VERSION",
74 )
75 singletons = (
76 "PRODID",
77 "VERSION",
78 "CALSCALE",
79 "METHOD",
80 "COLOR", # RFC 7986
81 )
82 multiple = (
83 "CATEGORIES", # RFC 7986
84 "DESCRIPTION", # RFC 7986
85 "NAME", # RFC 7986
86 )
87
88 @classmethod
89 def example(cls, name: str = "example") -> Calendar:
90 """Return the calendar example with the given name."""
91 return cls.from_ical(get_example("calendars", name))
92
93 @classmethod
94 def _get_ical_parser(cls, st: str | bytes) -> ComponentIcalParser:
95 """Get the iCal parser for the given input string."""
96 return CalendarIcalParser(st, cls._get_component_factory(), cls.types_factory)
97
98 @property
99 def events(self) -> list[Event]:
100 """All event components in the calendar.
101
102 This is a shortcut to get all events.
103 Modifications do not change the calendar.
104 Use :py:meth:`Component.add_component <icalendar.cal.component.Component.add_component>`.
105
106 >>> from icalendar import Calendar
107 >>> calendar = Calendar.example()
108 >>> event = calendar.events[0]
109 >>> event.start
110 datetime.date(2022, 1, 1)
111 >>> print(event["SUMMARY"])
112 New Year's Day
113 """
114 return self.walk("VEVENT")
115
116 @property
117 def todos(self) -> list[Todo]:
118 """All todo components in the calendar.
119
120 This is a shortcut to get all todos.
121 Modifications do not change the calendar.
122 Use :py:meth:`Component.add_component <icalendar.cal.component.Component.add_component>`.
123 """
124 return self.walk("VTODO")
125
126 @property
127 def journals(self) -> list[Journal]:
128 """All journal components in the calendar.
129
130 This is a shortcut to get all journals.
131 Modifications do not change the calendar.
132 Use :py:meth:`Component.add_component <icalendar.cal.component.Component.add_component>`.
133 """
134 return self.walk("VJOURNAL")
135
136 @property
137 def availabilities(self) -> list[Availability]:
138 """All :class:`Availability` components in the calendar.
139
140 This is a shortcut to get all availabilities.
141 Modifications do not change the calendar.
142 Use :py:meth:`Component.add_component <icalendar.cal.component.Component.add_component>`.
143 """
144 return self.walk("VAVAILABILITY")
145
146 @property
147 def freebusy(self) -> list[FreeBusy]:
148 """All FreeBusy components in the calendar.
149
150 This is a shortcut to get all FreeBusy.
151 Modifications do not change the calendar.
152 Use :py:meth:`Component.add_component <icalendar.cal.component.Component.add_component>`.
153 """
154 return self.walk("VFREEBUSY")
155
156 def get_used_tzids(self) -> set[str]:
157 """The set of TZIDs in use.
158
159 This goes through the whole calendar to find all occurrences of
160 timezone information like the TZID parameter in all attributes.
161
162 >>> from icalendar import Calendar
163 >>> calendar = Calendar.example("timezone_rdate")
164 >>> calendar.get_used_tzids()
165 {'posix/Europe/Vaduz'}
166
167 Even if you use UTC, this will not show up.
168 """
169 result = set()
170 for _name, value in self.property_items(sorted=False):
171 if hasattr(value, "params"):
172 result.add(value.params.get("TZID"))
173 return result - {None}
174
175 def get_missing_tzids(self) -> set[str]:
176 """The set of missing timezone component tzids.
177
178 To create a :rfc:`5545` compatible calendar,
179 all of these timezones should be added.
180
181 UTC is excluded: per :rfc:`5545#section-3.2.19`, UTC datetimes use
182 the ``Z`` suffix and never require a VTIMEZONE component.
183 """
184 tzids = self.get_used_tzids() - {"UTC"}
185 for timezone in self.timezones:
186 # discard (not remove) — a VTIMEZONE may exist for a timezone not
187 # referenced by any event TZID (e.g. added by x-wr-timezone conversion)
188 tzids.discard(timezone.tz_name)
189 return tzids
190
191 @property
192 def timezones(self) -> list[Timezone]:
193 """Return the timezones components in this calendar.
194
195 >>> from icalendar import Calendar
196 >>> calendar = Calendar.example("pacific_fiji")
197 >>> [timezone.tz_name for timezone in calendar.timezones]
198 ['custom_Pacific/Fiji']
199
200 .. note::
201
202 This is a read-only property.
203 """
204 return self.walk("VTIMEZONE")
205
206 def add_missing_timezones(
207 self,
208 first_date: date = Timezone.DEFAULT_FIRST_DATE,
209 last_date: date = Timezone.DEFAULT_LAST_DATE,
210 ):
211 """Add all missing VTIMEZONE components.
212
213 This adds all the timezone components that are required.
214 VTIMEZONE components are inserted at the beginning of the calendar
215 to ensure they appear before other components that reference them.
216
217 .. note::
218
219 Timezones that are not known will not be added.
220
221 Parameters:
222 first_date: Earlier than anything that happens in the calendar.
223 last_date: Later than anything happening in the calendar.
224
225 >>> from icalendar import Calendar, Event
226 >>> from datetime import datetime
227 >>> from zoneinfo import ZoneInfo
228 >>> calendar = Calendar()
229 >>> event = Event()
230 >>> calendar.add_component(event)
231 >>> event.start = datetime(1990, 10, 11, 12, tzinfo=ZoneInfo("Europe/Berlin"))
232 >>> calendar.timezones
233 []
234 >>> calendar.add_missing_timezones()
235 >>> calendar.timezones[0].tz_name
236 'Europe/Berlin'
237 >>> calendar.get_missing_tzids() # check that all are added
238 set()
239 """
240 missing_tzids = self.get_missing_tzids()
241 if not missing_tzids:
242 return
243
244 existing_timezone_count = len(self.timezones)
245
246 for tzid in missing_tzids:
247 try:
248 timezone = Timezone.from_tzid(
249 tzid, first_date=first_date, last_date=last_date
250 )
251 except ValueError:
252 continue
253 self.subcomponents.insert(existing_timezone_count, timezone)
254 existing_timezone_count += 1
255
256 calendar_name = multi_language_text_property(
257 "NAME",
258 "X-WR-CALNAME",
259 """This property specifies the name of the calendar.
260
261 This implements :rfc:`7986` ``NAME`` and ``X-WR-CALNAME``.
262
263 Property Parameters:
264 IANA, non-standard, alternate text
265 representation, and language property parameters can be specified
266 on this property.
267
268 Conformance:
269 This property can be specified multiple times in an
270 iCalendar object. However, each property MUST represent the name
271 of the calendar in a different language.
272
273 Description:
274 This property is used to specify a name of the
275 iCalendar object that can be used by calendar user agents when
276 presenting the calendar data to a user. Whilst a calendar only
277 has a single name, multiple language variants can be specified by
278 including this property multiple times with different "LANGUAGE"
279 parameter values on each.
280
281 Example:
282 Below, we set the name of the calendar.
283
284 .. code-block:: pycon
285
286 >>> from icalendar import Calendar
287 >>> calendar = Calendar()
288 >>> calendar.calendar_name = "My Calendar"
289 >>> print(calendar.to_ical())
290 BEGIN:VCALENDAR
291 NAME:My Calendar
292 X-WR-CALNAME:My Calendar
293 END:VCALENDAR
294 """,
295 )
296
297 description = multi_language_text_property(
298 "DESCRIPTION",
299 "X-WR-CALDESC",
300 """This property specifies the description of the calendar.
301
302 This implements :rfc:`7986` ``DESCRIPTION`` and ``X-WR-CALDESC``.
303
304 Conformance:
305 This property can be specified multiple times in an
306 iCalendar object. However, each property MUST represent the
307 description of the calendar in a different language.
308
309 Description:
310 This property is used to specify a lengthy textual
311 description of the iCalendar object that can be used by calendar
312 user agents when describing the nature of the calendar data to a
313 user. Whilst a calendar only has a single description, multiple
314 language variants can be specified by including this property
315 multiple times with different "LANGUAGE" parameter values on each.
316
317 Example:
318 Below, we add a description to a calendar.
319
320 .. code-block:: pycon
321
322 >>> from icalendar import Calendar
323 >>> calendar = Calendar()
324 >>> calendar.description = "This is a calendar"
325 >>> print(calendar.to_ical())
326 BEGIN:VCALENDAR
327 DESCRIPTION:This is a calendar
328 X-WR-CALDESC:This is a calendar
329 END:VCALENDAR
330 """,
331 )
332
333 color = single_string_property(
334 "COLOR",
335 """This property specifies a color used for displaying the calendar.
336
337 This implements :rfc:`7986` ``COLOR`` and ``X-APPLE-CALENDAR-COLOR``.
338 Please note that since :rfc:`7986`, subcomponents can have their own color.
339
340 Property Parameters:
341 IANA and non-standard property parameters can
342 be specified on this property.
343
344 Conformance:
345 This property can be specified once in an iCalendar
346 object or in ``VEVENT``, ``VTODO``, or ``VJOURNAL`` calendar components.
347
348 Description:
349 This property specifies a color that clients MAY use
350 when presenting the relevant data to a user. Typically, this
351 would appear as the "background" color of events or tasks. The
352 value is a case-insensitive color name taken from the CSS3 set of
353 names, defined in Section 4.3 of `W3C.REC-css3-color-20110607 <https://www.w3.org/TR/css-color-3/>`_.
354
355 Example:
356 ``"turquoise"``, ``"#ffffff"``
357
358 .. code-block:: pycon
359
360 >>> from icalendar import Calendar
361 >>> calendar = Calendar()
362 >>> calendar.color = "black"
363 >>> print(calendar.to_ical())
364 BEGIN:VCALENDAR
365 COLOR:black
366 END:VCALENDAR
367
368 """,
369 "X-APPLE-CALENDAR-COLOR",
370 )
371 categories = categories_property
372 uid = uid_property
373 prodid = single_string_property(
374 "PRODID",
375 """PRODID specifies the identifier for the product that created the iCalendar object.
376
377Conformance:
378 The property MUST be specified once in an iCalendar object.
379
380Description:
381 The vendor of the implementation SHOULD assure that
382 this is a globally unique identifier; using some technique such as
383 an FPI value, as defined in [ISO.9070.1991].
384
385 This property SHOULD NOT be used to alter the interpretation of an
386 iCalendar object beyond the semantics specified in this memo. For
387 example, it is not to be used to further the understanding of non-
388 standard properties.
389
390Example:
391 The following is an example of this property. It does not
392 imply that English is the default language.
393
394 .. code-block:: text
395
396 -//ABC Corporation//NONSGML My Product//EN
397""",
398 )
399 version = single_string_property(
400 "VERSION",
401 """VERSION of the calendar specification.
402
403The default is ``"2.0"`` for :rfc:`5545`.
404
405Purpose:
406 This property specifies the identifier corresponding to the
407 highest version number or the minimum and maximum range of the
408 iCalendar specification that is required in order to interpret the
409 iCalendar object.
410
411
412 """,
413 )
414
415 calscale = single_string_property(
416 "CALSCALE",
417 """CALSCALE defines the calendar scale used for the calendar information specified in the iCalendar object.
418
419Compatibility:
420 :rfc:`7529` makes the case that GREGORIAN stays the default and other calendar scales
421 are implemented on the RRULE.
422
423Conformance:
424 This property can be specified once in an iCalendar
425 object. The default value is "GREGORIAN".
426
427Description:
428 This memo is based on the Gregorian calendar scale.
429 The Gregorian calendar scale is assumed if this property is not
430 specified in the iCalendar object. It is expected that other
431 calendar scales will be defined in other specifications or by
432 future versions of this memo.
433 """,
434 default="GREGORIAN",
435 )
436 method = single_string_property(
437 "METHOD",
438 """METHOD defines the iCalendar object method associated with the calendar object.
439
440Description:
441 When used in a MIME message entity, the value of this
442 property MUST be the same as the Content-Type "method" parameter
443 value. If either the "METHOD" property or the Content-Type
444 "method" parameter is specified, then the other MUST also be
445 specified.
446
447 No methods are defined by this specification. This is the subject
448 of other specifications, such as the iCalendar Transport-
449 independent Interoperability Protocol (iTIP) defined by :rfc:`5546`.
450
451 If this property is not present in the iCalendar object, then a
452 scheduling transaction MUST NOT be assumed. In such cases, the
453 iCalendar object is merely being used to transport a snapshot of
454 some calendar information; without the intention of conveying a
455 scheduling semantic.
456""",
457 )
458 url = url_property
459 source = source_property
460
461 @property
462 def refresh_interval(self) -> timedelta | None:
463 """REFRESH-INTERVAL specifies a suggested minimum interval for
464 polling for changes of the calendar data from the original source
465 of that data.
466
467 Conformance:
468 This property can be specified once in an iCalendar
469 object, consisting of a positive duration of time.
470
471 Description:
472 This property specifies a positive duration that gives
473 a suggested minimum polling interval for checking for updates to
474 the calendar data. The value of this property SHOULD be used by
475 calendar user agents to limit the polling interval for calendar
476 data updates to the minimum interval specified.
477
478 Raises:
479 ValueError: When setting a negative duration.
480 """
481 refresh_interval = self.get("REFRESH-INTERVAL")
482 return refresh_interval.dt if refresh_interval else None
483
484 @refresh_interval.setter
485 def refresh_interval(self, value: timedelta | None):
486 """Set the REFRESH-INTERVAL."""
487 if not isinstance(value, timedelta) and value is not None:
488 raise TypeError(
489 "REFRESH-INTERVAL must be either a positive timedelta,"
490 " or None to delete it."
491 )
492 if value is not None and value.total_seconds() <= 0:
493 raise ValueError("REFRESH-INTERVAL must be a positive timedelta.")
494 if value is not None:
495 del self.refresh_interval
496 self.add("REFRESH-INTERVAL", value)
497 else:
498 del self.refresh_interval
499
500 @refresh_interval.deleter
501 def refresh_interval(self):
502 """Delete REFRESH-INTERVAL."""
503 self.pop("REFRESH-INTERVAL")
504
505 images = images_property
506
507 @classmethod
508 def new(
509 cls,
510 /,
511 calscale: str | None = None,
512 categories: Sequence[str] = (),
513 color: str | None = None,
514 concepts: CONCEPTS_TYPE_SETTER = None,
515 description: str | None = None,
516 language: str | None = None,
517 last_modified: date | datetime | None = None,
518 links: LINKS_TYPE_SETTER = None,
519 method: str | None = None,
520 name: str | None = None,
521 organization: str | None = None,
522 prodid: str | None = None,
523 refresh_interval: timedelta | None = None,
524 refids: list[str] | str | None = None,
525 related_to: RELATED_TO_TYPE_SETTER = None,
526 source: str | None = None,
527 subcomponents: Iterable[Component] | None = None,
528 uid: str | uuid.UUID | None = None,
529 url: str | None = None,
530 version: str = "2.0",
531 ):
532 """Create a new Calendar with all required properties.
533
534 This creates a new Calendar in accordance with :rfc:`5545` and :rfc:`7986`.
535
536 Parameters:
537 calscale: The :attr:`calscale` of the calendar.
538 categories: The :attr:`categories` of the calendar.
539 color: The :attr:`color` of the calendar.
540 concepts: The :attr:`~icalendar.Component.concepts` of the calendar.
541 description: The :attr:`description` of the calendar.
542 language: The language for the calendar. Used to generate localized `prodid`.
543 last_modified: The :attr:`~icalendar.Component.last_modified` of the calendar.
544 links: The :attr:`~icalendar.Component.links` of the calendar.
545 method: The :attr:`method` of the calendar.
546 name: The :attr:`calendar_name` of the calendar.
547 organization: The organization name. Used to generate `prodid` if not provided.
548 prodid: The :attr:`prodid` of the component. If None and organization is provided,
549 generates a `prodid` in format "-//organization//name//language".
550 refresh_interval: The :attr:`refresh_interval` of the calendar.
551 refids: :attr:`~icalendar.Component.refids` of the calendar.
552 related_to: :attr:`~icalendar.Component.related_to` of the calendar.
553 source: The :attr:`source` of the calendar.
554 subcomponents: The subcomponents of the calendar.
555 uid: The :attr:`uid` of the calendar.
556 If None, this is set to a new :func:`uuid.uuid4`.
557 url: The :attr:`url` of the calendar.
558 version: The :attr:`version` of the calendar.
559
560 Returns:
561 :class:`Calendar`
562
563 Raises:
564 ~error.InvalidCalendar: If the content is not valid according to :rfc:`5545`.
565
566 .. warning:: As time progresses, we will be stricter with the validation.
567 """
568 calendar: Calendar = super().new(
569 last_modified=last_modified,
570 links=links,
571 related_to=related_to,
572 refids=refids,
573 concepts=concepts,
574 subcomponents=subcomponents,
575 )
576
577 # Generate prodid if not provided but organization is given
578 if prodid is None and organization:
579 app_name = name or "Calendar"
580 lang = language.upper() if language else "EN"
581 prodid = f"-//{organization}//{app_name}//{lang}"
582 elif prodid is None:
583 prodid = DEFAULT_PRODID
584
585 calendar.prodid = prodid
586 calendar.version = version
587 calendar.calendar_name = name
588 calendar.color = color
589 calendar.description = description
590 calendar.method = method
591 calendar.calscale = calscale
592 calendar.categories = categories
593 calendar.uid = uid if uid is not None else uuid.uuid4()
594 calendar.url = url
595 calendar.refresh_interval = refresh_interval
596 calendar.source = source
597
598 return calendar
599
600 def validate(self):
601 """Validate that the calendar has required properties and components.
602
603 This method can be called explicitly to validate a calendar before output.
604
605 Raises:
606 ~error.IncompleteComponent: If the calendar lacks required properties or
607 components.
608 """
609 if not self.get("PRODID"):
610 raise IncompleteComponent("Calendar must have a PRODID")
611 if not self.get("VERSION"):
612 raise IncompleteComponent("Calendar must have a VERSION")
613 if not self.subcomponents:
614 raise IncompleteComponent(
615 "Calendar must contain at least one component (event, todo, etc.)"
616 )
617
618
619__all__ = ["Calendar"]