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

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 comps = Component.from_ical(st, multiple=True) 

89 all_timezones_so_far = True 

90 for comp in comps: 

91 for component in comp.subcomponents: 

92 if component.name == "VTIMEZONE": 

93 if not all_timezones_so_far: 

94 # If a preceding component refers to a VTIMEZONE defined 

95 # later in the source st 

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

97 # earlier component may have 

98 # the wrong timezone attached. 

99 # However, during computation of comps, all VTIMEZONEs 

100 # observed do end up in 

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

102 # rely on the cache 

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

104 # See test_create_america_new_york_forward_reference. 

105 return Component.from_ical(st, multiple) 

106 else: 

107 all_timezones_so_far = False 

108 

109 # No potentially forward VTIMEZONEs to worry about 

110 if multiple: 

111 return comps 

112 if len(comps) > 1: 

113 raise ValueError( 

114 cls._format_error( 

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

116 ) 

117 ) 

118 if len(comps) < 1: 

119 raise ValueError( 

120 cls._format_error( 

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

122 ) 

123 ) 

124 return comps[0] 

125 

126 @property 

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

128 """All event components in the calendar. 

129 

130 This is a shortcut to get all events. 

131 Modifications do not change the calendar. 

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

133 

134 >>> from icalendar import Calendar 

135 >>> calendar = Calendar.example() 

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

137 >>> event.start 

138 datetime.date(2022, 1, 1) 

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

140 New Year's Day 

141 """ 

142 return self.walk("VEVENT") 

143 

144 @property 

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

146 """All todo components in the calendar. 

147 

148 This is a shortcut to get all todos. 

149 Modifications do not change the calendar. 

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

151 """ 

152 return self.walk("VTODO") 

153 

154 @property 

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

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

157 

158 This is a shortcut to get all availabilities. 

159 Modifications do not change the calendar. 

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

161 """ 

162 return self.walk("VAVAILABILITY") 

163 

164 @property 

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

166 """All FreeBusy components in the calendar. 

167 

168 This is a shortcut to get all FreeBusy. 

169 Modifications do not change the calendar. 

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

171 """ 

172 return self.walk("VFREEBUSY") 

173 

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

175 """The set of TZIDs in use. 

176 

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

178 timezone information like the TZID parameter in all attributes. 

179 

180 >>> from icalendar import Calendar 

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

182 >>> calendar.get_used_tzids() 

183 {'posix/Europe/Vaduz'} 

184 

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

186 """ 

187 result = set() 

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

189 if hasattr(value, "params"): 

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

191 return result - {None} 

192 

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

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

195 

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

197 all of these timezones should be added. 

198 """ 

199 tzids = self.get_used_tzids() 

200 for timezone in self.timezones: 

201 tzids.remove(timezone.tz_name) 

202 return tzids 

203 

204 @property 

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

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

207 

208 >>> from icalendar import Calendar 

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

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

211 ['custom_Pacific/Fiji'] 

212 

213 .. note:: 

214 

215 This is a read-only property. 

216 """ 

217 return self.walk("VTIMEZONE") 

218 

219 def add_missing_timezones( 

220 self, 

221 first_date: date = Timezone.DEFAULT_FIRST_DATE, 

222 last_date: date = Timezone.DEFAULT_LAST_DATE, 

223 ): 

224 """Add all missing VTIMEZONE components. 

225 

226 This adds all the timezone components that are required. 

227 VTIMEZONE components are inserted at the beginning of the calendar 

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

229 

230 .. note:: 

231 

232 Timezones that are not known will not be added. 

233 

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

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

236 

237 >>> from icalendar import Calendar, Event 

238 >>> from datetime import datetime 

239 >>> from zoneinfo import ZoneInfo 

240 >>> calendar = Calendar() 

241 >>> event = Event() 

242 >>> calendar.add_component(event) 

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

244 >>> calendar.timezones 

245 [] 

246 >>> calendar.add_missing_timezones() 

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

248 'Europe/Berlin' 

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

250 set() 

