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

116 statements  

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

2 

3from __future__ import annotations 

4 

5from typing import TYPE_CHECKING, Sequence 

6 

7from icalendar.attr import ( 

8 categories_property, 

9 multi_language_text_property, 

10 single_string_property, 

11 uid_property, 

12) 

13from icalendar.cal.component import Component 

14from icalendar.cal.examples import get_example 

15from icalendar.cal.timezone import Timezone 

16from icalendar.error import IncompleteComponent 

17from icalendar.version import __version__ 

18 

19if TYPE_CHECKING: 

20 import uuid 

21 from datetime import date 

22 

23 from icalendar.cal.availability import Availability 

24 from icalendar.cal.event import Event 

25 from icalendar.cal.free_busy import FreeBusy 

26 from icalendar.cal.todo import Todo 

27 

28 

29class Calendar(Component): 

30 """ 

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

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

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

34 other type of calendar component. 

35 

36 Examples: 

37 Create a new Calendar: 

38 

39 >>> from icalendar import Calendar 

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

41 >>> print(calendar.calendar_name) 

42 My Calendar 

43 

44 """ 

45 

46 name = "VCALENDAR" 

47 canonical_order = ( 

48 "VERSION", 

49 "PRODID", 

50 "CALSCALE", 

51 "METHOD", 

52 "DESCRIPTION", 

53 "X-WR-CALDESC", 

54 "NAME", 

55 "X-WR-CALNAME", 

56 ) 

57 required = ( 

58 "PRODID", 

59 "VERSION", 

60 ) 

61 singletons = ( 

62 "PRODID", 

63 "VERSION", 

64 "CALSCALE", 

65 "METHOD", 

66 "COLOR", # RFC 7986 

67 ) 

68 multiple = ( 

69 "CATEGORIES", # RFC 7986 

70 "DESCRIPTION", # RFC 7986 

71 "NAME", # RFC 7986 

72 ) 

73 

74 @classmethod 

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

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

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

78 

79 @classmethod 

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

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

82 all_timezones_so_far = True 

83 for comp in comps: 

84 for component in comp.subcomponents: 

85 if component.name == "VTIMEZONE": 

86 if not all_timezones_so_far: 

87 # If a preceding component refers to a VTIMEZONE defined 

88 # later in the source st 

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

90 # earlier component may have 

91 # the wrong timezone attached. 

92 # However, during computation of comps, all VTIMEZONEs 

93 # observed do end up in 

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

95 # rely on the cache 

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

97 # See test_create_america_new_york_forward_reference. 

98 return Component.from_ical(st, multiple) 

99 else: 

100 all_timezones_so_far = False 

101 

102 # No potentially forward VTIMEZONEs to worry about 

103 if multiple: 

104 return comps 

105 if len(comps) > 1: 

106 raise ValueError( 

107 cls._format_error( 

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

109 ) 

110 ) 

111 if len(comps) < 1: 

112 raise ValueError( 

113 cls._format_error( 

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

115 ) 

116 ) 

117 return comps[0] 

118 

119 @property 

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

121 """All event components in the calendar. 

122 

123 This is a shortcut to get all events. 

124 Modifications do not change the calendar. 

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

126 

127 >>> from icalendar import Calendar 

128 >>> calendar = Calendar.example() 

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

130 >>> event.start 

131 datetime.date(2022, 1, 1) 

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

133 New Year's Day 

134 """ 

135 return self.walk("VEVENT") 

136 

137 @property 

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

139 """All todo components in the calendar. 

140 

141 This is a shortcut to get all todos. 

142 Modifications do not change the calendar. 

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

144 """ 

145 return self.walk("VTODO") 

146 

147 @property 

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

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

150 

151 This is a shortcut to get all availabilities. 

152 Modifications do not change the calendar. 

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

154 """ 

155 return self.walk("VAVAILABILITY") 

156 

157 @property 

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

159 """All FreeBusy components in the calendar. 

160 

161 This is a shortcut to get all FreeBusy. 

162 Modifications do not change the calendar. 

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

164 """ 

165 return self.walk("VFREEBUSY") 

166 

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

168 """The set of TZIDs in use. 

169 

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

171 timezone information like the TZID parameter in all attributes. 

