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, # noqa: FBT001 

102 ) -> Calendar | list[Calendar]: 

103 """Parse iCalendar data into Calendar instances. 

104 

105 Wraps :meth:`Component.from_ical() <icalendar.cal.component.Component.from_ical>` with 

106 timezone forward-reference resolution and VTIMEZONE caching. 

107 

108 Parameters: 

109 st: iCalendar data as bytes or string 

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

111 

112 Returns: 

113 Calendar or list of Calendars 

114 """ 

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

116 all_timezones_so_far = True 

117 for comp in comps: 

118 for component in comp.subcomponents: 

119 if component.name == "VTIMEZONE": 

120 if not all_timezones_so_far: 

121 # If a preceding component refers to a VTIMEZONE defined 

122 # later in the source st 

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

124 # earlier component may have 

125 # the wrong timezone attached. 

126 # However, during computation of comps, all VTIMEZONEs 

127 # observed do end up in 

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

129 # rely on the cache 

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

131 # See test_create_america_new_york_forward_reference. 

132 return Component.from_ical(st, multiple) 

133 else: 

134 all_timezones_so_far = False 

135 

136 # No potentially forward VTIMEZONEs to worry about 

137 if multiple: 

138 return comps 

139 if len(comps) > 1: 

140 raise ValueError( 

141 cls._format_error( 

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

143 ) 

144 ) 

145 if len(comps) < 1: 

146 raise ValueError( 

147 cls._format_error( 

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

149 ) 

150 ) 

151 return comps[0] 

152 

153 @property 

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

155 """All event components in the calendar. 

156 

157 This is a shortcut to get all events. 

158 Modifications do not change the calendar. 

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

160 

161 >>> from icalendar import Calendar 

162 >>> calendar = Calendar.example() 

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

164 >>> event.start 

165 datetime.date(2022, 1, 1) 

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

167 New Year's Day 

168 """ 

169 return self.walk("VEVENT") 

170 

171 @property 

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

173 """All todo components in the calendar. 

174 

175 This is a shortcut to get all todos. 

176 Modifications do not change the calendar. 

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

178 """ 

179 return self.walk("VTODO") 

180 

181 @property 

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

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

184 

185 This is a shortcut to get all availabilities. 

186 Modifications do not change the calendar. 

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

188 """ 

189 return self.walk("VAVAILABILITY") 

190 

191 @property 

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

193 """All FreeBusy components in the calendar. 

194 

195 This is a shortcut to get all FreeBusy. 

196 Modifications do not change the calendar. 

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

198 """ 

199 return self.walk("VFREEBUSY") 

200 

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

202 """The set of TZIDs in use. 

203 

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

205 timezone information like the TZID parameter in all attributes. 

206 

207 >>> from icalendar import Calendar 

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

209 >>> calendar.get_used_tzids() 

210 {'posix/Europe/Vaduz'} 

211 

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

213 """ 

214 result = set() 

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

216 if hasattr(value, "params"): 

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

218 return result - {None} 

219 

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

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

222 

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

224 all of these timezones should be added. 

225 """ 

226 tzids = self.get_used_tzids() 

227 for timezone in self.timezones: 

228 tzids.remove(timezone.tz_name) 

229 return tzids 

230 

231 @property 

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

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

234 

235 >>> from icalendar import Calendar 

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

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

238 ['custom_Pacific/Fiji'] 

239 

240 .. note:: 

241 

242 This is a read-only property. 

243 """ 

244 return self.walk("VTIMEZONE") 

245 

246 def add_missing_timezones( 

247 self, 

248 first_date: date = Timezone.DEFAULT_FIRST_DATE, 

249 last_date: date = Timezone.DEFAULT_LAST_DATE, 

250 ): 

251 """Add all missing VTIMEZONE components. 

252 

253 This adds all the timezone components that are required. 

254 VTIMEZONE components are inserted at the beginning of the calendar 

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

256 

257 .. note:: 

258 

259 Timezones that are not known will not be added. 

260 

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

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

263 

264 >>> from icalendar import Calendar, Event 

265 >>> from datetime import datetime 

266 >>> from zoneinfo import ZoneInfo 

267 >>> calendar = Calendar() 

