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