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