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

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

367 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 ( 

25 Contentline, 

26 Contentlines, 

27 Parameters, 

28 q_join, 

29 q_split, 

30 split_on_unescaped_comma, 

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[( 

72 tuple[str] | tuple[tuple[str, str]] 

73 )] = () 

74 """These properties are inclusive. 

75  

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

77 second one must also occur. 

78  

79 Example: 

80  

81 .. code-block:: python 

82 

83 ('duration', 'repeat') 

84 """ 

85 

86 ignore_exceptions: ClassVar[bool] = False 

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

88  

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

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

91 """ 

92 

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

94 _components_factory: ClassVar[ComponentFactory | None] = None 

95 

96 @classmethod 

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

98 """Return a component with this name. 

99 

100 Arguments: 

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

102 """ 

103 if cls._components_factory is None: 

104 cls._components_factory = ComponentFactory() 

105 return cls._components_factory.get_component_class(name) 

106 

107 @classmethod 

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

109 """Register a custom component class. 

110 

111 Parameters: 

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

113 

114 Raises: 

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

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

117 

118 Examples: 

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

120 

121 .. code-block:: pycon 

122 

123 >>> from icalendar import Component 

124 >>> class XExample(Component): 

125 ... name = "X-EXAMPLE" 

126 ... def custom_method(self): 

127 ... return "custom" 

128 >>> Component.register(XExample) 

129 """ 

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

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

132 

133 if cls._components_factory is None: 

134 cls._components_factory = ComponentFactory() 

135 

136 # Check if already registered 

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

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

139 raise ValueError( 

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

141 ) 

142 

143 cls._components_factory.add_component_class(component_class) 

144 

145 @staticmethod 

146 def _infer_value_type( 

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

148 ) -> str | None: 

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

150 

151 Args: 

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

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

154 or :py:class:`list`. 

155 

156 Returns: 

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

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

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

160 """ 

161 if isinstance(value, list): 

162 if not value: 

163 return None 

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

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

166 return "DATE" 

167 # Check if ALL items are time 

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

169 return "TIME" 

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

171 return None 

172 if is_date(value): 

173 return "DATE" 

174 if isinstance(value, time): 

175 return "TIME" 

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

177 return None 

178 

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

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

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

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

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

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

185 # parsing a property, contains error strings 

186 

187 def __bool__(self): 

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

189 return True 

190 

191 def is_empty(self): 

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

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

194 

195 ############################# 

196 # handling of property values 

197 

198 @classmethod 

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

200 """Encode values to icalendar property values. 

201 

202 :param name: Name of the property. 

203 :type name: string 

204 

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

206 any of the icalendar's own property types. 

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

208 

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

210 available, if encode is set to True. 

211 :type parameters: Dictionary 

212 

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

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

215 or False, if not. 

216 :type encode: Boolean 

217 

218 :returns: icalendar property value 

219 """ 

220 if not encode: 

221 return value 

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

223 # Don't encode already encoded values. 

224 obj = value 

225 else: 

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

227 value_param = None 

228 if parameters and "VALUE" in parameters: 

229 value_param = parameters["VALUE"] 

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

231 inferred = cls._infer_value_type(value) 

232 if inferred: 

233 value_param = inferred 

234 # Auto-set the VALUE parameter 

235 if parameters is None: 

236 parameters = {} 

237 if "VALUE" not in parameters: 

238 parameters["VALUE"] = inferred 

239 

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

241 obj = klass(value) 

242 if parameters: 

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

244 obj.params = Parameters() 

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

246 if item is None: 

247 if key in obj.params: 

248 del obj.params[key] 

249 else: 

250 obj.params[key] = item 

251 return obj 

252 

253 def add( 

254 self, 

255 name: str, 

256 value, 

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

258 encode: bool = True, # noqa: FBT001 

259 ): 

260 """Add a property. 

261 

262 :param name: Name of the property. 

263 :type name: string 

264 

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

266 any of the icalendar's own property types. 

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

268 

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

270 available, if encode is set to True. 

271 :type parameters: Dictionary 

272 

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

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

275 or False, if not. 

276 :type encode: Boolean 

277 

278 :returns: None 

279 """ 

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

281 "dtstamp", 

282 "created", 

283 "last-modified", 

284 ): 

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

