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