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

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

140 statements  

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

2 

3from __future__ import annotations 

4 

5from datetime import timedelta 

6from typing import TYPE_CHECKING, Sequence 

7 

8from icalendar.attr import ( 

9 CONCEPTS_TYPE_SETTER, 

10 LINKS_TYPE_SETTER, 

11 RELATED_TO_TYPE_SETTER, 

12 categories_property, 

13 images_property, 

14 multi_language_text_property, 

15 single_string_property, 

16 source_property, 

17 uid_property, 

18 url_property, 

19) 

20from icalendar.cal.component import Component 

21from icalendar.cal.examples import get_example 

22from icalendar.cal.timezone import Timezone 

23from icalendar.error import IncompleteComponent 

24from icalendar.version import __version__ 

25 

26if TYPE_CHECKING: 

27 import uuid 

28 from datetime import date, datetime 

29 

30 from icalendar.cal.availability import Availability 

31 from icalendar.cal.event import Event 

32 from icalendar.cal.free_busy import FreeBusy 

33 from icalendar.cal.todo import Todo 

34 

35 

36class Calendar(Component): 

37 """ 

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

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

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

41 other type of calendar component. 

42 

43 Examples: 

44 Create a new Calendar: 

45 

46 >>> from icalendar import Calendar 

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

48 >>> print(calendar.calendar_name) 

49 My Calendar 

50 

51 """ 

52 

53 name = "VCALENDAR" 

54 canonical_order = ( 

55 "VERSION", 

56 "PRODID", 

57 "CALSCALE", 

58 "METHOD", 

59 "DESCRIPTION", 

60 "X-WR-CALDESC", 

61 "NAME", 

62 "X-WR-CALNAME", 

63 ) 

64 required = ( 

65 "PRODID", 

66 "VERSION", 

67 ) 

68 singletons = ( 

69 "PRODID", 

70 "VERSION", 

71 "CALSCALE", 

72 "METHOD", 

73 "COLOR", # RFC 7986 

74 ) 

75 multiple = ( 

76 "CATEGORIES", # RFC 7986 

77 "DESCRIPTION", # RFC 7986 

78 "NAME", # RFC 7986 

79 ) 

80 

81 @classmethod 

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

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

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

85 

86 @classmethod 

87 def from_ical(cls, st, multiple=False): 

88 """Parse iCalendar data into Calendar instances. 

89 

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

91 timezone forward-reference resolution and VTIMEZONE caching. 

92 

93 Args: 

94 st: iCalendar data as bytes or string 

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

96 

97 Returns: 

98 Calendar or list of Calendars 

99 """ 

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

101 all_timezones_so_far = True 

102 for comp in comps: 

103 for component in comp.subcomponents: 

104 if component.name == "VTIMEZONE": 

105 if not all_timezones_so_far: 

106 # If a preceding component refers to a VTIMEZONE defined 

107 # later in the source st 

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

109 # earlier component may have 

110 # the wrong timezone attached. 

111 # However, during computation of comps, all VTIMEZONEs 

112 # observed do end up in 

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

114 # rely on the cache 

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

116 # See test_create_america_new_york_forward_reference. 

117 return Component.from_ical(st, multiple) 

118 else: 

119 all_timezones_so_far = False 

120 

121 # No potentially forward VTIMEZONEs to worry about 

122 if multiple: 

123 return comps 

124 if len(comps) > 1: 

125 raise ValueError( 

126 cls._format_error( 

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

128 ) 

129 ) 

130 if len(comps) < 1: 

131 raise ValueError( 

132 cls._format_error( 

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

134 ) 

135 ) 

136 return comps[0] 

137 

138 @property 

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

140 """All event components in the calendar. 

141 

142 This is a shortcut to get all events. 

143 Modifications do not change the calendar. 

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

145 

146 >>> from icalendar import Calendar 

147 >>> calendar = Calendar.example() 

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

149 >>> event.start 

150 datetime.date(2022, 1, 1) 

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

152 New Year's Day 

153 """ 

154 return self.walk("VEVENT") 

155 

156 @property 

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

158 """All todo components in the calendar. 

159 

160 This is a shortcut to get all todos. 

161 Modifications do not change the calendar. 

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

163 """ 

164 return self.walk("VTODO") 

165 

166 @property 

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

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

169 

170 This is a shortcut to get all availabilities. 

171 Modifications do not change the calendar. 

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

173 """ 

174 return self.walk("VAVAILABILITY") 

175 

176 @property 

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

178 """All FreeBusy components in the calendar. 

179 

180 This is a shortcut to get all FreeBusy. 

181 Modifications do not change the calendar. 

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

183 """ 

184 return self.walk("VFREEBUSY") 

185 

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

187 """The set of TZIDs in use. 

188 

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

190 timezone information like the TZID parameter in all attributes. 

191 

