Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/cal/component.py: 54%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

381 statements  

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 pathlib import Path 

9from typing import TYPE_CHECKING, Any, ClassVar, Literal, overload 

10 

11from icalendar.attr import ( 

12 CONCEPTS_TYPE_SETTER, 

13 LINKS_TYPE_SETTER, 

14 RELATED_TO_TYPE_SETTER, 

15 comments_property, 

16 concepts_property, 

17 links_property, 

18 refids_property, 

19 related_to_property, 

20 single_utc_property, 

21 uid_property, 

22) 

23from icalendar.cal.component_factory import ComponentFactory 

24from icalendar.caselessdict import CaselessDict 

25from icalendar.error import InvalidCalendar, JCalParsingError 

26from icalendar.parser import ( 

27 Contentline, 

28 Contentlines, 

29 Parameters, 

30 q_join, 

31 q_split, 

32) 

33from icalendar.parser.ical.component import ComponentIcalParser 

34from icalendar.parser_tools import DEFAULT_ENCODING 

35from icalendar.prop import VPROPERTY, TypesFactory, vDDDLists, vText 

36from icalendar.timezone import tzp 

37from icalendar.tools import is_date 

38 

39if TYPE_CHECKING: 

40 from collections.abc import Iterable 

41 

42 from icalendar.compatibility import Self 

43 

44_marker = [] 

45 

46 

47class Component(CaselessDict): 

48 """Base class for calendar components. 

49 

50 Component is the base object for calendar, Event and the other 

51 components defined in :rfc:`5545`. Normally you will not use this class 

52 directly, but rather one of the subclasses. 

53 """ 

54 

55 name: ClassVar[str | None] = None 

56 """The name of the component. 

57 

58 This is defined in each component class. 

59 

60 Example: 

61 

62 .. code-block:: pycon 

63 

64 >>> from icalendar import Calendar 

65 >>> cal = Calendar.new() 

66 >>> cal.name 

67 'VCALENDAR' 

68 

69 """ 

70 

71 required: ClassVar[tuple[()]] = () 

72 """These properties are required.""" 

73 

74 singletons: ClassVar[tuple[()]] = () 

75 """These properties must appear only once.""" 

76 

77 multiple: ClassVar[tuple[()]] = () 

78 """These properties may occur more than once.""" 

79 

80 exclusive: ClassVar[tuple[()]] = () 

81 """These properties are mutually exclusive.""" 

82 

83 inclusive: ClassVar[(tuple[str] | tuple[tuple[str, str]])] = () 

84 """These properties are inclusive. 

85 

86 In other words, if the first property in the tuple occurs, then the 

87 second one must also occur. 

88 

89 Example: 

90 

91 .. code-block:: python 

92 

93 ('duration', 'repeat') 

94 """ 

95 

96 ignore_exceptions: ClassVar[bool] = False 

97 """Whether or not to ignore exceptions when parsing. 

98 

99 If ``True``, and this component can't be parsed, then it will silently 

100 ignore it, rather than let the exception propagate upwards. 

101 """ 

102 

103 types_factory: ClassVar[TypesFactory] = TypesFactory.instance() 

104 _components_factory: ClassVar[ComponentFactory | None] = None 

105 

106 subcomponents: list[Component] 

107 """All subcomponents of this component.""" 

108 

109 @classmethod 

110 def _get_component_factory(cls) -> ComponentFactory: 

111 """Get the component factory.""" 

112 if cls._components_factory is None: 

113 cls._components_factory = ComponentFactory() 

114 return cls._components_factory 

115 

116 @classmethod 

117 def get_component_class(cls, name: str) -> type[Component]: 

118 """Return a component with this name. 

119 

120 Parameters: 

121 name: Name of the component, i.e. ``VCALENDAR`` 

122 """ 

123 return cls._get_component_factory().get_component_class(name) 

124 

125 @classmethod 

126 def register(cls, component_class: type[Component]) -> None: 

127 """Register a custom component class. 

128 

129 Parameters: 

130 component_class: Component subclass to register. 

131 Must have a ``name`` attribute. 

132 

133 Raises: 

134 ValueError: If ``component_class`` has no ``name`` attribute. 

135 ValueError: If a component with this name is already registered. 

136 

137 Examples: 

138 Create a custom icalendar component with the name ``X-EXAMPLE``: 

139 

140 .. code-block:: pycon 

141 

142 >>> from icalendar import Component 

143 >>> class XExample(Component): 

144 ... name = "X-EXAMPLE" 

145 ... def custom_method(self): 

146 ... return "custom" 

147 >>> Component.register(XExample) 

148 """ 

