1"""The base for :rfc:`5545` components."""
2
3from __future__ import annotations
4
5import json
6from datetime import date, datetime, time, timedelta, timezone
7from typing import TYPE_CHECKING, ClassVar
8
9from icalendar.attr import (
10 CONCEPTS_TYPE_SETTER,
11 LINKS_TYPE_SETTER,
12 RELATED_TO_TYPE_SETTER,
13 comments_property,
14 concepts_property,
15 links_property,
16 refids_property,
17 related_to_property,
18 single_utc_property,
19 uid_property,
20)
21from icalendar.cal.component_factory import ComponentFactory
22from icalendar.caselessdict import CaselessDict
23from icalendar.error import InvalidCalendar, JCalParsingError
24from icalendar.parser import Contentline, Contentlines, Parameters, q_join, q_split
25from icalendar.parser_tools import DEFAULT_ENCODING
26from icalendar.prop import VPROPERTY, TypesFactory, vDDDLists, vText
27from icalendar.timezone import tzp
28from icalendar.tools import is_date
29
30if TYPE_CHECKING:
31 from icalendar.compatibility import Self
32
33_marker = []
34
35
36class Component(CaselessDict):
37 """Base class for calendar components.
38
39 Component is the base object for calendar, Event and the other
40 components defined in :rfc:`5545`. Normally you will not use this class
41 directly, but rather one of the subclasses.
42
43 Attributes:
44 name: The name of the component. Example: ``VCALENDAR``.
45 required: These properties are required.
46 singletons: These properties must only appear once.
47 multiple: These properties may occur more than once.
48 exclusive: These properties are mutually exclusive.
49 inclusive: If the first in a tuple occurs, the second one must also occur.
50 ignore_exceptions: If True, and we cannot parse this
51 component, we will silently ignore it, rather than let the
52 exception propagate upwards.
53 types_factory: Factory for property types
54 """
55
56 name = None # should be defined in each component
57 required = () # These properties are required
58 singletons = () # These properties must only appear once
59 multiple = () # may occur more than once
60 exclusive = () # These properties are mutually exclusive
61 inclusive: (
62 tuple[str] | tuple[tuple[str, str]]
63 ) = () # if any occurs the other(s) MUST occur
64 # ('duration', 'repeat')
65 ignore_exceptions = False # if True, and we cannot parse this
66 # component, we will silently ignore
67 # it, rather than let the exception
68 # propagate upwards
69 # not_compliant = [''] # List of non-compliant properties.
70
71 types_factory = TypesFactory.instance()
72 _components_factory: ClassVar[ComponentFactory | None] = None
73
74 @classmethod
75 def get_component_class(cls, name: str) -> type[Component]:
76 """Return a component with this name.
77
78 Arguments:
79 name: Name of the component, i.e. ``VCALENDAR``
80 """
81 if cls._components_factory is None:
82 cls._components_factory = ComponentFactory()
83 return cls._components_factory.get_component_class(name)
84
85 @staticmethod
86 def _infer_value_type(
87 value: date | datetime | timedelta | time | tuple | list,
88 ) -> str | None:
89 """Infer the ``VALUE`` parameter from a Python type.
90
91 Args:
92 value: Python native type, one of :py:class:`date`, :py:mod:`datetime`,
93 :py:class:`timedelta`, :py:mod:`time`, :py:class:`tuple`,
94 or :py:class:`list`.
95
96 Returns:
97 str or None: The ``VALUE`` parameter string, for example, "DATE",
98 "TIME", or other string, or ``None``
99 if no specific ``VALUE`` is needed.
100 """
101 if isinstance(value, list):
102 if not value:
103 return None
104 # Check if ALL items are date (but not datetime)
105 if all(is_date(item) for item in value):
106 return "DATE"
107 # Check if ALL items are time
108 if all(isinstance(item, time) for item in value):
109 return "TIME"
110 # Mixed types or other types - don't infer
111 return None
112 if is_date(value):
113 return "DATE"
114 if isinstance(value, time):
115 return "TIME"
116 # Don't infer PERIOD - it's too risky and vPeriod already handles it
117 return None
118
119 def __init__(self, *args, **kwargs):
120 """Set keys to upper for initial dict."""
121 super().__init__(*args, **kwargs)
122 # set parameters here for properties that use non-default values
123 self.subcomponents: list[Component] = [] # Components can be nested.
124 self.errors = [] # If we ignored exception(s) while
125 # parsing a property, contains error strings
126
127 def __bool__(self):
128 """Returns True, CaselessDict would return False if it had no items."""
129 return True
130
131 def is_empty(self):
132 """Returns True if Component has no items or subcomponents, else False."""
133 return bool(not list(self.values()) + self.subcomponents)
134
135 #############################
136 # handling of property values
137
138 @classmethod
139 def _encode(cls, name, value, parameters=None, encode=1):
140 """Encode values to icalendar property values.
141
142 :param name: Name of the property.
143 :type name: string
144
145 :param value: Value of the property. Either of a basic Python type of
146 any of the icalendar's own property types.
147 :type value: Python native type or icalendar property type.
148
149 :param parameters: Property parameter dictionary for the value. Only
150 available, if encode is set to True.
151 :type parameters: Dictionary
152
153 :param encode: True, if the value should be encoded to one of
154 icalendar's own property types (Fallback is "vText")
155 or False, if not.
156 :type encode: Boolean
157
158 :returns: icalendar property value
159 """
160 if not encode:
161 return value
162 if isinstance(value, cls.types_factory.all_types):
163 # Don't encode already encoded values.
164 obj = value
165 else:
166 # Extract VALUE parameter if present, or infer it from the Python type
167 value_param = None
168 if parameters and "VALUE" in parameters:
169 value_param = parameters["VALUE"]
170 elif not isinstance(value, cls.types_factory.all_types):
171 inferred = cls._infer_value_type(value)
172 if inferred:
173 value_param = inferred
174 # Auto-set the VALUE parameter
175 if parameters is None:
176 parameters = {}
177 if "VALUE" not in parameters:
178 parameters["VALUE"] = inferred
179
180 klass = cls.types_factory.for_property(name, value_param)
181 obj = klass(value)
182 if parameters:
183 if not hasattr(obj, "params"):
184 obj.params = Parameters()
185 for key, item in parameters.items():
186 if item is None:
187 if key in obj.params:
188 del obj.params[key]
189 else:
190 obj.params[key] = item
191 return obj
192
193 def add(
194 self,
195 name: str,
196 value,
197 parameters: dict[str, str] | Parameters = None,
198 encode: bool = True, # noqa: FBT001
199 ):
200 """Add a property.
201
202 :param name: Name of the property.
203 :type name: string
204
205 :param value: Value of the property. Either of a basic Python type of
206 any of the icalendar's own property types.
207 :type value: Python native type or icalendar property type.
208
209 :param parameters: Property parameter dictionary for the value. Only
210 available, if encode is set to True.
211 :type parameters: Dictionary
212
213 :param encode: True, if the value should be encoded to one of
214 icalendar's own property types (Fallback is "vText")
215 or False, if not.
216 :type encode: Boolean
217
218 :returns: None
219 """
220 if isinstance(value, datetime) and name.lower() in (
221 "dtstamp",
222 "created",
223 "last-modified",
224 ):
225 # RFC expects UTC for those... force value conversion.
226 value = tzp.localize_utc(value)
227
228 # encode value
229 if (
230 encode
231 and isinstance(value, list)
232 and name.lower() not in ["rdate", "exdate", "categories"]
233 ):
234 # Individually convert each value to an ical type except rdate and
235 # exdate, where lists of dates might be passed to vDDDLists.
236 value = [self._encode(name, v, parameters, encode) for v in value]
237 else:
238 value = self._encode(name, value, parameters, encode)
239
240 # set value
241 if name in self:
242 # If property already exists, append it.
243 oldval = self[name]
244 if isinstance(oldval, list):
245 if isinstance(value, list):
246 value = oldval + value
247 else:
248 oldval.append(value)
249 value = oldval
250 else:
251 value = [oldval, value]
252 self[name] = value
253
254 def _decode(self, name, value):
255 """Internal for decoding property values."""
256
257 # TODO: Currently the decoded method calls the icalendar.prop instances
258 # from_ical. We probably want to decode properties into Python native
259 # types here. But when parsing from an ical string with from_ical, we
260 # want to encode the string into a real icalendar.prop property.
261 if isinstance(value, vDDDLists):
262 # TODO: Workaround unfinished decoding
263 return value
264 decoded = self.types_factory.from_ical(name, value)
265 # TODO: remove when proper decoded is implemented in every prop.* class
266 # Workaround to decode vText properly
267 if isinstance(decoded, vText):
268 decoded = decoded.encode(DEFAULT_ENCODING)
269 return decoded
270
271 def decoded(self, name, default=_marker):
272 """Returns decoded value of property."""
273 # XXX: fail. what's this function supposed to do in the end?
274 # -rnix
275
276 if name in self:
277 value = self[name]
278 if isinstance(value, list):
279 return [self._decode(name, v) for v in value]
280 return self._decode(name, value)
281 if default is _marker:
282 raise KeyError(name)
283 return default
284
285 ########################################################################
286 # Inline values. A few properties have multiple values inlined in in one
287 # property line. These methods are used for splitting and joining these.
288
289 def get_inline(self, name, decode=1):
290 """Returns a list of values (split on comma)."""
291 vals = [v.strip('" ') for v in q_split(self[name])]
292 if decode:
293 return [self._decode(name, val) for val in vals]
294 return vals
295
296 def set_inline(self, name, values, encode=1):
297 """Converts a list of values into comma separated string and sets value
298 to that.
299 """
300 if encode:
301 values = [self._encode(name, value, encode=1) for value in values]
302 self[name] = self.types_factory["inline"](q_join(values))
303
304 #########################
305 # Handling of components
306
307 def add_component(self, component: Component):
308 """Add a subcomponent to this component."""
309 self.subcomponents.append(component)
310
311 def _walk(self, name, select):
312 """Walk to given component."""
313 result = []
314 if (name is None or self.name == name) and select(self):
315 result.append(self)
316 for subcomponent in self.subcomponents:
317 result += subcomponent._walk(name, select) # noqa: SLF001
318 return result
319
320 def walk(self, name=None, select=lambda _: True) -> list[Component]:
321 """Recursively traverses component and subcomponents. Returns sequence
322 of same. If name is passed, only components with name will be returned.
323
324 :param name: The name of the component or None such as ``VEVENT``.
325 :param select: A function that takes the component as first argument
326 and returns True/False.
327 :returns: A list of components that match.
328 :rtype: list[Component]
329 """
330 if name is not None:
331 name = name.upper()
332 return self._walk(name, select)
333
334 #####################
335 # Generation
336
337 def property_items(
338 self,
339 recursive=True,
340 sorted: bool = True, # noqa: A002, FBT001
341 ) -> list[tuple[str, object]]:
342 """Returns properties in this component and subcomponents as:
343 [(name, value), ...]
344 """
345 v_text = self.types_factory["text"]
346 properties = [("BEGIN", v_text(self.name).to_ical())]
347 property_names = self.sorted_keys() if sorted else self.keys()
348
349 for name in property_names:
350 values = self[name]
351 if isinstance(values, list):
352 # normally one property is one line
353 for value in values:
354 properties.append((name, value))
355 else:
356 properties.append((name, values))
357 if recursive:
358 # recursion is fun!
359 for subcomponent in self.subcomponents:
360 properties += subcomponent.property_items(sorted=sorted)
361 properties.append(("END", v_text(self.name).to_ical()))
362 return properties
363
364 @classmethod
365 def from_ical(cls, st, multiple: bool = False) -> Self | list[Self]: # noqa: FBT001
366 """Populates the component recursively from a string."""
367 stack = [] # a stack of components
368 comps = []
369 for line in Contentlines.from_ical(st): # raw parsing
370 if not line:
371 continue
372
373 try:
374 name, params, vals = line.parts()
375 except ValueError as e:
376 # if unable to parse a line within a component
377 # that ignores exceptions, mark the component
378 # as broken and skip the line. otherwise raise.
379 component = stack[-1] if stack else None
380 if not component or not component.ignore_exceptions:
381 raise
382 component.errors.append((None, str(e)))
383 continue
384
385 uname = name.upper()
386 # check for start of component
387 if uname == "BEGIN":
388 # try and create one of the components defined in the spec,
389 # otherwise get a general Components for robustness.
390 c_name = vals.upper()
391 c_class = cls.get_component_class(c_name)
392 # If component factory cannot resolve ``c_name``, the generic
393 # ``Component`` class is used which does not have the name set.
394 # That's opposed to the usage of ``cls``, which represents a
395 # more concrete subclass with a name set (e.g. VCALENDAR).
396 component = c_class()
397 if not getattr(component, "name", ""): # undefined components
398 component.name = c_name
399 stack.append(component)
400 # check for end of event
401 elif uname == "END":
402 # we are done adding properties to this component
403 # so pop it from the stack and add it to the new top.
404 if not stack:
405 # The stack is currently empty, the input must be invalid
406 raise ValueError("END encountered without an accompanying BEGIN!")
407
408 component = stack.pop()
409 if not stack: # we are at the end
410 comps.append(component)
411 else:
412 stack[-1].add_component(component)
413 if vals == "VTIMEZONE" and "TZID" in component:
414 tzp.cache_timezone_component(component)
415 # we are adding properties to the current top of the stack
416 else:
417 # Extract VALUE parameter if present
418 value_param = params.get("VALUE") if params else None
419 factory = cls.types_factory.for_property(name, value_param)
420 component = stack[-1] if stack else None
421 if not component:
422 # only accept X-COMMENT at the end of the .ics file
423 # ignore these components in parsing
424 if uname == "X-COMMENT":
425 break
426 raise ValueError(
427 f'Property "{name}" does not have a parent component.'
428 )
429 datetime_names = (
430 "DTSTART",
431 "DTEND",
432 "RECURRENCE-ID",
433 "DUE",
434 "RDATE",
435 "EXDATE",
436 )
437 try:
438 if name == "FREEBUSY":
439 vals = vals.split(",")
440 if "TZID" in params:
441 parsed_components = [
442 factory(factory.from_ical(val, params["TZID"]))
443 for val in vals
444 ]
445 else:
446 parsed_components = [
447 factory(factory.from_ical(val)) for val in vals
448 ]
449 elif name in datetime_names and "TZID" in params:
450 parsed_components = [
451 factory(factory.from_ical(vals, params["TZID"]))
452 ]
453 # Workaround broken ICS files with empty RDATE
454 elif name == "RDATE" and vals == "":
455 parsed_components = []
456 else:
457 parsed_components = [factory(factory.from_ical(vals))]
458 except ValueError as e:
459 if not component.ignore_exceptions:
460 raise
461 component.errors.append((uname, str(e)))
462 else:
463 for parsed_component in parsed_components:
464 parsed_component.params = params
465 component.add(name, parsed_component, encode=0)
466
467 if multiple:
468 return comps
469 if len(comps) > 1:
470 raise ValueError(
471 cls._format_error(
472 "Found multiple components where only one is allowed", st
473 )
474 )
475 if len(comps) < 1:
476 raise ValueError(
477 cls._format_error(
478 "Found no components where exactly one is required", st
479 )
480 )
481 return comps[0]
482
483 @staticmethod
484 def _format_error(error_description, bad_input, elipsis="[...]"):
485 # there's three character more in the error, ie. ' ' x2 and a ':'
486 max_error_length = 100 - 3
487 if len(error_description) + len(bad_input) + len(elipsis) > max_error_length:
488 truncate_to = max_error_length - len(error_description) - len(elipsis)
489 return f"{error_description}: {bad_input[:truncate_to]} {elipsis}"
490 return f"{error_description}: {bad_input}"
491
492 def content_line(self, name, value, sorted: bool = True): # noqa: A002, FBT001
493 """Returns property as content line."""
494 params = getattr(value, "params", Parameters())
495 return Contentline.from_parts(name, params, value, sorted=sorted)
496
497 def content_lines(self, sorted: bool = True): # noqa: A002, FBT001
498 """Converts the Component and subcomponents into content lines."""
499 contentlines = Contentlines()
500 for name, value in self.property_items(sorted=sorted):
501 cl = self.content_line(name, value, sorted=sorted)
502 contentlines.append(cl)
503 contentlines.append("") # remember the empty string in the end
504 return contentlines
505
506 def to_ical(self, sorted: bool = True): # noqa: A002, FBT001
507 """
508 :param sorted: Whether parameters and properties should be
509 lexicographically sorted.
510 """
511
512 content_lines = self.content_lines(sorted=sorted)
513 return content_lines.to_ical()
514
515 def __repr__(self):
516 """String representation of class with all of it's subcomponents."""
517 subs = ", ".join(str(it) for it in self.subcomponents)
518 return (
519 f"{self.name or type(self).__name__}"
520 f"({dict(self)}{', ' + subs if subs else ''})"
521 )
522
523 def __eq__(self, other):
524 if len(self.subcomponents) != len(other.subcomponents):
525 return False
526
527 properties_equal = super().__eq__(other)
528 if not properties_equal:
529 return False
530
531 # The subcomponents might not be in the same order,
532 # neither there's a natural key we can sort the subcomponents by nor
533 # are the subcomponent types hashable, so we cant put them in a set to
534 # check for set equivalence. We have to iterate over the subcomponents
535 # and look for each of them in the list.
536 for subcomponent in self.subcomponents:
537 if subcomponent not in other.subcomponents:
538 return False
539
540 return True
541
542 DTSTAMP = stamp = single_utc_property(
543 "DTSTAMP",
544 """RFC 5545:
545
546 Conformance: This property MUST be included in the "VEVENT",
547 "VTODO", "VJOURNAL", or "VFREEBUSY" calendar components.
548
549 Description: In the case of an iCalendar object that specifies a
550 "METHOD" property, this property specifies the date and time that
551 the instance of the iCalendar object was created. In the case of
552 an iCalendar object that doesn't specify a "METHOD" property, this
553 property specifies the date and time that the information
554 associated with the calendar component was last revised in the
555 calendar store.
556
557 The value MUST be specified in the UTC time format.
558
559 In the case of an iCalendar object that doesn't specify a "METHOD"
560 property, this property is equivalent to the "LAST-MODIFIED"
561 property.
562 """,
563 )
564 LAST_MODIFIED = single_utc_property(
565 "LAST-MODIFIED",
566 """RFC 5545:
567
568 Purpose: This property specifies the date and time that the
569 information associated with the calendar component was last
570 revised in the calendar store.
571
572 Note: This is analogous to the modification date and time for a
573 file in the file system.
574
575 Conformance: This property can be specified in the "VEVENT",
576 "VTODO", "VJOURNAL", or "VTIMEZONE" calendar components.
577 """,
578 )
579
580 @property
581 def last_modified(self) -> datetime:
582 """Datetime when the information associated with the component was last revised.
583
584 Since :attr:`LAST_MODIFIED` is an optional property,
585 this returns :attr:`DTSTAMP` if :attr:`LAST_MODIFIED` is not set.
586 """
587 return self.LAST_MODIFIED or self.DTSTAMP
588
589 @last_modified.setter
590 def last_modified(self, value):
591 self.LAST_MODIFIED = value
592
593 @last_modified.deleter
594 def last_modified(self):
595 del self.LAST_MODIFIED
596
597 @property
598 def created(self) -> datetime:
599 """Datetime when the information associated with the component was created.
600
601 Since :attr:`CREATED` is an optional property,
602 this returns :attr:`DTSTAMP` if :attr:`CREATED` is not set.
603 """
604 return self.CREATED or self.DTSTAMP
605
606 @created.setter
607 def created(self, value):
608 self.CREATED = value
609
610 @created.deleter
611 def created(self):
612 del self.CREATED
613
614 def is_thunderbird(self) -> bool:
615 """Whether this component has attributes that indicate that Mozilla Thunderbird created it.""" # noqa: E501
616 return any(attr.startswith("X-MOZ-") for attr in self.keys())
617
618 @staticmethod
619 def _utc_now():
620 """Return now as UTC value."""
621 return datetime.now(timezone.utc)
622
623 uid = uid_property
624 comments = comments_property
625 links = links_property
626 related_to = related_to_property
627 concepts = concepts_property
628 refids = refids_property
629
630 CREATED = single_utc_property(
631 "CREATED",
632 """
633 CREATED specifies the date and time that the calendar
634 information was created by the calendar user agent in the calendar
635 store.
636
637 Conformance:
638 The property can be specified once in "VEVENT",
639 "VTODO", or "VJOURNAL" calendar components. The value MUST be
640 specified as a date with UTC time.
641
642 """,
643 )
644
645 _validate_new = True
646
647 @staticmethod
648 def _validate_start_and_end(start, end):
649 """This validates start and end.
650
651 Raises:
652 InvalidCalendar: If the information is not valid
653 """
654 if start is None or end is None:
655 return
656 if start > end:
657 raise InvalidCalendar("end must be after start")
658
659 @classmethod
660 def new(
661 cls,
662 created: date | None = None,
663 comments: list[str] | str | None = None,
664 concepts: CONCEPTS_TYPE_SETTER = None,
665 last_modified: date | None = None,
666 links: LINKS_TYPE_SETTER = None,
667 refids: list[str] | str | None = None,
668 related_to: RELATED_TO_TYPE_SETTER = None,
669 stamp: date | None = None,
670 ) -> Component:
671 """Create a new component.
672
673 Arguments:
674 comments: The :attr:`comments` of the component.
675 concepts: The :attr:`concepts` of the component.
676 created: The :attr:`created` of the component.
677 last_modified: The :attr:`last_modified` of the component.
678 links: The :attr:`links` of the component.
679 related_to: The :attr:`related_to` of the component.
680 stamp: The :attr:`DTSTAMP` of the component.
681
682 Raises:
683 InvalidCalendar: If the content is not valid according to :rfc:`5545`.
684
685 .. warning:: As time progresses, we will be stricter with the validation.
686 """
687 component = cls()
688 component.DTSTAMP = stamp
689 component.created = created
690 component.last_modified = last_modified
691 component.comments = comments
692 component.links = links
693 component.related_to = related_to
694 component.concepts = concepts
695 component.refids = refids
696 return component
697
698 def to_jcal(self) -> list:
699 """Convert this component to a jCal object.
700
701 Returns:
702 jCal object
703
704 See also :attr:`to_json`.
705
706 In this example, we create a simple VEVENT component and convert it to jCal:
707
708 .. code-block:: pycon
709
710 >>> from icalendar import Event
711 >>> from datetime import date
712 >>> from pprint import pprint
713 >>> event = Event.new(summary="My Event", start=date(2025, 11, 22))
714 >>> pprint(event.to_jcal())
715 ['vevent',
716 [['dtstamp', {}, 'date-time', '2025-05-17T08:06:12Z'],
717 ['summary', {}, 'text', 'My Event'],
718 ['uid', {}, 'text', 'd755cef5-2311-46ed-a0e1-6733c9e15c63'],
719 ['dtstart', {}, 'date', '2025-11-22']],
720 []]
721 """
722 properties = []
723 for key, value in self.items():
724 for item in value if isinstance(value, list) else [value]:
725 properties.append(item.to_jcal(key.lower()))
726 return [
727 self.name.lower(),
728 properties,
729 [subcomponent.to_jcal() for subcomponent in self.subcomponents],
730 ]
731
732 def to_json(self) -> str:
733 """Return this component as a jCal JSON string.
734
735 Returns:
736 JSON string
737
738 See also :attr:`to_jcal`.
739 """
740 return json.dumps(self.to_jcal())
741
742 @classmethod
743 def from_jcal(cls, jcal: str | list) -> Component:
744 """Create a component from a jCal list.
745
746 Args:
747 jcal: jCal list or JSON string according to :rfc:`7265`.
748
749 Raises:
750 JCalParsingError: If the jCal provided is invalid.
751 ~json.JSONDecodeError: If the provided string is not valid JSON.
752
753 This reverses :func:`to_json` and :func:`to_jcal`.
754
755 The following code parses an example from :rfc:`7265`:
756
757 .. code-block:: pycon
758
759 >>> from icalendar import Component
760 >>> jcal = ["vcalendar",
761 ... [
762 ... ["calscale", {}, "text", "GREGORIAN"],
763 ... ["prodid", {}, "text", "-//Example Inc.//Example Calendar//EN"],
764 ... ["version", {}, "text", "2.0"]
765 ... ],
766 ... [
767 ... ["vevent",
768 ... [
769 ... ["dtstamp", {}, "date-time", "2008-02-05T19:12:24Z"],
770 ... ["dtstart", {}, "date", "2008-10-06"],
771 ... ["summary", {}, "text", "Planning meeting"],
772 ... ["uid", {}, "text", "4088E990AD89CB3DBB484909"]
773 ... ],
774 ... []
775 ... ]
776 ... ]
777 ... ]
778 >>> calendar = Component.from_jcal(jcal)
779 >>> print(calendar.name)
780 VCALENDAR
781 >>> print(calendar.prodid)
782 -//Example Inc.//Example Calendar//EN
783 >>> event = calendar.events[0]
784 >>> print(event.summary)
785 Planning meeting
786
787 """
788 if isinstance(jcal, str):
789 jcal = json.loads(jcal)
790 if not isinstance(jcal, list) or len(jcal) != 3:
791 raise JCalParsingError(
792 "A component must be a list with 3 items.", cls, value=jcal
793 )
794 name, properties, subcomponents = jcal
795 if not isinstance(name, str):
796 raise JCalParsingError(
797 "The name must be a string.", cls, path=[0], value=name
798 )
799 if name.upper() != cls.name:
800 # delegate to correct component class
801 component_cls = cls.get_component_class(name.upper())
802 return component_cls.from_jcal(jcal)
803 component = cls()
804 if not isinstance(properties, list):
805 raise JCalParsingError(
806 "The properties must be a list.", cls, path=1, value=properties
807 )
808 for i, prop in enumerate(properties):
809 JCalParsingError.validate_property(prop, cls, path=[1, i])
810 prop_name = prop[0]
811 prop_value = prop[2]
812 prop_cls: type[VPROPERTY] = cls.types_factory.for_property(
813 prop_name, prop_value
814 )
815 with JCalParsingError.reraise_with_path_added(1, i):
816 v_prop = prop_cls.from_jcal(prop)
817 # if we use the default value for that property, we can delete the
818 # VALUE parameter
819 if prop_cls == cls.types_factory.for_property(prop_name):
820 del v_prop.VALUE
821 component.add(prop_name, v_prop)
822 if not isinstance(subcomponents, list):
823 raise JCalParsingError(
824 "The subcomponents must be a list.", cls, 2, value=subcomponents
825 )
826 for i, subcomponent in enumerate(subcomponents):
827 with JCalParsingError.reraise_with_path_added(2, i):
828 component.subcomponents.append(cls.from_jcal(subcomponent))
829 return component
830
831
832__all__ = ["Component"]