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

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

149 statements  

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

2 

3from __future__ import annotations 

4 

5import uuid 

6from datetime import timedelta 

7from typing import TYPE_CHECKING, Literal, 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.version import __version__ 

26 

27if TYPE_CHECKING: 

28 from collections.abc import Iterable, Sequence 

29 from datetime import date, datetime 

30 

31 from icalendar.cal.availability import Availability 

32 from icalendar.cal.event import Event 

33 from icalendar.cal.free_busy import FreeBusy 

34 from icalendar.cal.todo import Todo 

35 

36 

37class Calendar(Component): 

38 """ 

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

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

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

42 other type of calendar component. 

43 

44 Examples: 

45 Create a new Calendar: 

46 

47 >>> from icalendar import Calendar 

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

49 >>> print(calendar.calendar_name) 

50 My Calendar 

51 

52 """ 

53 

54 name = "VCALENDAR" 

55 canonical_order = ( 

56 "VERSION", 

57 "PRODID", 

58 "CALSCALE", 

59 "METHOD", 

60 "DESCRIPTION", 

61 "X-WR-CALDESC", 

62 "NAME", 

63 "X-WR-CALNAME", 

64 ) 

65 required = ( 

66 "PRODID", 

67 "VERSION", 

68 ) 

69 singletons = ( 

70 "PRODID", 

71 "VERSION", 

72 "CALSCALE", 

73 "METHOD", 

74 "COLOR", # RFC 7986 

75 ) 

76 multiple = ( 

77 "CATEGORIES", # RFC 7986 

78 "DESCRIPTION", # RFC 7986 

79 "NAME", # RFC 7986 

80 ) 

81 

82 @classmethod 

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

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

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

86 

87 @overload 

88 @classmethod 

89 def from_ical( 

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

91 ) -> Calendar: ... 

92 

93 @overload 

94 @classmethod 

95 def from_ical(cls, st: bytes | str, multiple: Literal[True]) -> list[Calendar]: ... 

96 

97 @classmethod 

98 def from_ical( 

99 cls, 

100 st: bytes | str, 

101 multiple: bool = False, 

102 ) -> Calendar | list[Calendar]: 

103 """Parse iCalendar data into Calendar instances. 

104 

105 Wraps :meth:`Component.from_ical() 

106 <icalendar.cal.component.Component.from_ical>` with timezone 

107 forward-reference resolution and VTIMEZONE caching. 

108 

109 Parameters: 

110 st: iCalendar data as bytes or string 

111 multiple: If ``True``, returns list. If ``False``, returns single calendar. 

112 

113 Returns: 

114 Calendar or list of Calendars 

115 """ 

116 comps = Component.from_ical(st, multiple=True) 

117 all_timezones_so_far = True 

118 for comp in comps: 

119 for component in comp.subcomponents: 

120 if component.name == "VTIMEZONE": 

121 if not all_timezones_so_far: 

122 # If a preceding component refers to a VTIMEZONE defined 

123 # later in the source st 

124 # (forward references are allowed by RFC 5545), then the 

125 # earlier component may have 

126 # the wrong timezone attached. 

127 # However, during computation of comps, all VTIMEZONEs 

128 # observed do end up in 

129 # the timezone cache. So simply re-running from_ical will 

130 # rely on the cache 

131 # for those forward references to produce the correct result. 

132 # See test_create_america_new_york_forward_reference. 

133 return Component.from_ical(st, multiple) 

134 else: 

135 all_timezones_so_far = False 

136 

137 # No potentially forward VTIMEZONEs to worry about 

138 if multiple: 

139 return comps 

140 if len(comps) > 1: 

141 raise ValueError( 

142 cls._format_error( 

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

144 ) 

145 ) 

146 if len(comps) < 1: 

147 raise ValueError( 

148 cls._format_error( 

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

150 ) 

151 ) 

152 return comps[0] 

153 

154 @property 

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

156 """All event components in the calendar. 

157 

158 This is a shortcut to get all events. 

159 Modifications do not change the calendar. 

160 Use :py:meth:`Component.add_component`. 

161 

162 >>> from icalendar import Calendar 

163 >>> calendar = Calendar.example() 

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

165 >>> event.start 

166 datetime.date(2022, 1, 1) 

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

168 New Year's Day 

169 """ 

170 return self.walk("VEVENT") 

171 

172 @property 

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

174 """All todo components in the calendar. 

175 

176 This is a shortcut to get all todos. 

177 Modifications do not change the calendar. 

178 Use :py:meth:`Component.add_component`. 

179 """ 

180 return self.walk("VTODO") 

181 

182 @property 

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

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

185 

186 This is a shortcut to get all availabilities. 

