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

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

142 statements  

1""":rfc:`5545` iCalendar component.""" 

2 

3from __future__ import annotations 

4 

5import uuid 

6from datetime import timedelta 

7from typing import TYPE_CHECKING, Literal, cast, overload 

8 

9from icalendar.attr import ( 

10 CONCEPTS_TYPE_SETTER, 

11 LINKS_TYPE_SETTER, 

12 RELATED_TO_TYPE_SETTER, 

13 categories_property, 

14 images_property, 

15 multi_language_text_property, 

16 single_string_property, 

17 source_property, 

18 uid_property, 

19 url_property, 

20) 

21from icalendar.cal.component import Component 

22from icalendar.cal.examples import get_example 

23from icalendar.cal.timezone import Timezone 

24from icalendar.error import IncompleteComponent 

25from icalendar.parser.ical.calendar import CalendarIcalParser 

26from icalendar.version import __version__ 

27 

28if TYPE_CHECKING: 

29 from collections.abc import Iterable, Sequence 

30 from datetime import date, datetime 

31 from pathlib import Path 

32 

33 from icalendar.cal import ( 

34 Availability, 

35 Event, 

36 FreeBusy, 

37 Journal, 

38 Todo, 

39 ) 

40 from icalendar.compatibility import Self 

41 from icalendar.parser.ical.component import ComponentIcalParser 

42 

43 

44DEFAULT_PRODID = f"-//collective//icalendar//{__version__}//EN" 

45"""The value for :attr:`~icalendar.cal.calendar.Calendar.prodid` when it's not provided.""" 

46 

47 

48class Calendar(Component): 

49 """ 

50 The "VCALENDAR" object is a collection of calendar information. 

51 This information can include a variety of components, such as 

52 "VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VTIMEZONE", or any 

53 other type of calendar component. 

54 

55 Example: 

56 Create a new Calendar: 

57 

58 >>> from icalendar import Calendar 

59 >>> calendar = Calendar.new(name="My Calendar") 

60 >>> print(calendar.calendar_name) 

61 My Calendar 

62 

63 Get the example Calendar: 

64 

65 .. code-block:: pycon 

66 

67 >>> from icalendar import Calendar 

68 >>> calendar = Calendar.example() 

69 >>> print(calendar.calendar_name) 

70 Holidays 

71 >>> len(calendar.events) 

72 3 

73 

74 """ 

75 

76 name = "VCALENDAR" 

77 canonical_order = ( 

78 "VERSION", 

79 "PRODID", 

80 "CALSCALE", 

81 "METHOD", 

82 "URL", 

83 "DESCRIPTION", 

84 "X-WR-CALDESC", 

85 "NAME", 

86 "X-WR-CALNAME", 

87 ) 

88 required = ( 

89 "PRODID", 

90 "VERSION", 

91 ) 

92 singletons = ( 

93 "PRODID", 

94 "VERSION", 

95 "CALSCALE", 

96 "METHOD", 

97 "COLOR", # RFC 7986 

98 ) 

99 multiple = ( 

100 "CATEGORIES", # RFC 7986 

101 "DESCRIPTION", # RFC 7986 

102 "NAME", # RFC 7986 

103 ) 

104 

105 @classmethod 

106 def example(cls, name: str = "example") -> Calendar: 

107 """Return the calendar example with the given name. 

108 

109 Example: 

110 

111 .. code-block:: pycon 

112 

113 >>> from icalendar import Calendar 

114 >>> print(Calendar.example().to_ical().decode()) 

115 BEGIN:VCALENDAR 

116 VERSION:2.0 

117 PRODID:collective/icalendar 

118 CALSCALE:GREGORIAN 

119 METHOD:PUBLISH 

120 X-WR-CALNAME:Holidays 

121 X-WR-TIMEZONE:Etc/GMT 

122 BEGIN:VEVENT 

123 SUMMARY:New Year's Day 

124 DTSTART:20220101 

125 DTEND:20220101 

126 DTSTAMP:20221108T080105Z 

127 UID:636a0cc1dbd5a1667894465@icalendar 

128 SEQUENCE:0 

129 DESCRIPTION:Happy New Year! 

130 STATUS:CONFIRMED 

131 TRANSP:TRANSPARENT 

132 END:VEVENT 

133 BEGIN:VEVENT 

134 SUMMARY:Orthodox Christmas 

135 DTSTART:20220107 

136 DTEND:20220107 

137 UID:636a0cc1dbfd91667894465@icalendar 

138 SEQUENCE:0 

139 DESCRIPTION:It is Christmas again! 

140 LOCATION:Russia 

141 STATUS:CONFIRMED 

142 TRANSP:TRANSPARENT 

143 END:VEVENT 

144 BEGIN:VEVENT 

145 SUMMARY:International Women's Day 

146 DTSTART:20220308 

147 DTEND:20220308 

148 UID:636a0cc1dc0f11667894465@icalendar 

149 SEQUENCE:0 

150 DESCRIPTION:May the feminine be honoured! 

151 STATUS:CONFIRMED 

152 TRANSP:TRANSPARENT 

153 END:VEVENT 

154 END:VCALENDAR 

155 """ 

