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

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

400 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 @classmethod 

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

96 """Return a component with this name. 

97 

98 Parameters: 

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

100 """ 

101 if cls._components_factory is None: 

102 cls._components_factory = ComponentFactory() 

103 return cls._components_factory.get_component_class(name) 

104 

105 @classmethod 

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

107 """Register a custom component class. 

108 

109 Parameters: 

110 component_class: Component subclass to register. Must have a ``name`` attribute. 

111 

112 Raises: 

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

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

115 

116 Examples: 

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

118 

119 .. code-block:: pycon 

120 

121 >>> from icalendar import Component 

122 >>> class XExample(Component): 

123 ... name = "X-EXAMPLE" 

124 ... def custom_method(self): 

125 ... return "custom" 

126 >>> Component.register(XExample) 

127 """ 

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

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

130 

131 if cls._components_factory is None: 

132 cls._components_factory = ComponentFactory() 

133 

134 # Check if already registered 

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

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

137 raise ValueError( 

138 f"Component '{component_class.name}' is already registered as {existing}" 

139 ) 

140 

141 cls._components_factory.add_component_class(component_class) 

142 

143 @staticmethod 

144 def _infer_value_type( 

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

146 ) -> str | None: 

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

148 

149 Parameters: 

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

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

152 or :py:class:`list`. 

153 

154 Returns: 

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

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

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

158 """ 

159 if isinstance(value, list): 

160 if not value: 

161 return None 

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

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

164 return "DATE" 

165 # Check if ALL items are time 

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

167 return "TIME" 

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

169 return None 

170 if is_date(value): 

171 return "DATE" 

172 if isinstance(value, time): 

173 return "TIME" 

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

175 return None 

176 

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

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

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

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

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

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

183 # parsing a property, contains error strings 

184 

185 def __bool__(self): 

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

187 return True 

188 

189 def __getitem__(self, key): 

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

191 return super().__getitem__(key) 

192 

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

194 """Get property value with default.""" 

195 try: 

196 return self[key] 

197 except KeyError: 

198 return default 

199 

200 def is_empty(self): 

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

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

203 

204 ############################# 

205 # handling of property values 

206 

207 @classmethod 

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

209 """Encode values to icalendar property values. 

210 

211 :param name: Name of the property. 

212 :type name: string 

213 

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

215 any of the icalendar's own property types. 

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

217 

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

219 available, if encode is set to True. 

220 :type parameters: Dictionary 

221 

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

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

224 or False, if not. 

225 :type encode: Boolean 

226 

227 :returns: icalendar property value 

228 """ 

229 if not encode: 

230 return value 

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

232 # Don't encode already encoded values. 

233 obj = value 

234 else: 

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

236 value_param = None 

237 if parameters and "VALUE" in parameters: 

238 value_param = parameters["VALUE"] 

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

240 inferred = cls._infer_value_type(value) 

241 if inferred: 

242 value_param = inferred 

243 # Auto-set the VALUE parameter 

244 if parameters is None: 

245 parameters = {} 

246 if "VALUE" not in parameters: 

247 parameters["VALUE"] = inferred 

248 

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

250 obj = klass(value) 

251 if parameters: 

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

253 obj.params = Parameters() 

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

255 if item is None: 

256 if key in obj.params: 

257 del obj.params[key] 

258 else: 

259 obj.params[key] = item 

260 return obj 

261 

262 def add( 

263 self, 

264 name: str, 

265 value, 

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

267 encode: bool = True, # noqa: FBT001 

268 ): 

269 """Add a property. 

270 

271 :param name: Name of the property. 

272 :type name: string 

273 

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

275 any of the icalendar's own property types. 

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

277 

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

279 available, if encode is set to True. 

280 :type parameters: Dictionary 

281 

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

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

284 or False, if not. 

285 :type encode: Boolean 

286 

287 :returns: None 