149 if not hasattr(component_class, "name") or component_class.name is None: 

150 raise ValueError(f"{component_class} must have a 'name' attribute") 

151 

152 # Check if already registered 

153 component_factory = cls._get_component_factory() 

154 existing = component_factory.get(component_class.name) 

155 if existing is not None and existing is not component_class: 

156 raise ValueError( 

157 f"Component '{component_class.name}' is already registered" 

158 f" as {existing}" 

159 ) 

160 

161 component_factory.add_component_class(component_class) 

162 

163 @staticmethod 

164 def _infer_value_type( 

165 value: date | datetime | timedelta | time | tuple | list, 

166 ) -> str | None: 

167 """Infer the ``VALUE`` parameter from a Python type. 

168 

169 Parameters: 

170 value: Python native type, one of :class:`datetime.date`, :class:`datetime.datetime`, 

171 :class:`datetime.timedelta`, :class:`datetime.time`, :class:`tuple`, 

172 or :class:`list`. 

173 

174 Returns: 

175 str or None: The ``VALUE`` parameter string, for example, "DATE", 

176 "TIME", or other string, or ``None`` 

177 if no specific ``VALUE`` is needed. 

178 """ 

179 if isinstance(value, list): 

180 if not value: 

181 return None 

182 # Check if ALL items are date (but not datetime) 

183 if all(is_date(item) for item in value): 

184 return "DATE" 

185 # Check if ALL items are time 

186 if all(isinstance(item, time) for item in value): 

187 return "TIME" 

188 # Mixed types or other types - don't infer 

189 return None 

190 if is_date(value): 

191 return "DATE" 

192 if isinstance(value, time): 

193 return "TIME" 

194 # Don't infer PERIOD - it's too risky and vPeriod already handles it 

195 return None 

196 

197 def __init__(self, *args, **kwargs): 

198 """Set keys to upper for initial dict.""" 

199 super().__init__(*args, **kwargs) 

200 # set parameters here for properties that use non-default values 

201 self.subcomponents: list[Component] = [] # Components can be nested. 

202 self.errors = [] # If we ignored exception(s) while 

203 # parsing a property, contains error strings 

204 

205 def __bool__(self): 

206 """Returns True, CaselessDict would return False if it had no items.""" 

207 return True 

208 

209 def __getitem__(self, key): 

210 """Get property value from the component dictionary.""" 

211 return super().__getitem__(key) 

212 

213 def get(self, key, default=None): 

214 """Get property value with default.""" 

215 try: 

216 return self[key] 

217 except KeyError: 

218 return default 

219 

220 def is_empty(self): 

221 """Returns True if Component has no items or subcomponents, else False.""" 

222 return bool(not list(self.values()) + self.subcomponents) 

223 

224 ############################# 

225 # handling of property values 

226 

227 @classmethod 

228 def _encode(cls, name, value, parameters=None, encode=1): 

229 """Encode values to icalendar property values. 

230 

231 :param name: Name of the property. 

232 :type name: string 

233 

234 :param value: Value of the property. Either of a basic Python type of 

235 any of the icalendar's own property types. 

236 :type value: Python native type or icalendar property type. 

237 

238 :param parameters: Property parameter dictionary for the value. Only 

239 available, if encode is set to True. 

240 :type parameters: Dictionary 

241 

242 :param encode: True, if the value should be encoded to one of 

243 icalendar's own property types (Fallback is "vText") 

244 or False, if not. 

245 :type encode: Boolean 

246 

247 :returns: icalendar property value 

248 """ 

249 if not encode: 

250 return value 

251 if isinstance(value, cls.types_factory.all_types): 

252 # Don't encode already encoded values. 

253 obj = value 

254 else: 

255 # Extract VALUE parameter if present, or infer it from the Python type 

256 value_param = None 

257 if parameters and "VALUE" in parameters: 

258 value_param = parameters["VALUE"] 

259 elif not isinstance(value, cls.types_factory.all_types): 

260 inferred = cls._infer_value_type(value) 

261 if inferred: 

262 value_param = inferred 

263 # Auto-set the VALUE parameter 

264 if parameters is None: 

265 parameters = {} 

266 if "VALUE" not in parameters: 

267 parameters["VALUE"] = inferred 

268 

269 klass = cls.types_factory.for_property(name, value_param) 

270 obj = klass(value) 

271 if parameters: 

