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

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

153 statements  

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

2 

3from __future__ import annotations 

4 

5import uuid 

6from datetime import date, datetime, timedelta 

7from typing import TYPE_CHECKING, Literal, Sequence 

8 

9from icalendar.attr import ( 

10 X_MOZ_LASTACK_property, 

11 X_MOZ_SNOOZE_TIME_property, 

12 attendees_property, 

13 categories_property, 

14 class_property, 

15 color_property, 

16 contacts_property, 

17 create_single_property, 

18 description_property, 

19 exdates_property, 

20 location_property, 

21 organizer_property, 

22 priority_property, 

23 property_del_duration, 

24 property_doc_duration_template, 

25 property_get_duration, 

26 property_set_duration, 

27 rdates_property, 

28 rrules_property, 

29 sequence_property, 

30 status_property, 

31 summary_property, 

32 transparency_property, 

33 uid_property, 

34 url_property, 

35) 

36from icalendar.cal.component import Component 

37from icalendar.cal.examples import get_example 

38from icalendar.error import IncompleteComponent, InvalidCalendar 

39from icalendar.tools import is_date 

40 

41if TYPE_CHECKING: 

42 from icalendar.alarms import Alarms 

43 from icalendar.enums import CLASS, STATUS, TRANSP 

44 from icalendar.prop import vCalAddress 

45 

46 

47class Event(Component): 