192 >>> from icalendar import Calendar 

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

194 >>> calendar.get_used_tzids() 

195 {'posix/Europe/Vaduz'} 

196 

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

198 """ 

199 result = set() 

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

201 if hasattr(value, "params"): 

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

203 return result - {None} 

204 

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

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

207 

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

209 all of these timezones should be added. 

210 """ 

211 tzids = self.get_used_tzids() 

212 for timezone in self.timezones: 

213 tzids.remove(timezone.tz_name) 

214 return tzids 

215 

216 @property 

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

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

219 

220 >>> from icalendar import Calendar 

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

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

223 ['custom_Pacific/Fiji'] 

224 

225 .. note:: 

226 

227 This is a read-only property. 

228 """ 

229 return self.walk("VTIMEZONE") 

230 

231 def add_missing_timezones( 

232 self, 

233 first_date: date = Timezone.DEFAULT_FIRST_DATE, 

234 last_date: date = Timezone.DEFAULT_LAST_DATE, 

235 ): 

236 """Add all missing VTIMEZONE components. 

237 

238 This adds all the timezone components that are required. 

239 VTIMEZONE components are inserted at the beginning of the calendar 

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

241 

242 .. note:: 

243 

244 Timezones that are not known will not be added. 

245 

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

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

248 

249 >>> from icalendar import Calendar, Event 

250 >>> from datetime import datetime 

251 >>> from zoneinfo import ZoneInfo 

252 >>> calendar = Calendar() 

253 >>> event = Event() 

254 >>> calendar.add_component(event) 

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

256 >>> calendar.timezones 

257 [] 

258 >>> calendar.add_missing_timezones() 

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

260 'Europe/Berlin' 

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

262 set() 

263 """ 

264 missing_tzids = self.get_missing_tzids() 

265 if not missing_tzids: 

266 return 

267 

268 existing_timezone_count = len(self.timezones) 

269 

270 for tzid in missing_tzids: 

271 try: 

272 timezone = Timezone.from_tzid( 

273 tzid, first_date=first_date, last_date=last_date 

274 ) 

275 except ValueError: 

276 continue 

277 self.subcomponents.insert(existing_timezone_count, timezone) 

278 existing_timezone_count += 1 

279 

280 calendar_name = multi_language_text_property( 

281 "NAME", 

282 "X-WR-CALNAME", 

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

284 

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

286 

287 Property Parameters: 

288 IANA, non-standard, alternate text 

289 representation, and language property parameters can be specified 

290 on this property. 

291 

292 Conformance: 

293 This property can be specified multiple times in an 

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

295 of the calendar in a different language. 

296 

297 Description: 

298 This property is used to specify a name of the 

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

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

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

302 including this property multiple times with different "LANGUAGE" 

303 parameter values on each. 

304 

305 Example: 

306 Below, we set the name of the calendar. 

307 

308 .. code-block:: pycon 

309 

310 >>> from icalendar import Calendar 

311 >>> calendar = Calendar() 

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

313 >>> print(calendar.to_ical()) 

314 BEGIN:VCALENDAR 

315 NAME:My Calendar 

316 END:VCALENDAR 

317 """, 

318 ) 

319 

320 description = multi_language_text_property( 

321 "DESCRIPTION", 

322 "X-WR-CALDESC", 

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

324 

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

326 

327 Conformance: 

328 This property can be specified multiple times in an 

329 iCalendar object. However, each property MUST represent the 

330 description of the calendar in a different language. 

331 

332 Description: 

333 This property is used to specify a lengthy textual 

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

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

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

337 language variants can be specified by including this property 

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

339 

340 Example: 

341 Below, we add a description to a calendar. 

342 

343 .. code-block:: pycon 

344 

345 >>> from icalendar import Calendar 

346 >>> calendar = Calendar() 

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

348 >>> print(calendar.to_ical()) 

349 BEGIN:VCALENDAR 

350 DESCRIPTION:This is a calendar 

351 END:VCALENDAR 

352 """, 

353 ) 

354 

355 color = single_string_property( 

356 "COLOR", 

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

358 

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

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

361 

362 Property Parameters: 

363 IANA and non-standard property parameters can 

364 be specified on this property. 

365 

366 Conformance: 

367 This property can be specified once in an iCalendar 

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

369 

370 Description: 

371 This property specifies a color that clients MAY use 

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

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

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

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

376 

377 Example: 

378 ``"turquoise"``, ``"#ffffff"`` 

379 

380 .. code-block:: pycon 

381 

382 >>> from icalendar import Calendar 

383 >>> calendar = Calendar() 

384 >>> calendar.color = "black" 

385 >>> print(calendar.to_ical()) 

386 BEGIN:VCALENDAR 

387 COLOR:black 

388 END:VCALENDAR 

389 

390 """, 

391 "X-APPLE-CALENDAR-COLOR", 

392 ) 

