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

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

359 statements  

1"""The base for :rfc:`5545` components.""" 

2 

3from __future__ import annotations 

4 

5import json 

6from datetime import date, datetime, time, timedelta, timezone 

7from typing import TYPE_CHECKING, Any, ClassVar 

8 

9from icalendar.attr import ( 

10 CONCEPTS_TYPE_SETTER, 

11 LINKS_TYPE_SETTER, 

12 RELATED_TO_TYPE_SETTER, 

13 comments_property, 

14 concepts_property, 

15 links_property, 

16 refids_property, 

17 related_to_property, 

18 single_utc_property, 

19 uid_property, 

20) 

21from icalendar.cal.component_factory import ComponentFactory 

22from icalendar.caselessdict import CaselessDict 

23from icalendar.error import InvalidCalendar, JCalParsingError 

24from icalendar.parser import Contentline, Contentlines, Parameters, q_join, q_split 

25from icalendar.parser_tools import DEFAULT_ENCODING 

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

27from icalendar.timezone import tzp 

28from icalendar.tools import is_date 

29 

30if TYPE_CHECKING: 

31 from icalendar.compatibility import Self 

32 

33_marker = [] 

34 

35 

36class Component(CaselessDict): 

37 """Base class for calendar components. 

38 

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

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

41 directly, but rather one of the subclasses. 

42 """ 

43 

44 name: ClassVar[str|None] = None 

45 """The name of the component. 

46  

47 This should be defined in each component class. 

48 

49 Example: ``VCALENDAR``. 

50 """ 

51 

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

53 """These properties are required.""" 

54 

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

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

57 

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

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

60 

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

62 """These properties are mutually exclusive.""" 

63 

64 inclusive: ClassVar[( 

65 tuple[str] | tuple[tuple[str, str]] 

66 )] = () 

67 """These properties are inclusive. 

68  

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

70 second one must also occur. 

71  

72 Example: 

73  

74 .. code-block:: python 

75 

76 ('duration', 'repeat') 

77 """ 

78 

79 ignore_exceptions: ClassVar[bool] = False 

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

81  

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

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

84 """ 

85 

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

87 _components_factory: ClassVar[ComponentFactory | None] = None 

88 

89 @classmethod 

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

91 """Return a component with this name. 

92 

93 Arguments: 

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

95 """ 

96 if cls._components_factory is None: 

97 cls._components_factory = ComponentFactory() 

98 return cls._components_factory.get_component_class(name) 

99 

100 @classmethod 

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

102 """Register a custom component class. 

103 

104 Args: 

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

106 

107 Raises: 

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

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

110 

111 Example: 

112 >>> from icalendar import Component 

113 >>> class XExample(Component): 

114 ... name = "X-EXAMPLE" 

115 ... def custom_method(self): 

116 ... return "custom" 

117 >>> Component.register(XExample) 

118 """ 

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

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

121 

122 if cls._components_factory is None: 

123 cls._components_factory = ComponentFactory() 

124 

125 # Check if already registered 

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

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

128 raise ValueError( 

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

130 ) 

131 

132 cls._components_factory.add_component_class(component_class) 

133 

134 @staticmethod 

135 def _infer_value_type( 

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

137 ) -> str | None: 

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

139 

140 Args: 

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

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

143 or :py:class:`list`. 

144 

145 Returns: 

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

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

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

149 """ 

150 if isinstance(value, list): 

151 if not value: 

152 return None 

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

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

155 return "DATE" 

156 # Check if ALL items are time 

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

158 return "TIME" 

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

160 return None 

161 if is_date(value): 

162 return "DATE" 

163 if isinstance(value, time): 

164 return "TIME" 

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

166 return None 

167 

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

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

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

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

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

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

174 # parsing a property, contains error strings 

175 

176 def __bool__(self): 

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

178 return True 

179 

180 def is_empty(self): 

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

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

183 

184 ############################# 

185 # handling of property values 

186 

187 @classmethod 

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

189 """Encode values to icalendar property values. 

190 

191 :param name: Name of the property. 

192 :type name: string 

193 

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

195 any of the icalendar's own property types. 

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

197 

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

199 available, if encode is set to True. 

200 :type parameters: Dictionary 

201 

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

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

204 or False, if not. 

205 :type encode: Boolean 

206 

207 :returns: icalendar property value 

208 """ 

209 if not encode: 

210 return value 

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

212 # Don't encode already encoded values. 

213 obj = value 

214 else: 

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

216 value_param = None 

217 if parameters and "VALUE" in parameters: 

218 value_param = parameters["VALUE"] 

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

220 inferred = cls._infer_value_type(value) 