251 """ 

252 missing_tzids = self.get_missing_tzids() 

253 if not missing_tzids: 

254 return 

255 

256 existing_timezone_count = len(self.timezones) 

257 

258 for tzid in missing_tzids: 

259 try: 

260 timezone = Timezone.from_tzid( 

261 tzid, first_date=first_date, last_date=last_date 

262 ) 

263 except ValueError: 

264 continue 

265 self.subcomponents.insert(existing_timezone_count, timezone) 

266 existing_timezone_count += 1 

267 

268 calendar_name = multi_language_text_property( 

269 "NAME", 

270 "X-WR-CALNAME", 

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

272 

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

274 

275 Property Parameters: 

276 IANA, non-standard, alternate text 

277 representation, and language property parameters can be specified 

278 on this property. 

279 

280 Conformance: 

281 This property can be specified multiple times in an 

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

283 of the calendar in a different language. 

284 

285 Description: 

286 This property is used to specify a name of the 

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

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

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

290 including this property multiple times with different "LANGUAGE" 

291 parameter values on each. 

292 

293 Example: 

294 Below, we set the name of the calendar. 

295 

296 .. code-block:: pycon 

297 

298 >>> from icalendar import Calendar 

299 >>> calendar = Calendar() 

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

301 >>> print(calendar.to_ical()) 

302 BEGIN:VCALENDAR 

303 NAME:My Calendar 

304 END:VCALENDAR 

305 """, 

306 ) 

307 

308 description = multi_language_text_property( 

309 "DESCRIPTION", 

310 "X-WR-CALDESC", 

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

312 

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

314 

315 Conformance: 

316 This property can be specified multiple times in an 

317 iCalendar object. However, each property MUST represent the 

318 description of the calendar in a different language. 

319 

320 Description: 

321 This property is used to specify a lengthy textual 

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

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

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

325 language variants can be specified by including this property 

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

327 

328 Example: 

329 Below, we add a description to a calendar. 

330 

331 .. code-block:: pycon 

332 

333 >>> from icalendar import Calendar 

334 >>> calendar = Calendar() 

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

336 >>> print(calendar.to_ical()) 

337 BEGIN:VCALENDAR 

338 DESCRIPTION:This is a calendar 

339 END:VCALENDAR 

340 """, 

341 ) 

342 

343 color = single_string_property( 

344 "COLOR", 

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

346 

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

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

349 

350 Property Parameters: 

351 IANA and non-standard property parameters can 

352 be specified on this property. 

353 

354 Conformance: 

355 This property can be specified once in an iCalendar 

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

357 

358 Description: 

359 This property specifies a color that clients MAY use 

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

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

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

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

364 

365 Example: 

366 ``"turquoise"``, ``"#ffffff"`` 

367 

368 .. code-block:: pycon 

369 

370 >>> from icalendar import Calendar 

371 >>> calendar = Calendar() 

372 >>> calendar.color = "black" 

373 >>> print(calendar.to_ical()) 

374 BEGIN:VCALENDAR 

375 COLOR:black 

376 END:VCALENDAR 

377 

378 """, 

379 "X-APPLE-CALENDAR-COLOR", 

380 ) 

381 categories = categories_property 

382 uid = uid_property 

383 prodid = single_string_property( 

384 "PRODID", 

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

386 

387Conformance: 

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

389 

390Description: 

391 The vendor of the implementation SHOULD assure that 

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

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

394 

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

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

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

398 standard properties. 

399 

400Example: 

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

402 imply that English is the default language. 

403 

404 .. code-block:: text 

405 

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

407""", # noqa: E501 

408 ) 

409 version = single_string_property( 

410 "VERSION", 

411 """VERSION of the calendar specification. 

412 

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

414 

415Purpose: 

416 This property specifies the identifier corresponding to the 

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

418 iCalendar specification that is required in order to interpret the 

419 iCalendar object. 

420 

421 

422 """, 

423 ) 

424 

425 calscale = single_string_property( 

426 "CALSCALE", 

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

428 

429Compatibility: 

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

431 are implemented on the RRULE. 

432 

433Conformance: 

434 This property can be specified once in an iCalendar 

435 object. The default value is "GREGORIAN". 

436 

437Description: 

438 This memo is based on the Gregorian calendar scale. 

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

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

441 calendar scales will be defined in other specifications or by 

442 future versions of this memo. 

443 """, # noqa: E501 

444 default="GREGORIAN", 

445 ) 

