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