187 Modifications do not change the calendar. 

188 Use :py:meth:`Component.add_component`. 

189 """ 

190 return self.walk("VAVAILABILITY") 

191 

192 @property 

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

194 """All FreeBusy components in the calendar. 

195 

196 This is a shortcut to get all FreeBusy. 

197 Modifications do not change the calendar. 

198 Use :py:meth:`Component.add_component`. 

199 """ 

200 return self.walk("VFREEBUSY") 

201 

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

203 """The set of TZIDs in use. 

204 

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

206 timezone information like the TZID parameter in all attributes. 

207 

208 >>> from icalendar import Calendar 

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

210 >>> calendar.get_used_tzids() 

211 {'posix/Europe/Vaduz'} 

212 

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

214 """ 

215 result = set() 

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

217 if hasattr(value, "params"): 

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

219 return result - {None} 

220 

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

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

223 

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

225 all of these timezones should be added. 

226 """ 

227 tzids = self.get_used_tzids() 

228 for timezone in self.timezones: 

229 tzids.remove(timezone.tz_name) 

230 return tzids 

231 

232 @property 

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

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

235 

236 >>> from icalendar import Calendar 

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

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

239 ['custom_Pacific/Fiji'] 

240 

241 .. note:: 

242 

243 This is a read-only property. 

244 """ 

245 return self.walk("VTIMEZONE") 

246 

247 def add_missing_timezones( 

248 self, 

249 first_date: date = Timezone.DEFAULT_FIRST_DATE, 

250 last_date: date = Timezone.DEFAULT_LAST_DATE, 

251 ): 

252 """Add all missing VTIMEZONE components. 

253 

254 This adds all the timezone components that are required. 

255 VTIMEZONE components are inserted at the beginning of the calendar 

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

257 

258 .. note:: 

259 

260 Timezones that are not known will not be added. 

261 

262 :param first_date: earlier than anything that happens in the calendar 

263 :param last_date: later than anything happening in the calendar 

264 

265 >>> from icalendar import Calendar, Event 

266 >>> from datetime import datetime 

267 >>> from zoneinfo import ZoneInfo 

268 >>> calendar = Calendar() 

269 >>> event = Event() 

270 >>> calendar.add_component(event) 

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

272 >>> calendar.timezones 

273 [] 

274 >>> calendar.add_missing_timezones() 

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

276 'Europe/Berlin' 

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

278 set() 

279 """ 

280 missing_tzids = self.get_missing_tzids() 

281 if not missing_tzids: 

282 return 

283 

284 existing_timezone_count = len(self.timezones) 

285 

286 for tzid in missing_tzids: 

287 try: 

288 timezone = Timezone.from_tzid( 

289 tzid, first_date=first_date, last_date=last_date 

290 ) 

291 except ValueError: 

292 continue 

293 self.subcomponents.insert(existing_timezone_count, timezone) 

294 existing_timezone_count += 1 

295 

296 calendar_name = multi_language_text_property( 

297 "NAME", 

298 "X-WR-CALNAME", 

299 """This property specifies the name of the calendar. 

300 

301 This implements :rfc:`7986` ``NAME`` and ``X-WR-CALNAME``. 

302 

303 Property Parameters: 

304 IANA, non-standard, alternate text 

305 representation, and language property parameters can be specified 

306 on this property. 

307 

308 Conformance: 

309 This property can be specified multiple times in an 

310 iCalendar object. However, each property MUST represent the name 

311 of the calendar in a different language. 

312 

313 Description: 

314 This property is used to specify a name of the 

315 iCalendar object that can be used by calendar user agents when 

316 presenting the calendar data to a user. Whilst a calendar only 

317 has a single name, multiple language variants can be specified by 

318 including this property multiple times with different "LANGUAGE" 

319 parameter values on each. 

320 

321 Example: 

322 Below, we set the name of the calendar. 

323 

324 .. code-block:: pycon 

325 

326 >>> from icalendar import Calendar 

327 >>> calendar = Calendar() 

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

329 >>> print(calendar.to_ical()) 

330 BEGIN:VCALENDAR 

331 NAME:My Calendar 

332 END:VCALENDAR 

333 """, 

334 ) 

335 

336 description = multi_language_text_property( 

337 "DESCRIPTION", 

338 "X-WR-CALDESC", 

339 """This property specifies the description of the calendar. 

340 

341 This implements :rfc:`7986` ``DESCRIPTION`` and ``X-WR-CALDESC``. 

342 

343 Conformance: 

344 This property can be specified multiple times in an 

345 iCalendar object. However, each property MUST represent the 

346 description of the calendar in a different language. 

347 

348 Description: 