272 if not hasattr(obj, "params"): 

273 obj.params = Parameters() 

274 for key, item in parameters.items(): 

275 if item is None: 

276 if key in obj.params: 

277 del obj.params[key] 

278 else: 

279 obj.params[key] = item 

280 return obj 

281 

282 def add( 

283 self, 

284 name: str, 

285 value, 

286 parameters: dict[str, str] | Parameters = None, 

287 encode: bool = True, 

288 ) -> None: 

289 """Add a property to this component. 

290 

291 If the property already exists, the new value is appended so the 

292 property carries a list of values rather than replacing the previous 

293 one. When ``name`` is ``DTSTAMP``, ``CREATED``, or ``LAST-MODIFIED`` 

294 and ``value`` is a ``datetime``, the value is converted to UTC as the 

295 RFC requires. 

296 

297 Parameters: 

298 name: Name of the property. 

299 value: 

300 Value of the property. Either a basic Python type or any of 

301 icalendar's own property types. 

302 parameters: 

303 Property parameter dictionary for the value. Only consulted 

304 when ``encode`` is ``True``. 

305 encode: 

306 ``True`` if the value should be encoded to one of icalendar's 

307 own property types (fallback is ``vText``); ``False`` to 

308 store the value as-is. 

309 

310 Returns: 

311 ``None`` 

312 

313 Example: 

314 

315 >>> from icalendar import Event 

316 >>> event = Event() 

317 >>> event.add("summary", "Team sync") 

318 >>> event["summary"] 

319 vText(b'Team sync') 

320 

321 """ 

322 if isinstance(value, datetime) and name.lower() in ( 

323 "dtstamp", 

324 "created", 

325 "last-modified", 

326 ): 

327 # RFC expects UTC for those... force value conversion. 

328 value = tzp.localize_utc(value) 

329 

330 # encode value 

331 if ( 

332 encode 

333 and isinstance(value, list) 

334 and name.lower() not in ["rdate", "exdate", "categories"] 

335 ): 

336 # Individually convert each value to an ical type except rdate and 

337 # exdate, where lists of dates might be passed to vDDDLists. 

338 value = [self._encode(name, v, parameters, encode) for v in value] 

339 else: 

340 value = self._encode(name, value, parameters, encode) 

341 

342 # set value 

343 if name in self: 

344 # If property already exists, append it. 

345 oldval = self[name] 

346 if isinstance(oldval, list): 

347 if isinstance(value, list): 

348 value = oldval + value 

349 else: 

350 oldval.append(value) 

351 value = oldval 

352 else: 

353 value = [oldval, value] 

354 self[name] = value 

355 

356 def _decode(self, name: str, value: VPROPERTY): 

357 """Internal for decoding property values.""" 

358 

359 # TODO: Currently the decoded method calls the icalendar.prop instances 

360 # from_ical. We probably want to decode properties into Python native 

361 # types here. But when parsing from an ical string with from_ical, we 

362 # want to encode the string into a real icalendar.prop property. 

363 if hasattr(value, "ical_value"): 

364 return value.ical_value 

365 if isinstance(value, vDDDLists): 

366 # TODO: Workaround unfinished decoding 

367 return value 

368 decoded = self.types_factory.from_ical(name, value) 

369 # TODO: remove when proper decoded is implemented in every prop.* class 

370 # Workaround to decode vText properly 

371 if isinstance(decoded, vText): 

372 decoded = decoded.encode(DEFAULT_ENCODING) 

373 return decoded 

374 

375 def decoded(self, name: str, default: Any = _marker) -> Any: 

376 """Returns decoded value of property. 

377 

378 A component maps keys to icalendar property value types. 

379 This function returns values compatible to native Python types. 

380 """ 

381 if name in self: 

382 value = self[name] 

383 if isinstance(value, list): 

384 return [self._decode(name, v) for v in value] 

385 return self._decode(name, value) 

386 if default is _marker: 

387 raise KeyError(name) 

388 return default 

389 

390 ######################################################################## 

391 # Inline values. A few properties have multiple values inlined in in one 

392 # property line. These methods are used for splitting and joining these. 

393 

394 def get_inline(self, name, decode=1): 

395 """Returns a list of values (split on comma).""" 

396 vals = [v.strip('" ') for v in q_split(self[name])] 

397 if decode: 

398 return [self._decode(name, val) for val in vals] 

399 return vals 

400 

401 def set_inline(self, name, values, encode=1): 

