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

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

141 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 

46 

47class Calendar(Component): 

48 """ 

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

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

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

52 other type of calendar component. 

53 

54 Example: 

55 Create a new Calendar: 

56 

57 >>> from icalendar import Calendar 

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

59 >>> print(calendar.calendar_name) 

60 My Calendar 

61 

62 """ 

63 

64 name = "VCALENDAR" 

65 canonical_order = ( 

66 "VERSION", 

67 "PRODID", 

68 "CALSCALE", 

69 "METHOD", 

70 "DESCRIPTION", 

71 "X-WR-CALDESC", 

72 "NAME", 

73 "X-WR-CALNAME", 

74 ) 

75 required = ( 

76 "PRODID", 

77 "VERSION", 

78 ) 

79 singletons = ( 

80 "PRODID", 

81 "VERSION", 

82 "CALSCALE", 

83 "METHOD", 

84 "COLOR", # RFC 7986 

85 ) 

86 multiple = ( 

87 "CATEGORIES", # RFC 7986 

88 "DESCRIPTION", # RFC 7986 

89 "NAME", # RFC 7986 

90 ) 

91 

92 @classmethod 

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

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

95 

96 Example: 

97 

98 .. code-block:: pycon 

99 

100 >>> from icalendar import Calendar 

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

102 BEGIN:VCALENDAR 

103 VERSION:2.0 

104 PRODID:collective/icalendar 

105 CALSCALE:GREGORIAN 

106 METHOD:PUBLISH 

107 X-WR-CALNAME:Holidays 

108 X-WR-TIMEZONE:Etc/GMT 

109 BEGIN:VEVENT 

110 SUMMARY:New Year's Day 

111 DTSTART:20220101 

112 DTEND:20220101 

113 DTSTAMP:20221108T080105Z 

114 UID:636a0cc1dbd5a1667894465@icalendar 

115 SEQUENCE:0 

116 DESCRIPTION:Happy New Year! 

117 STATUS:CONFIRMED 

118 TRANSP:TRANSPARENT 

119 END:VEVENT 

120 BEGIN:VEVENT 

121 SUMMARY:Orthodox Christmas 

122 DTSTART:20220107 

123 DTEND:20220107 

124 UID:636a0cc1dbfd91667894465@icalendar 

125 SEQUENCE:0 

126 DESCRIPTION:It is Christmas again! 

127 LOCATION:Russia 

128 STATUS:CONFIRMED 

129 TRANSP:TRANSPARENT 

130 END:VEVENT 

131 BEGIN:VEVENT 

132 SUMMARY:International Women's Day 

133 DTSTART:20220308 

134 DTEND:20220308 

135 UID:636a0cc1dc0f11667894465@icalendar 

136 SEQUENCE:0 

137 DESCRIPTION:May the feminine be honoured! 

138 STATUS:CONFIRMED 

139 TRANSP:TRANSPARENT 

140 END:VEVENT 

141 END:VCALENDAR 

142 """ 

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

144 

145 @classmethod 

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

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

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

149 

150 @overload 

151 @classmethod 

152 def from_ical( 

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

154 ) -> Calendar: ... 

155 

156 @overload 

157 @classmethod 

158 def from_ical( 

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

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

161 

162 @classmethod 

163 def from_ical( 

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

165 ) -> Calendar | list[Calendar]: 

166 """Parse iCalendar data into calendar instances. 

167 

168 Parameters: 

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

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

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

172 

173 Returns: 

174 Calendar or list of calendars. 

175 """ 

176 return cast( 

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

178 ) 

179 

180 @property 

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

182 """All event components in the calendar. 

183 

184 This is a shortcut to get all events. 

185 Modifications do not change the calendar. 

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

187 

188 >>> from icalendar import Calendar 

189 >>> calendar = Calendar.example() 

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

191 >>> event.start 

192 datetime.date(2022, 1, 1) 

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

194 New Year's Day 

195 """ 

196 return self.walk("VEVENT") 

197 

198 @property 

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

200 """All todo components in the calendar. 

201 

202 This is a shortcut to get all todos. 

203 Modifications do not change the calendar. 

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

205 """ 

206 return self.walk("VTODO") 

207 

208 @property 

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

210 """All journal components in the calendar. 

211 

212 This is a shortcut to get all journals. 

213 Modifications do not change the calendar. 

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

215 """ 

216 return self.walk("VJOURNAL") 

217 

218 @property 

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

220 """All :class:`Availability` components in the calendar. 

221 

222 This is a shortcut to get all availabilities. 

223 Modifications do not change the calendar. 

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

225 """ 

226 return self.walk("VAVAILABILITY") 

227 

228 @property 

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

230 """All FreeBusy components in the calendar. 

231 

232 This is a shortcut to get all FreeBusy. 