288 """ 

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

290 "dtstamp", 

291 "created", 

292 "last-modified", 

293 ): 

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

295 value = tzp.localize_utc(value) 

296 

297 # encode value 

298 if ( 

299 encode 

300 and isinstance(value, list) 

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

302 ): 

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

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

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

306 else: 

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

308 

309 # set value 

310 if name in self: 

311 # If property already exists, append it. 

312 oldval = self[name] 

313 if isinstance(oldval, list): 

314 if isinstance(value, list): 

315 value = oldval + value 

316 else: 

317 oldval.append(value) 

318 value = oldval 

319 else: 

320 value = [oldval, value] 

321 self[name] = value 

322 

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

324 """Internal for decoding property values.""" 

325 

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

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

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

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

330 if hasattr(value, "ical_value"): 

331 return value.ical_value 

332 if isinstance(value, vDDDLists): 

333 # TODO: Workaround unfinished decoding 

334 return value 

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

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

337 # Workaround to decode vText properly 

338 if isinstance(decoded, vText): 

339 decoded = decoded.encode(DEFAULT_ENCODING) 

340 return decoded 

341 

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

343 """Returns decoded value of property. 

344 

345 A component maps keys to icalendar property value types. 

346 This function returns values compatible to native Python types. 

347 """ 

348 if name in self: 

349 value = self[name] 

350 if isinstance(value, list): 

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

352 return self._decode(name, value) 

353 if default is _marker: 

354 raise KeyError(name) 

355 return default 

356 

357 ######################################################################## 

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

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

360 

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

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

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

364 if decode: 

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

366 return vals 

367 

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

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

370 to that. 

371 """ 

372 if encode: 

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

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

375 

376 ######################### 

377 # Handling of components 

378 

379 def add_component(self, component: Component): 

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

381 self.subcomponents.append(component) 

382 

383 def _walk(self, name, select): 

384 """Walk to given component.""" 

385 result = [] 

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

387 result.append(self) 

388 for subcomponent in self.subcomponents: 

389 result += subcomponent._walk(name, select) # noqa: SLF001 

390 return result 

391 

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

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

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

395 

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

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

398 and returns True/False. 

399 :returns: A list of components that match. 

400 :rtype: list[Component] 

401 """ 

402 if name is not None: 

403 name = name.upper() 

404 return self._walk(name, select) 

405 

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

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

408 

409 Parameters: 

410 uid: The UID of the component. 

411 

412 Returns: 

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

414 """ 

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

416 

417 ##################### 

418 # Generation 

419 

