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