402 """Converts a list of values into comma separated string and sets value 

403 to that. 

404 """ 

405 if encode: 

406 values = [self._encode(name, value, encode=1) for value in values] 

407 self[name] = self.types_factory["inline"](q_join(values)) 

408 

409 ######################### 

410 # Handling of components 

411 

412 def add_component(self, component: Component) -> None: 

413 """Add a subcomponent to this component.""" 

414 self.subcomponents.append(component) 

415 

416 def _walk( 

417 self, name: str | None, select: callable[[Component], bool] 

418 ) -> list[Component]: 

419 """Walk to given component.""" 

420 result = [] 

421 stack = [self] 

422 while stack: 

423 component = stack.pop() 

424 if (name is None or component.name == name) and select(component): 

425 result.append(component) 

426 stack.extend(reversed(component.subcomponents)) 

427 return result 

428 

429 def walk( 

430 self, 

431 name: str | None = None, 

432 select: callable[[Component], bool] = lambda _: True, 

433 ) -> list[Component]: 

434 """Recursively traverses component and subcomponents. Returns sequence 

435 of same. If name is passed, only components with name will be returned. 

436 

437 :param name: The name of the component or None such as ``VEVENT``. 

438 :param select: A function that takes the component as first argument 

439 and returns True/False. 

440 :returns: A list of components that match. 

441 :rtype: list[Component] 

442 """ 

443 if name is not None: 

444 name = name.upper() 

445 return self._walk(name, select) 

446 

447 def with_uid(self, uid: str) -> list[Component]: 

448 """Return a list of components with the given UID. 

449 

450 Parameters: 

451 uid: The UID of the component. 

452 

453 Returns: 

454 list[Component]: List of components with the given UID. 

455 """ 

456 return self.walk(select=lambda c: c.uid == uid) 

457 

458 ##################### 

459 # Generation 

460 

461 def property_items( 

462 self, 

463 recursive: bool = True, 

464 sorted: bool = True, 

465 ) -> list[tuple[str, object]]: 

466 """Returns properties in this component and subcomponents as: 

467 [(name, value), ...] 

468 """ 

469 # Iterative implementation to avoid RecursionError 

470 result = [] 

471 v_text = self.types_factory["text"] 

472 # Stack stores (component, state) 

473 # state: True means we are processing the END of the component 

474 # state: False means we are processing the BEGIN and properties of the component 

475 stack = [(self, False)] 

476 while stack: 

477 comp, is_end = stack.pop() 

478 if is_end: 

479 result.append(("END", v_text(comp.name).to_ical())) 

480 else: 

481 result.append(("BEGIN", v_text(comp.name).to_ical())) 

482 property_names = comp.sorted_keys() if sorted else comp.keys() 

483 

484 for name in property_names: 

485 values = comp[name] 

486 if isinstance(values, list): 

487 # normally one property is one line 

488 for value in values: 

489 result.append((name, value)) 

490 else: 

491 result.append((name, values)) 

492 

493 # Push the END marker for this component 

494 stack.append((comp, True)) 

495 # Push subcomponents if recursion is enabled 

496 if recursive: 

497 # Push in reverse order to maintain original order in result 

498 for subcomponent in reversed(comp.subcomponents): 

499 stack.append((subcomponent, False)) 

500 

501 return result 

502 

503 @overload 

504 @classmethod 

505 def from_ical( 

506 cls, st: str | bytes, multiple: Literal[False] = False 

507 ) -> Component: ... 

508 

509 @overload 

510 @classmethod 

511 def from_ical(cls, st: str | bytes, multiple: Literal[True]) -> list[Component]: ... 

512 

513 @classmethod 

514 def _get_ical_parser(cls, st: str | bytes) -> ComponentIcalParser: 

515 """Get the iCal parser for the given input string.""" 

516 return ComponentIcalParser(st, cls._get_component_factory(), cls.types_factory) 

517 

518 @classmethod 

519 def from_ical( 

520 cls, st: str | bytes | Path, multiple: bool = False 

521 ) -> Component | list[Component]: 

522 """Parse iCalendar data into component instances. 

523 

524 Handles standard and custom components (``X-*``, IANA-registered). 

525 

526 Parameters: 

527 st: iCalendar data as bytes or string, or a path to an iCalendar file as 

528 :class:`pathlib.Path` or string. 

529 multiple: If ``True``, returns list. If ``False``, returns single component. 

530 

531 Returns: 

532 Component or list of components 

533 

534 See Also: 

535 :doc:`/how-to/custom-components` for examples of parsing custom components 

536 """ 