48 """A grouping of component properties that describe an event. 

49 

50 Description: 

51 A "VEVENT" calendar component is a grouping of 

52 component properties, possibly including "VALARM" calendar 

53 components, that represents a scheduled amount of time on a 

54 calendar. For example, it can be an activity; such as a one-hour 

55 long, department meeting from 8:00 AM to 9:00 AM, tomorrow. 

56 Generally, an event will take up time on an individual calendar. 

57 Hence, the event will appear as an opaque interval in a search for 

58 busy time. Alternately, the event can have its Time Transparency 

59 set to "TRANSPARENT" in order to prevent blocking of the event in 

60 searches for busy time. 

61 

62 The "VEVENT" is also the calendar component used to specify an 

63 anniversary or daily reminder within a calendar. These events 

64 have a DATE value type for the "DTSTART" property instead of the 

65 default value type of DATE-TIME. If such a "VEVENT" has a "DTEND" 

66 property, it MUST be specified as a DATE value also. The 

67 anniversary type of "VEVENT" can span more than one date (i.e., 

68 "DTEND" property value is set to a calendar date after the 

69 "DTSTART" property value). If such a "VEVENT" has a "DURATION" 

70 property, it MUST be specified as a "dur-day" or "dur-week" value. 

71 

72 The "DTSTART" property for a "VEVENT" specifies the inclusive 

73 start of the event. For recurring events, it also specifies the 

74 very first instance in the recurrence set. The "DTEND" property 

75 for a "VEVENT" calendar component specifies the non-inclusive end 

76 of the event. For cases where a "VEVENT" calendar component 

77 specifies a "DTSTART" property with a DATE value type but no 

78 "DTEND" nor "DURATION" property, the event's duration is taken to 

79 be one day. For cases where a "VEVENT" calendar component 

80 specifies a "DTSTART" property with a DATE-TIME value type but no 

81 "DTEND" property, the event ends on the same calendar date and 

82 time of day specified by the "DTSTART" property. 

83 

84 The "VEVENT" calendar component cannot be nested within another 

85 calendar component. However, "VEVENT" calendar components can be 

86 related to each other or to a "VTODO" or to a "VJOURNAL" calendar 

87 component with the "RELATED-TO" property. 

88 

89 Examples: 

90 The following is an example of the "VEVENT" calendar 

91 component used to represent a meeting that will also be opaque to 

92 searches for busy time: 

93 

94 .. code-block:: text 

95 

96 BEGIN:VEVENT 

97 UID:19970901T130000Z-123401@example.com 

98 DTSTAMP:19970901T130000Z 

99 DTSTART:19970903T163000Z 

100 DTEND:19970903T190000Z 

101 SUMMARY:Annual Employee Review 

102 CLASS:PRIVATE 

103 CATEGORIES:BUSINESS,HUMAN RESOURCES 

104 END:VEVENT 

105 

106 The following is an example of the "VEVENT" calendar component 

107 used to represent a reminder that will not be opaque, but rather 

108 transparent, to searches for busy time: 

109 

110 .. code-block:: text 

111 

112 BEGIN:VEVENT 

113 UID:19970901T130000Z-123402@example.com 

114 DTSTAMP:19970901T130000Z 

115 DTSTART:19970401T163000Z 

116 DTEND:19970402T010000Z 

117 SUMMARY:Laurel is in sensitivity awareness class. 

118 CLASS:PUBLIC 

119 CATEGORIES:BUSINESS,HUMAN RESOURCES 

120 TRANSP:TRANSPARENT 

121 END:VEVENT 

122 

123 The following is an example of the "VEVENT" calendar component 

124 used to represent an anniversary that will occur annually: 

125 

126 .. code-block:: text 

127 

128 BEGIN:VEVENT 

129 UID:19970901T130000Z-123403@example.com 

130 DTSTAMP:19970901T130000Z 

131 DTSTART;VALUE=DATE:19971102 

132 SUMMARY:Our Blissful Anniversary 

133 TRANSP:TRANSPARENT 

134 CLASS:CONFIDENTIAL 

135 CATEGORIES:ANNIVERSARY,PERSONAL,SPECIAL OCCASION 

136 RRULE:FREQ=YEARLY 

137 END:VEVENT 

138 

139 The following is an example of the "VEVENT" calendar component 

140 used to represent a multi-day event scheduled from June 28th, 2007 

141 to July 8th, 2007 inclusively. Note that the "DTEND" property is 

142 set to July 9th, 2007, since the "DTEND" property specifies the 

143 non-inclusive end of the event. 

144 

145 .. code-block:: text 

146 

147 BEGIN:VEVENT 

148 UID:20070423T123432Z-541111@example.com 

149 DTSTAMP:20070423T123432Z 

150 DTSTART;VALUE=DATE:20070628 

151 DTEND;VALUE=DATE:20070709 

152 SUMMARY:Festival International de Jazz de Montreal 

153 TRANSP:TRANSPARENT 

154 END:VEVENT 

155 

156 Create a new Event: 

157 

158 .. code-block:: python 

159 

160 >>> from icalendar import Event 

161 >>> from datetime import datetime 

162 >>> event = Event.new(start=datetime(2021, 1, 1, 12, 30, 0)) 

163 >>> print(event.to_ical()) 

164 BEGIN:VEVENT 

165 DTSTART:20210101T123000 

166 DTSTAMP:20250517T080612Z 

167 UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 

168 END:VEVENT 

169 

170 """ 

171 

172 name = "VEVENT" 

173 

174 canonical_order = ( 

175 "SUMMARY", 

176 "DTSTART", 

177 "DTEND", 

178 "DURATION", 

179 "DTSTAMP", 

180 "UID", 

181 "RECURRENCE-ID", 

182 "SEQUENCE", 

183 "RRULE", 

184 "RDATE", 

185 "EXDATE", 

186 ) 

187 

188 required = ( 

189 "UID", 

190 "DTSTAMP", 

191 ) 

192 singletons = ( 

193 "CLASS", 

194 "CREATED", 

195 "COLOR", 

196 "DESCRIPTION", 

197 "DTSTART", 

198 "GEO", 

199 "LAST-MODIFIED", 

200 "LOCATION", 

201 "ORGANIZER", 

202 "PRIORITY", 

203 "DTSTAMP", 

204 "SEQUENCE", 

205 "STATUS", 

206 "SUMMARY", 

207 "TRANSP", 

208 "URL", 

209 "RECURRENCE-ID", 

210 "DTEND", 

211 "DURATION", 

212 "UID", 

213 ) 

214 exclusive = ( 

215 "DTEND", 

216 "DURATION", 

217 ) 