268 >>> event = Event() 

269 >>> calendar.add_component(event) 

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

271 >>> calendar.timezones 

272 [] 

273 >>> calendar.add_missing_timezones() 

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

275 'Europe/Berlin' 

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

277 set() 

278 """ 

279 missing_tzids = self.get_missing_tzids() 

280 if not missing_tzids: 

281 return 

282 

283 existing_timezone_count = len(self.timezones) 

284 

285 for tzid in missing_tzids: 

286 try: 

287 timezone = Timezone.from_tzid( 

288 tzid, first_date=first_date, last_date=last_date 

289 ) 

290 except ValueError: 

291 continue 

292 self.subcomponents.insert(existing_timezone_count, timezone) 

293 existing_timezone_count += 1 

294 

295 calendar_name = multi_language_text_property( 

296 "NAME", 

297 "X-WR-CALNAME", 

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

299 

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

301 

302 Property Parameters: 

303 IANA, non-standard, alternate text 

304 representation, and language property parameters can be specified 

305 on this property. 

306 

307 Conformance: 

308 This property can be specified multiple times in an 

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

310 of the calendar in a different language. 

311 

312 Description: 

313 This property is used to specify a name of the 

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

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

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

317 including this property multiple times with different "LANGUAGE" 

318 parameter values on each. 

319 

320 Example: 

321 Below, we set the name of the calendar. 

322 

323 .. code-block:: pycon 

324 

325 >>> from icalendar import Calendar 

326 >>> calendar = Calendar() 

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

328 >>> print(calendar.to_ical()) 

329 BEGIN:VCALENDAR 

330 NAME:My Calendar 

331 END:VCALENDAR 

332 """, 

333 ) 

334 

335 description = multi_language_text_property( 

336 "DESCRIPTION", 

337 "X-WR-CALDESC", 

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

339 

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

341 

342 Conformance: 

343 This property can be specified multiple times in an 

344 iCalendar object. However, each property MUST represent the 

345 description of the calendar in a different language. 

346 

347 Description: 

348 This property is used to specify a lengthy textual 

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

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

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

352 language variants can be specified by including this property 

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

354 

355 Example: 

356 Below, we add a description to a calendar. 

357 

358 .. code-block:: pycon 

359 

360 >>> from icalendar import Calendar 

361 >>> calendar = Calendar() 

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

363 >>> print(calendar.to_ical()) 

364 BEGIN:VCALENDAR 

365 DESCRIPTION:This is a calendar 

366 END:VCALENDAR 

367 """, 

368 ) 

369 

370 color = single_string_property( 

371 "COLOR", 

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

373 

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

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

376 

377 Property Parameters: 

378 IANA and non-standard property parameters can 

379 be specified on this property. 

380 

381 Conformance: 

382 This property can be specified once in an iCalendar 

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

384 

385 Description: 

386 This property specifies a color that clients MAY use 

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

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

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

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

391 

392 Example: 

393 ``"turquoise"``, ``"#ffffff"`` 

394 

395 .. code-block:: pycon 

396 

397 >>> from icalendar import Calendar 

398 >>> calendar = Calendar() 

399 >>> calendar.color = "black" 

400 >>> print(calendar.to_ical()) 

401 BEGIN:VCALENDAR 

402 COLOR:black 

403 END:VCALENDAR 

404 

405 """, 

406 "X-APPLE-CALENDAR-COLOR", 

407 ) 

408 categories = categories_property 

409 uid = uid_property 

410 prodid = single_string_property( 

411 "PRODID", 

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

413 

414Conformance: 

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

416 

417Description: 

418 The vendor of the implementation SHOULD assure that 

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

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

421 

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

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

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

425 standard properties. 

426 

427Example: 

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

429 imply that English is the default language. 

430 

431 .. code-block:: text 

432 

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

434""", # noqa: E501 

435 ) 

436 version = single_string_property( 

437 "VERSION", 

438 """VERSION of the calendar specification. 

439 

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

441 

442Purpose: 

443 This property specifies the identifier corresponding to the 

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

445 iCalendar specification that is required in order to interpret the 

446 iCalendar object. 

447 

448 

449 """, 

450 ) 

451 

452 calscale = single_string_property( 

453 "CALSCALE", 

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

455 

456Compatibility: 

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

458 are implemented on the RRULE. 

459 

460Conformance: 

461 This property can be specified once in an iCalendar 

462 object. The default value is "GREGORIAN". 

463 

464Description: 

465 This memo is based on the Gregorian calendar scale. 

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

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

468 calendar scales will be defined in other specifications or by 

469 future versions of this memo. 

470 """, # noqa: E501 

471 default="GREGORIAN", 

472 ) 