156 return cls.from_ical(get_example("calendars", name)) 

157 

158 @classmethod 

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

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

161 return CalendarIcalParser(st, cls._get_component_factory(), cls.types_factory) 

162 

163 @overload 

164 @classmethod 

165 def from_ical( 

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

167 ) -> Calendar: ... 

168 

169 @overload 

170 @classmethod 

171 def from_ical( 

172 cls, st: str | bytes | Path, multiple: Literal[True] 

173 ) -> list[Calendar]: ... 

174 

175 @classmethod 

176 def from_ical( 

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

178 ) -> Calendar | list[Calendar]: 

179 """Parse iCalendar data into calendar instances. 

180 

181 Parameters: 

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

183 multiple: If ``True``, returns a list of calendars. 

184 If ``False``, returns a single calendar. 

185 

186 Returns: 

187 Calendar or list of calendars. 

188 """ 

189 return cast( 

190 "Calendar | list[Calendar]", super().from_ical(st, multiple=multiple) 

191 ) 

192 

193 @property 

194 def events(self) -> list[Event]: 

195 """All event components in the calendar. 

196 

197 This is a shortcut to get all events. 

198 Modifications do not change the calendar. 

199 Use :meth:`Component.add_component <icalendar.cal.component.Component.add_component>`. 

200 

201 >>> from icalendar import Calendar 

202 >>> calendar = Calendar.example() 

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

204 >>> event.start 

205 datetime.date(2022, 1, 1) 

206 >>> print(event["SUMMARY"]) 

207 New Year's Day 

208 """ 

209 return self.walk("VEVENT") 

210 

211 @property 

212 def todos(self) -> list[Todo]: 

213 """All todo components in the calendar. 

214 

215 This is a shortcut to get all todos. 

216 Modifications do not change the calendar. 

217 Use :meth:`Component.add_component <icalendar.cal.component.Component.add_component>`. 

218 """ 

219 return self.walk("VTODO") 

220 

221 @property 

222 def journals(self) -> list[Journal]: 

223 """All journal components in the calendar. 

224 

225 This is a shortcut to get all journals. 

226 Modifications do not change the calendar. 

227 Use :meth:`Component.add_component <icalendar.cal.component.Component.add_component>`. 

228 """ 

229 return self.walk("VJOURNAL") 

230 

231 @property 

232 def availabilities(self) -> list[Availability]: 

233 """All :class:`~icalendar.cal.availability.Availability` components in the calendar. 

234 

235 This is a shortcut to get all availabilities. 

236 Modifications do not change the calendar. 

237 Use :meth:`Component.add_component <icalendar.cal.component.Component.add_component>`. 

238 """ 

239 return self.walk("VAVAILABILITY") 

240 

241 @property 

242 def freebusy(self) -> list[FreeBusy]: 

243 """All FreeBusy components in the calendar. 

244 

245 This is a shortcut to get all FreeBusy. 

246 Modifications do not change the calendar. 

247 Use :meth:`Component.add_component <icalendar.cal.component.Component.add_component>`. 

248 """ 

249 return self.walk("VFREEBUSY") 

250 

251 def get_used_tzids(self) -> set[str]: 

