Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/cal/todo.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

114 statements  

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

2 

3from __future__ import annotations 

4 

5import uuid 

6from datetime import date, datetime, timedelta 

7from typing import TYPE_CHECKING, Literal 

8 

9from icalendar.attr import ( 

10 ATTACHMENTS_TYPE_SETTER, 

11 ATTENDEE_TYPE_SETTER, 

12 CONCEPTS_TYPE_SETTER, 

13 LINKS_TYPE_SETTER, 

14 RELATED_TO_TYPE_SETTER, 

15 REQUEST_STATUS_property, 

16 RESOURCES_property, 

17 X_MOZ_LASTACK_property, 

18 X_MOZ_SNOOZE_TIME_property, 

19 attachments_property, 

20 attendees_property, 

21 categories_property, 

22 class_property, 

23 color_property, 

24 conferences_property, 

25 contacts_property, 

26 create_single_property, 

27 description_property, 

28 exdates_property, 

29 get_duration_property, 

30 get_end_property, 

31 get_start_end_duration_with_validation, 

32 get_start_property, 

33 images_property, 

34 location_property, 

35 organizer_property, 

36 priority_property, 

37 property_del_duration, 

38 property_doc_duration_template, 

39 property_get_duration, 

40 property_set_duration, 

41 rdates_property, 

42 rrules_property, 

43 sequence_property, 

44 set_duration_with_locking, 

45 set_end_with_locking, 

46 set_start_with_locking, 

47 status_property, 

48 summary_property, 

49 uid_property, 

50 url_property, 

51) 

52from icalendar.cal.component import Component 

53from icalendar.cal.examples import get_example 

54 

55if TYPE_CHECKING: 

56 from collections.abc import Iterable, Sequence 

57 

58 from icalendar.alarms import Alarms 

59 from icalendar.compatibility import Self 

60 from icalendar.enums import CLASS, STATUS 

61 from icalendar.prop import vCalAddress 

62 from icalendar.prop.conference import Conference 

63 

64 

65class Todo(Component): 

66 """ 

67 A "VTODO" calendar component is a grouping of component 

68 properties that represents an action item or assignment. For 

69 example, it can be used to represent an item of work assigned to 

70 an individual, such as "Prepare for the upcoming conference 

71 seminar on Internet Calendaring". 

72 

73 Examples: 

74 Create a new Todo: 

75 

76 >>> from icalendar import Todo 

77 >>> todo = Todo.new() 

78 >>> print(todo.to_ical()) 

79 BEGIN:VTODO 

80 DTSTAMP:20250517T080612Z 

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

82 END:VTODO 

83 

84 Complete the example Todo. 

85 

86 .. code-block:: pycon 

87 

88 >>> from datetime import datetime, timezone 

89 >>> from icalendar import Todo, STATUS 

90 >>> todo = Todo.example() 

91 >>> todo["PERCENT-COMPLETE"] = 100 

92 >>> todo["COMPLETED"] = datetime(2007, 5, 1, 12, tzinfo=timezone.utc) 

93 >>> todo.status = STATUS.COMPLETED 

94 >>> print(todo.to_ical().decode()) 

95 BEGIN:VTODO 

96 CATEGORIES:FAMILY,FINANCE 

97 CLASS:CONFIDENTIAL 

98 COMPLETED:2007-05-01 12:00:00+00:00 

99 DTSTAMP:20070313T123432Z 

100 DUE;VALUE=DATE:20070501 

101 PERCENT-COMPLETE:100 

102 STATUS:COMPLETED 

103 SUMMARY:Submit Quebec Income Tax Return for 2006 

104 UID:20070313T123432Z-456553@example.com 

105 END:VTODO 

106 

107 """ 

108 

109 name = "VTODO" 

110 

111 required = ( 

112 "UID", 

113 "DTSTAMP", 

114 ) 

115 singletons = ( 

116 "CLASS", 

117 "COLOR", 

118 "COMPLETED", 

119 "CREATED", 

120 "DESCRIPTION", 

121 "DTSTAMP", 

122 "DTSTART", 

123 "GEO", 

124 "LAST-MODIFIED", 

125 "LOCATION", 

126 "ORGANIZER", 

127 "PERCENT-COMPLETE", 

128 "PRIORITY", 

129 "RECURRENCE-ID", 

130 "SEQUENCE", 

131 "STATUS", 

132 "SUMMARY", 

133 "UID", 

134 "URL", 

135 "DUE", 

136 "DURATION", 

137 ) 

138 exclusive = ( 

139 "DUE", 

140 "DURATION", 

141 ) 

142 multiple = ( 

143 "ATTACH", 

144 "ATTENDEE", 

145 "CATEGORIES", 

146 "COMMENT", 

147 "CONTACT", 

148 "EXDATE", 

149 "REQUEST-STATUS", 

150 "RELATED", 

151 "RESOURCES", 

152 "RDATE", 

153 "RRULE", 

154 ) 