349 This property is used to specify a lengthy textual 

350 description of the iCalendar object that can be used by calendar 

351 user agents when describing the nature of the calendar data to a 

352 user. Whilst a calendar only has a single description, multiple 

353 language variants can be specified by including this property 

354 multiple times with different "LANGUAGE" parameter values on each. 

355 

356 Example: 

357 Below, we add a description to a calendar. 

358 

359 .. code-block:: pycon 

360 

361 >>> from icalendar import Calendar 

362 >>> calendar = Calendar() 

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

364 >>> print(calendar.to_ical()) 

365 BEGIN:VCALENDAR 

366 DESCRIPTION:This is a calendar 

367 END:VCALENDAR 

368 """, 

369 ) 

370 

371 color = single_string_property( 

372 "COLOR", 

373 """This property specifies a color used for displaying the calendar. 

374 

375 This implements :rfc:`7986` ``COLOR`` and ``X-APPLE-CALENDAR-COLOR``. 

376 Please note that since :rfc:`7986`, subcomponents can have their own color. 

377 

378 Property Parameters: 

379 IANA and non-standard property parameters can 

380 be specified on this property. 

381 

382 Conformance: 

383 This property can be specified once in an iCalendar 

384 object or in ``VEVENT``, ``VTODO``, or ``VJOURNAL`` calendar components. 

385 

386 Description: 

387 This property specifies a color that clients MAY use 

388 when presenting the relevant data to a user. Typically, this 

389 would appear as the "background" color of events or tasks. The 

390 value is a case-insensitive color name taken from the CSS3 set of 

391 names, defined in Section 4.3 of `W3C.REC-css3-color-20110607 <https://www.w3.org/TR/css-color-3/>`_. 

392 

393 Example: 

394 ``"turquoise"``, ``"#ffffff"`` 

395 

396 .. code-block:: pycon 

397 

398 >>> from icalendar import Calendar 

399 >>> calendar = Calendar() 

400 >>> calendar.color = "black" 

401 >>> print(calendar.to_ical()) 

402 BEGIN:VCALENDAR 

403 COLOR:black 

404 END:VCALENDAR 

405 

406 """, 

407 "X-APPLE-CALENDAR-COLOR", 

408 ) 

409 categories = categories_property 

410 uid = uid_property 

411 prodid = single_string_property( 

412 "PRODID", 

413 """PRODID specifies the identifier for the product that created the iCalendar object. 

414 

415Conformance: 

416 The property MUST be specified once in an iCalendar object. 

417 

418Description: 

419 The vendor of the implementation SHOULD assure that 

420 this is a globally unique identifier; using some technique such as 

421 an FPI value, as defined in [ISO.9070.1991]. 

422 

423 This property SHOULD NOT be used to alter the interpretation of an 

424 iCalendar object beyond the semantics specified in this memo. For 

425 example, it is not to be used to further the understanding of non- 

426 standard properties. 

427 

428Example: 

429 The following is an example of this property. It does not 

430 imply that English is the default language. 

431 

432 .. code-block:: text 

433 

434 -//ABC Corporation//NONSGML My Product//EN 

435""", 

436 ) 

437 version = single_string_property( 

438 "VERSION", 

439 """VERSION of the calendar specification. 

440 

441The default is ``"2.0"`` for :rfc:`5545`. 

442 

443Purpose: 

444 This property specifies the identifier corresponding to the 

445 highest version number or the minimum and maximum range of the 

446 iCalendar specification that is required in order to interpret the 

447 iCalendar object. 

448 

449 

450 """, 

451 ) 

452 

453 calscale = single_string_property( 

454 "CALSCALE", 

455 """CALSCALE defines the calendar scale used for the calendar information specified in the iCalendar object. 

456 

457Compatibility: 

458 :rfc:`7529` makes the case that GREGORIAN stays the default and other calendar scales 

459 are implemented on the RRULE. 

460 

461Conformance: 

462 This property can be specified once in an iCalendar 

463 object. The default value is "GREGORIAN". 

464 

465Description: 

466 This memo is based on the Gregorian calendar scale. 

467 The Gregorian calendar scale is assumed if this property is not 

468 specified in the iCalendar object. It is expected that other 

469 calendar scales will be defined in other specifications or by 

470 future versions of this memo. 

471 """, 

472 default="GREGORIAN", 

473 ) 

474 method = single_string_property( 

475 "METHOD", 

476 """METHOD defines the iCalendar object method associated with the calendar object. 

477 

478Description: 

479 When used in a MIME message entity, the value of this 

480 property MUST be the same as the Content-Type "method" parameter 

481 value. If either the "METHOD" property or the Content-Type 