233 Modifications do not change the calendar. 

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

235 """ 

236 return self.walk("VFREEBUSY") 

237 

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

239 """The set of TZIDs in use. 

240 

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

242 timezone information like the TZID parameter in all attributes. 

243 

244 >>> from icalendar import Calendar 

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

246 >>> calendar.get_used_tzids() 

247 {'posix/Europe/Vaduz'} 

248 

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

250 """ 

251 result = set() 

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

253 if hasattr(value, "params"): 

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

255 return result - {None} 

256 

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

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

259 

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

261 all of these timezones should be added. 

262 

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

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

265 """ 

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

267 for timezone in self.timezones: 

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

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

270 tzids.discard(timezone.tz_name) 

271 return tzids 

272 

273 @property 

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

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

276 

277 >>> from icalendar import Calendar 

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

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

280 ['custom_Pacific/Fiji'] 

281 

282 .. note:: 

283 

284 This is a read-only property. 

285 """ 

286 return self.walk("VTIMEZONE") 

287 

288 def add_missing_timezones( 

289 self, 

290 first_date: date = Timezone.DEFAULT_FIRST_DATE, 

291 last_date: date = Timezone.DEFAULT_LAST_DATE, 

292 ): 

293 """Add all missing VTIMEZONE components. 

294 

295 This adds all the timezone components that are required. 

296 VTIMEZONE components are inserted at the beginning of the calendar 

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

298 

299 .. note:: 

300 

301 Timezones that are not known will not be added. 

302 

303 Parameters: 

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

305 last_date: Later than anything happening in the calendar. 

306 

307 >>> from icalendar import Calendar, Event 

308 >>> from datetime import datetime 

309 >>> from zoneinfo import ZoneInfo 

310 >>> calendar = Calendar() 

311 >>> event = Event() 

312 >>> calendar.add_component(event) 

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

314 >>> calendar.timezones 

315 [] 

316 >>> calendar.add_missing_timezones() 

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

318 'Europe/Berlin' 

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

320 set() 

321 """ 

322 missing_tzids = self.get_missing_tzids() 

323 if not missing_tzids: 

324 return 

325 

326 existing_timezone_count = len(self.timezones) 

327 

328 for tzid in missing_tzids: 

329 try: 

330 timezone = Timezone.from_tzid( 

331 tzid, first_date=first_date, last_date=last_date 

332 ) 

333 except ValueError: 

334 continue 

335 self.subcomponents.insert(existing_timezone_count, timezone) 

336 existing_timezone_count += 1 

337 

338 calendar_name = multi_language_text_property( 

339 "NAME", 

340 "X-WR-CALNAME", 

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

342 

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

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

345 

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

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

348 

349 Example: 

350 Set the name of the calendar. 

351 

352 .. code-block:: pycon 

353 

354 >>> from icalendar import Calendar 

355 >>> calendar = Calendar() 

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

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

358 BEGIN:VCALENDAR 

359 NAME:My Calendar 

360 X-WR-CALNAME:My Calendar 

361 END:VCALENDAR 

362 

363 """, 

364 ) 

365 

366 description = multi_language_text_property( 

367 "DESCRIPTION", 

368 "X-WR-CALDESC", 

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

370 

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

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

373 compatibility. 

374 

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

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

377 

378 Example: 

379 Add a description to a calendar. 

380 

381 .. code-block:: pycon 

382 

383 >>> from icalendar import Calendar 

384 >>> calendar = Calendar() 

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

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

387 BEGIN:VCALENDAR 

388 DESCRIPTION:This is a calendar 

389 X-WR-CALDESC:This is a calendar 

390 END:VCALENDAR 

391 

392 """, 

393 ) 

394 

395 color = single_string_property( 

396 "COLOR", 

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

398 

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

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

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

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

403 

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

405 subcomponents may also carry their own color. 

406 

407 Example: 

408 .. code-block:: pycon 

409 

410 >>> from icalendar import Calendar 

411 >>> calendar = Calendar() 

412 >>> calendar.color = "black" 

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

414 BEGIN:VCALENDAR 

415 COLOR:black 

416 END:VCALENDAR 

417 

418 """, 

419 "X-APPLE-CALENDAR-COLOR", 

420 ) 

421 categories = categories_property 

422 uid = uid_property 