218 multiple = ( 

219 "ATTACH", 

220 "ATTENDEE", 

221 "CATEGORIES", 

222 "COMMENT", 

223 "CONTACT", 

224 "EXDATE", 

225 "RSTATUS", 

226 "RELATED", 

227 "RESOURCES", 

228 "RDATE", 

229 "RRULE", 

230 ) 

231 ignore_exceptions = True 

232 

233 @property 

234 def alarms(self) -> Alarms: 

235 """Compute the alarm times for this component. 

236 

237 >>> from icalendar import Event 

238 >>> event = Event.example("rfc_9074_example_1") 

239 >>> len(event.alarms.times) 

240 1 

241 >>> alarm_time = event.alarms.times[0] 

242 >>> alarm_time.trigger # The time when the alarm pops up 

243 datetime.datetime(2021, 3, 2, 10, 15, tzinfo=ZoneInfo(key='America/New_York')) 

244 >>> alarm_time.is_active() # This alarm has not been acknowledged 

245 True 

246 

247 Note that this only uses DTSTART and DTEND, but ignores 

248 RDATE, EXDATE, and RRULE properties. 

249 """ 

250 from icalendar.alarms import Alarms 

251 

252 return Alarms(self) 

253 

254 @classmethod 

255 def example(cls, name: str = "rfc_9074_example_3") -> Event: 

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

257 return cls.from_ical(get_example("events", name)) 

258 

259 DTSTART = create_single_property( 

260 "DTSTART", 

261 "dt", 

262 (datetime, date), 

263 date, 

264 'The "DTSTART" property for a "VEVENT" specifies the inclusive start of the event.', # noqa: E501 

265 ) 

266 DTEND = create_single_property( 

267 "DTEND", 

268 "dt", 

269 (datetime, date), 

270 date, 

271 'The "DTEND" property for a "VEVENT" calendar component specifies the non-inclusive end of the event.', # noqa: E501 

272 ) 

273 

274 def _get_start_end_duration(self): 

275 """Verify the calendar validity and return the right attributes.""" 

276 start = self.DTSTART 

277 end = self.DTEND 

278 duration = self.DURATION 

279 if duration is not None and end is not None: 

280 raise InvalidCalendar( 

281 "Only one of DTEND and DURATION may be in a VEVENT, not both." 

282 ) 

283 if ( 

284 start is not None 

285 and is_date(start) 

286 and duration is not None 

287 and duration.seconds != 0 

288 ): 

289 raise InvalidCalendar( 

290 "When DTSTART is a date, DURATION must be of days or weeks." 

291 ) 

292 if start is not None and end is not None and is_date(start) != is_date(end): 

293 raise InvalidCalendar( 

294 "DTSTART and DTEND must be of the same type, either date or datetime." 

295 ) 

296 return start, end, duration 

297 

298 DURATION = property( 

299 property_get_duration, 

300 property_set_duration, 

301 property_del_duration, 

302 property_doc_duration_template.format(component="VEVENT"), 

303 ) 

304 

305 @property 

306 def duration(self) -> timedelta: 

307 """The duration of the VEVENT. 

308 

309 Returns the DURATION property if set, otherwise calculated from start and end. 

310 When setting duration, the end time is automatically calculated from start + 

311 duration. 

312 You can set the duration to automatically adjust the end time while keeping 

313 start locked. 

314 

315 Setting the duration will: 

316 1. Keep the start time locked (unchanged) 

317 2. Adjust the end time to start + duration 

318 3. Remove any existing DTEND property 

319 4. Set the DURATION property 

320 """ 

321 # First check if DURATION property is explicitly set 

322 if "DURATION" in self: 

323 return self["DURATION"].dt 

324 

325 # Fall back to calculated duration from start and end 

326 return self.end - self.start 

327 

328 @duration.setter 

329 def duration(self, value: timedelta): 

330 if not isinstance(value, timedelta): 

331 raise TypeError(f"Use timedelta, not {type(value).__name__}.") 

332 

333 # Use the set_duration method with default start-locked behavior 

334 self.set_duration(value, locked="start") 

335 

336 @property 

337 def start(self) -> date | datetime: 

