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

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

134 statements  

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

2 

3from __future__ import annotations 

4 

5import uuid 

6from datetime import timedelta 

7from typing import TYPE_CHECKING 

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 

32 from icalendar.cal.availability import Availability 

33 from icalendar.cal.event import Event 

34 from icalendar.cal.free_busy import FreeBusy 

35 from icalendar.cal.journal import Journal 

36 from icalendar.cal.todo import Todo 

37 from icalendar.parser.ical.component import ComponentIcalParser 

38 

39 

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

41 

42 

43class Calendar(Component): 

44 """ 

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

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

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

48 other type of calendar component. 

49 

50 Examples: 

51 Create a new Calendar: 

52 

53 >>> from icalendar import Calendar 

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

55 >>> print(calendar.calendar_name) 

56 My Calendar 

57 

58 """ 

59 

60 name = "VCALENDAR" 

61 canonical_order = ( 

62 "VERSION", 

63 "PRODID", 

64 "CALSCALE", 

65 "METHOD", 

66 "DESCRIPTION", 

67 "X-WR-CALDESC", 

68 "NAME", 

69 "X-WR-CALNAME", 

70 ) 

71 required = ( 

72 "PRODID", 

73 "VERSION", 

74 ) 

75 singletons = ( 

76 "PRODID", 

77 "VERSION", 

78 "CALSCALE", 

79 "METHOD", 

80 "COLOR", # RFC 7986 

81 ) 

82 multiple = ( 

83 "CATEGORIES", # RFC 7986 

84 "DESCRIPTION", # RFC 7986 

85 "NAME", # RFC 7986 

86 ) 

87 

88 @classmethod 

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

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

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

92 

93 @classmethod 

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

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

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

97 

98 @property 

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

100 """All event components in the calendar. 

101 

102 This is a shortcut to get all events. 

103 Modifications do not change the calendar. 

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

105 

106 >>> from icalendar import Calendar 

107 >>> calendar = Calendar.example() 

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

109 >>> event.start 

110 datetime.date(2022, 1, 1) 

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

112 New Year's Day 

113 """ 

114 return self.walk("VEVENT") 

115 

116 @property 

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

118 """All todo components in the calendar. 

119 

120 This is a shortcut to get all todos. 

121 Modifications do not change the calendar. 

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

123 """ 

124 return self.walk("VTODO") 

125 

126 @property 

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

128 """All journal components in the calendar. 

129 

130 This is a shortcut to get all journals. 

131 Modifications do not change the calendar. 

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

133 """ 

134 return self.walk("VJOURNAL") 

135 

136 @property 

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

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

139 

140 This is a shortcut to get all availabilities. 

141 Modifications do not change the calendar. 

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

143 """ 

144 return self.walk("VAVAILABILITY") 

145 

146 @property 

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

148 """All FreeBusy components in the calendar. 

149 

150 This is a shortcut to get all FreeBusy. 

151 Modifications do not change the calendar. 

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

153 """ 

154 return self.walk("VFREEBUSY") 

155 

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

157 """The set of TZIDs in use. 

158 

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

160 timezone information like the TZID parameter in all attributes. 

161 

162 >>> from icalendar import Calendar 

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

164 >>> calendar.get_used_tzids() 

165 {'posix/Europe/Vaduz'} 

166 

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

168 """ 

169 result = set() 

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

171 if hasattr(value, "params"): 

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

173 return result - {None} 

174 

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

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

177 

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

179 all of these timezones should be added. 

180 

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

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

183 """ 

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

185 for timezone in self.timezones: 

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

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

188 tzids.discard(timezone.tz_name) 

189 return tzids 

190 

191 @property 

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

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

194 

195 >>> from icalendar import Calendar 

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

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

198 ['custom_Pacific/Fiji'] 

199 

200 .. note:: 

201 

202 This is a read-only property. 

203 """ 

204 return self.walk("VTIMEZONE") 

205 

206 def add_missing_timezones( 

207 self, 

208 first_date: date = Timezone.DEFAULT_FIRST_DATE, 

209 last_date: date = Timezone.DEFAULT_LAST_DATE, 

210 ): 

211 """Add all missing VTIMEZONE components. 

212 

213 This adds all the timezone components that are required. 

214 VTIMEZONE components are inserted at the beginning of the calendar 

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

216 

217 .. note:: 

218 

