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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

144 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.availability import Availability 

34 from icalendar.cal.event import Event 

35 from icalendar.cal.free_busy import FreeBusy 

36 from icalendar.cal.journal import Journal 

37 from icalendar.cal.todo import Todo 

38 from icalendar.parser.ical.component import ComponentIcalParser 

39 

40 

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

42 

43 

44class Calendar(Component): 

45 """ 

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

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

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

49 other type of calendar component. 

50 

51 Examples: 

52 Create a new Calendar: 

53 

54 >>> from icalendar import Calendar 

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

56 >>> print(calendar.calendar_name) 

57 My Calendar 

58 

59 """ 

60 

61 name = "VCALENDAR" 

62 canonical_order = ( 

63 "VERSION", 

64 "PRODID", 

65 "CALSCALE", 

66 "METHOD", 

67 "DESCRIPTION", 

68 "X-WR-CALDESC", 

69 "NAME", 

70 "X-WR-CALNAME", 

71 ) 

72 required = ( 

73 "PRODID", 

74 "VERSION", 

75 ) 

76 singletons = ( 

77 "PRODID", 

78 "VERSION", 

79 "CALSCALE", 

80 "METHOD", 

81 "COLOR", # RFC 7986 

82 ) 

83 multiple = ( 

84 "CATEGORIES", # RFC 7986 

85 "DESCRIPTION", # RFC 7986 

86 "NAME", # RFC 7986 

87 ) 

88 

89 @classmethod 

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

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

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

93 

94 @classmethod 

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

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

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

98 

99 @overload 

100 @classmethod 

101 def from_ical( 

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

103 ) -> Calendar: ... 

104 

105 @overload 

106 @classmethod 

107 def from_ical( 

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

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

110 

111 @classmethod 

112 def from_ical( 

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

114 ) -> Calendar | list[Calendar]: 

115 """Parse iCalendar data into calendar instances. 

116 

117 Parameters: 

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

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

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

121 

122 Returns: 

123 Calendar or list of calendars. 

124 """ 

125 return cast( 

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

127 ) 

128 

129 @property 

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

131 """All event components in the calendar. 

132 

133 This is a shortcut to get all events. 

134 Modifications do not change the calendar. 

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

136 

137 >>> from icalendar import Calendar 

138 >>> calendar = Calendar.example() 

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

140 >>> event.start 

141 datetime.date(2022, 1, 1) 

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

143 New Year's Day 

144 """ 

145 return self.walk("VEVENT") 

146 

147 @property 

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

149 """All todo components in the calendar. 

150 

151 This is a shortcut to get all todos. 

152 Modifications do not change the calendar. 

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

154 """ 

155 return self.walk("VTODO") 

156 

157 @property 

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

159 """All journal components in the calendar. 

160 

161 This is a shortcut to get all journals. 

162 Modifications do not change the calendar. 

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

164 """ 

165 return self.walk("VJOURNAL") 

166 

167 @property 

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

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

170 

171 This is a shortcut to get all availabilities. 

172 Modifications do not change the calendar. 

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

174 """ 

175 return self.walk("VAVAILABILITY") 

176 

177 @property 

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

179 """All FreeBusy components in the calendar. 

180 

181 This is a shortcut to get all FreeBusy. 

182 Modifications do not change the calendar. 

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

184 """ 

185 return self.walk("VFREEBUSY") 

186 

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

188 """The set of TZIDs in use. 

189 

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

191 timezone information like the TZID parameter in all attributes. 

192 

193 >>> from icalendar import Calendar 

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

195 >>> calendar.get_used_tzids() 

196 {'posix/Europe/Vaduz'} 

197 

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

199 """ 

200 result = set() 

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

202 if hasattr(value, "params"): 

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

204 return result - {None} 

205 

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

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

208 

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

210 all of these timezones should be added. 

211 

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

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

214 """ 

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

216 for timezone in self.timezones: 

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

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

219 tzids.discard(timezone.tz_name) 

220 return tzids 

221 

222 @property 

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

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

225 

226 >>> from icalendar import Calendar 

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

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

229 ['custom_Pacific/Fiji'] 

230 

231 .. note:: 

232 

233 This is a read-only property. 

234 """ 

235 return self.walk("VTIMEZONE") 

236 

237 def add_missing_timezones( 

238 self, 

239 first_date: date = Timezone.DEFAULT_FIRST_DATE, 

240 last_date: date = Timezone.DEFAULT_LAST_DATE, 

241 ): 

242 """Add all missing VTIMEZONE components. 

243 

244 This adds all the timezone components that are required. 

245 VTIMEZONE components are inserted at the beginning of the calendar 

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

247 

248 .. note:: 

249 

250 Timezones that are not known will not be added. 

251 

252 Parameters: 

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

254 last_date: Later than anything happening in the calendar. 

255 

256 >>> from icalendar import Calendar, Event 

257 >>> from datetime import datetime 

258 >>> from zoneinfo import ZoneInfo 

259 >>> calendar = Calendar() 

260 >>> event = Event() 

261 >>> calendar.add_component(event) 

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

263 >>> calendar.timezones 

264 [] 

265 >>> calendar.add_missing_timezones() 

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

267 'Europe/Berlin' 

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

269 set() 

270 """ 

271 missing_tzids = self.get_missing_tzids() 

272 if not missing_tzids: 

273 return 

274 

275 existing_timezone_count = len(self.timezones) 

276 

277 for tzid in missing_tzids: 

278 try: 

279 timezone = Timezone.from_tzid( 

280 tzid, first_date=first_date, last_date=last_date 

281 ) 

282 except ValueError: 

283 continue 

284 self.subcomponents.insert(existing_timezone_count, timezone) 

285 existing_timezone_count += 1 

286 

287 calendar_name = multi_language_text_property( 

288 "NAME", 

289 "X-WR-CALNAME", 

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

291 

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

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

294 

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

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

297 

298 Example: 

299 Set the name of the calendar. 

300 

301 .. code-block:: pycon 

302 

303 >>> from icalendar import Calendar 

304 >>> calendar = Calendar() 

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

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

307 BEGIN:VCALENDAR 

308 NAME:My Calendar 

309 X-WR-CALNAME:My Calendar 

310 END:VCALENDAR 

311 

312 """, 

313 ) 