172 

173 >>> from icalendar import Calendar 

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

175 >>> calendar.get_used_tzids() 

176 {'posix/Europe/Vaduz'} 

177 

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

179 """ 

180 result = set() 

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

182 if hasattr(value, "params"): 

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

184 return result - {None} 

185 

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

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

188 

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

190 all of these timezones should be added. 

191 """ 

192 tzids = self.get_used_tzids() 

193 for timezone in self.timezones: 

194 tzids.remove(timezone.tz_name) 

195 return tzids 

196 

197 @property 

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

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

200 

201 >>> from icalendar import Calendar 

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

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

204 ['custom_Pacific/Fiji'] 

205 

206 .. note:: 

207 

208 This is a read-only property. 

209 """ 

210 return self.walk("VTIMEZONE") 

211 

212 def add_missing_timezones( 

213 self, 

214 first_date: date = Timezone.DEFAULT_FIRST_DATE, 

215 last_date: date = Timezone.DEFAULT_LAST_DATE, 

216 ): 

217 """Add all missing VTIMEZONE components. 

218 

219 This adds all the timezone components that are required. 

220 VTIMEZONE components are inserted at the beginning of the calendar 

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

222 

223 .. note:: 

224 

225 Timezones that are not known will not be added. 

226 

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

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

229 

230 >>> from icalendar import Calendar, Event 

231 >>> from datetime import datetime 

232 >>> from zoneinfo import ZoneInfo 

233 >>> calendar = Calendar() 

234 >>> event = Event() 

235 >>> calendar.add_component(event) 

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

237 >>> calendar.timezones 

238 [] 

239 >>> calendar.add_missing_timezones() 

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

241 'Europe/Berlin' 

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

243 set() 

244 """ 

245 missing_tzids = self.get_missing_tzids() 

246 if not missing_tzids: 

247 return 

248 

249 existing_timezone_count = len(self.timezones) 

250 

251 for tzid in missing_tzids: 

252 try: 

253 timezone = Timezone.from_tzid( 

254 tzid, first_date=first_date, last_date=last_date 

255 ) 

256 except ValueError: 

257 continue 

258 self.subcomponents.insert(existing_timezone_count, timezone) 

259 existing_timezone_count += 1 

260 

261 calendar_name = multi_language_text_property( 

262 "NAME", 

263 "X-WR-CALNAME", 

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

265 

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

267 

268 Property Parameters: 

269 IANA, non-standard, alternate text 

270 representation, and language property parameters can be specified 

271 on this property. 

272 

273 Conformance: 

274 This property can be specified multiple times in an 

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

276 of the calendar in a different language. 

277 

278 Description: 

279 This property is used to specify a name of the 

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

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

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

283 including this property multiple times with different "LANGUAGE" 

284 parameter values on each. 

285 

286 Example: 

287 Below, we set the name of the calendar. 

288 

289 .. code-block:: pycon 

290 

291 >>> from icalendar import Calendar 

292 >>> calendar = Calendar() 

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

294 >>> print(calendar.to_ical()) 

295 BEGIN:VCALENDAR 

296 NAME:My Calendar 

297 END:VCALENDAR 

298 """, 

299 ) 

300 

301 description = multi_language_text_property( 

302 "DESCRIPTION", 

303 "X-WR-CALDESC", 

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

305 

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

307 

308 Conformance: 

309 This property can be specified multiple times in an 

310 iCalendar object. However, each property MUST represent the 

311 description of the calendar in a different language. 

312 

313 Description: 

314 This property is used to specify a lengthy textual 

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

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

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

318 language variants can be specified by including this property 

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

320 

321 Example: 

322 Below, we add a description to a calendar. 

323 

324 .. code-block:: pycon 

325 

326 >>> from icalendar import Calendar 

327 >>> calendar = Calendar() 

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

329 >>> print(calendar.to_ical()) 

330 BEGIN:VCALENDAR 

331 DESCRIPTION:This is a calendar 

332 END:VCALENDAR 

333 """, 

334 ) 

335 

336 color = single_string_property( 

337 "COLOR", 

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

339 

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

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

342 

343 Property Parameters: 

344 IANA and non-standard property parameters can 

345 be specified on this property. 

346 

347 Conformance: 

348 This property can be specified once in an iCalendar 

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

350 

351 Description: 

352 This property specifies a color that clients MAY use 

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

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

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

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

357 

358 Example: 

359 ``"turquoise"``, ``"#ffffff"`` 

360 

361 .. code-block:: pycon 

362 

363 >>> from icalendar import Calendar 

364 >>> calendar = Calendar() 

365 >>> calendar.color = "black" 

366 >>> print(calendar.to_ical()) 

367 BEGIN:VCALENDAR 

368 COLOR:black 

369 END:VCALENDAR 

370 

371 """, 

372 "X-APPLE-CALENDAR-COLOR", 

373 ) 

