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

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

402 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 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 subcomponents: list[Component] 

95 """All subcomponents of this component.""" 

96 

97 @classmethod 

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

99 """Return a component with this name. 

100 

101 Parameters: 

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

103 """ 

104 if cls._components_factory is None: 

105 cls._components_factory = ComponentFactory() 

106 return cls._components_factory.get_component_class(name) 

107 

108 @classmethod 

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

110 """Register a custom component class. 

111 

112 Parameters: 

113 component_class: Component subclass to register. 

114 Must have a ``name`` attribute. 

115 

116 Raises: 

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

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

119 

120 Examples: 

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

122 

123 .. code-block:: pycon 

124 

125 >>> from icalendar import Component 

126 >>> class XExample(Component): 

127 ... name = "X-EXAMPLE" 

128 ... def custom_method(self): 

129 ... return "custom" 

130 >>> Component.register(XExample) 

131 """ 

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

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

134 

135 if cls._components_factory is None: 

136 cls._components_factory = ComponentFactory() 

137 

138 # Check if already registered 

139 existing = cls._components_factory.get(component_class.name) 

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

141 raise ValueError( 

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

143 f" as {existing}" 

144 ) 

145 

146 cls._components_factory.add_component_class(component_class) 

147 

148 @staticmethod 

149 def _infer_value_type( 

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

151 ) -> str | None: 

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

153 

154 Parameters: 

155 value: Python native type, one of :py:class:`date`, :py:mod:`datetime`, 

156 :py:class:`timedelta`, :py:mod:`time`, :py:class:`tuple`, 

157 or :py:class:`list`. 

158 

159 Returns: 

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

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

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

163 """ 

164 if isinstance(value, list): 

165 if not value: 

166 return None 

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

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

169 return "DATE" 

170 # Check if ALL items are time 

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

172 return "TIME" 

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

174 return None 

175 if is_date(value): 

176 return "DATE" 

177 if isinstance(value, time): 

178 return "TIME" 

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

180 return None 

181 

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

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

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

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

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

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

188 # parsing a property, contains error strings 

189 

190 def __bool__(self): 

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

192 return True 

193 

194 def __getitem__(self, key): 

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

196 return super().__getitem__(key) 

197 

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

199 """Get property value with default.""" 

200 try: 

201 return self[key] 

202 except KeyError: 

203 return default 

204 

205 def is_empty(self): 

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

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

208 

209 ############################# 

210 # handling of property values 

211 

212 @classmethod 

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

214 """Encode values to icalendar property values. 

215 

216 :param name: Name of the property. 

217 :type name: string 

218 

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

220 any of the icalendar's own property types. 

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

222 

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

224 available, if encode is set to True. 

225 :type parameters: Dictionary 

226 

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

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

229 or False, if not. 

230 :type encode: Boolean 

231 

232 :returns: icalendar property value 

233 """ 

234 if not encode: 

235 return value 

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

237 # Don't encode already encoded values. 

238 obj = value 

239 else: 

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

241 value_param = None 

242 if parameters and "VALUE" in parameters: 

243 value_param = parameters["VALUE"] 

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

245 inferred = cls._infer_value_type(value) 

246 if inferred: 

247 value_param = inferred 

248 # Auto-set the VALUE parameter 

249 if parameters is None: 

250 parameters = {} 

251 if "VALUE" not in parameters: 

252 parameters["VALUE"] = inferred 

253 

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

255 obj = klass(value) 

256 if parameters: 

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

258 obj.params = Parameters() 

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

260 if item is None: 

261 if key in obj.params: 

262 del obj.params[key] 

263 else: 

264 obj.params[key] = item 

265 return obj 

266 

267 def add( 

268 self, 

269 name: str, 

270 value, 

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

272 encode: bool = True, 

273 ): 

274 """Add a property. 

275 

276 :param name: Name of the property. 

277 :type name: string 

278 

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

280 any of the icalendar's own property types. 

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

282 

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

284 available, if encode is set to True. 

285 :type parameters: Dictionary 

286 

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

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

289 or False, if not. 

290 :type encode: Boolean 

291 

292 :returns: None 