537 if isinstance(st, Path): 

538 st = st.read_bytes() 

539 elif isinstance(st, str) and "\n" not in st and "\r" not in st: 

540 path = Path(st) 

541 try: 

542 is_file = path.is_file() 

543 except OSError: 

544 is_file = False 

545 if is_file: 

546 st = path.read_bytes() 

547 parser = cls._get_ical_parser(st) 

548 components = parser.parse() 

549 if multiple: 

550 return components 

551 if len(components) > 1: 

552 raise ValueError( 

553 cls._format_error( 

554 "Found multiple components where only one is allowed", st 

555 ) 

556 ) 

557 if len(components) < 1: 

558 raise ValueError( 

559 cls._format_error( 

560 "Found no components where exactly one is required", st 

561 ) 

562 ) 

563 return components[0] 

564 

565 @staticmethod 

566 def _format_error(error_description, bad_input, elipsis="[...]"): 

567 # there's three character more in the error, ie. ' ' x2 and a ':' 

568 max_error_length = 100 - 3 

569 if len(error_description) + len(bad_input) + len(elipsis) > max_error_length: 

570 truncate_to = max_error_length - len(error_description) - len(elipsis) 

571 return f"{error_description}: {bad_input[:truncate_to]} {elipsis}" 

572 return f"{error_description}: {bad_input}" 

573 

574 def content_line(self, name, value, sorted: bool = True): 

575 """Returns property as content line.""" 

576 params = getattr(value, "params", Parameters()) 

577 return Contentline.from_parts(name, params, value, sorted=sorted) 

578 

579 def content_lines(self, sorted: bool = True): 

580 """Converts the Component and subcomponents into content lines.""" 

581 contentlines = Contentlines() 

582 for name, value in self.property_items(sorted=sorted): 

583 cl = self.content_line(name, value, sorted=sorted) 

584 contentlines.append(cl) 

585 contentlines.append("") # remember the empty string in the end 

586 return contentlines 

587 

588 def to_ical(self, sorted: bool = True): 

589 """ 

590 :param sorted: Whether parameters and properties should be 

591 lexicographically sorted. 

592 """ 

593 

594 content_lines = self.content_lines(sorted=sorted) 

595 return content_lines.to_ical() 

596 

597 def __repr__(self): 

598 """String representation of class with all of its subcomponents. 

599 

600 Implemented iteratively rather than recursively so that calendars 

601 with deeply nested subcomponents do not raise ``RecursionError``. 

602 A pathological ``.ics`` payload of only ~13 KB can otherwise nest 

603 ``BEGIN:VEVENT`` ~500 levels and crash any caller that performs 

604 ``repr()``/``str()``/``f"{cal}"`` on the parsed calendar 

605 (e.g. logging, error reporting, debug pages). 

606 """ 

607 # Stack-based traversal. Each frame is one of: 

608 # ("open", component) -> emit "Name({props}" and schedule children 

609 # ("close",) -> emit ")" 

610 # ("comma",) -> emit ", " 

611 out: list[str] = [] 

612 stack: list[tuple] = [("open", self)] 

613 while stack: 

614 frame = stack.pop() 

615 kind = frame[0] 

616 if kind == "comma": 

617 out.append(", ") 

618 elif kind == "close": 

619 out.append(")") 

620 else: # "open" 

621 node = frame[1] 

622 if isinstance(node, Component): 

623 out.append(f"{node.name or type(node).__name__}({dict(node)}") 

624 subs = node.subcomponents 

625 if subs: 

626 # Defer ")" then push children in reverse so that 

627 # popping yields original order, with ", " separators 

628 # (the first popped comma serves as the separator 

629 # between the component's dict and its first child). 

630 stack.append(("close",)) 

631 for sub in reversed(subs): 

632 stack.append(("open", sub)) 

633 stack.append(("comma",)) 

634 else: 

635 out.append(")") 

636 else: 

637 # Should not normally occur (subcomponents are Components), 

638 # but be safe and fall back to non-recursive str(). 

639 out.append(str(node)) 

640 return "".join(out) 

641 

642 def __eq__(self, other): 

643 if len(self.subcomponents) != len(other.subcomponents): 

644 return False 

645 

646 properties_equal = super().__eq__(other) 

647 if not properties_equal: 

648 return False 

649 

650 # The subcomponents might not be in the same order, 

651 # neither there's a natural key we can sort the subcomponents by nor 

652 # are the subcomponent types hashable, so we cant put them in a set to 

