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

141 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 categories_property, 

10 images_property, 

11 multi_language_text_property, 

12 single_string_property, 

13 source_property, 

14 uid_property, 

15 url_property, 

16) 

17from icalendar.cal.component import Component 

18from icalendar.cal.examples import get_example 

19from icalendar.cal.timezone import Timezone 

20from icalendar.error import IncompleteComponent 

21from icalendar.version import __version__ 

22 

23if TYPE_CHECKING: 

24 import uuid 

25 from datetime import date, datetime 

26 

27 from icalendar.cal.availability import Availability 

28 from icalendar.cal.event import Event 

29 from icalendar.cal.free_busy import FreeBusy 

30 from icalendar.cal.todo import Todo 

31 

32 

33class Calendar(Component): 

34 """ 

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

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

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

38 other type of calendar component. 

39 

40 Examples: 

41 Create a new Calendar: 

42 

43 >>> from icalendar import Calendar 

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

45 >>> print(calendar.calendar_name) 

46 My Calendar 

47 

48 """ 

49 

50 name = "VCALENDAR" 

51 canonical_order = ( 

52 "VERSION", 

53 "PRODID", 

54 "CALSCALE", 

55 "METHOD", 

56 "DESCRIPTION", 

57 "X-WR-CALDESC", 

58 "NAME", 

59 "X-WR-CALNAME", 

60 ) 

61 required = ( 

62 "PRODID", 

63 "VERSION", 

64 ) 

65 singletons = ( 

66 "PRODID", 

67 "VERSION", 

68 "CALSCALE", 

69 "METHOD", 

70 "COLOR", # RFC 7986 

71 ) 

72 multiple = ( 

73 "CATEGORIES", # RFC 7986 

74 "DESCRIPTION", # RFC 7986 

75 "NAME", # RFC 7986 

76 ) 

77 

78 @classmethod 

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

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

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

82 

83 @classmethod 

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

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

86 all_timezones_so_far = True 

87 for comp in comps: 

88 for component in comp.subcomponents: 

89 if component.name == "VTIMEZONE": 

90 if not all_timezones_so_far: 

91 # If a preceding component refers to a VTIMEZONE defined 

92 # later in the source st 

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

94 # earlier component may have 

95 # the wrong timezone attached. 

96 # However, during computation of comps, all VTIMEZONEs 

97 # observed do end up in 

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

99 # rely on the cache 

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

101 # See test_create_america_new_york_forward_reference. 

102 return Component.from_ical(st, multiple) 

103 else: 

104 all_timezones_so_far = False 

105 

106 # No potentially forward VTIMEZONEs to worry about 

107 if multiple: 

108 return comps 

109 if len(comps) > 1: 

110 raise ValueError( 

111 cls._format_error( 

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

113 ) 

114 ) 

115 if len(comps) < 1: 

116 raise ValueError( 

117 cls._format_error( 

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

119 ) 

120 ) 

121 return comps[0] 

122 

123 @property 

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

125 """All event components in the calendar. 

126 

127 This is a shortcut to get all events. 

128 Modifications do not change the calendar. 

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

130 

131 >>> from icalendar import Calendar 

132 >>> calendar = Calendar.example() 

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

134 >>> event.start 

135 datetime.date(2022, 1, 1) 

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

137 New Year's Day 

138 """ 

139 return self.walk("VEVENT") 

140 

141 @property 

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

143 """All todo components in the calendar. 

144 

145 This is a shortcut to get all todos. 

146 Modifications do not change the calendar. 

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

148 """ 

149 return self.walk("VTODO") 

150 

151 @property 

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

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

154 

155 This is a shortcut to get all availabilities. 

156 Modifications do not change the calendar. 

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

158 """ 

159 return self.walk("VAVAILABILITY") 

160 

161 @property 

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

163 """All FreeBusy components in the calendar. 

164 

165 This is a shortcut to get all FreeBusy. 

166 Modifications do not change the calendar. 

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

168 """ 

169 return self.walk("VFREEBUSY") 

170 

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

172 """The set of TZIDs in use. 

173 

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

175 timezone information like the TZID parameter in all attributes. 

176 

177 >>> from icalendar import Calendar 

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

179 >>> calendar.get_used_tzids() 

180 {'posix/Europe/Vaduz'} 

181 

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

183 """ 

184 result = set() 

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

186 if hasattr(value, "params"): 

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

188 return result - {None} 

189 

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

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

192 

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

194 all of these timezones should be added. 

195 """ 

196 tzids = self.get_used_tzids() 

197 for timezone in self.timezones: 

198 tzids.remove(timezone.tz_name) 