473 method = single_string_property( 

474 "METHOD", 

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

476 

477Description: 

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

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

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

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

482 specified. 

483 

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

485 of other specifications, such as the iCalendar Transport- 

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

487 

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

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

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

491 some calendar information; without the intention of conveying a 

492 scheduling semantic. 

493""", # noqa: E501 

494 ) 

495 url = url_property 

496 source = source_property 

497 

498 @property 

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

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

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

502 of that data. 

503 

504 Conformance: 

505 This property can be specified once in an iCalendar 

506 object, consisting of a positive duration of time. 

507 

508 Description: 

509 This property specifies a positive duration that gives 

510 a suggested minimum polling interval for checking for updates to 

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

512 calendar user agents to limit the polling interval for calendar 

513 data updates to the minimum interval specified. 

514 

515 Raises: 

516 ValueError: When setting a negative duration. 

517 """ 

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

519 return refresh_interval.dt if refresh_interval else None 

520 

521 @refresh_interval.setter 

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

523 """Set the REFRESH-INTERVAL.""" 

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

525 raise TypeError( 

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

527 " or None to delete it." 

528 ) 

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

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

531 if value is not None: 

532 del self.refresh_interval 

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

534 else: 

535 del self.refresh_interval 

536 

537 @refresh_interval.deleter 

538 def refresh_interval(self): 

539 """Delete REFRESH-INTERVAL.""" 

540 self.pop("REFRESH-INTERVAL") 

541 

542 images = images_property 

543 

544 @classmethod 

545 def new( 

546 cls, 

547 /, 

548 calscale: str | None = None, 

549 categories: Sequence[str] = (), 

550 color: str | None = None, 

551 concepts: CONCEPTS_TYPE_SETTER = None, 

552 description: str | None = None, 

553 language: str | None = None, 

554 last_modified: date | datetime | None = None, 

555 links: LINKS_TYPE_SETTER = None, 

556 method: str | None = None, 

557 name: str | None = None, 

558 organization: str | None = None, 

559 prodid: str | None = None, 

560 refresh_interval: timedelta | None = None, 

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

562 related_to: RELATED_TO_TYPE_SETTER = None, 

563 source: str | None = None, 

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

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

566 url: str | None = None, 

567 version: str = "2.0", 

568 ): 

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

570 

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

572 

573 Parameters: 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

591 subcomponents: The subcomponents of the calendar. 

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

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

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

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

596 

597 Returns: 

598 :class:`Calendar` 

599 

600 Raises: 

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

602 

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

604 """ # noqa: E501 

605 calendar: Calendar = super().new( 

606 last_modified=last_modified, 

607 links=links, 

608 related_to=related_to, 

609 refids=refids, 

610 concepts=concepts, 

611 ) 

612 

613 # Generate prodid if not provided but organization is given 

614 if prodid is None and organization: 

615 app_name = name or "Calendar" 

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

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

618 elif prodid is None: 

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

620 

621 calendar.prodid = prodid 

622 calendar.version = version 

623 calendar.calendar_name = name 

624 calendar.color = color 

625 calendar.description = description 

626 calendar.method = method 

627 calendar.calscale = calscale 

628 calendar.categories = categories 

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

630 calendar.url = url 

631 calendar.refresh_interval = refresh_interval 

632 calendar.source = source 

633 if subcomponents is not None: 

634 calendar.subcomponents = list(subcomponents) 

635 

636 return calendar 

637 

638 def validate(self): 

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

640 

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

642 

643 Raises: 

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

645 components. 

646 """ 

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

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

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

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

651 if not self.subcomponents: 

652 raise IncompleteComponent( 

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

654 ) 

655 

656 

657__all__ = ["Calendar"]