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