252 """The set of TZIDs in use. 

253 

254 This goes through the whole calendar to find all occurrences of 

255 timezone information like the TZID parameter in all attributes. 

256 

257 >>> from icalendar import Calendar 

258 >>> calendar = Calendar.example("timezone_rdate") 

259 >>> calendar.get_used_tzids() 

260 {'posix/Europe/Vaduz'} 

261 

262 Even if you use UTC, this will not show up. 

263 """ 

264 result = set() 

265 for _name, value in self.property_items(sorted=False): 

266 if hasattr(value, "params"): 

267 result.add(value.params.get("TZID")) 

268 return result - {None} 

269 

270 def get_missing_tzids(self) -> set[str]: 

271 """The set of missing timezone component tzids. 

272 

273 To create a :rfc:`5545` compatible calendar, 

274 all of these timezones should be added. 

275 

276 UTC is excluded: per :rfc:`5545#section-3.2.19`, UTC datetimes use 

277 the ``Z`` suffix and never require a VTIMEZONE component. 

278 """ 

279 tzids = self.get_used_tzids() - {"UTC"} 

280 for timezone in self.timezones: 

281 # discard (not remove) — a VTIMEZONE may exist for a timezone not 

282 # referenced by any event TZID (e.g. added by x-wr-timezone conversion) 

283 tzids.discard(timezone.tz_name) 

284 return tzids 

285 

286 @property 

287 def timezones(self) -> list[Timezone]: 

288 """Return the timezones components in this calendar. 

289 

290 >>> from icalendar import Calendar 

291 >>> calendar = Calendar.example("pacific_fiji") 

292 >>> [timezone.tz_name for timezone in calendar.timezones] 

293 ['custom_Pacific/Fiji'] 

294 

295 .. note:: 

296 

297 This is a read-only property. 

298 """ 

299 return self.walk("VTIMEZONE") 

300 

301 def add_missing_timezones( 

302 self, 

303 first_date: date = Timezone.DEFAULT_FIRST_DATE, 

304 last_date: date = Timezone.DEFAULT_LAST_DATE, 

305 ): 

306 """Add all missing VTIMEZONE components. 

307 

308 This adds all the timezone components that are required. 

309 VTIMEZONE components are inserted at the beginning of the calendar 

310 to ensure they appear before other components that reference them. 

311 

312 .. note:: 

313 

314 Timezones that are not known will not be added. 

315 

316 Parameters: 

317 first_date: Earlier than anything that happens in the calendar. 

318 last_date: Later than anything happening in the calendar. 

319 

320 >>> from icalendar import Calendar, Event 

321 >>> from datetime import datetime 

322 >>> from zoneinfo import ZoneInfo 

323 >>> calendar = Calendar() 

324 >>> event = Event() 

325 >>> calendar.add_component(event) 

326 >>> event.start = datetime(1990, 10, 11, 12, tzinfo=ZoneInfo("Europe/Berlin")) 

327 >>> calendar.timezones 

328 [] 

329 >>> calendar.add_missing_timezones() 

330 >>> calendar.timezones[0].tz_name 

331 'Europe/Berlin' 

332 >>> calendar.get_missing_tzids() # check that all are added 

333 set() 

334 """ 

335 missing_tzids = self.get_missing_tzids() 

336 if not missing_tzids: 

337 return 

338 

339 existing_timezone_count = len(self.timezones) 

340 

341 for tzid in missing_tzids: 

342 try: 

343 timezone = Timezone.from_tzid( 

344 tzid, first_date=first_date, last_date=last_date 

345 ) 

346 except ValueError: 

347 continue 

348 self.subcomponents.insert(existing_timezone_count, timezone) 

349 existing_timezone_count += 1 

350 

351 calendar_name = multi_language_text_property( 

352 "NAME", 

353 "X-WR-CALNAME", 

354 """The display name of this calendar, per :rfc:`7986#section-5.1`. 

355 

356 Implements both the ``NAME`` property from :rfc:`7986#section-5.1` and the widely used 

357 ``X-WR-CALNAME`` extension for broader client compatibility. 

358 

359 Multiple language variants can be stored by setting this property more than 

360 once, each with a different ``LANGUAGE`` parameter value. 

361 

362 Example: 

363 Set the name of the calendar. 

364 

365 .. code-block:: pycon 

366 

367 >>> from icalendar import Calendar 

368 >>> calendar = Calendar() 

369 >>> calendar.calendar_name = "My Calendar" 

370 >>> print(calendar.to_ical().decode()) 

371 BEGIN:VCALENDAR 

372 NAME:My Calendar 

373 X-WR-CALNAME:My Calendar 

374 END:VCALENDAR 

375 

376 """, 

377 ) 