314 

315 description = multi_language_text_property( 

316 "DESCRIPTION", 

317 "X-WR-CALDESC", 

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

319 

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

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

322 compatibility. 

323 

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

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

326 

327 Example: 

328 Add a description to a calendar. 

329 

330 .. code-block:: pycon 

331 

332 >>> from icalendar import Calendar 

333 >>> calendar = Calendar() 

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

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

336 BEGIN:VCALENDAR 

337 DESCRIPTION:This is a calendar 

338 X-WR-CALDESC:This is a calendar 

339 END:VCALENDAR 

340 

341 """, 

342 ) 

343 

344 color = single_string_property( 

345 "COLOR", 

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

347 

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

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

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

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

352 

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

354 subcomponents may also carry their own color. 

355 

356 Example: 

357 .. code-block:: pycon 

358 

359 >>> from icalendar import Calendar 

360 >>> calendar = Calendar() 

361 >>> calendar.color = "black" 

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

363 BEGIN:VCALENDAR 

364 COLOR:black 

365 END:VCALENDAR 

366 

367 """, 

368 "X-APPLE-CALENDAR-COLOR", 

369 ) 

370 categories = categories_property 

371 uid = uid_property 

372 prodid = single_string_property( 

373 "PRODID", 

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

375 

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

377It's required exactly once per iCalendar object. 

378 

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

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

381unique string is acceptable. 

382 

383Example: 

384 Set a custom product identifier on a new calendar. 

385 

386 .. code-block:: pycon 

387 

388 >>> from icalendar import Calendar 

389 >>> cal = Calendar() 

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

391 >>> str(cal.prodid) 

392 '-//MyApp//MyCalendar//EN' 

393 

394See also: 

395 :attr:`version` 

396""", 

397 ) 

398 version = single_string_property( 

399 "VERSION", 

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

401 

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

403It's required exactly once per calendar object. 

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

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

406supported versions. 

407 

408Example: 

409 .. code-block:: pycon 

410 

411 >>> from icalendar import Calendar 

412 >>> cal = Calendar() 

413 >>> cal.version = "2.0" 

414 >>> str(cal.version) 

415 '2.0' 

416 

417See also: 

418 :attr:`prodid` 

419""", 

420 ) 