219 Timezones that are not known will not be added. 

220 

221 Parameters: 

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

223 last_date: Later than anything happening in the calendar. 

224 

225 >>> from icalendar import Calendar, Event 

226 >>> from datetime import datetime 

227 >>> from zoneinfo import ZoneInfo 

228 >>> calendar = Calendar() 

229 >>> event = Event() 

230 >>> calendar.add_component(event) 

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

232 >>> calendar.timezones 

233 [] 

234 >>> calendar.add_missing_timezones() 

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

236 'Europe/Berlin' 

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

238 set() 

239 """ 

240 missing_tzids = self.get_missing_tzids() 

241 if not missing_tzids: 

242 return 

243 

244 existing_timezone_count = len(self.timezones) 

245 

246 for tzid in missing_tzids: 

247 try: 

248 timezone = Timezone.from_tzid( 

249 tzid, first_date=first_date, last_date=last_date 

250 ) 

251 except ValueError: 

252 continue 

253 self.subcomponents.insert(existing_timezone_count, timezone) 

254 existing_timezone_count += 1 

255 

256 calendar_name = multi_language_text_property( 

257 "NAME", 

258 "X-WR-CALNAME", 

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

260 

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

262 

263 Property Parameters: 

264 IANA, non-standard, alternate text 

265 representation, and language property parameters can be specified 

266 on this property. 

267 

268 Conformance: 

269 This property can be specified multiple times in an 

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

271 of the calendar in a different language. 

272 

273 Description: 

274 This property is used to specify a name of the 

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

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

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

278 including this property multiple times with different "LANGUAGE" 

279 parameter values on each. 

280 

281 Example: 

282 Below, we set the name of the calendar. 

283 

284 .. code-block:: pycon 

285 

286 >>> from icalendar import Calendar 

287 >>> calendar = Calendar() 

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

289 >>> print(calendar.to_ical()) 

290 BEGIN:VCALENDAR 

291 NAME:My Calendar 

292 X-WR-CALNAME:My Calendar 

293 END:VCALENDAR 

294 """, 

295 ) 

296 

297 description = multi_language_text_property( 

298 "DESCRIPTION", 

299 "X-WR-CALDESC", 

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

301 

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

303 

304 Conformance: 

305 This property can be specified multiple times in an 

306 iCalendar object. However, each property MUST represent the 

307 description of the calendar in a different language. 

308 

309 Description: 

310 This property is used to specify a lengthy textual 

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

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

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

314 language variants can be specified by including this property 

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

316 

317 Example: 

318 Below, we add a description to a calendar. 

319 

320 .. code-block:: pycon 

321 

322 >>> from icalendar import Calendar 

323 >>> calendar = Calendar() 

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

325 >>> print(calendar.to_ical()) 

326 BEGIN:VCALENDAR 

327 DESCRIPTION:This is a calendar 

328 X-WR-CALDESC:This is a calendar 

329 END:VCALENDAR 

330 """, 

331 ) 

332 

333 color = single_string_property( 

334 "COLOR", 

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

336 

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

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

339 

340 Property Parameters: 

341 IANA and non-standard property parameters can 

342 be specified on this property. 

343 

344 Conformance: 

345 This property can be specified once in an iCalendar 

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

347 

348 Description: 

349 This property specifies a color that clients MAY use 

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

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

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

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

354 

355 Example: 

356 ``"turquoise"``, ``"#ffffff"`` 

357 

358 .. code-block:: pycon 

359 

360 >>> from icalendar import Calendar 

361 >>> calendar = Calendar() 

362 >>> calendar.color = "black" 

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

364 BEGIN:VCALENDAR 

365 COLOR:black 

366 END:VCALENDAR 

367 

368 """, 

369 "X-APPLE-CALENDAR-COLOR", 

370 ) 

371 categories = categories_property 

372 uid = uid_property 