393 categories = categories_property 

394 uid = uid_property 

395 prodid = single_string_property( 

396 "PRODID", 

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

398 

399Conformance: 

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

401 

402Description: 

403 The vendor of the implementation SHOULD assure that 

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

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

406 

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

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

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

410 standard properties. 

411 

412Example: 

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

414 imply that English is the default language. 

415 

416 .. code-block:: text 

417 

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

419""", # noqa: E501 

420 ) 

421 version = single_string_property( 

422 "VERSION", 

423 """VERSION of the calendar specification. 

424 

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

426 

427Purpose: 

428 This property specifies the identifier corresponding to the 

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

430 iCalendar specification that is required in order to interpret the 

431 iCalendar object. 

432 

433 

434 """, 

435 ) 

436 

437 calscale = single_string_property( 

438 "CALSCALE", 

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

440 

441Compatibility: 

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

443 are implemented on the RRULE. 

444 

445Conformance: 

446 This property can be specified once in an iCalendar 

447 object. The default value is "GREGORIAN". 

448 

449Description: 

450 This memo is based on the Gregorian calendar scale. 

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

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

453 calendar scales will be defined in other specifications or by 

454 future versions of this memo. 

455 """, # noqa: E501 

456 default="GREGORIAN", 

457 ) 

458 method = single_string_property( 

459 "METHOD", 

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

461 

462Description: 

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

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

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

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

467 specified. 

468 

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

470 of other specifications, such as the iCalendar Transport- 

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

472 

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

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

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

476 some calendar information; without the intention of conveying a 

477 scheduling semantic. 

478""", # noqa: E501 

479 ) 

480 url = url_property 

481 source = source_property 

482 

483 @property 

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

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

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

487 of that data. 

488 

489 Conformance: 

490 This property can be specified once in an iCalendar 

491 object, consisting of a positive duration of time. 

492 

493 Description: 

494 This property specifies a positive duration that gives 

495 a suggested minimum polling interval for checking for updates to 

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

497 calendar user agents to limit the polling interval for calendar 

498 data updates to the minimum interval specified. 

499 

500 Raises: 

501 ValueError: When setting a negative duration. 

502 """ 

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

504 return refresh_interval.dt if refresh_interval else None 

505 

506 @refresh_interval.setter 

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

508 """Set the REFRESH-INTERVAL.""" 

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

510 raise TypeError( 

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

512 " or None to delete it." 

513 ) 

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

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

516 if value is not None: 

517 del self.refresh_interval 

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

519 else: 

520 del self.refresh_interval 

521 

522 @refresh_interval.deleter 

523 def refresh_interval(self): 

524 """Delete REFRESH-INTERVAL.""" 

525 self.pop("REFRESH-INTERVAL") 

526 

527 images = images_property 

528 

529 @classmethod 

530 def new( 

531 cls, 

532 /, 

533 calscale: str | None = None, 

534 categories: Sequence[str] = (), 

535 color: str | None = None, 

536 concepts: CONCEPTS_TYPE_SETTER = None, 

537 description: str | None = None, 

538 language: str | None = None, 

539 last_modified: date | datetime | None = None, 

540 links: LINKS_TYPE_SETTER = None, 

541 method: str | None = None, 

542 name: str | None = None, 

543 organization: str | None = None, 

544 prodid: str | None = None, 

545 refresh_interval: timedelta | None = None, 

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

547 related_to: RELATED_TO_TYPE_SETTER = None, 

548 source: str | None = None, 

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

550 url: str | None = None, 

551 version: str = "2.0", 

552 ): 

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

554 

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

556 

557 Arguments: 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

579 

580 Returns: 

581 :class:`Calendar` 

582 

583 Raises: 

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

585 

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

587 """ # noqa: E501 

588 calendar: Calendar = super().new( 

589 last_modified=last_modified, 

590 links=links, 

591 related_to=related_to, 

592 refids=refids, 

593 concepts=concepts, 

594 ) 

595 

596 # Generate prodid if not provided but organization is given 

597 if prodid is None and organization: 

598 app_name = name or "Calendar" 

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

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

601 elif prodid is None: 

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

603 

604 calendar.prodid = prodid 

605 calendar.version = version 

606 calendar.calendar_name = name 

607 calendar.color = color 

608 calendar.description = description 

609 calendar.method = method 

610 calendar.calscale = calscale 

611 calendar.categories = categories 

612 calendar.uid = uid 

613 calendar.url = url 

614 calendar.refresh_interval = refresh_interval 

615 calendar.source = source 

616 

617 return calendar 

618 

619 def validate(self): 

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

621 

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

623 

624 Raises: 

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

626 components. 

627 """ 

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

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

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

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

632 if not self.subcomponents: 

633 raise IncompleteComponent( 

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

635 ) 

636 

637 

638__all__ = ["Calendar"]