286 value = tzp.localize_utc(value) 

287 

288 # encode value 

289 if ( 

290 encode 

291 and isinstance(value, list) 

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

293 ): 

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

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

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

297 else: 

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

299 

300 # set value 

301 if name in self: 

302 # If property already exists, append it. 

303 oldval = self[name] 

304 if isinstance(oldval, list): 

305 if isinstance(value, list): 

306 value = oldval + value 

307 else: 

308 oldval.append(value) 

309 value = oldval 

310 else: 

311 value = [oldval, value] 

312 self[name] = value 

313 

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

315 """Internal for decoding property values.""" 

316 

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

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

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

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

321 if hasattr(value, "ical_value"): 

322 return value.ical_value 

323 if isinstance(value, vDDDLists): 

324 # TODO: Workaround unfinished decoding 

325 return value 

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

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

328 # Workaround to decode vText properly 

329 if isinstance(decoded, vText): 

330 decoded = decoded.encode(DEFAULT_ENCODING) 

331 return decoded 

332 

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

334 """Returns decoded value of property. 

335 

336 A component maps keys to icalendar property value types. 

337 This function returns values compatible to native Python types. 

338 """ 

339 if name in self: 

340 value = self[name] 

341 if isinstance(value, list): 

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

343 return self._decode(name, value) 

344 if default is _marker: 

345 raise KeyError(name) 

346 return default 

347 

348 ######################################################################## 

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

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

351 

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

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

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

355 if decode: 

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

357 return vals 

358 

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

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

361 to that. 

362 """ 

363 if encode: 

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

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

366 

367 ######################### 

368 # Handling of components 

369 

370 def add_component(self, component: Component): 

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

372 self.subcomponents.append(component) 

373 

374 def _walk(self, name, select): 

375 """Walk to given component.""" 

376 result = [] 

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

378 result.append(self) 

379 for subcomponent in self.subcomponents: 

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

381 return result 

382 

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

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

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

386 

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

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

389 and returns True/False. 

390 :returns: A list of components that match. 

391 :rtype: list[Component] 

392 """ 

393 if name is not None: 

394 name = name.upper() 

395 return self._walk(name, select) 

396 

397 ##################### 

398 # Generation 

399 