155 DTSTART = create_single_property( 

156 "DTSTART", 

157 "dt", 

158 (datetime, date), 

159 date, 

160 'The "DTSTART" property for a "VTODO" specifies the inclusive start of the Todo.', 

161 ) 

162 DUE = create_single_property( 

163 "DUE", 

164 "dt", 

165 (datetime, date), 

166 date, 

167 'The "DUE" property for a "VTODO" calendar component specifies the non-inclusive end of the Todo.', 

168 ) 

169 DURATION = property( 

170 property_get_duration, 

171 property_set_duration, 

172 property_del_duration, 

173 property_doc_duration_template.format(component="VTODO"), 

174 ) 

175 

176 def _get_start_end_duration(self): 

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

178 return get_start_end_duration_with_validation(self, "DTSTART", "DUE", "VTODO") 

179 

180 @property 

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

182 """The start of the VTODO. 

183 

184 Invalid values raise an :exc:`~icalendar.error.InvalidCalendar`. 

185 If there is no start, we also raise an :exc:`~icalendar.error.IncompleteComponent` error. 

186 

187 You can get the start, end and duration of a Todo as follows: 

188 

189 >>> from datetime import datetime 

190 >>> from icalendar import Todo 

191 >>> todo = Todo() 

192 >>> todo.start = datetime(2021, 1, 1, 12) 

193 >>> todo.end = datetime(2021, 1, 1, 12, 30) # 30 minutes 

194 >>> todo.duration # 1800 seconds == 30 minutes 

195 datetime.timedelta(seconds=1800) 

196 >>> print(todo.to_ical()) 

197 BEGIN:VTODO 

198 DTSTART:20210101T120000 

199 DUE:20210101T123000 

200 END:VTODO 

201 """ 

202 return get_start_property(self) 

203 

204 @start.setter 

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

206 """Set the start.""" 

207 self.DTSTART = start 

208 

209 @property 

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

211 """The end of the todo. 

212 

213 Invalid values raise an :exc:`~icalendar.error.InvalidCalendar` error. 

214 If there is no end, we also raise an :exc:`~icalendar.error.IncompleteComponent` error. 

215 """ 

216 return get_end_property(self, "DUE") 

217 

218 @end.setter 

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

220 """Set the end.""" 

221 self.DUE = end 

222 

223 @property 

224 def duration(self) -> timedelta: 

225 """The duration of the VTODO. 

226 

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

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

229 start locked. 

230 

231 Setting the duration will do the following. 

232 

233 1. Keep the start time locked (unchanged) 

234 2. Adjust the end time to start + duration 

235 3. Remove any existing DUE property 

236 4. Set the DURATION property 

237 """ 

238 if "DURATION" not in self and "DUE" in self and "DTSTART" not in self: 

239 return timedelta(0) 

240 return get_duration_property(self) 

241 

242 @duration.setter 

243 def duration(self, value: timedelta): 

244 if not isinstance(value, timedelta): 

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

246 

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

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

249 

250 def set_duration( 

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

252 ): 

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

254 

255 Parameters: 

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

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

258 """ 

259 set_duration_with_locking(self, duration, locked, "DUE") 

260 

261 def set_start( 

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

263 ): 

264 """Set the start with explicit locking behavior. 

265 

266 Parameters: 

267 start: The start time to set 

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

269 for auto-detect) 

270 """ 

271 set_start_with_locking(self, start, locked, "DUE") 

272 

273 def set_end( 

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

275 ): 

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

277 

278 Parameters: 

279 end: The end time to set 

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

281 """ 

282 set_end_with_locking(self, end, locked, "DUE") 

283 

284 X_MOZ_SNOOZE_TIME = X_MOZ_SNOOZE_TIME_property 

285 X_MOZ_LASTACK = X_MOZ_LASTACK_property 

286 

287 @property 

288 def alarms(self) -> Alarms: 

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

290 

291 >>> from datetime import datetime 

292 >>> from icalendar import Todo 

293 >>> todo = Todo() # empty without alarms 

294 >>> todo.start = datetime(2024, 10, 26, 10, 21) 

295 >>> len(todo.alarms.times) 

296 0 

297 

298 Note that this only uses DTSTART and DUE, but ignores 

299 RDATE, EXDATE, and RRULE properties. 