338 """The start of the event. 

339 

340 Invalid values raise an InvalidCalendar. 

341 If there is no start, we also raise an IncompleteComponent error. 

342 

343 You can get the start, end and duration of an event as follows: 

344 

345 >>> from datetime import datetime 

346 >>> from icalendar import Event 

347 >>> event = Event() 

348 >>> event.start = datetime(2021, 1, 1, 12) 

349 >>> event.end = datetime(2021, 1, 1, 12, 30) # 30 minutes 

350 >>> event.duration # 1800 seconds == 30 minutes 

351 datetime.timedelta(seconds=1800) 

352 >>> print(event.to_ical()) 

353 BEGIN:VEVENT 

354 DTSTART:20210101T120000 

355 DTEND:20210101T123000 

356 END:VEVENT 

357 """ 

358 start = self._get_start_end_duration()[0] 

359 if start is None: 

360 raise IncompleteComponent("No DTSTART given.") 

361 return start 

362 

363 @start.setter 

364 def start(self, start: date | datetime | None): 

365 """Set the start.""" 

366 self.DTSTART = start 

367 

368 @property 

369 def end(self) -> date | datetime: 

370 """The end of the event. 

371 

372 Invalid values raise an InvalidCalendar error. 

373 If there is no end, we also raise an IncompleteComponent error. 

374 """ 

375 start, end, duration = self._get_start_end_duration() 

376 if end is None and duration is None: 

377 if start is None: 

378 raise IncompleteComponent("No DTEND or DURATION+DTSTART given.") 

379 if is_date(start): 

380 return start + timedelta(days=1) 

381 return start 

382 if duration is not None: 

383 if start is not None: 

384 return start + duration 

385 raise IncompleteComponent("No DTEND or DURATION+DTSTART given.") 

386 return end 

387 

388 @end.setter 

389 def end(self, end: date | datetime | None): 

390 """Set the end.""" 

391 self.DTEND = end 

392 

393 def set_duration( 

394 self, duration: timedelta | None, locked: Literal["start", "end"] = "start" 

395 ): 

396 """Set the duration of the event relative to either start or end. 

397 

398 Args: 

399 duration: The duration to set, or None to convert to DURATION property 

400 locked: Which property to keep unchanged ('start' or 'end') 

401 """ 

402 from icalendar.attr import set_duration_with_locking 

403 

404 set_duration_with_locking(self, duration, locked, "DTEND") 

405 

406 def set_start( 

407 self, start: date | datetime, locked: Literal["duration", "end"] | None = None 

408 ): 

409 """Set the start and keep the duration or end of the event. 

410 

411 Args: 

412 start: The start time to set 

413 locked: Which property to keep unchanged ('duration', 'end', or None 

414 for auto-detect) 

415 """ 

416 if locked is None: 

417 # Auto-detect based on existing properties 

418 if "DURATION" in self: 

419 locked = "duration" 

420 elif "DTEND" in self: 

421 locked = "end" 

422 else: 

423 # Default to duration if no existing properties 

424 locked = "duration" 

425 

426 if locked == "duration": 

427 # Keep duration locked, adjust end 

428 current_duration = ( 

429 self.duration if "DURATION" in self or "DTEND" in self else None 

430 ) 

431 self.DTSTART = start 

432 if current_duration is not None: 

433 self.DURATION = current_duration 

434 elif locked == "end": 

435 # Keep end locked, adjust duration 

436 current_end = self.end 

437 self.DTSTART = start 

438 self.pop("DURATION", None) 

439 self.DTEND = current_end 

440 else: 

441 raise ValueError( 

442 f"locked must be 'duration', 'end', or None, not {locked!r}" 

443 ) 

444 

445 def set_end( 

446 self, end: date | datetime, locked: Literal["start", "duration"] = "start" 

447 ): 

448 """Set the end of the component, keeping either the start or the duration same. 

449 

450 Args: 

451 end: The end time to set 

452 locked: Which property to keep unchanged ('start' or 'duration') 

453 """ 

454 if locked == "start": 

455 # Keep start locked, adjust duration 

456 self.pop("DURATION", None) 