400 def property_items( 

401 self, 

402 recursive=True, 

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

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

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

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

407 """ 

408 v_text = self.types_factory["text"] 

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

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

411 

412 for name in property_names: 

413 values = self[name] 

414 if isinstance(values, list): 

415 # normally one property is one line 

416 for value in values: 

417 properties.append((name, value)) 

418 else: 

419 properties.append((name, values)) 

420 if recursive: 

421 # recursion is fun! 

422 for subcomponent in self.subcomponents: 

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

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

425 return properties 

426 

427 @classmethod 

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

429 """Parse iCalendar data into component instances. 

430 

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

432 

433 Args: 

434 st: iCalendar data as bytes or string 

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

436 

437 Returns: 

438 Component or list of components 

439 

440 See Also: 

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

442 """ 

443 stack = [] # a stack of components 

444 comps = [] 

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

446 if not line: 

447 continue 

448 

449 try: 

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

451 except ValueError as e: 

452 # if unable to parse a line within a component 

453 # that ignores exceptions, mark the component 

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

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

456 if not component or not component.ignore_exceptions: 

457 raise 

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

459 continue 

460 

461 uname = name.upper() 

462 # check for start of component 

463 if uname == "BEGIN": 

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

465 # otherwise get a general Components for robustness. 

466 c_name = vals.upper() 

467 c_class = cls.get_component_class(c_name) 

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

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

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

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

472 component = c_class() 

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

474 component.name = c_name 

475 stack.append(component) 

476 # check for end of event 

477 elif uname == "END": 

478 # we are done adding properties to this component 

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

480 if not stack: 

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

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

483 

484 component = stack.pop() 

485 if not stack: # we are at the end 

486 comps.append(component) 

487 else: 

488 stack[-1].add_component(component) 

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

490 tzp.cache_timezone_component(component) 

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

492 else: 

493 # Extract VALUE parameter if present 

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

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

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

497 if not component: 

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

499 # ignore these components in parsing 

500 if uname == "X-COMMENT": 

501 break 

502 raise ValueError( 

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

504 ) 

505 datetime_names = ( 

506 "DTSTART", 

507 "DTEND", 

508 "RECURRENCE-ID", 

509 "DUE", 

510 "RDATE", 

511 "EXDATE", 

512 ) 

513 try: 

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

515 # Special handling for CATEGORIES - need raw value 

516 # before unescaping to properly split on unescaped commas 

517 line_str = str(line) 

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

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

520 colon_idx = line_str.rfind(":") 

521 if colon_idx > 0: 

522 raw_value = line_str[colon_idx + 1:] 

523 category_list = split_on_unescaped_comma(raw_value) 

524 parsed_components = [factory(category_list)] 

525 else: 

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

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

528 elif name == "FREEBUSY": 

529 vals = vals.split(",") 

530 if "TZID" in params: 

531 parsed_components = [ 

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

533 for val in vals 

534 ] 

535 else: 

536 parsed_components = [ 

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

538 ] 

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

540 parsed_components = [ 

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

542 ] 

543 # Workaround broken ICS files with empty RDATE 

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

545 parsed_components = [] 

546 else: 

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

548 except ValueError as e: 

549 if not component.ignore_exceptions: 

550 raise 

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

552 else: 

553 for parsed_component in parsed_components: 

554 parsed_component.params = params 

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

556 

557 if multiple: 

558 return comps 

559 if len(comps) > 1: 

560 raise ValueError( 

561 cls._format_error( 

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

563 ) 

564 ) 

565 if len(comps) < 1: 

566 raise ValueError( 

567 cls._format_error( 

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

569 ) 

570 ) 

571 return comps[0] 

572 

573 @staticmethod 

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

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

576 max_error_length = 100 - 3 

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

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

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

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

581 

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

583 """Returns property as content line.""" 

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

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

586 

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

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

589 contentlines = Contentlines() 

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

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

592 contentlines.append(cl) 

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

594 return contentlines 

595 

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

597 """ 

598 :param sorted: Whether parameters and properties should be 

599 lexicographically sorted. 

600 """ 

601 

602 content_lines = self.content_lines(sorted=sorted) 

603 return content_lines.to_ical() 

604 

605 def __repr__(self): 

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

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

608 return ( 

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

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

611 ) 

612 

613 def __eq__(self, other): 

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

615 return False 

616 

617 properties_equal = super().__eq__(other) 

618 if not properties_equal: 

619 return False 

620 

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

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

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

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

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

626 for subcomponent in self.subcomponents: 

627 if subcomponent not in other.subcomponents: 

628 return False 

629 

630 return True 

631 

632 DTSTAMP = stamp = single_utc_property( 

633 "DTSTAMP", 

634 """RFC 5545: 

635 

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

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

638 

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

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

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

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

643 property specifies the date and time that the information 

644 associated with the calendar component was last revised in the 

645 calendar store. 

646 

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

648 

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

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

651 property. 

652 """, 

653 ) 

654 LAST_MODIFIED = single_utc_property( 

655 "LAST-MODIFIED", 

656 """RFC 5545: 

657 

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

659 information associated with the calendar component was last 

660 revised in the calendar store. 

661 

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

663 file in the file system. 

664 

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

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

667 """, 

668 ) 

669 

670 @property 

671 def last_modified(self) -> datetime: 

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

673 

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

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

676 """ 

677 return self.LAST_MODIFIED or self.DTSTAMP 

678 

679 @last_modified.setter 

680 def last_modified(self, value): 

681 self.LAST_MODIFIED = value 

682 

683 @last_modified.deleter 

684 def last_modified(self): 

685 del self.LAST_MODIFIED 

686 

687 @property 

688 def created(self) -> datetime: 

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

690 

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

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

693 """ 

694 return self.CREATED or self.DTSTAMP 

695 

696 @created.setter 

697 def created(self, value): 

698 self.CREATED = value 

699 

700 @created.deleter 

701 def created(self): 

702 del self.CREATED 

703 

704 def is_thunderbird(self) -> bool: 

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

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

707 

708 @staticmethod 

709 def _utc_now(): 

710 """Return now as UTC value.""" 

711 return datetime.now(timezone.utc) 

712 

713 uid = uid_property 

714 comments = comments_property 

715 links = links_property 

716 related_to = related_to_property 

717 concepts = concepts_property 

718 refids = refids_property 

719 

720 CREATED = single_utc_property( 

721 "CREATED", 

722 """ 

723 CREATED specifies the date and time that the calendar 

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

725 store. 

726 

727 Conformance: 

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

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

730 specified as a date with UTC time. 

731 

732 """, 

733 ) 

734 

735 _validate_new = True 

736 

737 @staticmethod 

738 def _validate_start_and_end(start, end): 

739 """This validates start and end. 

740 

741 Raises: 

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

743 """ 

744 if start is None or end is None: 

745 return 

746 if start > end: 

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

748 

749 @classmethod 

750 def new( 

751 cls, 

752 created: date | None = None, 

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

754 concepts: CONCEPTS_TYPE_SETTER = None, 

755 last_modified: date | None = None, 

756 links: LINKS_TYPE_SETTER = None, 

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

758 related_to: RELATED_TO_TYPE_SETTER = None, 

759 stamp: date | None = None, 

760 ) -> Component: 

761 """Create a new component. 

762 

763 Arguments: 

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

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

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

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

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

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

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

771 

772 Raises: 

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

774 

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

776 """ 

777 component = cls() 

778 component.DTSTAMP = stamp 

779 component.created = created 

780 component.last_modified = last_modified 

781 component.comments = comments 

782 component.links = links 

783 component.related_to = related_to 

784 component.concepts = concepts 

785 component.refids = refids 

786 return component 

787 

788 def to_jcal(self) -> list: 

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

790 

791 Returns: 

792 jCal object 

793 

794 See also :attr:`to_json`. 

795 

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

797 

798 .. code-block:: pycon 

799 

800 >>> from icalendar import Event 

801 >>> from datetime import date 

802 >>> from pprint import pprint 

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

804 >>> pprint(event.to_jcal()) 

805 ['vevent', 

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

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

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

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

810 []] 

811 """ 

812 properties = [] 

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

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

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

816 return [ 

817 self.name.lower(), 

818 properties, 

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

820 ] 

821 

822 def to_json(self) -> str: 

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

824 

825 Returns: 

826 JSON string 

827 

828 See also :attr:`to_jcal`. 

829 """ 

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

831 

832 @classmethod 

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

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

835 

836 Args: 

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

838 

839 Raises: 

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

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

842 

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

844 

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

846 

847 .. code-block:: pycon 

848 

849 >>> from icalendar import Component 

850 >>> jcal = ["vcalendar", 

851 ... [ 

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

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

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

855 ... ], 

856 ... [ 

857 ... ["vevent", 

858 ... [ 

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

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

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

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

863 ... ], 

864 ... [] 

865 ... ] 

866 ... ] 

867 ... ] 

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

869 >>> print(calendar.name) 

870 VCALENDAR 

871 >>> print(calendar.prodid) 

872 -//Example Inc.//Example Calendar//EN 

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

874 >>> print(event.summary) 

875 Planning meeting 

876 

877 """ 

878 if isinstance(jcal, str): 

879 jcal = json.loads(jcal) 

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

881 raise JCalParsingError( 

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

883 ) 

884 name, properties, subcomponents = jcal 

885 if not isinstance(name, str): 

886 raise JCalParsingError( 

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

888 ) 

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

890 # delegate to correct component class 

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

892 return component_cls.from_jcal(jcal) 

893 component = cls() 

894 if not isinstance(properties, list): 

895 raise JCalParsingError( 

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

897 ) 

898 for i, prop in enumerate(properties): 

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

900 prop_name = prop[0] 

901 prop_value = prop[2] 

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

903 prop_name, prop_value 

904 ) 

905 with JCalParsingError.reraise_with_path_added(1, i): 

906 v_prop = prop_cls.from_jcal(prop) 

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

908 # VALUE parameter 

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

910 del v_prop.VALUE 

911 component.add(prop_name, v_prop) 

912 if not isinstance(subcomponents, list): 

913 raise JCalParsingError( 

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

915 ) 

916 for i, subcomponent in enumerate(subcomponents): 

917 with JCalParsingError.reraise_with_path_added(2, i): 

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

919 return component 

920 

921 

922__all__ = ["Component"]