374 categories = categories_property 

375 uid = uid_property 

376 prodid = single_string_property( 

377 "PRODID", 

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

379 

380Conformance: 

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

382 

383Description: 

384 The vendor of the implementation SHOULD assure that 

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

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

387 

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

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

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

391 standard properties. 

392 

393Example: 

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

395 imply that English is the default language. 

396 

397 .. code-block:: text 

398 

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

400""", # noqa: E501 

401 ) 

402 version = single_string_property( 

403 "VERSION", 

404 """VERSION of the calendar specification. 

405 

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

407 

408Purpose: 

409 This property specifies the identifier corresponding to the 

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

411 iCalendar specification that is required in order to interpret the 

412 iCalendar object. 

413 

414 

415 """, 

416 ) 

417 

418 calscale = single_string_property( 

419 "CALSCALE", 

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

421 

422Compatibility: 

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

424 are implemented on the RRULE. 

425 

426Conformance: 

427 This property can be specified once in an iCalendar 

428 object. The default value is "GREGORIAN". 

429 

430Description: 

431 This memo is based on the Gregorian calendar scale. 

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

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

434 calendar scales will be defined in other specifications or by 

435 future versions of this memo. 

436 """, # noqa: E501 

437 default="GREGORIAN", 

438 ) 

439 method = single_string_property( 

440 "METHOD", 

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

442 

443Description: 

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

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

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

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

448 specified. 

449 

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

451 of other specifications, such as the iCalendar Transport- 

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

453 

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

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

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

457 some calendar information; without the intention of conveying a 

458 scheduling semantic. 

459""", # noqa: E501 

460 ) 

461 

462 @classmethod 

463 def new( 

464 cls, 

465 /, 

466 calscale: str | None = None, 

467 categories: Sequence[str] = (), 

468 color: str | None = None, 

469 description: str | None = None, 

470 language: str | None = None, 

471 method: str | None = None, 

472 name: str | None = None, 

473 organization: str | None = None, 

474 prodid: str | None = None, 

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

476 version: str = "2.0", 

477 ): 

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

479 

480 This creates a new Calendar in accordance with :rfc:`5545`. 

481 

482 Arguments: 

483 calscale: The :attr:`calscale` of the component. 

484 categories: The :attr:`categories` of the component. 

485 color: The :attr:`color` of the component. 

486 description: The :attr:`description` of the component. 

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

488 method: The :attr:`method` of the component. 

489 name: The :attr:`calendar_name` of the component. 

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

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

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

493 uid: The :attr:`uid` of the component. 

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

495 version: The :attr:`version` of the component. 

496 

497 Returns: 

498 :class:`Calendar` 

499 

500 Raises: 

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

502 

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

504 """ 

505 calendar = cls() 

506 

507 # Generate prodid if not provided but organization is given 

508 if prodid is None and organization: 

509 app_name = name or "Calendar" 

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

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

512 elif prodid is None: 

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

514 

515 calendar.prodid = prodid 

516 calendar.version = version 

517 calendar.calendar_name = name 

518 calendar.color = color 

519 calendar.description = description 

520 calendar.method = method 

521 calendar.calscale = calscale 

522 calendar.categories = categories 

523 calendar.uid = uid 

524 return calendar 

525 

526 def validate(self): 

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

528 

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

530 

531 Raises: 

532 IncompleteComponent: If the calendar lacks required properties or components. 

533 """ 

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

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

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

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

538 if not self.subcomponents: 

539 raise IncompleteComponent( 

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

541 ) 

542 

543 

544__all__ = ["Calendar"]