199 return tzids 

200 

201 @property 

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

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

204 

205 >>> from icalendar import Calendar 

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

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

208 ['custom_Pacific/Fiji'] 

209 

210 .. note:: 

211 

212 This is a read-only property. 

213 """ 

214 return self.walk("VTIMEZONE") 

215 

216 def add_missing_timezones( 

217 self, 

218 first_date: date = Timezone.DEFAULT_FIRST_DATE, 

219 last_date: date = Timezone.DEFAULT_LAST_DATE, 

220 ): 

221 """Add all missing VTIMEZONE components. 

222 

223 This adds all the timezone components that are required. 

224 VTIMEZONE components are inserted at the beginning of the calendar 

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

226 

227 .. note:: 

228 

229 Timezones that are not known will not be added. 

230 

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

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

233 

234 >>> from icalendar import Calendar, Event 

235 >>> from datetime import datetime 

236 >>> from zoneinfo import ZoneInfo 

237 >>> calendar = Calendar() 

238 >>> event = Event() 

239 >>> calendar.add_component(event) 

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

241 >>> calendar.timezones 

242 [] 

243 >>> calendar.add_missing_timezones() 

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

245 'Europe/Berlin' 

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

247 set() 

248 """ 

249 missing_tzids = self.get_missing_tzids() 

250 if not missing_tzids: 

251 return 

252 

253 existing_timezone_count = len(self.timezones) 

254 

255 for tzid in missing_tzids: 

256 try: 

257 timezone = Timezone.from_tzid( 

258 tzid, first_date=first_date, last_date=last_date 

259 ) 

260 except ValueError: 

261 continue 

262 self.subcomponents.insert(existing_timezone_count, timezone) 

263 existing_timezone_count += 1 

264 

265 calendar_name = multi_language_text_property( 

266 "NAME", 

267 "X-WR-CALNAME", 

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

269 

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

271 

272 Property Parameters: 

273 IANA, non-standard, alternate text 

274 representation, and language property parameters can be specified 

275 on this property. 

276 

277 Conformance: 

278 This property can be specified multiple times in an 

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

280 of the calendar in a different language. 

281 

282 Description: 

283 This property is used to specify a name of the 

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

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

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

287 including this property multiple times with different "LANGUAGE" 

288 parameter values on each. 

289 

290 Example: 

291 Below, we set the name of the calendar. 

292 

293 .. code-block:: pycon 

294 

295 >>> from icalendar import Calendar 

296 >>> calendar = Calendar() 

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

298 >>> print(calendar.to_ical()) 

299 BEGIN:VCALENDAR 

300 NAME:My Calendar 

301 END:VCALENDAR 

302 """, 

303 ) 

304 

305 description = multi_language_text_property( 

306 "DESCRIPTION", 

307 "X-WR-CALDESC", 

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

309 

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

311 

312 Conformance: 

313 This property can be specified multiple times in an 

314 iCalendar object. However, each property MUST represent the 

315 description of the calendar in a different language. 

316 

317 Description: 

318 This property is used to specify a lengthy textual 

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

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

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

322 language variants can be specified by including this property 

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

324 

325 Example: 

326 Below, we add a description to a calendar. 

327 

328 .. code-block:: pycon 

329 

330 >>> from icalendar import Calendar 

331 >>> calendar = Calendar() 

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

333 >>> print(calendar.to_ical()) 

334 BEGIN:VCALENDAR 

335 DESCRIPTION:This is a calendar 

336 END:VCALENDAR 

337 """, 

338 ) 

339 

340 color = single_string_property( 

341 "COLOR", 

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

343 

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

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

346 

347 Property Parameters: 

348 IANA and non-standard property parameters can 

349 be specified on this property. 

350 

351 Conformance: 

352 This property can be specified once in an iCalendar 

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

354 

355 Description: 

356 This property specifies a color that clients MAY use 

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

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

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

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

361 

362 Example: 

363 ``"turquoise"``, ``"#ffffff"`` 

364 

365 .. code-block:: pycon 

366 

367 >>> from icalendar import Calendar 

368 >>> calendar = Calendar() 

369 >>> calendar.color = "black" 

370 >>> print(calendar.to_ical()) 

371 BEGIN:VCALENDAR 

372 COLOR:black 

373 END:VCALENDAR 

374 

375 """, 

376 "X-APPLE-CALENDAR-COLOR", 

377 ) 

378 categories = categories_property 

379 uid = uid_property 