446 method = single_string_property( 

447 "METHOD", 

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

449 

450Description: 

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

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

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

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

455 specified. 

456 

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

458 of other specifications, such as the iCalendar Transport- 

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

460 

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

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

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

464 some calendar information; without the intention of conveying a 

465 scheduling semantic. 

466""", # noqa: E501 

467 ) 

468 url = url_property 

469 source = source_property 

470 

471 @property 

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

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

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

475 of that data. 

476 

477 Conformance: 

478 This property can be specified once in an iCalendar 

479 object, consisting of a positive duration of time. 

480 

481 Description: 

482 This property specifies a positive duration that gives 

483 a suggested minimum polling interval for checking for updates to 

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

485 calendar user agents to limit the polling interval for calendar 

486 data updates to the minimum interval specified. 

487 

488 Raises: 

489 ValueError: When setting a negative duration. 

490 """ 

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

492 return refresh_interval.dt if refresh_interval else None 

493 

494 @refresh_interval.setter 

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

496 """Set the REFRESH-INTERVAL.""" 

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

498 raise TypeError( 

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

500 " or None to delete it." 

501 ) 

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

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

504 if value is not None: 

505 del self.refresh_interval 

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

507 else: 

508 del self.refresh_interval 

509 

510 @refresh_interval.deleter 

511 def refresh_interval(self): 

512 """Delete REFRESH-INTERVAL.""" 

513 self.pop("REFRESH-INTERVAL") 

514 

515 images = images_property 

516 

517 @classmethod 

518 def new( 

519 cls, 

520 /, 

521 calscale: str | None = None, 

522 categories: Sequence[str] = (), 

523 color: str | None = None, 

524 concepts: CONCEPTS_TYPE_SETTER = None, 

525 description: str | None = None, 

526 language: str | None = None, 

527 last_modified: date | datetime | None = None, 

528 links: LINKS_TYPE_SETTER = None, 

529 method: str | None = None, 

530 name: str | None = None, 

531 organization: str | None = None, 

532 prodid: str | None = None, 

533 refresh_interval: timedelta | None = None, 

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

535 related_to: RELATED_TO_TYPE_SETTER = None, 

536 source: str | None = None, 

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

538 url: str | None = None, 

539 version: str = "2.0", 

540 ): 

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

542 

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

544 

545 Arguments: 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

567 

568 Returns: 

569 :class:`Calendar` 

570 

571 Raises: 

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

573 

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

575 """ # noqa: E501 

576 calendar: Calendar = super().new( 

577 last_modified=last_modified, 

578 links=links, 

579 related_to=related_to, 

580 refids=refids, 

581 concepts=concepts, 

582 ) 

583 

584 # Generate prodid if not provided but organization is given 

585 if prodid is None and organization: 

586 app_name = name or "Calendar" 

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

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

589 elif prodid is None: 

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

591 

592 calendar.prodid = prodid 

593 calendar.version = version 

594 calendar.calendar_name = name 

595 calendar.color = color 

596 calendar.description = description 

597 calendar.method = method 

598 calendar.calscale = calscale 

599 calendar.categories = categories 

600 calendar.uid = uid 

601 calendar.url = url 

602 calendar.refresh_interval = refresh_interval 

603 calendar.source = source 

604 

605 return calendar 

606 

607 def validate(self): 

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

609 

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

611 

612 Raises: 

613 IncompleteComponent: If the calendar lacks required properties or 

614 components. 

615 """ 

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

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

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

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

620 if not self.subcomponents: 

621 raise IncompleteComponent( 

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

623 ) 

624 

625 

626__all__ = ["Calendar"]