421 

422 calscale = single_string_property( 

423 "CALSCALE", 

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

425 

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

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

428 

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

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

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

432 

433Example: 

434 .. code-block:: pycon 

435 

436 >>> from icalendar import Calendar 

437 >>> cal = Calendar() 

438 >>> cal.calscale 

439 'GREGORIAN' 

440 

441 """, 

442 default="GREGORIAN", 

443 ) 

444 method = single_string_property( 

445 "METHOD", 

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

447 

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

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

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

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

452 

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

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

455data snapshot with no scheduling semantics. 

456 

457Example: 

458 .. code-block:: pycon 

459 

460 >>> from icalendar import Calendar 

461 >>> cal = Calendar() 

462 >>> cal.method = "REQUEST" 

463 >>> str(cal.method) 

464 'REQUEST' 

465 

466""", 

467 ) 

468 url = url_property 

469 source = source_property 

470 

471 @property 

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

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

474 

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

476 The value must be a positive duration. 

477 

478 Returns: 

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

480 

481 Raises: 

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

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

484 

485 Example: 

486 .. code-block:: pycon 

487 

488 >>> from datetime import timedelta 

489 >>> from icalendar import Calendar 

490 >>> cal = Calendar() 

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

492 >>> cal.refresh_interval 

493 datetime.timedelta(seconds=3600) 

494 

495 """ 

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

497 return refresh_interval.dt if refresh_interval else None 

498 

499 @refresh_interval.setter 

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

501 """Set the REFRESH-INTERVAL.""" 

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

503 raise TypeError( 

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

505 " or None to delete it." 

506 ) 

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

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

509 if value is not None: 

510 del self.refresh_interval 

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

512 else: 

513 del self.refresh_interval 

514 

515 @refresh_interval.deleter 

516 def refresh_interval(self): 

517 """Delete REFRESH-INTERVAL.""" 

518 self.pop("REFRESH-INTERVAL") 

519 

520 images = images_property 

521 

522 @classmethod 

523 def new( 

524 cls, 

525 /, 

526 calscale: str | None = None, 

527 categories: Sequence[str] = (), 

528 color: str | None = None, 

529 concepts: CONCEPTS_TYPE_SETTER = None, 

530 description: str | None = None, 

531 language: str | None = None, 

532 last_modified: date | datetime | None = None, 

533 links: LINKS_TYPE_SETTER = None, 

534 method: str | None = None, 

535 name: str | None = None, 

536 organization: str | None = None, 

537 prodid: str | None = None, 

538 refresh_interval: timedelta | None = None, 

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

540 related_to: RELATED_TO_TYPE_SETTER = None, 

541 source: str | None = None, 

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

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

544 url: str | None = None, 

545 version: str = "2.0", 

546 ): 

547 """Create a new Calendar. 

548 

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

550 

551 Parameters: 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

571 subcomponents: The subcomponents of the calendar. 

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

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

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

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

576 

577 Returns: 

578 :class:`Calendar` 

579 

580 Raises: 

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

582 

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

584 """ 

585 calendar: Calendar = super().new( 

586 last_modified=last_modified, 

587 links=links, 

588 related_to=related_to, 

589 refids=refids, 

590 concepts=concepts, 

591 subcomponents=subcomponents, 

592 ) 

593 

594 # Generate prodid if not provided but organization is given 

595 if prodid is None and organization: 

596 app_name = name or "Calendar" 

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

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

599 elif prodid is None: 

600 prodid = DEFAULT_PRODID 

601 

602 calendar.prodid = prodid 

603 calendar.version = version 

604 calendar.calendar_name = name 

605 calendar.color = color 

606 calendar.description = description 

607 calendar.method = method 

608 calendar.calscale = calscale 

609 calendar.categories = categories 

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

611 calendar.url = url 

612 calendar.refresh_interval = refresh_interval 

613 calendar.source = source 

614 

615 return calendar 

616 

617 def validate(self): 

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

619 

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

621 

622 Raises: 

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

624 components. 

625 """ 

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

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

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

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

630 if not self.subcomponents: 

631 raise IncompleteComponent( 

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

633 ) 

634 

635 

636__all__ = ["Calendar"]