482 "method" parameter is specified, then the other MUST also be 

483 specified. 

484 

485 No methods are defined by this specification. This is the subject 

486 of other specifications, such as the iCalendar Transport- 

487 independent Interoperability Protocol (iTIP) defined by :rfc:`5546`. 

488 

489 If this property is not present in the iCalendar object, then a 

490 scheduling transaction MUST NOT be assumed. In such cases, the 

491 iCalendar object is merely being used to transport a snapshot of 

492 some calendar information; without the intention of conveying a 

493 scheduling semantic. 

494""", 

495 ) 

496 url = url_property 

497 source = source_property 

498 

499 @property 

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

501 """REFRESH-INTERVAL specifies a suggested minimum interval for 

502 polling for changes of the calendar data from the original source 

503 of that data. 

504 

505 Conformance: 

506 This property can be specified once in an iCalendar 

507 object, consisting of a positive duration of time. 

508 

509 Description: 

510 This property specifies a positive duration that gives 

511 a suggested minimum polling interval for checking for updates to 

512 the calendar data. The value of this property SHOULD be used by 

513 calendar user agents to limit the polling interval for calendar 

514 data updates to the minimum interval specified. 

515 

516 Raises: 

517 ValueError: When setting a negative duration. 

518 """ 

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

520 return refresh_interval.dt if refresh_interval else None 

521 

522 @refresh_interval.setter 

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

524 """Set the REFRESH-INTERVAL.""" 

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

526 raise TypeError( 

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

528 " or None to delete it." 

529 ) 

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

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

532 if value is not None: 

533 del self.refresh_interval 

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

535 else: 

536 del self.refresh_interval 

537 

538 @refresh_interval.deleter 

539 def refresh_interval(self): 

540 """Delete REFRESH-INTERVAL.""" 

541 self.pop("REFRESH-INTERVAL") 

542 

543 images = images_property 

544 

545 @classmethod 

546 def new( 

547 cls, 

548 /, 

549 calscale: str | None = None, 

550 categories: Sequence[str] = (), 

551 color: str | None = None, 

552 concepts: CONCEPTS_TYPE_SETTER = None, 

553 description: str | None = None, 

554 language: str | None = None, 

555 last_modified: date | datetime | None = None, 

556 links: LINKS_TYPE_SETTER = None, 

557 method: str | None = None, 

558 name: str | None = None, 

559 organization: str | None = None, 

560 prodid: str | None = None, 

561 refresh_interval: timedelta | None = None, 

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

563 related_to: RELATED_TO_TYPE_SETTER = None, 

564 source: str | None = None, 

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

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

567 url: str | None = None, 

568 version: str = "2.0", 

569 ): 

570 """Create a new Calendar with all required properties. 

571 

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

573 

574 Parameters: 

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

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

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

578 concepts: The :attr:`~icalendar.Component.concepts` of the calendar. 

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

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

581 last_modified: The :attr:`~icalendar.Component.last_modified` of the calendar. 

582 links: The :attr:`~icalendar.Component.links` of the calendar. 

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

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

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

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

587 generates a `prodid` in format "-//organization//name//language". 

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

589 refids: :attr:`~icalendar.Component.refids` of the calendar. 

590 related_to: :attr:`~icalendar.Component.related_to` of the calendar. 

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

592 subcomponents: The subcomponents of the calendar. 

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

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

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

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

597 

598 Returns: 

599 :class:`Calendar` 

600 

601 Raises: 

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

603 

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

605 """ 

606 calendar: Calendar = super().new( 

607 last_modified=last_modified, 

608 links=links, 

609 related_to=related_to, 

610 refids=refids, 

611 concepts=concepts, 

612 ) 

613 

614 # Generate prodid if not provided but organization is given 

615 if prodid is None and organization: 

616 app_name = name or "Calendar" 

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

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

619 elif prodid is None: 

620 prodid = f"-//collective//icalendar//{__version__}//EN" 

621 

622 calendar.prodid = prodid 

623 calendar.version = version 

624 calendar.calendar_name = name 

625 calendar.color = color 

626 calendar.description = description 

627 calendar.method = method 

628 calendar.calscale = calscale 

629 calendar.categories = categories 

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

631 calendar.url = url 

632 calendar.refresh_interval = refresh_interval 

633 calendar.source = source 

634 if subcomponents is not None: 

635 calendar.subcomponents = list(subcomponents) 

636 

637 return calendar 

638 

639 def validate(self): 

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

641 

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

643 

644 Raises: 

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

646 components. 

647 """ 

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

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

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

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

652 if not self.subcomponents: 

653 raise IncompleteComponent( 

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

655 ) 

656 

657 

658__all__ = ["Calendar"]