300 """ 

301 from icalendar.alarms import Alarms 

302 

303 return Alarms(self) 

304 

305 color = color_property 

306 sequence = sequence_property 

307 categories = categories_property 

308 rdates = rdates_property 

309 exdates = exdates_property 

310 rrules = rrules_property 

311 REQUEST_STATUS = REQUEST_STATUS_property 

312 RESOURCES = RESOURCES_property 

313 uid = uid_property 

314 summary = summary_property 

315 description = description_property 

316 classification = class_property 

317 url = url_property 

318 organizer = organizer_property 

319 location = location_property 

320 priority = priority_property 

321 contacts = contacts_property 

322 status = status_property 

323 attendees = attendees_property 

324 attachments = attachments_property 

325 images = images_property 

326 conferences = conferences_property 

327 from icalendar.attr import RECURRENCE_ID 

328 

329 @classmethod 

330 def new( 

331 cls, 

332 /, 

333 attachments: ATTACHMENTS_TYPE_SETTER = None, 

334 attendees: ATTENDEE_TYPE_SETTER = None, 

335 categories: Sequence[str] = (), 

336 classification: CLASS | None = None, 

337 color: str | None = None, 

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

339 concepts: CONCEPTS_TYPE_SETTER = None, 

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

341 conferences: list[Conference] | None = None, 

342 created: date | None = None, 

343 description: str | None = None, 

344 end: date | datetime | None = None, 

345 last_modified: date | None = None, 

346 links: LINKS_TYPE_SETTER = None, 

347 location: str | None = None, 

348 organizer: vCalAddress | str | None = None, 

349 priority: int | None = None, 

350 recurrence_id: date | datetime | None = None, 

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

352 related_to: RELATED_TO_TYPE_SETTER = None, 

353 request_status: list[str] | str | None = None, 

354 resources: list[str] | str | None = None, 

355 sequence: int | None = None, 

356 stamp: date | None = None, 

357 start: date | datetime | None = None, 

358 status: STATUS | None = None, 

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

360 summary: str | None = None, 

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

362 url: str | None = None, 

363 ) -> Self: 

364 """Create a new TODO with all required properties. 

365 

366 This creates a new Todo in accordance with :rfc:`5545`. 

367 

368 Parameters: 

369 attachments: The :attr:`attachments` of the todo. 

370 attendees: The :attr:`attendees` of the todo. 

371 categories: The :attr:`categories` of the todo. 

372 classification: The :attr:`classification` of the todo. 

373 color: The :attr:`color` of the todo. 

374 comments: The :attr:`~icalendar.Component.comments` of the todo. 

375 concepts: The :attr:`~icalendar.Component.concepts` of the todo. 

376 contacts: The :attr:`contacts` of the todo. 

377 conferences: The :attr:`conferences` of the todo. 

378 created: The :attr:`~icalendar.Component.created` of the todo. 

379 description: The :attr:`description` of the todo. 

380 end: The :attr:`end` of the todo. 

381 last_modified: The :attr:`~icalendar.Component.last_modified` of the todo. 

382 links: The :attr:`~icalendar.Component.links` of the todo. 

383 location: The :attr:`location` of the todo. 

384 organizer: The :attr:`organizer` of the todo. 

385 recurrence_id: The :attr:`RECURRENCE_ID` of the todo. 

386 refids: :attr:`~icalendar.Component.refids` of the todo. 

387 related_to: :attr:`~icalendar.Component.related_to` of the todo. 

388 request_status: The :attr:`REQUEST_STATUS` of the todo. 

389 resources: The :attr:`RESOURCES` of the todo. 

390 sequence: The :attr:`sequence` of the todo. 

391 stamp: The :attr:`~icalendar.Component.DTSTAMP` of the todo. 

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

393 start: The :attr:`start` of the todo. 

394 status: The :attr:`status` of the todo. 

395 subcomponents: The subcomponents of the todo. 

396 summary: The :attr:`summary` of the todo. 

397 uid: The :attr:`uid` of the todo. 

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

399 url: The :attr:`url` of the todo. 

400 

401 Returns: 

402 :class:`Todo` 

403 

404 Raises: 

405 ~error.InvalidCalendar: If the content is not valid 

406 according to :rfc:`5545`. 

407 

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

409 """ 

410 todo: Self = super().new( 

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

412 created=created, 

413 last_modified=last_modified, 

414 comments=comments, 

415 links=links, 

416 related_to=related_to, 

417 refids=refids, 

418 concepts=concepts, 

419 subcomponents=subcomponents, 

420 ) 

421 todo.summary = summary 

422 todo.description = description 

423 todo.uid = uid if uid is not None else uuid.uuid4() 

424 todo.start = start 

425 todo.end = end 

426 todo.color = color 

427 todo.categories = categories 

428 todo.sequence = sequence 

429 todo.classification = classification 

430 todo.url = url 

431 todo.organizer = organizer 

432 todo.location = location 

433 todo.priority = priority 

434 todo.attachments = attachments 

435 todo.contacts = contacts 

436 todo.status = status 

437 todo.REQUEST_STATUS = request_status 

438 todo.RESOURCES = resources 

439 todo.attendees = attendees 

440 todo.conferences = conferences 

441 todo.RECURRENCE_ID = recurrence_id 

442 

443 if cls._validate_new: 

444 cls._validate_start_and_end(start, end) 

445 return todo 

446 

447 @classmethod 

448 def example(cls, name: str = "example") -> Todo: 

449 """Return the todo example with the given name.""" 

450 return cls.from_ical(get_example("todos", name)) 

451 

452 

453__all__ = ["Todo"]