420 def property_items( 

421 self, 

422 recursive=True, 

423 sorted: bool = True, # noqa: A002, FBT001 

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

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

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

427 """ 

428 v_text = self.types_factory["text"] 

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

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

431 

432 for name in property_names: 

433 values = self[name] 

434 if isinstance(values, list): 

435 # normally one property is one line 

436 for value in values: 

437 properties.append((name, value)) 

438 else: 

439 properties.append((name, values)) 

440 if recursive: 

441 # recursion is fun! 

442 for subcomponent in self.subcomponents: 

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

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

445 return properties 

446 

447 @overload 

448 @classmethod 

449 def from_ical(cls, st: str | bytes, multiple: Literal[False] = False) -> Component: ... 

450 

451 @overload 

452 @classmethod 

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

454 

455 @classmethod 

456 def from_ical(cls, st: str | bytes, multiple: bool = False) -> Component | list[Component]: # noqa: FBT001 

457 """Parse iCalendar data into component instances. 

458 

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

460 

461 Parameters: 

462 st: iCalendar data as bytes or string 

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

464 

465 Returns: 

466 Component or list of components 

467 

468 See Also: 

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

470 """ 

471 stack = [] # a stack of components 

472 comps = [] 

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

474 if not line: 

475 continue 

476 

477 try: 

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

479 except ValueError as e: 

480 # if unable to parse a line within a component 

481 # that ignores exceptions, mark the component 

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

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

484 if not component or not component.ignore_exceptions: 

485 raise 

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

487 continue 

488 

489 uname = name.upper() 

490 # check for start of component 

491 if uname == "BEGIN": 

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

493 # otherwise get a general Components for robustness. 

494 c_name = vals.upper() 

495 c_class = cls.get_component_class(c_name) 

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

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

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

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

500 component = c_class() 

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

502 component.name = c_name 

503 stack.append(component) 

504 # check for end of event 

505 elif uname == "END": 

506 # we are done adding properties to this component 

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

508 if not stack: 

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

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

511 

512 component = stack.pop() 

513 if not stack: # we are at the end 

514 comps.append(component) 

515 else: 

516 stack[-1].add_component(component) 

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

518 tzp.cache_timezone_component(component) 

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

520 else: 

521 # Extract VALUE parameter if present 

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

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

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

525 if not component: 

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

527 # ignore these components in parsing 

528 if uname == "X-COMMENT": 

529 break 

530 raise ValueError( 

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

532 ) 

533 datetime_names = ( 

534 "DTSTART", 

535 "DTEND", 

536 "RECURRENCE-ID", 

537 "DUE", 

538 "RDATE", 

539 "EXDATE", 

540 ) 

541 

542 # Determine TZID for datetime properties 

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

544 

545 # Handle special cases for value list preparation 

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

547 # Special handling for CATEGORIES - need raw value 

548 # before unescaping to properly split on unescaped commas 

549 from icalendar.parser import split_on_unescaped_comma 

550 

551 line_str = str(line) 

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

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

554 colon_idx = line_str.rfind(":") 

555 if colon_idx > 0: 

556 raw_value = line_str[colon_idx + 1 :] 

557 # Parse categories immediately (not lazily) for both strict and tolerant components 

558 # This is because CATEGORIES needs special comma handling 

559 try: 

560 category_list = split_on_unescaped_comma(raw_value) 

561 vals_inst = factory(category_list) 

562 vals_inst.params = params 

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

564 except ValueError as e: 

565 if not component.ignore_exceptions: 

566 raise 

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

568 continue 

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

570 vals_list = [vals] 

571 elif name == "FREEBUSY": 

572 # Handle FREEBUSY comma-separated values 

573 vals_list = vals.split(",") 

574 # Workaround broken ICS files with empty RDATE 

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

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

577 vals_list = [] 

578 else: 

579 vals_list = [vals] 

580 

581 # Parse all properties eagerly 

582 for val in vals_list: 

583 try: 

584 if tzid: 

585 parsed_val = factory.from_ical(val, tzid) 

586 else: 

587 parsed_val = factory.from_ical(val) 

588 vals_inst = factory(parsed_val) 

589 vals_inst.params = params 

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

591 except Exception as e: 

592 if not component.ignore_exceptions: 

593 raise 

594 # Error-tolerant mode: create vBrokenProperty 

595 from icalendar.prop import vBrokenProperty 

596 

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

598 broken_prop = vBrokenProperty.from_parse_error( 

599 raw_value=val, 

600 params=params, 

601 property_name=name, 

602 expected_type=expected_type, 

603 error=e, 

604 ) 

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

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

607 

608 if multiple: 

609 return comps 

610 if len(comps) > 1: 

611 raise ValueError( 

612 cls._format_error( 

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

614 ) 

615 ) 

616 if len(comps) < 1: 

617 raise ValueError( 

618 cls._format_error( 

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

620 ) 

621 ) 

622 return comps[0] 

623 

624 @staticmethod 

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

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

627 max_error_length = 100 - 3 

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

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

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

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

632 

633 def content_line(self, name, value, sorted: bool = True): # noqa: A002, FBT001 

634 """Returns property as content line.""" 

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

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

637 

638 def content_lines(self, sorted: bool = True): # noqa: A002, FBT001 

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

640 contentlines = Contentlines() 

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

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

643 contentlines.append(cl) 

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

645 return contentlines 

646 

647 def to_ical(self, sorted: bool = True): # noqa: A002, FBT001 

648 """ 

649 :param sorted: Whether parameters and properties should be 

650 lexicographically sorted. 

651 """ 

652 

653 content_lines = self.content_lines(sorted=sorted) 

654 return content_lines.to_ical() 

655 

656 def __repr__(self): 

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

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

659 return ( 

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

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

662 ) 

663 

664 def __eq__(self, other): 

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

666 return False 

667 

668 properties_equal = super().__eq__(other) 

669 if not properties_equal: 

670 return False 

671 

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

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

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

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

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

677 for subcomponent in self.subcomponents: 

678 if subcomponent not in other.subcomponents: 

679 return False 

680 

681 return True 

682 

683 DTSTAMP = stamp = single_utc_property( 

684 "DTSTAMP", 

685 """RFC 5545: 

686 

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

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

689 

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

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

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

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

694 property specifies the date and time that the information 

695 associated with the calendar component was last revised in the 

696 calendar store. 

697 

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

699 

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

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

702 property. 

703 """, 

704 ) 

705 LAST_MODIFIED = single_utc_property( 

706 "LAST-MODIFIED", 

707 """RFC 5545: 

708 

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

710 information associated with the calendar component was last 

711 revised in the calendar store. 

712 

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

714 file in the file system. 

715 

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

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

718 """, 

719 ) 

720 

721 @property 

722 def last_modified(self) -> datetime: 

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

724 

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

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

727 """ 

728 return self.LAST_MODIFIED or self.DTSTAMP 

729 

730 @last_modified.setter 

731 def last_modified(self, value): 

732 self.LAST_MODIFIED = value 

733 

734 @last_modified.deleter 

735 def last_modified(self): 

736 del self.LAST_MODIFIED 

737 

738 @property 

739 def created(self) -> datetime: 

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

741 

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

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

744 """ 

745 return self.CREATED or self.DTSTAMP 

746 

747 @created.setter 

748 def created(self, value): 

749 self.CREATED = value 

750 

751 @created.deleter 

752 def created(self): 

753 del self.CREATED 

754 

755 def is_thunderbird(self) -> bool: 

756 """Whether this component has attributes that indicate that Mozilla Thunderbird created it.""" # noqa: E501 

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

758 

759 @staticmethod 

760 def _utc_now(): 

761 """Return now as UTC value.""" 

762 return datetime.now(timezone.utc) 

763 

764 uid = uid_property 

765 comments = comments_property 

766 links = links_property 

767 related_to = related_to_property 

768 concepts = concepts_property 

769 refids = refids_property 

770 

771 CREATED = single_utc_property( 

772 "CREATED", 

773 """ 

774 CREATED specifies the date and time that the calendar 

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

776 store. 

777 

778 Conformance: 

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

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

781 specified as a date with UTC time. 

782 

783 """, 

784 ) 

785 

786 _validate_new = True 

787 

788 @staticmethod 

789 def _validate_start_and_end(start, end): 

790 """This validates start and end. 

791 

792 Raises: 

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

794 """ 

795 if start is None or end is None: 

796 return 

797 if start > end: 

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

799 

800 @classmethod 

801 def new( 

802 cls, 

803 created: date | None = None, 

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

805 concepts: CONCEPTS_TYPE_SETTER = None, 

806 last_modified: date | None = None, 

807 links: LINKS_TYPE_SETTER = None, 

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

809 related_to: RELATED_TO_TYPE_SETTER = None, 

810 stamp: date | None = None, 

811 ) -> Component: 

812 """Create a new component. 

813 

814 Parameters: 

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

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

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

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

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

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

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

822 

823 Raises: 

824 ~error.InvalidCalendar: If the content is not valid according to :rfc:`5545`. 

825 

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

827 """ 

828 component = cls() 

829 component.DTSTAMP = stamp 

830 component.created = created 

831 component.last_modified = last_modified 

832 component.comments = comments 

833 component.links = links 

834 component.related_to = related_to 

835 component.concepts = concepts 

836 component.refids = refids 

837 return component 

838 

839 def to_jcal(self) -> list: 

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

841 

842 Returns: 

843 jCal object 

844 

845 See also :attr:`to_json`. 

846 

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

848 

849 .. code-block:: pycon 

850 

851 >>> from icalendar import Event 

852 >>> from datetime import date 

853 >>> from pprint import pprint 

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

855 >>> pprint(event.to_jcal()) 

856 ['vevent', 

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

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

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

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

861 []] 

862 """ 

863 properties = [] 

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

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

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

867 return [ 

868 self.name.lower(), 

869 properties, 

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

871 ] 

872 

873 def to_json(self) -> str: 

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

875 

876 Returns: 

877 JSON string 

878 

879 See also :attr:`to_jcal`. 

880 """ 

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

882 

883 @classmethod 

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

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

886 

887 Parameters: 

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

889 

890 Raises: 

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

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

893 

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

895 

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

897 

898 .. code-block:: pycon 

899 

900 >>> from icalendar import Component 

901 >>> jcal = ["vcalendar", 

902 ... [ 

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

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

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

906 ... ], 

907 ... [ 

908 ... ["vevent", 

909 ... [ 

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

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

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

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

914 ... ], 

915 ... [] 

916 ... ] 

917 ... ] 

918 ... ] 

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

920 >>> print(calendar.name) 

921 VCALENDAR 

922 >>> print(calendar.prodid) 

923 -//Example Inc.//Example Calendar//EN 

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

925 >>> print(event.summary) 

926 Planning meeting 

927 

928 """ 

929 if isinstance(jcal, str): 

930 jcal = json.loads(jcal) 

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

932 raise JCalParsingError( 

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

934 ) 

935 name, properties, subcomponents = jcal 

936 if not isinstance(name, str): 

937 raise JCalParsingError( 

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

939 ) 

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

941 # delegate to correct component class 

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

943 return component_cls.from_jcal(jcal) 

944 component = cls() 

945 if not isinstance(properties, list): 

946 raise JCalParsingError( 

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

948 ) 

949 for i, prop in enumerate(properties): 

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

951 prop_name = prop[0] 

952 prop_value = prop[2] 

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

954 prop_name, prop_value 

955 ) 