653 # check for set equivalence. We have to iterate over the subcomponents 

654 # and look for each of them in the list. 

655 for subcomponent in self.subcomponents: 

656 if subcomponent not in other.subcomponents: 

657 return False 

658 

659 # We now know the other component's subcomponents are not a strict subset 

660 # of this component's. However, we still need to check the other way around. 

661 for subcomponent in other.subcomponents: 

662 if subcomponent not in self.subcomponents: 

663 return False 

664 

665 return True 

666 

667 DTSTAMP = stamp = single_utc_property( 

668 "DTSTAMP", 

669 """RFC 5545: 

670 

671 Conformance: This property MUST be included in the "VEVENT", 

672 "VTODO", "VJOURNAL", or "VFREEBUSY" calendar components. 

673 

674 Description: In the case of an iCalendar object that specifies a 

675 "METHOD" property, this property specifies the date and time that 

676 the instance of the iCalendar object was created. In the case of 

677 an iCalendar object that doesn't specify a "METHOD" property, this 

678 property specifies the date and time that the information 

679 associated with the calendar component was last revised in the 

680 calendar store. 

681 

682 The value MUST be specified in the UTC time format. 

683 

684 In the case of an iCalendar object that doesn't specify a "METHOD" 

685 property, this property is equivalent to the "LAST-MODIFIED" 

686 property. 

687 """, 

688 ) 

689 LAST_MODIFIED = single_utc_property( 

690 "LAST-MODIFIED", 

691 """RFC 5545: 

692 

693 Purpose: This property specifies the date and time that the 

694 information associated with the calendar component was last 

695 revised in the calendar store. 

696 

697 Note: This is analogous to the modification date and time for a 

698 file in the file system. 

699 

700 Conformance: This property can be specified in the "VEVENT", 

701 "VTODO", "VJOURNAL", or "VTIMEZONE" calendar components. 

702 """, 

703 ) 

704 

705 @property 

706 def last_modified(self) -> datetime: 

707 """Datetime when the information associated with the component was last revised. 

708 

709 Since :attr:`LAST_MODIFIED` is an optional property, 

710 this returns :attr:`DTSTAMP` if :attr:`LAST_MODIFIED` is not set. 

711 """ 

712 return self.LAST_MODIFIED or self.DTSTAMP 

713 

714 @last_modified.setter 

715 def last_modified(self, value): 

716 self.LAST_MODIFIED = value 

717 

718 @last_modified.deleter 

719 def last_modified(self): 

720 del self.LAST_MODIFIED 

721 

722 @property 

723 def created(self) -> datetime: 

724 """Datetime when the information associated with the component was created. 

725 

726 Since :attr:`CREATED` is an optional property, 

727 this returns :attr:`DTSTAMP` if :attr:`CREATED` is not set. 

728 """ 

729 return self.CREATED or self.DTSTAMP 

730 

731 @created.setter 

732 def created(self, value): 

733 self.CREATED = value 

734 

735 @created.deleter 

736 def created(self): 

737 del self.CREATED 

738 

739 def is_thunderbird(self) -> bool: 

740 """Whether this component has attributes that indicate that Mozilla Thunderbird created it.""" 

741 return any(attr.startswith("X-MOZ-") for attr in self.keys()) 

742 

743 @staticmethod 

744 def _utc_now(): 

745 """Return now as UTC value.""" 

746 return datetime.now(timezone.utc) 

747 

748 uid = uid_property 

749 comments = comments_property 

750 links = links_property 

751 related_to = related_to_property 

752 concepts = concepts_property 

753 refids = refids_property 

754 

755 CREATED = single_utc_property( 

756 "CREATED", 

757 """ 

758 CREATED specifies the date and time that the calendar 

759 information was created by the calendar user agent in the calendar 

760 store. 

761 

762 Conformance: 

763 The property can be specified once in "VEVENT", 

764 "VTODO", or "VJOURNAL" calendar components. The value MUST be 

765 specified as a date with UTC time. 

766 

767 """, 

768 ) 

769 

770 _validate_new = True 

771 

772 @staticmethod 

773 def _validate_start_and_end(start, end): 

774 """This validates start and end. 

775 

776 Raises: 

777 ~error.InvalidCalendar: If the information is not valid 

778 """ 

779 if start is None or end is None: 

780 return 

781 if start > end: 

782 raise InvalidCalendar("end must be after start") 

783 

784 @classmethod 