378 

379 description = multi_language_text_property( 

380 "DESCRIPTION", 

381 "X-WR-CALDESC", 

382 """A description of the calendar's content. 

383 

384 Implements both ``DESCRIPTION`` from :rfc:`5545#section-3.8.1.5` and 

385 :rfc:`7986#section-5.2` and ``X-WR-CALDESC`` for broader calendar client 

386 compatibility. 

387 

388 Multiple language variants can be stored by setting this property more than 

389 once with different ``LANGUAGE`` parameter values. 

390 

391 Example: 

392 Add a description to a calendar. 

393 

394 .. code-block:: pycon 

395 

396 >>> from icalendar import Calendar 

397 >>> calendar = Calendar() 

398 >>> calendar.description = "This is a calendar" 

399 >>> print(calendar.to_ical().decode()) 

400 BEGIN:VCALENDAR 

401 DESCRIPTION:This is a calendar 

402 X-WR-CALDESC:This is a calendar 

403 END:VCALENDAR 

404 

405 """, 

406 ) 

407 

408 color = single_string_property( 

409 "COLOR", 

410 """A CSS3 color name or value used to visually distinguish this calendar, per :rfc:`7986#section-5.9`. 

411 

412 Implements both ``COLOR`` from :rfc:`7986#section-5.9` and ``X-APPLE-CALENDAR-COLOR``. 

413 The value is a case-insensitive CSS3 color name, for example, ``"turquoise"``, or 

414 a hex code, for example, ``"#ffffff"``, drawn from the 

415 `CSS3 color specification <https://www.w3.org/TR/css-color-3/>`_. 

416 

417 Since :rfc:`7986`, individual ``VEVENT``, ``VTODO``, and ``VJOURNAL`` 

418 subcomponents may also carry their own color. 

419 

420 Example: 

421 .. code-block:: pycon 

422 

423 >>> from icalendar import Calendar 

424 >>> calendar = Calendar() 

425 >>> calendar.color = "black" 

426 >>> print(calendar.to_ical().decode()) 

427 BEGIN:VCALENDAR 

428 COLOR:black 

429 END:VCALENDAR 

430 

431 """, 

432 "X-APPLE-CALENDAR-COLOR", 

433 ) 

434 categories = categories_property 

435 uid = uid_property 

436 prodid = single_string_property( 

437 "PRODID", 

438 """The product identifier for the software that created this iCalendar object. 

439 

440This property is defined in :rfc:`5545#section-3.7.3`. 

441It's required exactly once per iCalendar object. 

442 

443The value should be a globally unique string. The conventional format is a 

444Formal Public Identifier (FPI), for example, ``-//My Company//My Product//EN``, but any 

445unique string is acceptable. 

446 

447Example: 

448 Set a custom product identifier on a new calendar. 

449 

450 .. code-block:: pycon 

451 

452 >>> from icalendar import Calendar 

453 >>> cal = Calendar() 

454 >>> cal.prodid = "-//MyApp//MyCalendar//EN" 

455 >>> str(cal.prodid) 

456 '-//MyApp//MyCalendar//EN' 

457 

458See also: 

459 :attr:`version` 

460""", 

461 ) 

462 version = single_string_property( 

463 "VERSION", 

464 """The iCalendar specification version required to interpret this object. 

465 

466This property is defined in :rfc:`5545#section-3.7.4`. 

467It's required exactly once per calendar object. 

468The value ``"2.0"`` indicates :rfc:`5545` compliance, which is the default used 

469by this library. A range such as ``"1.0;2.0"`` may indicate minimum and maximum 

470supported versions. 

471 

472Example: 

473 .. code-block:: pycon 

474 

475 >>> from icalendar import Calendar 

476 >>> cal = Calendar() 

477 >>> cal.version = "2.0" 

478 >>> str(cal.version) 

479 '2.0' 

480 

481See also: 

482 :attr:`prodid` 

483""", 

484 ) 