221 if inferred: 

222 value_param = inferred 

223 # Auto-set the VALUE parameter 

224 if parameters is None: 

225 parameters = {} 

226 if "VALUE" not in parameters: 

227 parameters["VALUE"] = inferred 

228 

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

230 obj = klass(value) 

231 if parameters: 

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

233 obj.params = Parameters() 

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

235 if item is None: 

236 if key in obj.params: 

237 del obj.params[key] 

238 else: 

239 obj.params[key] = item 

240 return obj 

241 

242 def add( 

243 self, 

244 name: str, 

245 value, 

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

247 encode: bool = True, # noqa: FBT001 

248 ): 

249 """Add a property. 

250 

251 :param name: Name of the property. 

252 :type name: string 

253 

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

255 any of the icalendar's own property types. 

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

257 

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

259 available, if encode is set to True. 

260 :type parameters: Dictionary 

261 

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

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

264 or False, if not. 

265 :type encode: Boolean 

266 

267 :returns: None 

268 """ 

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

270 "dtstamp", 

271 "created", 

272 "last-modified", 

273 ): 

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

275 value = tzp.localize_utc(value) 

276 

277 # encode value 

278 if ( 

279 encode 

280 and isinstance(value, list) 

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

282 ): 

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

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

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

286 else: 

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

288 

289 # set value 

290 if name in self: 

291 # If property already exists, append it. 

292 oldval = self[name] 

293 if isinstance(oldval, list): 

294 if isinstance(value, list): 

295 value = oldval + value 

296 else: 

297 oldval.append(value) 

298 value = oldval 

299 else: 

300 value = [oldval, value] 

301 self[name] = value 

302 

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

304 """Internal for decoding property values.""" 

305 

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

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

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

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

310 if hasattr(value, "ical_value"): 

311 return value.ical_value 

312 if isinstance(value, vDDDLists): 

313 # TODO: Workaround unfinished decoding 

314 return value 

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

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

317 # Workaround to decode vText properly 

318 if isinstance(decoded, vText): 

319 decoded = decoded.encode(DEFAULT_ENCODING) 

320 return decoded 

321 

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

323 """Returns decoded value of property. 

324 

325 A component maps keys to icalendar property value types. 

326 This function returns values compatible to native Python types. 

327 """ 

328 if name in self: 

329 value = self[name] 

330 if isinstance(value, list): 

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

332 return self._decode(name, value) 

333 if default is _marker: 

334 raise KeyError(name) 

335 return default 

336 

337 ######################################################################## 

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

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

340 

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

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

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

344 if decode: 

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

346 return vals 

347 

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

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

350 to that. 

351 """ 

352 if encode: 

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

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

355 

356 ######################### 

357 # Handling of components 

358 

359 def add_component(self, component: Component): 

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

361 self.subcomponents.append(component) 

362 

363 def _walk(self, name, select): 

364 """Walk to given component.""" 

365 result = [] 

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

367 result.append(self) 

368 for subcomponent in self.subcomponents: 

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

370 return result 

371 

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

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

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

375 

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

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

378 and returns True/False. 

379 :returns: A list of components that match. 

380 :rtype: list[Component] 

381 """ 

382 if name is not None: 

383 name = name.upper() 

384 return self._walk(name, select) 

385 

386 ##################### 

387 # Generation 

388 