423 prodid = single_string_property( 

424 "PRODID", 

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

426 

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

428It's required exactly once per iCalendar object. 

429 

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

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

432unique string is acceptable. 

433 

434Example: 

435 Set a custom product identifier on a new calendar. 

436 

437 .. code-block:: pycon 

438 

439 >>> from icalendar import Calendar 

440 >>> cal = Calendar() 

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

442 >>> str(cal.prodid) 

443 '-//MyApp//MyCalendar//EN' 

444 

445See also: 

446 :attr:`version` 

447""", 

448 ) 

449 version = single_string_property( 

450 "VERSION", 

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

452 

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

454It's required exactly once per calendar object. 

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

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

457supported versions. 

458 

459Example: 

460 .. code-block:: pycon 

461 

462 >>> from icalendar import Calendar 

463 >>> cal = Calendar() 

464 >>> cal.version = "2.0" 

465 >>> str(cal.version) 

466 '2.0' 

467 

468See also: 

469 :attr:`prodid` 

470""", 

471 ) 

472 

473 calscale = single_string_property( 

474 "CALSCALE", 

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

476 

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

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

479 

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

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

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

483 

484Example: 

485 .. code-block:: pycon 

486 

487 >>> from icalendar import Calendar 

488 >>> cal = Calendar() 

489 >>> cal.calscale 

490 'GREGORIAN' 

491 

492 """, 

493 default="GREGORIAN", 

494 ) 

495 method = single_string_property( 

496 "METHOD", 

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

498 

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

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

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

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

503 

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

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

506data snapshot with no scheduling semantics. 

507 

508Example: 

509 .. code-block:: pycon 

510 

511 >>> from icalendar import Calendar 

512 >>> cal = Calendar() 

513 >>> cal.method = "REQUEST" 

514 >>> str(cal.method) 

515 'REQUEST' 

516 

517""", 

518 ) 

519 url = url_property 

520 source = source_property 

521 

522 @property 

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

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

525 

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

527 The value must be a positive duration. 

528 

529 Returns: 

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

531 

532 Raises: 

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

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

535 

536 Example: 

537 .. code-block:: pycon 

538 

539 >>> from datetime import timedelta 

540 >>> from icalendar import Calendar 

541 >>> cal = Calendar() 

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

543 >>> cal.refresh_interval 

544 datetime.timedelta(seconds=3600) 

545 

546 """ 

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

548 return refresh_interval.dt if refresh_interval else None 

549 

550 @refresh_interval.setter 

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

552 """Set the REFRESH-INTERVAL.""" 

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

554 raise TypeError( 

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

556 " or None to delete it." 

557 ) 

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

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

560 if value is not None: 

561 del self.refresh_interval 

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

563 else: 

564 del self.refresh_interval 

565 

566 @refresh_interval.deleter 

567 def refresh_interval(self): 

568 """Delete REFRESH-INTERVAL.""" 

569 self.pop("REFRESH-INTERVAL") 

570 

571 images = images_property 

572 

573 @classmethod 

574 def new( 

575 cls, 

576 /, 

577 calscale: str | None = None, 

578 categories: Sequence[str] = (), 

579 color: str | None = None, 

580 concepts: CONCEPTS_TYPE_SETTER = None, 

581 description: str | None = None, 

582 language: str | None = None, 

583 last_modified: date | datetime | None = None, 

584 links: LINKS_TYPE_SETTER = None, 

585 method: str | None = None, 

586 name: str | None = None, 

587 organization: str | None = None, 

588 prodid: str | None = None, 

589 refresh_interval: timedelta | None = None, 

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

591 related_to: RELATED_TO_TYPE_SETTER = None, 

592 source: str | None = None, 

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

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

595 url: str | None = None, 

596 version: str = "2.0", 

597 ) -> Self: 

598 """Create a new Calendar. 

599 

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

601 

602 Parameters: 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

622 subcomponents: The subcomponents of the calendar. 

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

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

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

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

627 

628 Returns: 

629 :class:`Calendar` 

630 

631 Raises: 

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

633 

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

635 """ 

636 calendar: Self = super().new( 

637 last_modified=last_modified, 

638 links=links, 

639 related_to=related_to, 

640 refids=refids, 

641 concepts=concepts, 

642 subcomponents=subcomponents, 

643 ) 

644 

645 # Generate prodid if not provided but organization is given 

646 if prodid is None and organization: 

647 app_name = name or "Calendar" 

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

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

650 elif prodid is None: 

651 prodid = DEFAULT_PRODID 

652 

653 calendar.prodid = prodid 

654 calendar.version = version 

655 calendar.calendar_name = name 

656 calendar.color = color 

657 calendar.description = description 

658 calendar.method = method 

659 calendar.calscale = calscale 

660 calendar.categories = categories 

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

662 calendar.url = url 

663 calendar.refresh_interval = refresh_interval 

664 calendar.source = source 

665 

666 return calendar 

667 

668 def validate(self): 

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

670 

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

672 

673 Raises: 

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

675 components. 

676 """ 

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

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

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

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

681 if not self.subcomponents: 

682 raise IncompleteComponent( 

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

684 ) 

685 

686 

687__all__ = ["Calendar"]