485 

486 calscale = single_string_property( 

487 "CALSCALE", 

488 """The calendar scale for date and time values in this iCalendar object. 

489 

490This property is defined in :rfc:`5545#section-3.7.1`. The only value currently defined is 

491``"GREGORIAN"`` (the default). When this property is absent, Gregorian is assumed. 

492 

493Per :rfc:`7529`, non-Gregorian calendar systems are expressed via ``RRULE`` 

494transformations rather than a different ``CALSCALE`` value. However, icalendar 

495currently implements only the parsing of this value, not a transformation. 

496 

497Example: 

498 .. code-block:: pycon 

499 

500 >>> from icalendar import Calendar 

501 >>> cal = Calendar() 

502 >>> cal.calscale 

503 'GREGORIAN' 

504 

505 """, 

506 default="GREGORIAN", 

507 ) 

508 method = single_string_property( 

509 "METHOD", 

510 """The iTIP scheduling method associated with this calendar object, per :rfc:`5545#section-3.7.2`. 

511 

512When present, ``METHOD`` indicates that this object is part of a scheduling 

513transaction, such as a meeting invitation or cancellation. Scheduling methods 

514are defined by :rfc:`5546#section-1.4` (iTIP), with values such as ``"REQUEST"``, 

515``"REPLY"``, ``"CANCEL"``, and ``"PUBLISH"``. 

516 

517When used inside a MIME message, this value must match the ``method`` parameter 

518of the ``Content-Type`` header. If absent, the calendar is treated as a plain 

519data snapshot with no scheduling semantics. 

520 

521Example: 

522 .. code-block:: pycon 

523 

524 >>> from icalendar import Calendar 

525 >>> cal = Calendar() 

526 >>> cal.method = "REQUEST" 

527 >>> str(cal.method) 

528 'REQUEST' 

529 

530""", 

531 ) 

532 url = url_property 

533 source = source_property 

534 

535 @property 

536 def refresh_interval(self) -> timedelta | None: 

537 """A suggested minimum polling interval for fetching updates to this calendar, per :rfc:`7986#section-5.7`. 

538 

539 Calendar clients should not poll more frequently than this interval. 

540 The value must be a positive duration. 

541 

542 Returns: 

543 A :class:`~datetime.timedelta`, or ``None`` when not set. 

544 

545 Raises: 

546 ValueError: When setting a non-positive (zero or negative) duration. 

547 TypeError: When setting a value that is not a :class:`~datetime.timedelta` or ``None``. 

548 

549 Example: 

550 .. code-block:: pycon 

551 

552 >>> from datetime import timedelta 

553 >>> from icalendar import Calendar 

554 >>> cal = Calendar() 

555 >>> cal.refresh_interval = timedelta(hours=1) 

556 >>> cal.refresh_interval 

557 datetime.timedelta(seconds=3600) 

558 

559 """ 

560 refresh_interval = self.get("REFRESH-INTERVAL") 

561 return refresh_interval.dt if refresh_interval else None 

562 

563 @refresh_interval.setter 

564 def refresh_interval(self, value: timedelta | None): 

565 """Set the REFRESH-INTERVAL.""" 

566 if not isinstance(value, timedelta) and value is not None: 

567 raise TypeError( 

568 "REFRESH-INTERVAL must be either a positive timedelta," 

569 " or None to delete it." 

570 ) 

571 if value is not None and value.total_seconds() <= 0: 

572 raise ValueError("REFRESH-INTERVAL must be a positive timedelta.") 

573 if value is not None: 

574 del self.refresh_interval 

575 self.add("REFRESH-INTERVAL", value) 

576 else: 

577 del self.refresh_interval 

578 

579 @refresh_interval.deleter 

580 def refresh_interval(self): 

581 """Delete REFRESH-INTERVAL.""" 

582 self.pop("REFRESH-INTERVAL") 

583 

584 images = images_property 

585 

586 @classmethod 