373 prodid = single_string_property( 

374 "PRODID", 

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

376 

377Conformance: 

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

379 

380Description: 

381 The vendor of the implementation SHOULD assure that 

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

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

384 

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

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

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

388 standard properties. 

389 

390Example: 

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

392 imply that English is the default language. 

393 

394 .. code-block:: text 

395 

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

397""", 

398 ) 

399 version = single_string_property( 

400 "VERSION", 

401 """VERSION of the calendar specification. 

402 

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

404 

405Purpose: 

406 This property specifies the identifier corresponding to the 

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

408 iCalendar specification that is required in order to interpret the 

409 iCalendar object. 

410 

411 

412 """, 

413 ) 

414 

415 calscale = single_string_property( 

416 "CALSCALE", 

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

418 

419Compatibility: 

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

421 are implemented on the RRULE. 

422 

423Conformance: 

424 This property can be specified once in an iCalendar 

425 object. The default value is "GREGORIAN". 

426 

427Description: 

428 This memo is based on the Gregorian calendar scale. 

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

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

431 calendar scales will be defined in other specifications or by 

432 future versions of this memo. 

433 """, 

434 default="GREGORIAN", 

435 ) 

436 method = single_string_property( 

437 "METHOD", 

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

439 

440Description: 

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

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

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

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

445 specified. 

446 

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

448 of other specifications, such as the iCalendar Transport- 

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

450 

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

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

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

454 some calendar information; without the intention of conveying a 

455 scheduling semantic. 

456""", 

457 ) 

458 url = url_property 

459 source = source_property 

460 

461 @property 

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

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

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

465 of that data. 

466 

467 Conformance: 

468 This property can be specified once in an iCalendar 

469 object, consisting of a positive duration of time. 

470 

471 Description: 

472 This property specifies a positive duration that gives 

473 a suggested minimum polling interval for checking for updates to 

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

475 calendar user agents to limit the polling interval for calendar 

476 data updates to the minimum interval specified. 

477 

478 Raises: 

479 ValueError: When setting a negative duration. 

480 """ 

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

482 return refresh_interval.dt if refresh_interval else None 

483 

484 @refresh_interval.setter 

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

486 """Set the REFRESH-INTERVAL.""" 

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

488 raise TypeError( 

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

490 " or None to delete it." 

491 ) 

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

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

494 if value is not None: 

495 del self.refresh_interval 

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

497 else: 

498 del self.refresh_interval 

499 

500 @refresh_interval.deleter 

501 def refresh_interval(self): 

502 """Delete REFRESH-INTERVAL.""" 

503 self.pop("REFRESH-INTERVAL") 

504 

505 images = images_property 

506 

507 @classmethod 

508 def new( 

509 cls, 

510 /, 

511 calscale: str | None = None, 

512 categories: Sequence[str] = (), 

513 color: str | None = None, 

514 concepts: CONCEPTS_TYPE_SETTER = None, 

515 description: str | None = None, 

516 language: str | None = None, 

517 last_modified: date | datetime | None = None, 

518 links: LINKS_TYPE_SETTER = None, 

519 method: str | None = None, 

520 name: str | None = None, 

521 organization: str | None = None, 

522 prodid: str | None = None, 

523 refresh_interval: timedelta | None = None, 

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

525 related_to: RELATED_TO_TYPE_SETTER = None, 

526 source: str | None = None, 

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

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

529 url: str | None = None, 

530 version: str = "2.0", 

531 ): 

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

533 

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

535 

536 Parameters: 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

554 subcomponents: The subcomponents of the calendar. 

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

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

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

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

559 

560 Returns: 

561 :class:`Calendar` 

562 

563 Raises: 

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

565 

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

567 """ 

568 calendar: Calendar = super().new( 

569 last_modified=last_modified, 

570 links=links, 

571 related_to=related_to, 

572 refids=refids, 

573 concepts=concepts, 

574 subcomponents=subcomponents, 

575 ) 

576 

577 # Generate prodid if not provided but organization is given 

578 if prodid is None and organization: 

579 app_name = name or "Calendar" 

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

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

582 elif prodid is None: 

583 prodid = DEFAULT_PRODID 

584 

585 calendar.prodid = prodid 

586 calendar.version = version 

587 calendar.calendar_name = name 

588 calendar.color = color 

589 calendar.description = description 

590 calendar.method = method 

591 calendar.calscale = calscale 

592 calendar.categories = categories 

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

594 calendar.url = url 

595 calendar.refresh_interval = refresh_interval 

596 calendar.source = source 

597 

598 return calendar 

599 

600 def validate(self): 

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

602 

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

604 

605 Raises: 

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

607 components. 

608 """ 

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

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

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

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

613 if not self.subcomponents: 

614 raise IncompleteComponent( 

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

616 ) 

617 

618 

619__all__ = ["Calendar"]