293 """ 

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

295 "dtstamp", 

296 "created", 

297 "last-modified", 

298 ): 

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

300 value = tzp.localize_utc(value) 

301 

302 # encode value 

303 if ( 

304 encode 

305 and isinstance(value, list) 

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

307 ): 

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

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

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

311 else: 

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

313 

314 # set value 

315 if name in self: 

316 # If property already exists, append it. 

317 oldval = self[name] 

318 if isinstance(oldval, list): 

319 if isinstance(value, list): 

320 value = oldval + value 

321 else: 

322 oldval.append(value) 

323 value = oldval 

324 else: 

325 value = [oldval, value] 

326 self[name] = value 

327 

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

329 """Internal for decoding property values.""" 

330 

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

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

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

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

335 if hasattr(value, "ical_value"): 

336 return value.ical_value 

337 if isinstance(value, vDDDLists): 

338 # TODO: Workaround unfinished decoding 

339 return value 

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

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

342 # Workaround to decode vText properly 

343 if isinstance(decoded, vText): 

344 decoded = decoded.encode(DEFAULT_ENCODING) 

345 return decoded 

346 

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

348 """Returns decoded value of property. 

349 

350 A component maps keys to icalendar property value types. 

351 This function returns values compatible to native Python types. 

352 """ 

353 if name in self: 

354 value = self[name] 

355 if isinstance(value, list): 

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

357 return self._decode(name, value) 

358 if default is _marker: 

359 raise KeyError(name) 

360 return default 

361 

362 ######################################################################## 

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

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

365 

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

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

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

369 if decode: 

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

371 return vals 

372 

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

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

375 to that. 

376 """ 

377 if encode: 

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

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

380 

381 ######################### 

382 # Handling of components 

383 

384 def add_component(self, component: Component): 

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

386 self.subcomponents.append(component) 

387 

388 def _walk(self, name, select): 

389 """Walk to given component.""" 

390 result = [] 

391 if (name is None or self.name == name) and select(self): 

392 result.append(self) 

393 for subcomponent in self.subcomponents: 

394 result += subcomponent._walk(name, select) 

395 return result 

396 

397 def walk(self, name=None, select=lambda _: True) -> list[Component]: 

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

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

400 

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

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

403 and returns True/False. 

404 :returns: A list of components that match. 

405 :rtype: list[Component] 

406 """ 

407 if name is not None: 

408 name = name.upper() 

409 return self._walk(name, select) 

410 

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

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

413 

414 Parameters: 

415 uid: The UID of the component. 

416 

417 Returns: 

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

419 """ 

420 return [c for c in self.walk() if c.get("uid") == uid] 

421 

422 ##################### 

423 # Generation 

424 

