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