380 prodid = single_string_property( 

381 "PRODID", 

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

383 

384Conformance: 

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

386 

387Description: 

388 The vendor of the implementation SHOULD assure that 

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

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

391 

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

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

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

395 standard properties. 

396 

397Example: 

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

399 imply that English is the default language. 

400 

401 .. code-block:: text 

402 

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

404""", # noqa: E501 

405 ) 

406 version = single_string_property( 

407 "VERSION", 

408 """VERSION of the calendar specification. 

409 

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

411 

412Purpose: 

413 This property specifies the identifier corresponding to the 

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

415 iCalendar specification that is required in order to interpret the 

416 iCalendar object. 

417 

418 

419 """, 

420 ) 

421 

422 calscale = single_string_property( 

423 "CALSCALE", 

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

425 

426Compatibility: 

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

428 are implemented on the RRULE. 

429 

430Conformance: 

431 This property can be specified once in an iCalendar 

432 object. The default value is "GREGORIAN". 

433 

434Description: 

435 This memo is based on the Gregorian calendar scale. 

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

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

438 calendar scales will be defined in other specifications or by 

439 future versions of this memo. 

440 """, # noqa: E501 

441 default="GREGORIAN", 

442 ) 

443 method = single_string_property( 

444 "METHOD", 

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

446 

447Description: 

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

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

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

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

452 specified. 

453 

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

455 of other specifications, such as the iCalendar Transport- 

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

457 

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

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

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

461 some calendar information; without the intention of conveying a 

462 scheduling semantic. 

463""", # noqa: E501 

464 ) 

465 url = url_property 

466 source = source_property 

467 

468 @property 

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

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

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

472 of that data. 

473 

474 Conformance: 

475 This property can be specified once in an iCalendar 

476 object, consisting of a positive duration of time. 

477 

478 Description: 

479 This property specifies a positive duration that gives 

480 a suggested minimum polling interval for checking for updates to 

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

482 calendar user agents to limit the polling interval for calendar 

483 data updates to the minimum interval specified. 

484 

485 Raises: 

486 ValueError: When setting a negative duration. 

487 """ 

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

489 return refresh_interval.dt if refresh_interval else None 

490 

491 @refresh_interval.setter 

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

493 """Set the REFRESH-INTERVAL.""" 

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

495 raise TypeError( 

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

497 " or None to delete it." 

498 ) 

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

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

501 if value is not None: 

502 del self.refresh_interval 

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

504 else: 

505 del self.refresh_interval 

506 

507 @refresh_interval.deleter 

508 def refresh_interval(self): 

509 """Delete REFRESH-INTERVAL.""" 

510 self.pop("REFRESH-INTERVAL") 

511 

512 images = images_property 

513 

514 @classmethod 

515 def new( 

516 cls, 

517 /, 

518 calscale: str | None = None, 

519 categories: Sequence[str] = (), 

520 color: str | None = None, 

521 description: str | None = None, 

522 language: str | None = None, 

523 last_modified: date | datetime | None = None, 

524 method: str | None = None, 

525 name: str | None = None, 

526 organization: str | None = None, 

527 prodid: str | None = None, 

528 refresh_interval: timedelta | None = None, 

529 source: str | None = None, 

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

531 url: str | None = None, 

532 version: str = "2.0", 

533 ): 

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

535 

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

537 

538 Arguments: 

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

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

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

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

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

544 last_modified: The :attr:`Component.last_modified` 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 source: The :attr:`source` of the calendar. 

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

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

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

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

556 

557 Returns: 

558 :class:`Calendar` 

559 

560 Raises: 

561 InvalidCalendar: If the content is not valid according to :rfc:`5545`. 

562 

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

564 """ # noqa: E501 

565 calendar = cls() 

566 

567 # Generate prodid if not provided but organization is given 

568 if prodid is None and organization: 

569 app_name = name or "Calendar" 

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

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

572 elif prodid is None: 

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

574 

575 calendar.prodid = prodid 

576 calendar.version = version 

577 calendar.calendar_name = name 

578 calendar.color = color 

579 calendar.description = description 

580 calendar.method = method 

581 calendar.calscale = calscale 

582 calendar.categories = categories 

583 calendar.uid = uid 

584 calendar.url = url 

585 calendar.refresh_interval = refresh_interval 

586 calendar.source = source 

587 calendar.last_modified = last_modified 

588 return calendar 

589 

590 def validate(self): 

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

592 

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

594 

595 Raises: 

596 IncompleteComponent: If the calendar lacks required properties or 

597 components. 

598 """ 

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

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

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

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

603 if not self.subcomponents: 

604 raise IncompleteComponent( 

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

606 ) 

607 

608 

609__all__ = ["Calendar"]