785 def new( 

786 cls, 

787 created: date | None = None, 

788 comments: list[str] | str | None = None, 

789 concepts: CONCEPTS_TYPE_SETTER = None, 

790 last_modified: date | None = None, 

791 links: LINKS_TYPE_SETTER = None, 

792 refids: list[str] | str | None = None, 

793 related_to: RELATED_TO_TYPE_SETTER = None, 

794 stamp: date | None = None, 

795 subcomponents: Iterable[Component] | None = None, 

796 ) -> Component: 

797 """Create a new component. 

798 

799 Parameters: 

800 comments: The :attr:`comments` of the component. 

801 concepts: The :attr:`concepts` of the component. 

802 created: The :attr:`created` of the component. 

803 last_modified: The :attr:`last_modified` of the component. 

804 links: The :attr:`links` of the component. 

805 related_to: The :attr:`related_to` of the component. 

806 stamp: The :attr:`DTSTAMP` of the component. 

807 subcomponents: The subcomponents of the component. 

808 

809 Raises: 

810 ~error.InvalidCalendar: If the content is not valid 

811 according to :rfc:`5545`. 

812 

813 .. warning:: As time progresses, we will be stricter with the 

814 validation. 

815 """ 

816 component = cls() 

817 component.DTSTAMP = stamp 

818 component.created = created 

819 component.last_modified = last_modified 

820 component.comments = comments 

821 component.links = links 

822 component.related_to = related_to 

823 component.concepts = concepts 

824 component.refids = refids 

825 if subcomponents is not None: 

826 component.subcomponents = ( 

827 subcomponents 

828 if isinstance(subcomponents, list) 

829 else list(subcomponents) 

830 ) 

831 return component 

832 

833 def to_jcal(self) -> list: 

834 """Convert this component to a jCal object. 

835 

836 Returns: 

837 jCal object 

838 

839 See also :attr:`to_json`. 

840 

841 In this example, we create a simple VEVENT component and convert it to jCal: 

842 

843 .. code-block:: pycon 

844 

845 >>> from icalendar import Event 

846 >>> from datetime import date 

847 >>> from pprint import pprint 

848 >>> event = Event.new(summary="My Event", start=date(2025, 11, 22)) 

849 >>> pprint(event.to_jcal()) 

850 ['vevent', 

851 [['dtstamp', {}, 'date-time', '2025-05-17T08:06:12Z'], 

852 ['summary', {}, 'text', 'My Event'], 

853 ['uid', {}, 'text', 'd755cef5-2311-46ed-a0e1-6733c9e15c63'], 

854 ['dtstart', {}, 'date', '2025-11-22']], 

855 []] 

856 """ 

857 properties = [] 

858 for key, value in self.items(): 

859 for item in value if isinstance(value, list) else [value]: 

860 properties.append(item.to_jcal(key.lower())) 

861 return [ 

862 self.name.lower(), 

863 properties, 

864 [subcomponent.to_jcal() for subcomponent in self.subcomponents], 

865 ] 

866 

867 def to_json(self) -> str: 

868 """Return this component as a jCal JSON string. 

869 

870 Returns: 

871 JSON string 

872 

873 See also :attr:`to_jcal`. 

874 """ 

875 return json.dumps(self.to_jcal()) 

876 

877 @classmethod 

878 def from_jcal(cls, jcal: str | list) -> Component: 

879 """Create a component from a jCal list. 

880 

881 Parameters: 

882 jcal: jCal list or JSON string according to :rfc:`7265`. 

883 

884 Raises: 

885 ~error.JCalParsingError: If the jCal provided is invalid. 

886 ~json.JSONDecodeError: If the provided string is not valid JSON. 

887 

888 This reverses :func:`to_json` and :func:`to_jcal`. 

889 

890 The following code parses an example from :rfc:`7265`: 

891 

892 .. code-block:: pycon 

893 

894 >>> from icalendar import Component 

895 >>> jcal = ["vcalendar", 

896 ... [ 

897 ... ["calscale", {}, "text", "GREGORIAN"], 

898 ... ["prodid", {}, "text", "-//Example Inc.//Example Calendar//EN"], 

899 ... ["version", {}, "text", "2.0"] 

900 ... ], 

901 ... [ 

902 ... ["vevent", 

903 ... [ 

904 ... ["dtstamp", {}, "date-time", "2008-02-05T19:12:24Z"], 

905 ... ["dtstart", {}, "date", "2008-10-06"], 

906 ... ["summary", {}, "text", "Planning meeting"], 

907 ... ["uid", {}, "text", "4088E990AD89CB3DBB484909"] 

908 ... ], 

909 ... [] 

910 ... ] 

911 ... ] 

912 ... ] 

913 >>> calendar = Component.from_jcal(jcal) 

914 >>> print(calendar.name) 

915 VCALENDAR 

916 >>> print(calendar.prodid) 

917 -//Example Inc.//Example Calendar//EN 

918 >>> event = calendar.events[0] 

919 >>> print(event.summary) 

920 Planning meeting 

921 

922 """ 