389 def property_items( 

390 self, 

391 recursive=True, 

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

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

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

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

396 """ 

397 v_text = self.types_factory["text"] 

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

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

400 

401 for name in property_names: 

402 values = self[name] 

403 if isinstance(values, list): 

404 # normally one property is one line 

405 for value in values: 

406 properties.append((name, value)) 

407 else: 

408 properties.append((name, values)) 

409 if recursive: 

410 # recursion is fun! 

411 for subcomponent in self.subcomponents: 

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

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

414 return properties 

415 

416 @classmethod 

417 def from_ical(cls, st, multiple: bool = False) -> Self | list[Self]: # noqa: FBT001 

418 """Parse iCalendar data into component instances. 

419 

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

421 

422 Args: 

423 st: iCalendar data as bytes or string 

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

425 

426 Returns: 

427 Component or list of components 

428 

429 See Also: 

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

431 """ 

432 stack = [] # a stack of components 

433 comps = [] 

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

435 if not line: 

436 continue 

437 

438 try: 

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

440 except ValueError as e: 

441 # if unable to parse a line within a component 

442 # that ignores exceptions, mark the component 

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

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

445 if not component or not component.ignore_exceptions: 

446 raise 

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

448 continue 

449 

450 uname = name.upper() 

451 # check for start of component 

452 if uname == "BEGIN": 

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

454 # otherwise get a general Components for robustness. 

455 c_name = vals.upper() 

456 c_class = cls.get_component_class(c_name) 

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

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

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

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

461 component = c_class() 

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

463 component.name = c_name 

464 stack.append(component) 

465 # check for end of event 

466 elif uname == "END": 

467 # we are done adding properties to this component 

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

469 if not stack: 

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

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

472 

473 component = stack.pop() 

474 if not stack: # we are at the end 

475 comps.append(component) 

476 else: 

477 stack[-1].add_component(component) 

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

479 tzp.cache_timezone_component(component) 

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

481 else: 

482 # Extract VALUE parameter if present 

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

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

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

486 if not component: 

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

488 # ignore these components in parsing 

489 if uname == "X-COMMENT": 

490 break 

491 raise ValueError( 

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

493 ) 

494 datetime_names = ( 

495 "DTSTART", 

496 "DTEND", 

497 "RECURRENCE-ID", 

498 "DUE", 

499 "RDATE", 

500 "EXDATE", 

501 ) 

502 try: 

503 if name == "FREEBUSY": 

504 vals = vals.split(",") 

505 if "TZID" in params: 

506 parsed_components = [ 

507 factory(factory.from_ical(val, params["TZID"])) 

508 for val in vals 

509 ] 

510 else: 

511 parsed_components = [ 

512 factory(factory.from_ical(val)) for val in vals 

513 ] 

514 elif name in datetime_names and "TZID" in params: 

515 parsed_components = [ 

516 factory(factory.from_ical(vals, params["TZID"])) 

517 ] 

518 # Workaround broken ICS files with empty RDATE 

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

520 parsed_components = [] 

521 else: 

522 parsed_components = [factory(factory.from_ical(vals))] 

523 except ValueError as e: 

524 if not component.ignore_exceptions: 

525 raise 

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

527 else: 

528 for parsed_component in parsed_components: 

529 parsed_component.params = params 

530 component.add(name, parsed_component, encode=0) 

531 

532 if multiple: 

533 return comps 

534 if len(comps) > 1: 

535 raise ValueError( 

536 cls._format_error( 

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

538 ) 

539 ) 

540 if len(comps) < 1: 

541 raise ValueError( 

542 cls._format_error( 

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

544 ) 

545 ) 

546 return comps[0] 

547 

548 @staticmethod 

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

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

551 max_error_length = 100 - 3 

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

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

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

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

556 

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

558 """Returns property as content line.""" 

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

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

561 

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

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

564 contentlines = Contentlines() 

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

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

567 contentlines.append(cl) 

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

569 return contentlines 

570 

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

572 """ 

573 :param sorted: Whether parameters and properties should be 

574 lexicographically sorted. 

575 """ 

576 

577 content_lines = self.content_lines(sorted=sorted) 

578 return content_lines.to_ical() 

579 

580 def __repr__(self): 

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

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

583 return ( 

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

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

586 ) 

587 

588 def __eq__(self, other): 

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

590 return False 

591 

592 properties_equal = super().__eq__(other) 

593 if not properties_equal: 

594 return False 

595 

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

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

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

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

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

601 for subcomponent in self.subcomponents: 

602 if subcomponent not in other.subcomponents: 

603 return False 

604 

605 return True 

606 

607 DTSTAMP = stamp = single_utc_property( 

608 "DTSTAMP", 

609 """RFC 5545: 

610 

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

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

613 

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

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

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

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

618 property specifies the date and time that the information 

619 associated with the calendar component was last revised in the 

620 calendar store. 

621 

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

623 

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

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

626 property. 

627 """, 

628 ) 

629 LAST_MODIFIED = single_utc_property( 

630 "LAST-MODIFIED", 

631 """RFC 5545: 

632 

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

634 information associated with the calendar component was last 

635 revised in the calendar store. 

636 

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

638 file in the file system. 

639 

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

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

642 """, 

643 ) 

644 

645 @property 

646 def last_modified(self) -> datetime: 

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

648 

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

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

651 """ 

652 return self.LAST_MODIFIED or self.DTSTAMP 

653 

654 @last_modified.setter 

655 def last_modified(self, value): 

656 self.LAST_MODIFIED = value 

657 

658 @last_modified.deleter 

659 def last_modified(self): 

660 del self.LAST_MODIFIED 

661 

662 @property 

663 def created(self) -> datetime: 

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

665 

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

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

668 """ 

669 return self.CREATED or self.DTSTAMP 

670 

671 @created.setter 

672 def created(self, value): 

673 self.CREATED = value 

674 

675 @created.deleter 

676 def created(self): 

677 del self.CREATED 

678 

679 def is_thunderbird(self) -> bool: 

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

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

682 

683 @staticmethod 

684 def _utc_now(): 

685 """Return now as UTC value.""" 

686 return datetime.now(timezone.utc) 

687 

688 uid = uid_property 

689 comments = comments_property 

690 links = links_property 

691 related_to = related_to_property 

692 concepts = concepts_property 

693 refids = refids_property 

694 

695 CREATED = single_utc_property( 

696 "CREATED", 

697 """ 

698 CREATED specifies the date and time that the calendar 

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

700 store. 

701 

702 Conformance: 

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

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

705 specified as a date with UTC time. 

706 

707 """, 

708 ) 

709 

710 _validate_new = True 

711 

712 @staticmethod 

713 def _validate_start_and_end(start, end): 

714 """This validates start and end. 

715 

716 Raises: 

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

718 """ 

719 if start is None or end is None: 

720 return 

721 if start > end: 

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

723 

724 @classmethod 

725 def new( 

726 cls, 

727 created: date | None = None, 

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

729 concepts: CONCEPTS_TYPE_SETTER = None, 

730 last_modified: date | None = None, 

731 links: LINKS_TYPE_SETTER = None, 

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

733 related_to: RELATED_TO_TYPE_SETTER = None, 

734 stamp: date | None = None, 

735 ) -> Component: 

736 """Create a new component. 

737 

738 Arguments: 

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

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

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

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

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

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

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

746 

747 Raises: 

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

749 

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

751 """ 

752 component = cls() 

753 component.DTSTAMP = stamp 

754 component.created = created 

755 component.last_modified = last_modified 

756 component.comments = comments 

757 component.links = links 

758 component.related_to = related_to 

759 component.concepts = concepts 

760 component.refids = refids 

761 return component 

762 

763 def to_jcal(self) -> list: 

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

765 

766 Returns: 

767 jCal object 

768 

769 See also :attr:`to_json`. 

770 

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

772 

773 .. code-block:: pycon 

774 

775 >>> from icalendar import Event 

776 >>> from datetime import date 

777 >>> from pprint import pprint 

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

779 >>> pprint(event.to_jcal()) 

780 ['vevent', 

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

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

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

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

785 []] 

786 """ 

787 properties = [] 

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

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

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

791 return [ 

792 self.name.lower(), 

793 properties, 

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

795 ] 

796 

797 def to_json(self) -> str: 

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

799 

800 Returns: 

801 JSON string 

802 

803 See also :attr:`to_jcal`. 

804 """ 

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

806 

807 @classmethod 

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

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

810 

811 Args: 

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

813 

814 Raises: 

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

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

817 

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

819 

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

821 

822 .. code-block:: pycon 

823 

824 >>> from icalendar import Component 

825 >>> jcal = ["vcalendar", 

826 ... [ 

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

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

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

830 ... ], 

831 ... [ 

832 ... ["vevent", 

833 ... [ 

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

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

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

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

838 ... ], 

839 ... [] 

840 ... ] 

841 ... ] 

842 ... ] 

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

844 >>> print(calendar.name) 

845 VCALENDAR 

846 >>> print(calendar.prodid) 

847 -//Example Inc.//Example Calendar//EN 

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

849 >>> print(event.summary) 

850 Planning meeting 

851 

852 """ 

853 if isinstance(jcal, str): 

854 jcal = json.loads(jcal) 

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

856 raise JCalParsingError( 

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

858 ) 

859 name, properties, subcomponents = jcal 

860 if not isinstance(name, str): 

861 raise JCalParsingError( 

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

863 ) 

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

865 # delegate to correct component class 

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

867 return component_cls.from_jcal(jcal) 

868 component = cls() 

869 if not isinstance(properties, list): 

870 raise JCalParsingError( 

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

872 ) 

873 for i, prop in enumerate(properties): 

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

875 prop_name = prop[0] 

876 prop_value = prop[2] 

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

878 prop_name, prop_value 

879 ) 

880 with JCalParsingError.reraise_with_path_added(1, i): 

881 v_prop = prop_cls.from_jcal(prop) 

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

883 # VALUE parameter 

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

885 del v_prop.VALUE 

886 component.add(prop_name, v_prop) 

887 if not isinstance(subcomponents, list): 

888 raise JCalParsingError( 

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

890 ) 

891 for i, subcomponent in enumerate(subcomponents): 

892 with JCalParsingError.reraise_with_path_added(2, i): 

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

894 return component 

895 

896 

897__all__ = ["Component"]