1"""The base for :rfc:`5545` components."""
2
3from __future__ import annotations
4
5import json
6from copy import deepcopy
7from dataclasses import dataclass
8from datetime import date, datetime, time, timedelta, timezone
9from pathlib import Path
10from typing import TYPE_CHECKING, Any, ClassVar, Literal, overload
11
12from icalendar.attr import (
13 CONCEPTS_TYPE_SETTER,
14 LINKS_TYPE_SETTER,
15 RELATED_TO_TYPE_SETTER,
16 comments_property,
17 concepts_property,
18 links_property,
19 refids_property,
20 related_to_property,
21 single_utc_property,
22 uid_property,
23)
24from icalendar.cal.component_factory import ComponentFactory
25from icalendar.caselessdict import CaselessDict
26from icalendar.error import InvalidCalendar, JCalParsingError
27from icalendar.parser import (
28 Contentline,
29 Contentlines,
30 Parameters,
31 q_join,
32 q_split,
33)
34from icalendar.parser.ical.component import ComponentIcalParser
35from icalendar.parser_tools import DEFAULT_ENCODING
36from icalendar.prop import VPROPERTY, TypesFactory, vDDDLists, vText, vUnknown
37from icalendar.timezone import tzp
38from icalendar.tools import is_date
39
40if TYPE_CHECKING:
41 from collections.abc import Iterable
42
43 from icalendar.compatibility import Self
44
45_marker = []
46
47
48@dataclass
49class _ComponentEqFrame:
50 """A pending component-equality comparison on the iterative stack.
51
52 See ``Component.__eq__`` for how the fields are used.
53 """
54
55 #: the two components being compared
56 a: Component
57 b: Component
58 #: ``b``'s subcomponents not yet matched against one of ``a``'s. ``None``
59 #: until ``a`` and ``b``'s own properties have been compared and found equal.
60 unmatched: list | None = None
61 #: index of the ``a`` subcomponent we are currently trying to match
62 a_index: int = 0
63 #: index of the unmatched ``b`` subcomponent we are currently testing
64 candidate_index: int = 0
65
66
67class Component(CaselessDict):
68 """Base class for calendar components.
69
70 Component is the base object for calendar, Event and the other
71 components defined in :rfc:`5545`. Normally you will not use this class
72 directly, but rather one of the subclasses.
73 """
74
75 name: ClassVar[str | None] = None
76 """The name of the component.
77
78 This is defined in each component class.
79
80 Example:
81
82 .. code-block:: pycon
83
84 >>> from icalendar import Calendar
85 >>> cal = Calendar.new()
86 >>> cal.name
87 'VCALENDAR'
88
89 """
90
91 required: ClassVar[tuple[()]] = ()
92 """These properties are required."""
93
94 singletons: ClassVar[tuple[()]] = ()
95 """These properties must appear only once."""
96
97 multiple: ClassVar[tuple[()]] = ()
98 """These properties may occur more than once."""
99
100 exclusive: ClassVar[tuple[()]] = ()
101 """These properties are mutually exclusive."""
102
103 inclusive: ClassVar[(tuple[str] | tuple[tuple[str, str]])] = ()
104 """These properties are inclusive.
105
106 In other words, if the first property in the tuple occurs, then the
107 second one must also occur.
108
109 Example:
110
111 .. code-block:: python
112
113 ('duration', 'repeat')
114 """
115
116 ignore_exceptions: ClassVar[bool] = False
117 """Whether or not to ignore exceptions when parsing.
118
119 If ``True``, and this component can't be parsed, then it will silently
120 ignore it, rather than let the exception propagate upwards.
121 """
122
123 types_factory: ClassVar[TypesFactory] = TypesFactory.instance()
124 _components_factory: ClassVar[ComponentFactory | None] = None
125
126 subcomponents: list[Component]
127 """All subcomponents of this component."""
128
129 @classmethod
130 def _get_component_factory(cls) -> ComponentFactory:
131 """Get the component factory."""
132 if cls._components_factory is None:
133 cls._components_factory = ComponentFactory()
134 return cls._components_factory
135
136 @classmethod
137 def get_component_class(cls, name: str) -> type[Component]:
138 """Return a component with this name.
139
140 Parameters:
141 name: Name of the component, i.e. ``VCALENDAR``
142 """
143 return cls._get_component_factory().get_component_class(name)
144
145 @classmethod
146 def register(cls, component_class: type[Component]) -> None:
147 """Register a custom component class.
148
149 Parameters:
150 component_class: Component subclass to register.
151 Must have a ``name`` attribute.
152
153 Raises:
154 ValueError: If ``component_class`` has no ``name`` attribute.
155 ValueError: If a component with this name is already registered.
156
157 Examples:
158 Create a custom icalendar component with the name ``X-EXAMPLE``:
159
160 .. code-block:: pycon
161
162 >>> from icalendar import Component
163 >>> class XExample(Component):
164 ... name = "X-EXAMPLE"
165 ... def custom_method(self):
166 ... return "custom"
167 >>> Component.register(XExample)
168 """
169 if not hasattr(component_class, "name") or component_class.name is None:
170 raise ValueError(f"{component_class} must have a 'name' attribute")
171
172 # Check if already registered
173 component_factory = cls._get_component_factory()
174 existing = component_factory.get(component_class.name)
175 if existing is not None and existing is not component_class:
176 raise ValueError(
177 f"Component '{component_class.name}' is already registered"
178 f" as {existing}"
179 )
180
181 component_factory.add_component_class(component_class)
182
183 @staticmethod
184 def _infer_value_type(
185 value: date | datetime | timedelta | time | tuple | list,
186 ) -> str | None:
187 """Infer the ``VALUE`` parameter from a Python type.
188
189 Parameters:
190 value: Python native type, one of :class:`datetime.date`, :class:`datetime.datetime`,
191 :class:`datetime.timedelta`, :class:`datetime.time`, :class:`tuple`,
192 or :class:`list`.
193
194 Returns:
195 str or None: The ``VALUE`` parameter string, for example, "DATE",
196 "TIME", or other string, or ``None``
197 if no specific ``VALUE`` is needed.
198 """
199 if isinstance(value, list):
200 if not value:
201 return None
202 # Check if ALL items are date (but not datetime)
203 if all(is_date(item) for item in value):
204 return "DATE"
205 # Check if ALL items are time
206 if all(isinstance(item, time) for item in value):
207 return "TIME"
208 # Mixed types or other types - don't infer
209 return None
210 if is_date(value):
211 return "DATE"
212 if isinstance(value, time):
213 return "TIME"
214 # Don't infer PERIOD - it's too risky and vPeriod already handles it
215 return None
216
217 def __init__(self, *args: Any, **kwargs: Any) -> None:
218 """Set keys to upper for initial dict."""
219 super().__init__(*args, **kwargs)
220 # set parameters here for properties that use non-default values
221 self.subcomponents: list[Component] = [] # Components can be nested.
222 self.errors: list[
223 tuple[str | None, str]
224 ] = [] # If we ignored exception(s) while
225 # parsing a property, contains error strings
226
227 def __bool__(self) -> bool:
228 """Returns True, CaselessDict would return False if it had no items."""
229 return True
230
231 def __getitem__(self, key) -> VPROPERTY:
232 """Get property value from the component dictionary."""
233 return super().__getitem__(key)
234
235 def get(self, key, default=None) -> Any:
236 """Get property value with default."""
237 try:
238 return self[key]
239 except KeyError:
240 return default
241
242 def is_empty(self) -> bool:
243 """Returns True if Component has no items or subcomponents, else False."""
244 return bool(not list(self.values()) + self.subcomponents)
245
246 #############################
247 # handling of property values
248
249 @classmethod
250 def _encode(cls, name, value, parameters=None, encode=1):
251 """Encode values to icalendar property values.
252
253 :param name: Name of the property.
254 :type name: string
255
256 :param value: Value of the property. Either of a basic Python type of
257 any of the icalendar's own property types.
258 :type value: Python native type or icalendar property type.
259
260 :param parameters: Property parameter dictionary for the value. Only
261 available, if encode is set to True.
262 :type parameters: Dictionary
263
264 :param encode: True, if the value should be encoded to one of
265 icalendar's own property types (Fallback is "vText")
266 or False, if not.
267 :type encode: Boolean
268
269 :returns: icalendar property value
270 """
271 if not encode:
272 return value
273 if isinstance(value, cls.types_factory.all_types):
274 # Don't encode already encoded values.
275 obj = value
276 else:
277 # Extract VALUE parameter if present, or infer it from the Python type
278 value_param = None
279 if parameters and "VALUE" in parameters:
280 value_param = parameters["VALUE"]
281 elif not isinstance(value, cls.types_factory.all_types):
282 inferred = cls._infer_value_type(value)
283 if inferred:
284 value_param = inferred
285 # Auto-set the VALUE parameter
286 if parameters is None:
287 parameters = {}
288 if "VALUE" not in parameters:
289 parameters["VALUE"] = inferred
290
291 klass = cls.types_factory.for_property(name, value_param)
292 obj = klass(value)
293 if parameters:
294 if not hasattr(obj, "params"):
295 obj.params = Parameters()
296 for key, item in parameters.items():
297 if item is None:
298 if key in obj.params:
299 del obj.params[key]
300 else:
301 obj.params[key] = item
302 return obj
303
304 def add(
305 self,
306 name: str,
307 value,
308 parameters: dict[str, str] | Parameters = None,
309 encode: bool = True,
310 ) -> None:
311 """Add a property to this component.
312
313 If the property already exists, the new value is appended so the
314 property carries a list of values rather than replacing the previous
315 one. When ``name`` is ``DTSTAMP``, ``CREATED``, or ``LAST-MODIFIED``
316 and ``value`` is a ``datetime``, the value is converted to UTC as the
317 RFC requires.
318
319 Parameters:
320 name: Name of the property.
321 value:
322 Value of the property. Either a basic Python type or any of
323 icalendar's own property types.
324 parameters:
325 Property parameter dictionary for the value. Only consulted
326 when ``encode`` is ``True``.
327 encode:
328 ``True`` if the value should be encoded to one of icalendar's
329 own property types (fallback is ``vText``); ``False`` to
330 store the value as-is.
331
332 Returns:
333 ``None``
334
335 Example:
336
337 >>> from icalendar import Event
338 >>> event = Event()
339 >>> event.add("summary", "Team sync")
340 >>> event["summary"]
341 vText(b'Team sync')
342
343 """
344 if isinstance(value, datetime) and name.lower() in (
345 "dtstamp",
346 "created",
347 "last-modified",
348 ):
349 # RFC expects UTC for those... force value conversion.
350 value = tzp.localize_utc(value)
351
352 # encode value
353 if (
354 encode
355 and isinstance(value, list)
356 and name.lower() not in ["rdate", "exdate", "categories"]
357 ):
358 # Individually convert each value to an ical type except rdate and
359 # exdate, where lists of dates might be passed to vDDDLists.
360 value = [self._encode(name, v, parameters, encode) for v in value]
361 else:
362 value = self._encode(name, value, parameters, encode)
363
364 # set value
365 if name in self:
366 # If property already exists, append it.
367 oldval = self[name]
368 if isinstance(oldval, list):
369 if isinstance(value, list):
370 value = oldval + value
371 else:
372 oldval.append(value)
373 value = oldval
374 else:
375 value = [oldval, value]
376 self[name] = value
377
378 def _decode(self, name: str, value: VPROPERTY):
379 """Internal for decoding property values."""
380
381 # TODO: Currently the decoded method calls the icalendar.prop instances
382 # from_ical. We probably want to decode properties into Python native
383 # types here. But when parsing from an ical string with from_ical, we
384 # want to encode the string into a real icalendar.prop property.
385 if hasattr(value, "ical_value"):
386 return value.ical_value
387 if isinstance(value, vDDDLists):
388 # TODO: Workaround unfinished decoding
389 return value
390 decoded = self.types_factory.from_ical(name, value)
391 # TODO: remove when proper decoded is implemented in every prop.* class
392 # Workaround to decode vText properly. vUnknown is not a vText
393 # subclass (RFC 7265), but its value is decoded the same way here.
394 if isinstance(decoded, (vText, vUnknown)):
395 decoded = decoded.encode(DEFAULT_ENCODING)
396 return decoded
397
398 def decoded(self, name: str, default: Any = _marker) -> Any:
399 """Returns decoded value of property.
400
401 A component maps keys to icalendar property value types.
402 This function returns values compatible to native Python types.
403 """
404 if name in self:
405 value = self[name]
406 if isinstance(value, list):
407 return [self._decode(name, v) for v in value]
408 return self._decode(name, value)
409 if default is _marker:
410 raise KeyError(name)
411 return default
412
413 ########################################################################
414 # Inline values. A few properties have multiple values inlined in in one
415 # property line. These methods are used for splitting and joining these.
416
417 def get_inline(self, name, decode=1):
418 """Returns a list of values (split on comma)."""
419 vals = [v.strip('" ') for v in q_split(self[name])]
420 if decode:
421 return [self._decode(name, val) for val in vals]
422 return vals
423
424 def set_inline(self, name, values, encode=1):
425 """Converts a list of values into comma separated string and sets value
426 to that.
427 """
428 if encode:
429 values = [self._encode(name, value, encode=1) for value in values]
430 self[name] = self.types_factory["inline"](q_join(values))
431
432 #########################
433 # Handling of components
434
435 def add_component(self, component: Component) -> None:
436 """Add a subcomponent to this component."""
437 self.subcomponents.append(component)
438
439 def _walk(
440 self, name: str | None, select: callable[[Component], bool]
441 ) -> list[Component]:
442 """Walk to given component."""
443 result = []
444 stack = [self]
445 while stack:
446 component = stack.pop()
447 if (name is None or component.name == name) and select(component):
448 result.append(component)
449 stack.extend(reversed(component.subcomponents))
450 return result
451
452 def walk(
453 self,
454 name: str | None = None,
455 select: callable[[Component], bool] = lambda _: True,
456 ) -> list[Component]:
457 """Recursively traverses component and subcomponents. Returns sequence
458 of same. If name is passed, only components with name will be returned.
459
460 :param name: The name of the component or None such as ``VEVENT``.
461 :param select: A function that takes the component as first argument
462 and returns True/False.
463 :returns: A list of components that match.
464 :rtype: list[Component]
465 """
466 if name is not None:
467 name = name.upper()
468 return self._walk(name, select)
469
470 def with_uid(self, uid: str) -> list[Component]:
471 """Return a list of components with the given UID.
472
473 Parameters:
474 uid: The UID of the component.
475
476 Returns:
477 list[Component]: List of components with the given UID.
478 """
479 return self.walk(select=lambda c: c.uid == uid)
480
481 #####################
482 # Generation
483
484 def property_items(
485 self,
486 recursive: bool = True,
487 sorted: bool = True,
488 ) -> list[tuple[str, object]]:
489 """Returns properties in this component and subcomponents as a list.
490
491 The list contains ``(name, value)`` tuples.
492 """
493 # Iterative implementation to avoid RecursionError
494 result = []
495 v_text = self.types_factory["text"]
496 # Stack stores (component, state)
497 # state: True means we are processing the END of the component
498 # state: False means we are processing the BEGIN and properties of the component
499 stack = [(self, False)]
500 while stack:
501 comp, is_end = stack.pop()
502 if is_end:
503 result.append(("END", v_text(comp.name).to_ical()))
504 else:
505 result.append(("BEGIN", v_text(comp.name).to_ical()))
506 property_names = comp.sorted_keys() if sorted else comp.keys()
507
508 for name in property_names:
509 values = comp[name]
510 if isinstance(values, list):
511 # normally one property is one line
512 for value in values:
513 result.append((name, value))
514 else:
515 result.append((name, values))
516
517 # Push the END marker for this component
518 stack.append((comp, True))
519 # Push subcomponents if recursion is enabled
520 if recursive:
521 # Push in reverse order to maintain original order in result
522 for subcomponent in reversed(comp.subcomponents):
523 stack.append((subcomponent, False))
524
525 return result
526
527 @overload
528 @classmethod
529 def from_ical(
530 cls, st: str | bytes, multiple: Literal[False] = False
531 ) -> Component: ...
532
533 @overload
534 @classmethod
535 def from_ical(cls, st: str | bytes, multiple: Literal[True]) -> list[Component]: ...
536
537 @classmethod
538 def _get_ical_parser(cls, st: str | bytes) -> ComponentIcalParser:
539 """Get the iCal parser for the given input string."""
540 return ComponentIcalParser(st, cls._get_component_factory(), cls.types_factory)
541
542 @classmethod
543 def from_ical(
544 cls, st: str | bytes | Path, multiple: bool = False
545 ) -> Component | list[Component]:
546 """Parse iCalendar data into component instances.
547
548 Handles standard and custom components (``X-*``, IANA-registered).
549
550 Parameters:
551 st: iCalendar data as bytes or string, or a path to an iCalendar file as
552 :class:`pathlib.Path` or string.
553 multiple: If ``True``, returns list. If ``False``, returns single component.
554
555 Returns:
556 Component or list of components
557
558 See Also:
559 :doc:`/how-to/custom-components` for examples of parsing custom components
560 """
561 if isinstance(st, Path):
562 st = st.read_bytes()
563 elif isinstance(st, str) and "\n" not in st and "\r" not in st:
564 # A string is only probed as a file path when it contains no line
565 # breaks. Valid iCalendar data is always folded with CRLF line
566 # endings (RFC 5545), so real calendar content never reaches this
567 # branch and is never read from disk. File paths, conversely, do
568 # not contain line breaks on the platforms we support.
569 try:
570 is_file = Path(st).is_file()
571 except (OSError, ValueError):
572 # The string is not usable as a path on this platform (e.g. it
573 # is too long, or contains characters the OS rejects such as an
574 # embedded null byte). Treat it as calendar data, not a file, so
575 # the parser raises a consistent ValueError across platforms.
576 is_file = False
577 if is_file:
578 st = Path(st).read_bytes()
579 parser = cls._get_ical_parser(st)
580 components = parser.parse()
581 if multiple:
582 return components
583 if len(components) > 1:
584 raise ValueError(
585 cls._format_error(
586 "Found multiple components where only one is allowed", st
587 )
588 )
589 if len(components) < 1:
590 raise ValueError(
591 cls._format_error(
592 "Found no components where exactly one is required", st
593 )
594 )
595 return components[0]
596
597 @staticmethod
598 def _format_error(error_description, bad_input, elipsis="[...]"):
599 # there's three character more in the error, ie. ' ' x2 and a ':'
600 max_error_length = 100 - 3
601 if len(error_description) + len(bad_input) + len(elipsis) > max_error_length:
602 truncate_to = max_error_length - len(error_description) - len(elipsis)
603 return f"{error_description}: {bad_input[:truncate_to]} {elipsis}"
604 return f"{error_description}: {bad_input}"
605
606 def content_line(self, name, value, sorted: bool = True):
607 """Returns property as content line."""
608 params = getattr(value, "params", Parameters())
609 return Contentline.from_parts(name, params, value, sorted=sorted)
610
611 def content_lines(self, sorted: bool = True):
612 """Converts the Component and subcomponents into content lines."""
613 contentlines = Contentlines()
614 for name, value in self.property_items(sorted=sorted):
615 cl = self.content_line(name, value, sorted=sorted)
616 contentlines.append(cl)
617 contentlines.append("") # remember the empty string in the end
618 return contentlines
619
620 def to_ical(self, sorted: bool = True):
621 """
622 :param sorted: Whether parameters and properties should be
623 lexicographically sorted.
624 """
625
626 content_lines = self.content_lines(sorted=sorted)
627 return content_lines.to_ical()
628
629 def __repr__(self) -> str:
630 """String representation of class with all of its subcomponents.
631
632 Implemented iteratively rather than recursively so that calendars
633 with deeply nested subcomponents do not raise ``RecursionError``.
634 A pathological ``.ics`` payload of only ~13 KB can otherwise nest
635 ``BEGIN:VEVENT`` ~500 levels and crash any caller that performs
636 ``repr()``/``str()``/``f"{cal}"`` on the parsed calendar
637 (e.g. logging, error reporting, debug pages).
638 """
639 # Stack-based traversal. Each frame is one of:
640 # ("open", component) -> emit "Name({props}" and schedule children
641 # ("close",) -> emit ")"
642 # ("comma",) -> emit ", "
643 out: list[str] = []
644 stack: list[tuple] = [("open", self)]
645 while stack:
646 frame = stack.pop()
647 kind = frame[0]
648 if kind == "comma":
649 out.append(", ")
650 elif kind == "close":
651 out.append(")")
652 else: # "open"
653 node = frame[1]
654 if isinstance(node, Component):
655 out.append(f"{node.name or type(node).__name__}({dict(node)}")
656 subs = node.subcomponents
657 if subs:
658 # Defer ")" then push children in reverse so that
659 # popping yields original order, with ", " separators
660 # (the first popped comma serves as the separator
661 # between the component's dict and its first child).
662 stack.append(("close",))
663 for sub in reversed(subs):
664 stack.append(("open", sub))
665 stack.append(("comma",))
666 else:
667 out.append(")")
668 else:
669 # Should not normally occur (subcomponents are Components),
670 # but be safe and fall back to non-recursive str().
671 out.append(str(node))
672 return "".join(out)
673
674 def __eq__(self, other: Component) -> bool:
675 if not isinstance(other, Component):
676 return NotImplemented
677
678 # Two components are equal when their own properties are equal and their
679 # subcomponents are equal as a multiset: order does not matter, and each
680 # nested pair is compared the same way recursively. Subcomponents are
681 # neither sortable nor hashable, so we can't use a set; we have to match
682 # each one by searching. Done recursively that search is exponential for
683 # deeply nested components (GHSA-cv84-9p8j-fj68), so we walk an explicit
684 # stack instead of recursing.
685 #
686 # Each frame holds the pair being compared plus b's subcomponents
687 # still unmatched. child_result carries the outcome of the comparison
688 # that just finished back up to its parent frame: a successful child
689 # match removes that subcomponent from unmatched and advances to the
690 # next a subcomponent, while a failure tries the next candidate.
691 # Exhausting a's subcomponents means every one found a partner ->
692 # equal; running out of candidates for some subcomponent -> not equal.
693 # (Greedy matching is sufficient because equality is transitive, so equal
694 # candidates are interchangeable.)
695 stack = [_ComponentEqFrame(self, other)]
696 child_result = None
697 while stack:
698 frame = stack[-1]
699 if frame.unmatched is None:
700 if len(frame.a.subcomponents) != len(frame.b.subcomponents) or not (
701 CaselessDict.__eq__(frame.a, frame.b)
702 ):
703 stack.pop()
704 child_result = False
705 continue
706 frame.unmatched = list(frame.b.subcomponents)
707 elif child_result is not None:
708 if child_result:
709 del frame.unmatched[frame.candidate_index]
710 frame.a_index += 1
711 frame.candidate_index = 0
712 else:
713 frame.candidate_index += 1
714 child_result = None
715 if frame.a_index >= len(frame.a.subcomponents):
716 stack.pop()
717 child_result = True
718 elif frame.candidate_index >= len(frame.unmatched):
719 stack.pop()
720 child_result = False
721 else:
722 stack.append(
723 _ComponentEqFrame(
724 frame.a.subcomponents[frame.a_index],
725 frame.unmatched[frame.candidate_index],
726 )
727 )
728 return child_result
729
730 DTSTAMP = stamp = single_utc_property(
731 "DTSTAMP",
732 """The UTC datetime stamp recording when this component instance was created or last revised.
733
734 This property is defined in :rfc:`5545#section-3.8.7.2`. It's required
735 in ``VEVENT``, ``VTODO``, ``VJOURNAL``, and ``VFREEBUSY`` components.
736
737 When the calendar object carries a ``METHOD`` property, such as for
738 scheduling, this value is the creation time of *this particular revision*.
739 Without a ``METHOD`` property, it's equivalent to :attr:`LAST_MODIFIED`.
740
741 The value is always in UTC. It's also accessible as :attr:`stamp`.
742
743 Example:
744 .. code-block:: pycon
745
746 >>> from datetime import timezone, datetime
747 >>> from icalendar import Event
748 >>> event = Event()
749 >>> event.DTSTAMP = datetime(2024, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
750 >>> event.DTSTAMP
751 datetime.datetime(2024, 6, 1, 12, 0, tzinfo=ZoneInfo(key='UTC'))
752
753 See also:
754 :attr:`CREATED`, :attr:`DTSTAMP`, :attr:`LAST_MODIFIED`,
755 :attr:`created`, :attr:`stamp`, :attr:`last_modified`
756 """,
757 )
758
759 LAST_MODIFIED = single_utc_property(
760 "LAST-MODIFIED",
761 """The UTC datetime when this component's information was last revised, per :rfc:`5545#section-3.8.7.3`.
762
763 It's analogous to a file's modification timestamp. This property is optional.
764 When it's absent, :attr:`last_modified` falls back to :attr:`DTSTAMP`.
765
766 This property is applicable to ``VEVENT``, ``VTODO``, ``VJOURNAL``, and ``VTIMEZONE``
767 components. The value is always in UTC.
768
769 Example:
770 .. code-block:: pycon
771
772 >>> from datetime import timezone, datetime
773 >>> from icalendar import Event
774 >>> event = Event()
775 >>> event.LAST_MODIFIED = datetime(2024, 6, 1, 9, 0, 0, tzinfo=timezone.utc)
776 >>> event.LAST_MODIFIED
777 datetime.datetime(2024, 6, 1, 9, 0, tzinfo=ZoneInfo(key='UTC'))
778
779 See also:
780 :attr:`CREATED`, :attr:`DTSTAMP`, :attr:`LAST_MODIFIED`,
781 :attr:`created`, :attr:`stamp`, :attr:`last_modified`
782 """,
783 )
784
785 @property
786 def last_modified(self) -> datetime:
787 """Datetime when the information associated with the component was last revised.
788
789 Since :attr:`LAST_MODIFIED` is an optional property,
790 this returns :attr:`DTSTAMP` if :attr:`LAST_MODIFIED` is not set.
791 """
792 return self.LAST_MODIFIED or self.DTSTAMP
793
794 @last_modified.setter
795 def last_modified(self, value):
796 self.LAST_MODIFIED = value
797
798 @last_modified.deleter
799 def last_modified(self):
800 del self.LAST_MODIFIED
801
802 @property
803 def created(self) -> datetime:
804 """Datetime when the information associated with the component was created.
805
806 Since :attr:`CREATED` is an optional property,
807 this returns :attr:`DTSTAMP` if :attr:`CREATED` is not set.
808 """
809 return self.CREATED or self.DTSTAMP
810
811 @created.setter
812 def created(self, value):
813 self.CREATED = value
814
815 @created.deleter
816 def created(self):
817 del self.CREATED
818
819 def is_thunderbird(self) -> bool:
820 """Whether this component has attributes that indicate that Mozilla Thunderbird created it."""
821 return any(attr.startswith("X-MOZ-") for attr in self.keys())
822
823 @staticmethod
824 def _utc_now() -> datetime:
825 """Return now as UTC value."""
826 return datetime.now(timezone.utc)
827
828 uid = uid_property
829 comments = comments_property
830 links = links_property
831 related_to = related_to_property
832 concepts = concepts_property
833 refids = refids_property
834
835 CREATED = single_utc_property(
836 "CREATED",
837 """The UTC datetime when this calendar component was first created, per :rfc:`5545#section-3.8.7.1`.
838
839 This property records when the calendar user agent originally stored the component.
840 This property is optional. When it's absent, :attr:`created` falls back to
841 :attr:`DTSTAMP`.
842
843 This property is applicable to ``VEVENT``, ``VTODO``, and ``VJOURNAL`` components.
844 The value is always in UTC.
845
846 Example:
847 .. code-block:: pycon
848
849 >>> from datetime import timezone, datetime
850 >>> from icalendar import Event
851 >>> event = Event()
852 >>> event.CREATED = datetime(2024, 1, 1, 8, 0, 0, tzinfo=timezone.utc)
853 >>> event.CREATED
854 datetime.datetime(2024, 1, 1, 8, 0, tzinfo=ZoneInfo(key='UTC'))
855
856 See also:
857 :attr:`CREATED`, :attr:`DTSTAMP`, :attr:`LAST_MODIFIED`,
858 :attr:`created`, :attr:`stamp`, :attr:`last_modified`
859 """,
860 )
861
862 _validate_new = True
863
864 @staticmethod
865 def _validate_start_and_end(start, end):
866 """This validates start and end.
867
868 Raises:
869 ~error.InvalidCalendar: If the information is not valid
870 """
871 if start is None or end is None:
872 return
873 if start > end:
874 raise InvalidCalendar("end must be after start")
875
876 @classmethod
877 def new(
878 cls,
879 created: date | None = None,
880 comments: list[str] | str | None = None,
881 concepts: CONCEPTS_TYPE_SETTER = None,
882 last_modified: date | None = None,
883 links: LINKS_TYPE_SETTER = None,
884 refids: list[str] | str | None = None,
885 related_to: RELATED_TO_TYPE_SETTER = None,
886 stamp: date | None = None,
887 subcomponents: Iterable[Component] | None = None,
888 ) -> Component:
889 """Create a new component.
890
891 Parameters:
892 comments: The :attr:`comments` of the component.
893 concepts: The :attr:`concepts` of the component.
894 created: The :attr:`created` of the component.
895 last_modified: The :attr:`last_modified` of the component.
896 links: The :attr:`links` of the component.
897 related_to: The :attr:`related_to` of the component.
898 stamp: The :attr:`DTSTAMP` of the component.
899 subcomponents: The subcomponents of the component.
900
901 Raises:
902 ~error.InvalidCalendar: If the content is not valid
903 according to :rfc:`5545`.
904
905 .. warning:: As time progresses, we will be stricter with the
906 validation.
907 """
908 component = cls()
909 component.DTSTAMP = stamp
910 component.created = created
911 component.last_modified = last_modified
912 component.comments = comments
913 component.links = links
914 component.related_to = related_to
915 component.concepts = concepts
916 component.refids = refids
917 if subcomponents is not None:
918 component.subcomponents = (
919 subcomponents
920 if isinstance(subcomponents, list)
921 else list(subcomponents)
922 )
923 return component
924
925 def to_jcal(self) -> list:
926 """Convert this component to a jCal object.
927
928 Returns:
929 jCal object
930
931 See also :attr:`to_json`.
932
933 In this example, we create a simple VEVENT component and convert it to jCal:
934
935 .. code-block:: pycon
936
937 >>> from icalendar import Event
938 >>> from datetime import date
939 >>> from pprint import pprint
940 >>> event = Event.new(summary="My Event", start=date(2025, 11, 22))
941 >>> pprint(event.to_jcal())
942 ['vevent',
943 [['dtstamp', {}, 'date-time', '2025-05-17T08:06:12Z'],
944 ['summary', {}, 'text', 'My Event'],
945 ['uid', {}, 'text', 'd755cef5-2311-46ed-a0e1-6733c9e15c63'],
946 ['dtstart', {}, 'date', '2025-11-22']],
947 []]
948 """
949
950 # Iterative tree walk to avoid RecursionError on deeply nested
951 # components, mirroring the iterative iCal parser/serializer (GH #1370).
952 def make_node(comp: Component) -> list:
953 properties = [
954 item.to_jcal(key.lower())
955 for key, value in comp.items()
956 for item in (value if isinstance(value, list) else [value])
957 ]
958 return [comp.name.lower(), properties, []]
959
960 root_node = make_node(self)
961 # stack of (component, jCal node) pairs still to expand
962 stack: list[tuple[Component, list]] = [(self, root_node)]
963 while stack:
964 comp, node = stack.pop()
965 children = node[2]
966 for subcomponent in comp.subcomponents:
967 child_node = make_node(subcomponent)
968 children.append(child_node)
969 stack.append((subcomponent, child_node))
970 return root_node
971
972 def to_json(self) -> str:
973 """Return this component as a jCal JSON string.
974
975 Returns:
976 JSON string
977
978 See also :attr:`to_jcal`.
979 """
980 return json.dumps(self.to_jcal())
981
982 @classmethod
983 def from_jcal(cls, jcal: str | list) -> Component:
984 """Create a component from a jCal list.
985
986 Parameters:
987 jcal: jCal list or JSON string according to :rfc:`7265`.
988
989 Raises:
990 ~error.JCalParsingError: If the jCal provided is invalid.
991 ~json.JSONDecodeError: If the provided string is not valid JSON.
992
993 This reverses :func:`to_json` and :func:`to_jcal`.
994
995 The following code parses an example from :rfc:`7265`:
996
997 .. code-block:: pycon
998
999 >>> from icalendar import Component
1000 >>> jcal = ["vcalendar",
1001 ... [
1002 ... ["calscale", {}, "text", "GREGORIAN"],
1003 ... ["prodid", {}, "text", "-//Example Inc.//Example Calendar//EN"],
1004 ... ["version", {}, "text", "2.0"]
1005 ... ],
1006 ... [
1007 ... ["vevent",
1008 ... [
1009 ... ["dtstamp", {}, "date-time", "2008-02-05T19:12:24Z"],
1010 ... ["dtstart", {}, "date", "2008-10-06"],
1011 ... ["summary", {}, "text", "Planning meeting"],
1012 ... ["uid", {}, "text", "4088E990AD89CB3DBB484909"]
1013 ... ],
1014 ... []
1015 ... ]
1016 ... ]
1017 ... ]
1018 >>> calendar = Component.from_jcal(jcal)
1019 >>> print(calendar.name)
1020 VCALENDAR
1021 >>> print(calendar.prodid)
1022 -//Example Inc.//Example Calendar//EN
1023 >>> event = calendar.events[0]
1024 >>> print(event.summary)
1025 Planning meeting
1026
1027 """
1028 if isinstance(jcal, str):
1029 jcal = json.loads(jcal)
1030 # Iterative tree build to avoid RecursionError on deeply nested jCal,
1031 # mirroring the iterative iCal parser (GH #1370). ``_node_from_jcal``
1032 # parses a single component (without its subcomponents); the stack walks
1033 # the subcomponent tree, accumulating the jCal error path ([2, i] per
1034 # nesting level) so error messages match the recursive implementation.
1035 root, root_subcomponents = _node_from_jcal(jcal, cls)
1036 stack: list[tuple[Component, list, list]] = [(root, root_subcomponents, [])]
1037 while stack:
1038 parent, subcomponents, prefix = stack.pop()
1039 for i, subcomponent in enumerate(subcomponents):
1040 child_prefix = [*prefix, 2, i]
1041 # Prepend the full nesting path so errors match the recursive
1042 # implementation. This also preserves the error value and
1043 # traceback, like the nested context managers did before.
1044 with JCalParsingError.reraise_with_path_added(*child_prefix):
1045 child, child_subcomponents = _node_from_jcal(
1046 subcomponent, type(parent)
1047 )
1048 parent.subcomponents.append(child)
1049 stack.append((child, child_subcomponents, child_prefix))
1050 return root
1051
1052 def copy(self, recursive: bool = False) -> Self:
1053 """Copy the component.
1054
1055 Parameters:
1056 recursive:
1057 If ``True``, this creates copies of the component, its subcomponents,
1058 and all its properties.
1059 If ``False``, this only creates a shallow copy of the component.
1060
1061 Returns:
1062 A copy of the component.
1063
1064 Examples:
1065
1066 Create a shallow copy of a component:
1067
1068 .. code-block:: pycon
1069
1070 >>> from icalendar import Event
1071 >>> event = Event.new(description="Event to be copied")
1072 >>> event_copy = event.copy()
1073 >>> str(event_copy.description)
1074 'Event to be copied'
1075
1076 Shallow copies lose their subcomponents:
1077
1078 .. code-block:: pycon
1079
1080 >>> from icalendar import Calendar
1081 >>> calendar = Calendar.example()
1082 >>> len(calendar.subcomponents)
1083 3
1084 >>> calendar_copy = calendar.copy()
1085 >>> len(calendar_copy.subcomponents)
1086 0
1087
1088 A recursive copy also copies all the subcomponents:
1089
1090 .. code-block:: pycon
1091
1092 >>> full_calendar_copy = calendar.copy(recursive=True)
1093 >>> len(full_calendar_copy.subcomponents)
1094 3
1095 >>> full_calendar_copy.events[0] == calendar.events[0]
1096 True
1097 >>> full_calendar_copy.events[0] is calendar.events[0]
1098 False
1099
1100 """
1101 if recursive:
1102 return deepcopy(self)
1103 return super().copy()
1104
1105 def is_lazy(self) -> bool:
1106 """This component is fully parsed."""
1107 return False
1108
1109 def parse(self) -> Self:
1110 """Return the fully parsed component.
1111
1112 For non-lazy components, this returns self.
1113 For lazy components, this parses the component and returns the result.
1114 """
1115 return self
1116
1117
1118def _node_from_jcal(jcal, starting_cls: type[Component]) -> tuple[Component, list]:
1119 """Parse a single jCal component without recursing into subcomponents.
1120
1121 Module-level helper for :meth:`Component.from_jcal`: it has no ties to a
1122 class or instance (the relevant class is passed in as ``starting_cls``), so
1123 it is a plain function rather than a (static) method.
1124
1125 Parameters:
1126 jcal: The jCal list for one component.
1127 starting_cls: The class used as the parser for structural validation
1128 before the component type is resolved from its name (the entry
1129 class for the root, the parent's resolved class for a child).
1130
1131 Returns:
1132 A ``(component, raw_subcomponents)`` tuple. The raw subcomponents are
1133 returned for the caller to walk iteratively.
1134
1135 Raises:
1136 ~error.JCalParsingError: If this component node is invalid. The path
1137 is relative to this node; callers prepend the nesting path.
1138 """
1139 if not isinstance(jcal, list) or len(jcal) != 3:
1140 raise JCalParsingError(
1141 "A component must be a list with 3 items.", starting_cls, value=jcal
1142 )
1143 name, properties, subcomponents = jcal
1144 if not isinstance(name, str):
1145 raise JCalParsingError(
1146 "The name must be a string.", starting_cls, path=[0], value=name
1147 )
1148 if name.upper() != starting_cls.name:
1149 # delegate to correct component class
1150 component_cls = starting_cls.get_component_class(name.upper())
1151 else:
1152 component_cls = starting_cls
1153 component = component_cls()
1154 if not isinstance(properties, list):
1155 raise JCalParsingError(
1156 "The properties must be a list.",
1157 component_cls,
1158 path=1,
1159 value=properties,
1160 )
1161 for i, prop in enumerate(properties):
1162 JCalParsingError.validate_property(prop, component_cls, path=[1, i])
1163 prop_name = prop[0]
1164 prop_value = prop[2]
1165 prop_cls: type[VPROPERTY] = component_cls.types_factory.for_property(
1166 prop_name, prop_value
1167 )
1168 with JCalParsingError.reraise_with_path_added(1, i):
1169 v_prop = prop_cls.from_jcal(prop)
1170 # jCal encodes the value type in the type field (``prop[2]``)
1171 # instead of as a ``VALUE`` parameter (RFC 7265). Restore that
1172 # parameter when the type differs from the property's default, so
1173 # explicit value types such as ``RDATE;VALUE=PERIOD`` or
1174 # ``TRIGGER;VALUE=DATE-TIME`` survive the round-trip (GH #1426).
1175 # A type equal to the default needs no VALUE parameter, and the
1176 # reserved ``unknown`` type must never become ``VALUE=UNKNOWN``
1177 # (RFC 7265, section 5.2).
1178 default_type = component_cls.types_factory.default_value_type(prop_name)
1179 if isinstance(prop_value, str) and prop_value.lower() not in (
1180 "unknown",
1181 default_type,
1182 ):
1183 v_prop.VALUE = prop_value.upper()
1184 elif "VALUE" in v_prop.params:
1185 del v_prop.VALUE
1186 component.add(prop_name, v_prop)
1187 if not isinstance(subcomponents, list):
1188 raise JCalParsingError(
1189 "The subcomponents must be a list.",
1190 component_cls,
1191 2,
1192 value=subcomponents,
1193 )
1194 return component, subcomponents
1195
1196
1197__all__ = ["Component"]