923 if isinstance(jcal, str): 

924 jcal = json.loads(jcal) 

925 if not isinstance(jcal, list) or len(jcal) != 3: 

926 raise JCalParsingError( 

927 "A component must be a list with 3 items.", cls, value=jcal 

928 ) 

929 name, properties, subcomponents = jcal 

930 if not isinstance(name, str): 

931 raise JCalParsingError( 

932 "The name must be a string.", cls, path=[0], value=name 

933 ) 

934 if name.upper() != cls.name: 

935 # delegate to correct component class 

936 component_cls = cls.get_component_class(name.upper()) 

937 return component_cls.from_jcal(jcal) 

938 component = cls() 

939 if not isinstance(properties, list): 

940 raise JCalParsingError( 

941 "The properties must be a list.", cls, path=1, value=properties 

942 ) 

943 for i, prop in enumerate(properties): 

944 JCalParsingError.validate_property(prop, cls, path=[1, i]) 

945 prop_name = prop[0] 

946 prop_value = prop[2] 

947 prop_cls: type[VPROPERTY] = cls.types_factory.for_property( 

948 prop_name, prop_value 

949 ) 

950 with JCalParsingError.reraise_with_path_added(1, i): 

951 v_prop = prop_cls.from_jcal(prop) 

952 # if we use the default value for that property, we can delete the 

953 # VALUE parameter 

954 if prop_cls == cls.types_factory.for_property(prop_name): 

955 del v_prop.VALUE 

956 component.add(prop_name, v_prop) 

957 if not isinstance(subcomponents, list): 

958 raise JCalParsingError( 

959 "The subcomponents must be a list.", cls, 2, value=subcomponents 

960 ) 

961 for i, subcomponent in enumerate(subcomponents): 

962 with JCalParsingError.reraise_with_path_added(2, i): 

963 component.subcomponents.append(cls.from_jcal(subcomponent)) 

964 return component 

965 

966 def copy(self, recursive: bool = False) -> Self: 

967 """Copy the component. 

968 

969 Parameters: 

970 recursive: 

971 If ``True``, this creates copies of the component, its subcomponents, 

972 and all its properties. 

973 If ``False``, this only creates a shallow copy of the component. 

974 

975 Returns: 

976 A copy of the component. 

977 

978 Examples: 

979 

980 Create a shallow copy of a component: 

981 

982 .. code-block:: pycon 

983 

984 >>> from icalendar import Event 

985 >>> event = Event.new(description="Event to be copied") 

986 >>> event_copy = event.copy() 

987 >>> str(event_copy.description) 

988 'Event to be copied' 

989 

990 Shallow copies lose their subcomponents: 

991 

992 .. code-block:: pycon 

993 

994 >>> from icalendar import Calendar 

995 >>> calendar = Calendar.example() 

996 >>> len(calendar.subcomponents) 

997 3 

998 >>> calendar_copy = calendar.copy() 

999 >>> len(calendar_copy.subcomponents) 

1000 0 

1001 

1002 A recursive copy also copies all the subcomponents: 

1003 

1004 .. code-block:: pycon 

1005 

1006 >>> full_calendar_copy = calendar.copy(recursive=True) 

1007 >>> len(full_calendar_copy.subcomponents) 

1008 3 

1009 >>> full_calendar_copy.events[0] == calendar.events[0] 

1010 True 

1011 >>> full_calendar_copy.events[0] is calendar.events[0] 

1012 False 

1013 

1014 """ 

1015 if recursive: 

1016 return deepcopy(self) 

1017 return super().copy() 

1018 

1019 def is_lazy(self) -> bool: 

1020 """This component is fully parsed.""" 

1021 return False 

1022 

1023 def parse(self) -> Self: 

1024 """Return the fully parsed component. 

1025 

1026 For non-lazy components, this returns self. 

1027 For lazy components, this parses the component and returns the result. 

1028 """ 

1029 return self 

1030 

1031 

1032__all__ = ["Component"]