425 def property_items( 

426 self, 

427 recursive=True, 

428 sorted: bool = True, 

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

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

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

432 """ 

433 v_text = self.types_factory["text"] 

434 properties = [("BEGIN", v_text(self.name).to_ical())] 

435 property_names = self.sorted_keys() if sorted else self.keys() 

436 

437 for name in property_names: 

438 values = self[name] 

439 if isinstance(values, list): 

440 # normally one property is one line 

441 for value in values: 

442 properties.append((name, value)) 

443 else: 

444 properties.append((name, values)) 

445 if recursive: 

446 # recursion is fun! 

447 for subcomponent in self.subcomponents: 

448 properties += subcomponent.property_items(sorted=sorted) 

449 properties.append(("END", v_text(self.name).to_ical())) 

450 return properties 

451 

452 @overload 

453 @classmethod 

454 def from_ical( 

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

456 ) -> Component: ... 

457 

458 @overload 

459 @classmethod 

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

461 

462 @classmethod 

463 def from_ical( 

464 cls, st: str | bytes, multiple: bool = False 

465 ) -> Component | list[Component]: 

466 """Parse iCalendar data into component instances. 

467 

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

469 

470 Parameters: 

471 st: iCalendar data as bytes or string 

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

473 

474 Returns: 

475 Component or list of components 

476 

477 See Also: 

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

479 """ 

480 from icalendar.prop import vBroken 

481 

482 stack = [] # a stack of components 

483 comps = [] 

484 for line in Contentlines.from_ical(st): # raw parsing 

485 if not line: 

486 continue 

487 

488 try: 

489 name, params, vals = line.parts() 

490 except ValueError as e: 

491 # if unable to parse a line within a component 

492 # that ignores exceptions, mark the component 

493 # as broken and skip the line. otherwise raise. 

494 component = stack[-1] if stack else None 

495 if not component or not component.ignore_exceptions: 

496 raise 

497 component.errors.append((None, str(e))) 

498 continue 

499 

500 uname = name.upper() 

501 # check for start of component 

502 if uname == "BEGIN": 

503 # try and create one of the components defined in the spec, 

504 # otherwise get a general Components for robustness. 

505 c_name = vals.upper() 

506 c_class = cls.get_component_class(c_name) 

507 # If component factory cannot resolve ``c_name``, the generic 

508 # ``Component`` class is used which does not have the name set. 

509 # That's opposed to the usage of ``cls``, which represents a 

510 # more concrete subclass with a name set (e.g. VCALENDAR). 

511 component = c_class() 

512 if not getattr(component, "name", ""): # undefined components 

513 component.name = c_name 

514 stack.append(component) 

515 # check for end of event 

516 elif uname == "END": 

517 # we are done adding properties to this component 

518 # so pop it from the stack and add it to the new top. 

519 if not stack: 

520 # The stack is currently empty, the input must be invalid 

521 raise ValueError("END encountered without an accompanying BEGIN!") 

522 

523 component = stack.pop() 

524 if not stack: # we are at the end 

525 comps.append(component) 

526 else: 

527 stack[-1].add_component(component) 

528 if vals == "VTIMEZONE" and "TZID" in component: 

529 tzp.cache_timezone_component(component) 

530 # we are adding properties to the current top of the stack 

531 else: 

532 # Extract VALUE parameter if present 

533 value_param = params.get("VALUE") if params else None 

534 factory = cls.types_factory.for_property(name, value_param) 

535 component = stack[-1] if stack else None 

536 if not component: 

537 # only accept X-COMMENT at the end of the .ics file 

538 # ignore these components in parsing 

539 if uname == "X-COMMENT": 

540 break 

541 raise ValueError( 

542 f'Property "{name}" does not have a parent component.' 

543 ) 

544 datetime_names = ( 

545 "DTSTART", 

546 "DTEND", 

547 "RECURRENCE-ID", 

548 "DUE", 

549 "RDATE", 

550 "EXDATE", 

551 ) 

552 

553 # Determine TZID for datetime properties 

554 tzid = params.get("TZID") if params and name in datetime_names else None 

555 

556 # Handle special cases for value list preparation 

557 if name.upper() == "CATEGORIES": 

558 # Special handling for CATEGORIES - need raw value 

559 # before unescaping to properly split on unescaped commas 

560 from icalendar.parser import ( 

561 split_on_unescaped_comma, 

562 ) 

563 

564 line_str = str(line) 

565 # Use rfind to get the last colon (value separator) 

566 # to handle parameters with colons like ALTREP="http://..." 

567 colon_idx = line_str.rfind(":") 

568 if colon_idx > 0: 

569 raw_value = line_str[colon_idx + 1 :] 

570 # Parse categories immediately (not lazily) for both 

571 # strict and tolerant components. 

572 # CATEGORIES needs special comma handling 

573 try: 

574 category_list = split_on_unescaped_comma(raw_value) 

575 vals_inst = factory(category_list) 

576 vals_inst.params = params 

577 component.add(name, vals_inst, encode=0) 

578 except ValueError as e: 

579 if not component.ignore_exceptions: 

580 raise 

581 component.errors.append((uname, str(e))) 

582 continue 

583 # Fallback to normal processing if we can't find colon 

584 vals_list = [vals] 

585 elif name == "FREEBUSY": 

586 # Handle FREEBUSY comma-separated values 

587 vals_list = vals.split(",") 

588 # Workaround broken ICS files with empty RDATE 

589 # (not EXDATE - let it parse and fail) 

590 elif name == "RDATE" and vals == "": 

591 vals_list = [] 

592 else: 

593 vals_list = [vals] 

594 

595 # Parse all properties eagerly 

596 for val in vals_list: 

597 try: 

598 if tzid: 

599 parsed_val = factory.from_ical(val, tzid) 

600 else: 

601 parsed_val = factory.from_ical(val) 

602 vals_inst = factory(parsed_val) 

603 vals_inst.params = params 

604 component.add(name, vals_inst, encode=0) 

605 except Exception as e: 

606 if not component.ignore_exceptions: 

607 raise 

608 # Error-tolerant mode: create vBroken 

609 expected_type = getattr(factory, "__name__", "unknown") 

610 broken_prop = vBroken.from_parse_error( 

611 raw_value=val, 

612 params=params, 

613 property_name=name, 

614 expected_type=expected_type, 

615 error=e, 

616 ) 

617 component.errors.append((name, str(e))) 

618 component.add(name, broken_prop, encode=0) 

619 

620 if multiple: 

621 return comps 

622 if len(comps) > 1: 

623 raise ValueError( 

624 cls._format_error( 

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

626 ) 

627 ) 

628 if len(comps) < 1: 

629 raise ValueError( 

630 cls._format_error( 

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

632 ) 

633 ) 

634 return comps[0] 

635 

636 @staticmethod 

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

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

639 max_error_length = 100 - 3 

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

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

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

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

644 

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

646 """Returns property as content line.""" 

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

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

649 

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

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

652 contentlines = Contentlines() 

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

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

655 contentlines.append(cl) 

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

657 return contentlines 

658 

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

660 """ 

661 :param sorted: Whether parameters and properties should be 

662 lexicographically sorted. 

663 """ 

664 

665 content_lines = self.content_lines(sorted=sorted) 

666 return content_lines.to_ical() 

667 

668 def __repr__(self): 

669 """String representation of class with all of it's subcomponents.""" 

670 subs = ", ".join(str(it) for it in self.subcomponents) 

671 return ( 

672 f"{self.name or type(self).__name__}" 

673 f"({dict(self)}{', ' + subs if subs else ''})" 

674 ) 

675 

676 def __eq__(self, other): 

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

678 return False 

679 

680 properties_equal = super().__eq__(other) 

681 if not properties_equal: 

682 return False 

683 

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

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

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

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

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

689 for subcomponent in self.subcomponents: 

690 if subcomponent not in other.subcomponents: 

691 return False 

692 

693 return True 

694 

695 DTSTAMP = stamp = single_utc_property( 

696 "DTSTAMP", 

697 """RFC 5545: 

698 

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

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

701 

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

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

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

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

706 property specifies the date and time that the information 

707 associated with the calendar component was last revised in the 

708 calendar store. 

709 

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

711 

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

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

714 property. 

715 """, 

716 ) 

717 LAST_MODIFIED = single_utc_property( 

718 "LAST-MODIFIED", 

719 """RFC 5545: 

720 

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

722 information associated with the calendar component was last 

723 revised in the calendar store. 

724 

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

726 file in the file system. 

727 

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

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

730 """, 

731 ) 

732 

733 @property 

734 def last_modified(self) -> datetime: 

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

736 

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

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

739 """ 

740 return self.LAST_MODIFIED or self.DTSTAMP 

741 

742 @last_modified.setter 

743 def last_modified(self, value): 

744 self.LAST_MODIFIED = value 

745 

746 @last_modified.deleter 

747 def last_modified(self): 

748 del self.LAST_MODIFIED 

749 

750 @property 

751 def created(self) -> datetime: 

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

753 

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

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

756 """ 

757 return self.CREATED or self.DTSTAMP 

758 

759 @created.setter 

760 def created(self, value): 

761 self.CREATED = value 

762 

763 @created.deleter 

764 def created(self): 

765 del self.CREATED 

766 

767 def is_thunderbird(self) -> bool: 

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

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

770 

771 @staticmethod 

772 def _utc_now(): 

773 """Return now as UTC value.""" 

774 return datetime.now(timezone.utc) 

775 

776 uid = uid_property 

777 comments = comments_property 

778 links = links_property 

779 related_to = related_to_property 

780 concepts = concepts_property 

781 refids = refids_property 

782 

783 CREATED = single_utc_property( 

784 "CREATED", 

785 """ 

786 CREATED specifies the date and time that the calendar 

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

788 store. 

789 

790 Conformance: 

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

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

793 specified as a date with UTC time. 

794 

795 """, 

796 ) 

797 

798 _validate_new = True 

799 

800 @staticmethod 

801 def _validate_start_and_end(start, end): 

802 """This validates start and end. 

803 

804 Raises: 

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

806 """ 

807 if start is None or end is None: 

808 return 

809 if start > end: 

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

811 

812 @classmethod 

813 def new( 

814 cls, 

815 created: date | None = None, 

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

817 concepts: CONCEPTS_TYPE_SETTER = None, 

818 last_modified: date | None = None, 

819 links: LINKS_TYPE_SETTER = None, 

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

821 related_to: RELATED_TO_TYPE_SETTER = None, 

822 stamp: date | None = None, 

823 ) -> Component: 

824 """Create a new component. 

825 

826 Parameters: 

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

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

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

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

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

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

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

834 

835 Raises: 

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

837 according to :rfc:`5545`. 

838 

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

840 validation. 

841 """ 

842 component = cls() 

843 component.DTSTAMP = stamp 

844 component.created = created 

845 component.last_modified = last_modified 

846 component.comments = comments 

847 component.links = links 

848 component.related_to = related_to 

849 component.concepts = concepts 

850 component.refids = refids 

851 return component 

852 

853 def to_jcal(self) -> list: 

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

855 

856 Returns: 

857 jCal object 

858 

859 See also :attr:`to_json`. 

860 

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

862 

863 .. code-block:: pycon 

864 

865 >>> from icalendar import Event 

866 >>> from datetime import date 

867 >>> from pprint import pprint 

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

869 >>> pprint(event.to_jcal()) 

870 ['vevent', 

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

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

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

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

875 []] 

876 """ 

877 properties = [] 

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

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

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

881 return [ 

882 self.name.lower(), 

883 properties, 

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

885 ] 

886 

887 def to_json(self) -> str: 

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

889 

890 Returns: 

891 JSON string 

892 

893 See also :attr:`to_jcal`. 

894 """ 

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

896 

897 @classmethod 

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

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

900 

901 Parameters: 

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

903 

904 Raises: 

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

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

907 

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

909 

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

911 

912 .. code-block:: pycon 

913 

914 >>> from icalendar import Component 

915 >>> jcal = ["vcalendar", 

916 ... [ 

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

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

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

920 ... ], 

921 ... [ 

922 ... ["vevent", 

923 ... [ 

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

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

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

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

928 ... ], 

929 ... [] 

930 ... ] 

931 ... ] 

932 ... ] 

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

934 >>> print(calendar.name) 

935 VCALENDAR 

936 >>> print(calendar.prodid) 

937 -//Example Inc.//Example Calendar//EN 

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

939 >>> print(event.summary) 

940 Planning meeting 

941 

942 """ 

943 if isinstance(jcal, str): 

944 jcal = json.loads(jcal) 

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

946 raise JCalParsingError( 

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

948 ) 

949 name, properties, subcomponents = jcal 

950 if not isinstance(name, str): 

951 raise JCalParsingError( 

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

953 ) 

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

955 # delegate to correct component class 

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

957 return component_cls.from_jcal(jcal) 

958 component = cls() 

959 if not isinstance(properties, list): 

960 raise JCalParsingError( 

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

962 ) 

963 for i, prop in enumerate(properties): 

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

965 prop_name = prop[0] 

966 prop_value = prop[2] 

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

968 prop_name, prop_value 

969 ) 

970 with JCalParsingError.reraise_with_path_added(1, i): 

971 v_prop = prop_cls.from_jcal(prop) 

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

973 # VALUE parameter 

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

975 del v_prop.VALUE 

976 component.add(prop_name, v_prop) 

977 if not isinstance(subcomponents, list): 

978 raise JCalParsingError( 

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

980 ) 

981 for i, subcomponent in enumerate(subcomponents): 

982 with JCalParsingError.reraise_with_path_added(2, i): 

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

984 return component 

985 

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

987 """Copy the component. 

988 

989 Parameters: 

990 recursive: 

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

992 and all its properties. 

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

994 

995 Returns: 

996 A copy of the component. 

997 

998 Examples: 

999 

1000 Create a shallow copy of a component: 

1001 

1002 .. code-block:: pycon 

1003 

1004 >>> from icalendar import Event 

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

1006 >>> event_copy = event.copy() 

1007 >>> str(event_copy.description) 

1008 'Event to be copied' 

1009 

1010 Shallow copies lose their subcomponents: 

1011 

1012 .. code-block:: pycon 

1013 

1014 >>> from icalendar import Calendar 

1015 >>> calendar = Calendar.example() 

1016 >>> len(calendar.subcomponents) 

1017 3 

1018 >>> calendar_copy = calendar.copy() 

1019 >>> len(calendar_copy.subcomponents) 

1020 0 

1021 

1022 A recursive copy also copies all the subcomponents: 

1023 

1024 .. code-block:: pycon 

1025 

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

1027 >>> len(full_calendar_copy.subcomponents) 

1028 3 

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

1030 True 

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

1032 False 

1033 

1034 """ 

1035 if recursive: 

1036 return deepcopy(self) 

1037 return super().copy() 

1038 

1039 

1040__all__ = ["Component"]