457 self.DTEND = end 

458 elif locked == "duration": 

459 # Keep duration locked, adjust start 

460 current_duration = self.duration 

461 self.DTSTART = end - current_duration 

462 self.DURATION = current_duration 

463 else: 

464 raise ValueError(f"locked must be 'start' or 'duration', not {locked!r}") 

465 

466 X_MOZ_SNOOZE_TIME = X_MOZ_SNOOZE_TIME_property 

467 X_MOZ_LASTACK = X_MOZ_LASTACK_property 

468 color = color_property 

469 sequence = sequence_property 

470 categories = categories_property 

471 rdates = rdates_property 

472 exdates = exdates_property 

473 rrules = rrules_property 

474 uid = uid_property 

475 summary = summary_property 

476 description = description_property 

477 classification = class_property 

478 url = url_property 

479 organizer = organizer_property 

480 location = location_property 

481 priority = priority_property 

482 contacts = contacts_property 

483 transparency = transparency_property 

484 status = status_property 

485 attendees = attendees_property 

486 

487 @classmethod 

488 def new( 

489 cls, 

490 /, 

491 attendees: list[vCalAddress] | None = None, 

492 categories: Sequence[str] = (), 

493 classification: CLASS | None = None, 

494 color: str | None = None, 

495 comments: list[str] | str | None = None, 

496 contacts: list[str] | str | None = None, 

497 created: date | None = None, 

498 description: str | None = None, 

499 end: date | datetime | None = None, 

500 last_modified: date | None = None, 

501 location: str | None = None, 

502 organizer: vCalAddress | str | None = None, 

503 priority: int | None = None, 

504 sequence: int | None = None, 

505 stamp: date | None = None, 

506 start: date | datetime | None = None, 

507 status: STATUS | None = None, 

508 transparency: TRANSP | None = None, 

509 summary: str | None = None, 

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

511 url: str | None = None, 

512 ): 

513 """Create a new event with all required properties. 

514 

515 This creates a new Event in accordance with :rfc:`5545`. 

516 

517 Arguments: 

518 attendees: The :attr:`attendees` of the event. 

519 categories: The :attr:`categories` of the event. 

520 classification: The :attr:`classification` of the event. 

521 color: The :attr:`color` of the event. 

522 comments: The :attr:`Component.comments` of the event. 

523 created: The :attr:`Component.created` of the event. 

524 description: The :attr:`description` of the event. 

525 end: The :attr:`end` of the event. 

526 last_modified: The :attr:`Component.last_modified` of the event. 

527 location: The :attr:`location` of the event. 

528 organizer: The :attr:`organizer` of the event. 

529 priority: The :attr:`priority` of the event. 

530 sequence: The :attr:`sequence` of the event. 

531 stamp: The :attr:`Component.stamp` of the event. 

532 If None, this is set to the current time. 

533 start: The :attr:`start` of the event. 

534 status: The :attr:`status` of the event. 

535 summary: The :attr:`summary` of the event. 

536 transparency: The :attr:`transparency` of the event. 

537 uid: The :attr:`uid` of the event. 

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

539 url: The :attr:`url` of the event. 

540 

541 Returns: 

542 :class:`Event` 

543 

544 Raises: 

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

546 

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

548 """ 

549 event = super().new( 

550 stamp=stamp if stamp is not None else cls._utc_now(), 

551 created=created, 

552 last_modified=last_modified, 

553 comments=comments, 

554 ) 

555 event.summary = summary 

556 event.description = description 

557 event.uid = uid if uid is not None else uuid.uuid4() 

558 event.start = start 

559 event.end = end 

560 event.color = color 

561 event.categories = categories 

562 event.sequence = sequence 

563 event.classification = classification 

564 event.url = url 

565 event.organizer = organizer 

566 event.location = location 

567 event.priority = priority 

568 event.transparency = transparency 

569 event.contacts = contacts 

570 event.status = status 

571 event.attendees = attendees 

572 if cls._validate_new: 

573 cls._validate_start_and_end(start, end) 

574 return event 

575 

576 

577__all__ = ["Event"]