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