587 def new( 

588 cls, 

589 /, 

590 calscale: str | None = None, 

591 categories: Sequence[str] = (), 

592 color: str | None = None, 

593 concepts: CONCEPTS_TYPE_SETTER = None, 

594 description: str | None = None, 

595 language: str | None = None, 

596 last_modified: date | datetime | None = None, 

597 links: LINKS_TYPE_SETTER = None, 

598 method: str | None = None, 

599 name: str | None = None, 

600 organization: str | None = None, 

601 prodid: str | None = None, 

602 refresh_interval: timedelta | None = None, 

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

604 related_to: RELATED_TO_TYPE_SETTER = None, 

605 source: str | None = None, 

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

607 uid: str | uuid.UUID | None = None, 

608 url: str | None = None, 

609 version: str = "2.0", 

610 ) -> Self: 

611 """Create a new Calendar. 

612 

613 This creates a new Calendar in accordance with :rfc:`5545` and :rfc:`7986`. 

614 

615 Parameters: 

616 calscale: The :attr:`calscale` of the calendar. 

617 categories: The :attr:`categories` of the calendar. 

618 color: The :attr:`color` of the calendar. 

619 concepts: The :attr:`~icalendar.cal.component.Component.concepts` of the calendar. 

620 description: The :attr:`description` of the calendar. 

621 language: The language for the calendar. Used to generate localized `prodid`. 

622 last_modified: The :attr:`~icalendar.cal.component.Component.last_modified` of the calendar. 

623 links: The :attr:`~icalendar.cal.component.Component.links` of the calendar. 

624 method: The :attr:`method` of the calendar. 

625 name: The :attr:`calendar_name` of the calendar. 

626 organization: The organization name. Used to generate `prodid` if not provided. 

627 prodid: The :attr:`prodid` of the component. If ``None`` and ``organization`` is provided, 

628 generates a `prodid` in the format of "-//organization//name//language". 

629 If ``None`` and ``organization`` is not provided, sets it to 

630 :attr:`~icalendar.cal.calendar.DEFAULT_PRODID`. 

631 refresh_interval: The :attr:`refresh_interval` of the calendar. 

632 refids: :attr:`~icalendar.cal.component.Component.refids` of the calendar. 

633 related_to: :attr:`~icalendar.cal.component.Component.related_to` of the calendar. 

634 source: The :attr:`source` of the calendar. 

635 subcomponents: The subcomponents of the calendar. 

636 uid: The :attr:`uid` of the calendar. 

637 If None, this is set to a new :func:`uuid.uuid4`. 

638 url: The :attr:`url` of the calendar. 

639 version: The :attr:`version` of the calendar. 

640 

641 Returns: 

642 :class:`Calendar` 

643 

644 Raises: 

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

646 

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

648 """ 

649 calendar: Self = super().new( 

650 last_modified=last_modified, 

651 links=links, 

652 related_to=related_to, 

653 refids=refids, 

654 concepts=concepts, 

655 subcomponents=subcomponents, 

656 ) 

657 

658 # Generate prodid if not provided but organization is given 

659 if prodid is None and organization: 

660 app_name = name or "Calendar" 

661 lang = language.upper() if language else "EN" 

662 prodid = f"-//{organization}//{app_name}//{lang}" 

663 elif prodid is None: 

664 prodid = DEFAULT_PRODID 

665 

666 calendar.prodid = prodid 

667 calendar.version = version 

668 calendar.calendar_name = name 

669 calendar.color = color 

670 calendar.description = description 

671 calendar.method = method 

672 calendar.calscale = calscale 

673 calendar.categories = categories 

674 calendar.uid = uid if uid is not None else uuid.uuid4() 

675 calendar.url = url 

676 calendar.refresh_interval = refresh_interval 

677 calendar.source = source 

678 

679 return calendar 

680 

681 def validate(self): 

682 """Validate that the calendar has required properties and components. 

683 

684 This method can be called explicitly to validate a calendar before output. 

685 

686 Raises: 

687 ~icalendar.error.IncompleteComponent: If the calendar lacks required properties or 

688 components. 

689 """ 

690 if not self.get("PRODID"): 

691 raise IncompleteComponent("Calendar must have a PRODID") 

692 if not self.get("VERSION"): 

693 raise IncompleteComponent("Calendar must have a VERSION") 

694 if not self.subcomponents: 

695 raise IncompleteComponent( 

696 "Calendar must contain at least one component (event, todo, etc.)" 

697 ) 

698 

699 

700__all__ = ["Calendar"]