956 with JCalParsingError.reraise_with_path_added(1, i): 

957 v_prop = prop_cls.from_jcal(prop) 

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

959 # VALUE parameter 

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

961 del v_prop.VALUE 

962 component.add(prop_name, v_prop) 

963 if not isinstance(subcomponents, list): 

964 raise JCalParsingError( 

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

966 ) 

967 for i, subcomponent in enumerate(subcomponents): 

968 with JCalParsingError.reraise_with_path_added(2, i): 

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

970 return component 

971 

972 def copy(self, recursive:bool=False) -> Self: # noqa: FBT001 

973 """Copy the component. 

974 

975 Parameters: 

976 recursive: 

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

978 and all its properties. 

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

980 

981 Returns: 

982 A copy of the component. 

983 

984 Examples: 

985 

986 Create a shallow copy of a component: 

987 

988 .. code-block:: pycon 

989 

990 >>> from icalendar import Event 

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

992 >>> event_copy = event.copy() 

993 >>> str(event_copy.description) 

994 'Event to be copied' 

995 

996 Shallow copies lose their subcomponents: 

997 

998 .. code-block:: pycon 

999 

1000 >>> from icalendar import Calendar 

1001 >>> calendar = Calendar.example() 

1002 >>> len(calendar.subcomponents) 

1003 3 

1004 >>> calendar_copy = calendar.copy() 

1005 >>> len(calendar_copy.subcomponents) 

1006 0 

1007 

1008 A recursive copy also copies all the subcomponents: 

1009 

1010 .. code-block:: pycon 

1011 

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

1013 >>> len(full_calendar_copy.subcomponents) 

1014 3 

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

1016 True 

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

1018 False 

1019 

1020 """ 

1021 if recursive: 

1022 return deepcopy(self) 

1023 return super().copy() 

1024 

1025 

1026__all__ = ["Component"]