Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/prop.py: 84%

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

691 statements  

1"""This module contains the parser/generators (or coders/encoders if you 

2prefer) for the classes/datatypes that are used in iCalendar: 

3 

4########################################################################### 

5 

6# This module defines these property value data types and property parameters 

7 

84.2 Defined property parameters are: 

9 

10.. code-block:: text 

11 

12 ALTREP, CN, CUTYPE, DELEGATED-FROM, DELEGATED-TO, DIR, ENCODING, FMTTYPE, 

13 FBTYPE, LANGUAGE, MEMBER, PARTSTAT, RANGE, RELATED, RELTYPE, ROLE, RSVP, 

14 SENT-BY, TZID, VALUE 

15 

164.3 Defined value data types are: 

17 

18.. code-block:: text 

19 

20 BINARY, BOOLEAN, CAL-ADDRESS, DATE, DATE-TIME, DURATION, FLOAT, INTEGER, 

21 PERIOD, RECUR, TEXT, TIME, URI, UTC-OFFSET 

22 

23########################################################################### 

24 

25iCalendar properties have values. The values are strongly typed. This module 

26defines these types, calling val.to_ical() on them will render them as defined 

27in rfc5545. 

28 

29If you pass any of these classes a Python primitive, you will have an object 

30that can render itself as iCalendar formatted date. 

31 

32Property Value Data Types start with a 'v'. they all have an to_ical() and 

33from_ical() method. The to_ical() method generates a text string in the 

34iCalendar format. The from_ical() method can parse this format and return a 

35primitive Python datatype. So it should always be true that: 

36 

37.. code-block:: python 

38 

39 x == vDataType.from_ical(VDataType(x).to_ical()) 

40 

41These types are mainly used for parsing and file generation. But you can set 

42them directly. 

43""" 

44 

45from __future__ import annotations 

46 

47import base64 

48import binascii 

49import re 

50from datetime import date, datetime, time, timedelta 

51from typing import Any, Optional, Union 

52 

53from icalendar.caselessdict import CaselessDict 

54from icalendar.enums import VALUE, Enum 

55from icalendar.parser import Parameters, escape_char, unescape_char 

56from icalendar.parser_tools import ( 

57 DEFAULT_ENCODING, 

58 ICAL_TYPE, 

59 SEQUENCE_TYPES, 

60 from_unicode, 

61 to_unicode, 

62) 

63from icalendar.tools import to_datetime 

64 

65from .timezone import tzid_from_dt, tzid_from_tzinfo, tzp 

66 

67DURATION_REGEX = re.compile( 

68 r"([-+]?)P(?:(\d+)W)?(?:(\d+)D)?" r"(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$" 

69) 

70 

71WEEKDAY_RULE = re.compile( 

72 r"(?P<signal>[+-]?)(?P<relative>[\d]{0,2})" r"(?P<weekday>[\w]{2})$" 

73) 

74 

75 

76class vBinary: 

77 """Binary property values are base 64 encoded.""" 

78 

79 params: Parameters 

80 

81 def __init__(self, obj): 

82 self.obj = to_unicode(obj) 

83 self.params = Parameters(encoding="BASE64", value="BINARY") 

84 

85 def __repr__(self): 

86 return f"vBinary({self.to_ical()})" 

87 

88 def to_ical(self): 

89 return binascii.b2a_base64(self.obj.encode("utf-8"))[:-1] 

90 

91 @staticmethod 

92 def from_ical(ical): 

93 try: 

94 return base64.b64decode(ical) 

95 except ValueError as e: 

96 raise ValueError("Not valid base 64 encoding.") from e 

97 

98 def __eq__(self, other): 

99 """self == other""" 

100 return isinstance(other, vBinary) and self.obj == other.obj 

101 

102 

103class vBoolean(int): 

104 """Boolean 

105 

106 Value Name: BOOLEAN 

107 

108 Purpose: This value type is used to identify properties that contain 

109 either a "TRUE" or "FALSE" Boolean value. 

110 

111 Format Definition: This value type is defined by the following 

112 notation: 

113 

114 .. code-block:: text 

115 

116 boolean = "TRUE" / "FALSE" 

117 

118 Description: These values are case-insensitive text. No additional 

119 content value encoding is defined for this value type. 

120 

121 Example: The following is an example of a hypothetical property that 

122 has a BOOLEAN value type: 

123 

124 .. code-block:: python 

125 

126 TRUE 

127 

128 .. code-block:: pycon 

129 

130 >>> from icalendar.prop import vBoolean 

131 >>> boolean = vBoolean.from_ical('TRUE') 

132 >>> boolean 

133 True 

134 >>> boolean = vBoolean.from_ical('FALSE') 

135 >>> boolean 

136 False 

137 >>> boolean = vBoolean.from_ical('True') 

138 >>> boolean 

139 True 

140 """ 

141 

142 params: Parameters 

143 

144 BOOL_MAP = CaselessDict({"true": True, "false": False}) 

145 

146 def __new__(cls, *args, params: Optional[dict[str, Any]] = None, **kwargs): 

147 if params is None: 

148 params = {} 

149 self = super().__new__(cls, *args, **kwargs) 

150 self.params = Parameters(params) 

151 return self 

152 

153 def to_ical(self): 

154 return b"TRUE" if self else b"FALSE" 

155 

156 @classmethod 

157 def from_ical(cls, ical): 

158 try: 

159 return cls.BOOL_MAP[ical] 

160 except Exception as e: 

161 raise ValueError(f"Expected 'TRUE' or 'FALSE'. Got {ical}") from e 

162 

163 

164class vText(str): 

165 """Simple text.""" 

166 

167 params: Parameters 

168 __slots__ = ("encoding", "params") 

169 

170 def __new__( 

171 cls, 

172 value, 

173 encoding=DEFAULT_ENCODING, 

174 /, 

175 params: Optional[dict[str, Any]] = None, 

176 ): 

177 if params is None: 

178 params = {} 

179 value = to_unicode(value, encoding=encoding) 

180 self = super().__new__(cls, value) 

181 self.encoding = encoding 

182 self.params = Parameters(params) 

183 return self 

184 

185 def __repr__(self) -> str: 

186 return f"vText({self.to_ical()!r})" 

187 

188 def to_ical(self) -> bytes: 

189 return escape_char(self).encode(self.encoding) 

190 

191 @classmethod 

192 def from_ical(cls, ical: ICAL_TYPE): 

193 ical_unesc = unescape_char(ical) 

194 return cls(ical_unesc) 

195 

196 from icalendar.param import ALTREP, LANGUAGE, RELTYPE 

197 

198 

199class vCalAddress(str): 

200 """Calendar User Address 

201 

202 Value Name: 

203 CAL-ADDRESS 

204 

205 Purpose: 

206 This value type is used to identify properties that contain a 

207 calendar user address. 

208 

209 Description: 

210 The value is a URI as defined by [RFC3986] or any other 

211 IANA-registered form for a URI. When used to address an Internet 

212 email transport address for a calendar user, the value MUST be a 

213 mailto URI, as defined by [RFC2368]. 

214 

215 Example: 

216 ``mailto:`` is in front of the address. 

217 

218 .. code-block:: text 

219 

220 mailto:jane_doe@example.com 

221 

222 Parsing: 

223 

224 .. code-block:: pycon 

225 

226 >>> from icalendar import vCalAddress 

227 >>> cal_address = vCalAddress.from_ical('mailto:jane_doe@example.com') 

228 >>> cal_address 

229 vCalAddress('mailto:jane_doe@example.com') 

230 

231 Encoding: 

232 

233 .. code-block:: pycon 

234 

235 >>> from icalendar import vCalAddress, Event 

236 >>> event = Event() 

237 >>> jane = vCalAddress("mailto:jane_doe@example.com") 

238 >>> jane.name = "Jane" 

239 >>> event["organizer"] = jane 

240 >>> print(event.to_ical()) 

241 BEGIN:VEVENT 

242 ORGANIZER;CN=Jane:mailto:jane_doe@example.com 

243 END:VEVENT 

244 """ 

245 

246 params: Parameters 

247 __slots__ = ("params",) 

248 

249 def __new__( 

250 cls, 

251 value, 

252 encoding=DEFAULT_ENCODING, 

253 /, 

254 params: Optional[dict[str, Any]] = None, 

255 ): 

256 if params is None: 

257 params = {} 

258 value = to_unicode(value, encoding=encoding) 

259 self = super().__new__(cls, value) 

260 self.params = Parameters(params) 

261 return self 

262 

263 def __repr__(self): 

264 return f"vCalAddress('{self}')" 

265 

266 def to_ical(self): 

267 return self.encode(DEFAULT_ENCODING) 

268 

269 @classmethod 

270 def from_ical(cls, ical): 

271 return cls(ical) 

272 

273 @property 

274 def email(self) -> str: 

275 """The email address without mailto: at the start.""" 

276 if self.lower().startswith("mailto:"): 

277 return self[7:] 

278 return str(self) 

279 

280 from icalendar.param import ( 

281 CN, 

282 CUTYPE, 

283 DELEGATED_FROM, 

284 DELEGATED_TO, 

285 DIR, 

286 LANGUAGE, 

287 PARTSTAT, 

288 ROLE, 

289 RSVP, 

290 SENT_BY, 

291 ) 

292 

293 name = CN 

294 

295 

296class vFloat(float): 

297 """Float 

298 

299 Value Name: 

300 FLOAT 

301 

302 Purpose: 

303 This value type is used to identify properties that contain 

304 a real-number value. 

305 

306 Format Definition: 

307 This value type is defined by the following notation: 

308 

309 .. code-block:: text 

310 

311 float = (["+"] / "-") 1*DIGIT ["." 1*DIGIT] 

312 

313 Description: 

314 If the property permits, multiple "float" values are 

315 specified by a COMMA-separated list of values. 

316 

317 Example: 

318 

319 .. code-block:: text 

320 

321 1000000.0000001 

322 1.333 

323 -3.14 

324 

325 .. code-block:: pycon 

326 

327 >>> from icalendar.prop import vFloat 

328 >>> float = vFloat.from_ical('1000000.0000001') 

329 >>> float 

330 1000000.0000001 

331 >>> float = vFloat.from_ical('1.333') 

332 >>> float 

333 1.333 

334 >>> float = vFloat.from_ical('+1.333') 

335 >>> float 

336 1.333 

337 >>> float = vFloat.from_ical('-3.14') 

338 >>> float 

339 -3.14 

340 """ 

341 

342 params: Parameters 

343 

344 def __new__(cls, *args, params: Optional[dict[str, Any]] = None, **kwargs): 

345 if params is None: 

346 params = {} 

347 self = super().__new__(cls, *args, **kwargs) 

348 self.params = Parameters(params) 

349 return self 

350 

351 def to_ical(self): 

352 return str(self).encode("utf-8") 

353 

354 @classmethod 

355 def from_ical(cls, ical): 

356 try: 

357 return cls(ical) 

358 except Exception as e: 

359 raise ValueError(f"Expected float value, got: {ical}") from e 

360 

361 

362class vInt(int): 

363 """Integer 

364 

365 Value Name: 

366 INTEGER 

367 

368 Purpose: 

369 This value type is used to identify properties that contain a 

370 signed integer value. 

371 

372 Format Definition: 

373 This value type is defined by the following notation: 

374 

375 .. code-block:: text 

376 

377 integer = (["+"] / "-") 1*DIGIT 

378 

379 Description: 

380 If the property permits, multiple "integer" values are 

381 specified by a COMMA-separated list of values. The valid range 

382 for "integer" is -2147483648 to 2147483647. If the sign is not 

383 specified, then the value is assumed to be positive. 

384 

385 Example: 

386 

387 .. code-block:: text 

388 

389 1234567890 

390 -1234567890 

391 +1234567890 

392 432109876 

393 

394 .. code-block:: pycon 

395 

396 >>> from icalendar.prop import vInt 

397 >>> integer = vInt.from_ical('1234567890') 

398 >>> integer 

399 1234567890 

400 >>> integer = vInt.from_ical('-1234567890') 

401 >>> integer 

402 -1234567890 

403 >>> integer = vInt.from_ical('+1234567890') 

404 >>> integer 

405 1234567890 

406 >>> integer = vInt.from_ical('432109876') 

407 >>> integer 

408 432109876 

409 """ 

410 

411 params: Parameters 

412 

413 def __new__(cls, *args, params: Optional[dict[str, Any]] = None, **kwargs): 

414 if params is None: 

415 params = {} 

416 self = super().__new__(cls, *args, **kwargs) 

417 self.params = Parameters(params) 

418 return self 

419 

420 def to_ical(self) -> bytes: 

421 return str(self).encode("utf-8") 

422 

423 @classmethod 

424 def from_ical(cls, ical: ICAL_TYPE): 

425 try: 

426 return cls(ical) 

427 except Exception as e: 

428 raise ValueError(f"Expected int, got: {ical}") from e 

429 

430 

431class vDDDLists: 

432 """A list of vDDDTypes values.""" 

433 

434 params: Parameters 

435 dts: list 

436 

437 def __init__(self, dt_list): 

438 if not hasattr(dt_list, "__iter__"): 

439 dt_list = [dt_list] 

440 vddd = [] 

441 tzid = None 

442 for dt_l in dt_list: 

443 dt = vDDDTypes(dt_l) 

444 vddd.append(dt) 

445 if "TZID" in dt.params: 

446 tzid = dt.params["TZID"] 

447 

448 params = {} 

449 if tzid: 

450 # NOTE: no support for multiple timezones here! 

451 params["TZID"] = tzid 

452 self.params = Parameters(params) 

453 self.dts = vddd 

454 

455 def to_ical(self): 

456 dts_ical = (from_unicode(dt.to_ical()) for dt in self.dts) 

457 return b",".join(dts_ical) 

458 

459 @staticmethod 

460 def from_ical(ical, timezone=None): 

461 out = [] 

462 ical_dates = ical.split(",") 

463 for ical_dt in ical_dates: 

464 out.append(vDDDTypes.from_ical(ical_dt, timezone=timezone)) 

465 return out 

466 

467 def __eq__(self, other): 

468 if isinstance(other, vDDDLists): 

469 return self.dts == other.dts 

470 if isinstance(other, (TimeBase, date)): 

471 return self.dts == [other] 

472 return False 

473 

474 def __repr__(self): 

475 """String representation.""" 

476 return f"{self.__class__.__name__}({self.dts})" 

477 

478 

479class vCategory: 

480 params: Parameters 

481 

482 def __init__( 

483 self, c_list: list[str] | str, /, params: Optional[dict[str, Any]] = None 

484 ): 

485 if params is None: 

486 params = {} 

487 if not hasattr(c_list, "__iter__") or isinstance(c_list, str): 

488 c_list = [c_list] 

489 self.cats: list[vText | str] = [vText(c) for c in c_list] 

490 self.params = Parameters(params) 

491 

492 def __iter__(self): 

493 return iter(vCategory.from_ical(self.to_ical())) 

494 

495 def to_ical(self): 

496 return b",".join( 

497 [ 

498 c.to_ical() if hasattr(c, "to_ical") else vText(c).to_ical() 

499 for c in self.cats 

500 ] 

501 ) 

502 

503 @staticmethod 

504 def from_ical(ical): 

505 ical = to_unicode(ical) 

506 return unescape_char(ical).split(",") 

507 

508 def __eq__(self, other): 

509 """self == other""" 

510 return isinstance(other, vCategory) and self.cats == other.cats 

511 

512 def __repr__(self): 

513 """String representation.""" 

514 return f"{self.__class__.__name__}({self.cats}, params={self.params})" 

515 

516 

517class TimeBase: 

518 """Make classes with a datetime/date comparable.""" 

519 

520 params: Parameters 

521 ignore_for_equality = {"TZID", "VALUE"} 

522 

523 def __eq__(self, other): 

524 """self == other""" 

525 if isinstance(other, date): 

526 return self.dt == other 

527 if isinstance(other, TimeBase): 

528 default = object() 

529 for key in ( 

530 set(self.params) | set(other.params) 

531 ) - self.ignore_for_equality: 

532 if key[:2].lower() != "x-" and self.params.get( 

533 key, default 

534 ) != other.params.get(key, default): 

535 return False 

536 return self.dt == other.dt 

537 if isinstance(other, vDDDLists): 

538 return other == self 

539 return False 

540 

541 def __hash__(self): 

542 return hash(self.dt) 

543 

544 from icalendar.param import RANGE, RELATED, TZID 

545 

546 def __repr__(self): 

547 """String representation.""" 

548 return f"{self.__class__.__name__}({self.dt}, {self.params})" 

549 

550 

551class vDDDTypes(TimeBase): 

552 """A combined Datetime, Date or Duration parser/generator. Their format 

553 cannot be confused, and often values can be of either types. 

554 So this is practical. 

555 """ 

556 

557 params: Parameters 

558 

559 def __init__(self, dt): 

560 if not isinstance(dt, (datetime, date, timedelta, time, tuple)): 

561 raise TypeError( 

562 "You must use datetime, date, timedelta, time or tuple (for periods)" 

563 ) 

564 if isinstance(dt, (datetime, timedelta)): 

565 self.params = Parameters() 

566 elif isinstance(dt, date): 

567 self.params = Parameters({"value": "DATE"}) 

568 elif isinstance(dt, time): 

569 self.params = Parameters({"value": "TIME"}) 

570 else: # isinstance(dt, tuple) 

571 self.params = Parameters({"value": "PERIOD"}) 

572 

573 tzid = tzid_from_dt(dt) if isinstance(dt, (datetime, time)) else None 

574 if tzid is not None and tzid != "UTC": 

575 self.params.update({"TZID": tzid}) 

576 

577 self.dt = dt 

578 

579 def to_ical(self): 

580 dt = self.dt 

581 if isinstance(dt, datetime): 

582 return vDatetime(dt).to_ical() 

583 if isinstance(dt, date): 

584 return vDate(dt).to_ical() 

585 if isinstance(dt, timedelta): 

586 return vDuration(dt).to_ical() 

587 if isinstance(dt, time): 

588 return vTime(dt).to_ical() 

589 if isinstance(dt, tuple) and len(dt) == 2: 

590 return vPeriod(dt).to_ical() 

591 raise ValueError(f"Unknown date type: {type(dt)}") 

592 

593 @classmethod 

594 def from_ical(cls, ical, timezone=None): 

595 if isinstance(ical, cls): 

596 return ical.dt 

597 u = ical.upper() 

598 if u.startswith(("P", "-P", "+P")): 

599 return vDuration.from_ical(ical) 

600 if "/" in u: 

601 return vPeriod.from_ical(ical, timezone=timezone) 

602 

603 if len(ical) in (15, 16): 

604 return vDatetime.from_ical(ical, timezone=timezone) 

605 if len(ical) == 8: 

606 if timezone: 

607 tzinfo = tzp.timezone(timezone) 

608 if tzinfo is not None: 

609 return to_datetime(vDate.from_ical(ical)).replace(tzinfo=tzinfo) 

610 return vDate.from_ical(ical) 

611 if len(ical) in (6, 7): 

612 return vTime.from_ical(ical) 

613 raise ValueError(f"Expected datetime, date, or time. Got: '{ical}'") 

614 

615 

616class vDate(TimeBase): 

617 """Date 

618 

619 Value Name: 

620 DATE 

621 

622 Purpose: 

623 This value type is used to identify values that contain a 

624 calendar date. 

625 

626 Format Definition: 

627 This value type is defined by the following notation: 

628 

629 .. code-block:: text 

630 

631 date = date-value 

632 

633 date-value = date-fullyear date-month date-mday 

634 date-fullyear = 4DIGIT 

635 date-month = 2DIGIT ;01-12 

636 date-mday = 2DIGIT ;01-28, 01-29, 01-30, 01-31 

637 ;based on month/year 

638 

639 Description: 

640 If the property permits, multiple "date" values are 

641 specified as a COMMA-separated list of values. The format for the 

642 value type is based on the [ISO.8601.2004] complete 

643 representation, basic format for a calendar date. The textual 

644 format specifies a four-digit year, two-digit month, and two-digit 

645 day of the month. There are no separator characters between the 

646 year, month, and day component text. 

647 

648 Example: 

649 The following represents July 14, 1997: 

650 

651 .. code-block:: text 

652 

653 19970714 

654 

655 .. code-block:: pycon 

656 

657 >>> from icalendar.prop import vDate 

658 >>> date = vDate.from_ical('19970714') 

659 >>> date.year 

660 1997 

661 >>> date.month 

662 7 

663 >>> date.day 

664 14 

665 """ 

666 

667 params: Parameters 

668 

669 def __init__(self, dt): 

670 if not isinstance(dt, date): 

671 raise TypeError("Value MUST be a date instance") 

672 self.dt = dt 

673 self.params = Parameters({"value": "DATE"}) 

674 

675 def to_ical(self): 

676 s = f"{self.dt.year:04}{self.dt.month:02}{self.dt.day:02}" 

677 return s.encode("utf-8") 

678 

679 @staticmethod 

680 def from_ical(ical): 

681 try: 

682 timetuple = ( 

683 int(ical[:4]), # year 

684 int(ical[4:6]), # month 

685 int(ical[6:8]), # day 

686 ) 

687 return date(*timetuple) 

688 except Exception as e: 

689 raise ValueError(f"Wrong date format {ical}") from e 

690 

691 

692class vDatetime(TimeBase): 

693 """Render and generates icalendar datetime format. 

694 

695 vDatetime is timezone aware and uses a timezone library. 

696 When a vDatetime object is created from an 

697 ical string, you can pass a valid timezone identifier. When a 

698 vDatetime object is created from a python datetime object, it uses the 

699 tzinfo component, if present. Otherwise a timezone-naive object is 

700 created. Be aware that there are certain limitations with timezone naive 

701 DATE-TIME components in the icalendar standard. 

702 """ 

703 

704 params: Parameters 

705 

706 def __init__(self, dt, /, params: Optional[dict[str, Any]] = None): 

707 if params is None: 

708 params = {} 

709 self.dt = dt 

710 self.params = Parameters(params) 

711 

712 def to_ical(self): 

713 dt = self.dt 

714 tzid = tzid_from_dt(dt) 

715 

716 s = ( 

717 f"{dt.year:04}{dt.month:02}{dt.day:02}" 

718 f"T{dt.hour:02}{dt.minute:02}{dt.second:02}" 

719 ) 

720 if tzid == "UTC": 

721 s += "Z" 

722 elif tzid: 

723 self.params.update({"TZID": tzid}) 

724 return s.encode("utf-8") 

725 

726 @staticmethod 

727 def from_ical(ical, timezone=None): 

728 """Create a datetime from the RFC string. 

729 

730 Format: 

731 

732 .. code-block:: text 

733 

734 YYYYMMDDTHHMMSS 

735 

736 .. code-block:: pycon 

737 

738 >>> from icalendar import vDatetime 

739 >>> vDatetime.from_ical("20210302T101500") 

740 datetime.datetime(2021, 3, 2, 10, 15) 

741 

742 >>> vDatetime.from_ical("20210302T101500", "America/New_York") 

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

744 

745 >>> from zoneinfo import ZoneInfo 

746 >>> timezone = ZoneInfo("Europe/Berlin") 

747 >>> vDatetime.from_ical("20210302T101500", timezone) 

748 datetime.datetime(2021, 3, 2, 10, 15, tzinfo=ZoneInfo(key='Europe/Berlin')) 

749 """ # noqa: E501 

750 tzinfo = None 

751 if isinstance(timezone, str): 

752 tzinfo = tzp.timezone(timezone) 

753 elif timezone is not None: 

754 tzinfo = timezone 

755 

756 try: 

757 timetuple = ( 

758 int(ical[:4]), # year 

759 int(ical[4:6]), # month 

760 int(ical[6:8]), # day 

761 int(ical[9:11]), # hour 

762 int(ical[11:13]), # minute 

763 int(ical[13:15]), # second 

764 ) 

765 if tzinfo: 

766 return tzp.localize(datetime(*timetuple), tzinfo) # noqa: DTZ001 

767 if not ical[15:]: 

768 return datetime(*timetuple) # noqa: DTZ001 

769 if ical[15:16] == "Z": 

770 return tzp.localize_utc(datetime(*timetuple)) # noqa: DTZ001 

771 except Exception as e: 

772 raise ValueError(f"Wrong datetime format: {ical}") from e 

773 raise ValueError(f"Wrong datetime format: {ical}") 

774 

775 

776class vDuration(TimeBase): 

777 """Duration 

778 

779 Value Name: 

780 DURATION 

781 

782 Purpose: 

783 This value type is used to identify properties that contain 

784 a duration of time. 

785 

786 Format Definition: 

787 This value type is defined by the following notation: 

788 

789 .. code-block:: text 

790 

791 dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week) 

792 

793 dur-date = dur-day [dur-time] 

794 dur-time = "T" (dur-hour / dur-minute / dur-second) 

795 dur-week = 1*DIGIT "W" 

796 dur-hour = 1*DIGIT "H" [dur-minute] 

797 dur-minute = 1*DIGIT "M" [dur-second] 

798 dur-second = 1*DIGIT "S" 

799 dur-day = 1*DIGIT "D" 

800 

801 Description: 

802 If the property permits, multiple "duration" values are 

803 specified by a COMMA-separated list of values. The format is 

804 based on the [ISO.8601.2004] complete representation basic format 

805 with designators for the duration of time. The format can 

806 represent nominal durations (weeks and days) and accurate 

807 durations (hours, minutes, and seconds). Note that unlike 

808 [ISO.8601.2004], this value type doesn't support the "Y" and "M" 

809 designators to specify durations in terms of years and months. 

810 The duration of a week or a day depends on its position in the 

811 calendar. In the case of discontinuities in the time scale, such 

812 as the change from standard time to daylight time and back, the 

813 computation of the exact duration requires the subtraction or 

814 addition of the change of duration of the discontinuity. Leap 

815 seconds MUST NOT be considered when computing an exact duration. 

816 When computing an exact duration, the greatest order time 

817 components MUST be added first, that is, the number of days MUST 

818 be added first, followed by the number of hours, number of 

819 minutes, and number of seconds. 

820 

821 Example: 

822 A duration of 15 days, 5 hours, and 20 seconds would be: 

823 

824 .. code-block:: text 

825 

826 P15DT5H0M20S 

827 

828 A duration of 7 weeks would be: 

829 

830 .. code-block:: text 

831 

832 P7W 

833 

834 .. code-block:: pycon 

835 

836 >>> from icalendar.prop import vDuration 

837 >>> duration = vDuration.from_ical('P15DT5H0M20S') 

838 >>> duration 

839 datetime.timedelta(days=15, seconds=18020) 

840 >>> duration = vDuration.from_ical('P7W') 

841 >>> duration 

842 datetime.timedelta(days=49) 

843 """ 

844 

845 params: Parameters 

846 

847 def __init__(self, td, /, params: Optional[dict[str, Any]] = None): 

848 if params is None: 

849 params = {} 

850 if not isinstance(td, timedelta): 

851 raise TypeError("Value MUST be a timedelta instance") 

852 self.td = td 

853 self.params = Parameters(params) 

854 

855 def to_ical(self): 

856 sign = "" 

857 td = self.td 

858 if td.days < 0: 

859 sign = "-" 

860 td = -td 

861 timepart = "" 

862 if td.seconds: 

863 timepart = "T" 

864 hours = td.seconds // 3600 

865 minutes = td.seconds % 3600 // 60 

866 seconds = td.seconds % 60 

867 if hours: 

868 timepart += f"{hours}H" 

869 if minutes or (hours and seconds): 

870 timepart += f"{minutes}M" 

871 if seconds: 

872 timepart += f"{seconds}S" 

873 if td.days == 0 and timepart: 

874 return str(sign).encode("utf-8") + b"P" + str(timepart).encode("utf-8") 

875 return ( 

876 str(sign).encode("utf-8") 

877 + b"P" 

878 + str(abs(td.days)).encode("utf-8") 

879 + b"D" 

880 + str(timepart).encode("utf-8") 

881 ) 

882 

883 @staticmethod 

884 def from_ical(ical): 

885 match = DURATION_REGEX.match(ical) 

886 if not match: 

887 raise ValueError(f"Invalid iCalendar duration: {ical}") 

888 

889 sign, weeks, days, hours, minutes, seconds = match.groups() 

890 value = timedelta( 

891 weeks=int(weeks or 0), 

892 days=int(days or 0), 

893 hours=int(hours or 0), 

894 minutes=int(minutes or 0), 

895 seconds=int(seconds or 0), 

896 ) 

897 

898 if sign == "-": 

899 value = -value 

900 

901 return value 

902 

903 @property 

904 def dt(self) -> timedelta: 

905 """The time delta for compatibility.""" 

906 return self.td 

907 

908 

909class vPeriod(TimeBase): 

910 """Period of Time 

911 

912 Value Name: 

913 PERIOD 

914 

915 Purpose: 

916 This value type is used to identify values that contain a 

917 precise period of time. 

918 

919 Format Definition: 

920 This value type is defined by the following notation: 

921 

922 .. code-block:: text 

923 

924 period = period-explicit / period-start 

925 

926 period-explicit = date-time "/" date-time 

927 ; [ISO.8601.2004] complete representation basic format for a 

928 ; period of time consisting of a start and end. The start MUST 

929 ; be before the end. 

930 

931 period-start = date-time "/" dur-value 

932 ; [ISO.8601.2004] complete representation basic format for a 

933 ; period of time consisting of a start and positive duration 

934 ; of time. 

935 

936 Description: 

937 If the property permits, multiple "period" values are 

938 specified by a COMMA-separated list of values. There are two 

939 forms of a period of time. First, a period of time is identified 

940 by its start and its end. This format is based on the 

941 [ISO.8601.2004] complete representation, basic format for "DATE- 

942 TIME" start of the period, followed by a SOLIDUS character 

943 followed by the "DATE-TIME" of the end of the period. The start 

944 of the period MUST be before the end of the period. Second, a 

945 period of time can also be defined by a start and a positive 

946 duration of time. The format is based on the [ISO.8601.2004] 

947 complete representation, basic format for the "DATE-TIME" start of 

948 the period, followed by a SOLIDUS character, followed by the 

949 [ISO.8601.2004] basic format for "DURATION" of the period. 

950 

951 Example: 

952 The period starting at 18:00:00 UTC, on January 1, 1997 and 

953 ending at 07:00:00 UTC on January 2, 1997 would be: 

954 

955 .. code-block:: text 

956 

957 19970101T180000Z/19970102T070000Z 

958 

959 The period start at 18:00:00 on January 1, 1997 and lasting 5 hours 

960 and 30 minutes would be: 

961 

962 .. code-block:: text 

963 

964 19970101T180000Z/PT5H30M 

965 

966 .. code-block:: pycon 

967 

968 >>> from icalendar.prop import vPeriod 

969 >>> period = vPeriod.from_ical('19970101T180000Z/19970102T070000Z') 

970 >>> period = vPeriod.from_ical('19970101T180000Z/PT5H30M') 

971 """ 

972 

973 params: Parameters 

974 

975 def __init__(self, per: tuple[datetime, Union[datetime, timedelta]]): 

976 start, end_or_duration = per 

977 if not (isinstance(start, (datetime, date))): 

978 raise TypeError("Start value MUST be a datetime or date instance") 

979 if not (isinstance(end_or_duration, (datetime, date, timedelta))): 

980 raise TypeError( 

981 "end_or_duration MUST be a datetime, date or timedelta instance" 

982 ) 

983 by_duration = 0 

984 if isinstance(end_or_duration, timedelta): 

985 by_duration = 1 

986 duration = end_or_duration 

987 end = start + duration 

988 else: 

989 end = end_or_duration 

990 duration = end - start 

991 if start > end: 

992 raise ValueError("Start time is greater than end time") 

993 

994 self.params = Parameters({"value": "PERIOD"}) 

995 # set the timezone identifier 

996 # does not support different timezones for start and end 

997 tzid = tzid_from_dt(start) 

998 if tzid: 

999 self.params["TZID"] = tzid 

1000 

1001 self.start = start 

1002 self.end = end 

1003 self.by_duration = by_duration 

1004 self.duration = duration 

1005 

1006 def overlaps(self, other): 

1007 if self.start > other.start: 

1008 return other.overlaps(self) 

1009 return self.start <= other.start < self.end 

1010 

1011 def to_ical(self): 

1012 if self.by_duration: 

1013 return ( 

1014 vDatetime(self.start).to_ical() 

1015 + b"/" 

1016 + vDuration(self.duration).to_ical() 

1017 ) 

1018 return vDatetime(self.start).to_ical() + b"/" + vDatetime(self.end).to_ical() 

1019 

1020 @staticmethod 

1021 def from_ical(ical, timezone=None): 

1022 try: 

1023 start, end_or_duration = ical.split("/") 

1024 start = vDDDTypes.from_ical(start, timezone=timezone) 

1025 end_or_duration = vDDDTypes.from_ical(end_or_duration, timezone=timezone) 

1026 except Exception as e: 

1027 raise ValueError(f"Expected period format, got: {ical}") from e 

1028 return (start, end_or_duration) 

1029 

1030 def __repr__(self): 

1031 p = (self.start, self.duration) if self.by_duration else (self.start, self.end) 

1032 return f"vPeriod({p!r})" 

1033 

1034 @property 

1035 def dt(self): 

1036 """Make this cooperate with the other vDDDTypes.""" 

1037 return (self.start, (self.duration if self.by_duration else self.end)) 

1038 

1039 from icalendar.param import FBTYPE 

1040 

1041 

1042class vWeekday(str): 

1043 """Either a ``weekday`` or a ``weekdaynum`` 

1044 

1045 .. code-block:: pycon 

1046 

1047 >>> from icalendar import vWeekday 

1048 >>> vWeekday("MO") # Simple weekday 

1049 'MO' 

1050 >>> vWeekday("2FR").relative # Second friday 

1051 2 

1052 >>> vWeekday("2FR").weekday 

1053 'FR' 

1054 >>> vWeekday("-1SU").relative # Last Sunday 

1055 -1 

1056 

1057 Definition from `RFC 5545, Section 3.3.10 <https://www.rfc-editor.org/rfc/rfc5545#section-3.3.10>`_: 

1058 

1059 .. code-block:: text 

1060 

1061 weekdaynum = [[plus / minus] ordwk] weekday 

1062 plus = "+" 

1063 minus = "-" 

1064 ordwk = 1*2DIGIT ;1 to 53 

1065 weekday = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA" 

1066 ;Corresponding to SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, 

1067 ;FRIDAY, and SATURDAY days of the week. 

1068 

1069 """ 

1070 

1071 params: Parameters 

1072 __slots__ = ("params", "relative", "weekday") 

1073 

1074 week_days = CaselessDict( 

1075 { 

1076 "SU": 0, 

1077 "MO": 1, 

1078 "TU": 2, 

1079 "WE": 3, 

1080 "TH": 4, 

1081 "FR": 5, 

1082 "SA": 6, 

1083 } 

1084 ) 

1085 

1086 def __new__( 

1087 cls, 

1088 value, 

1089 encoding=DEFAULT_ENCODING, 

1090 /, 

1091 params: Optional[dict[str, Any]] = None, 

1092 ): 

1093 if params is None: 

1094 params = {} 

1095 value = to_unicode(value, encoding=encoding) 

1096 self = super().__new__(cls, value) 

1097 match = WEEKDAY_RULE.match(self) 

1098 if match is None: 

1099 raise ValueError(f"Expected weekday abbrevation, got: {self}") 

1100 match = match.groupdict() 

1101 sign = match["signal"] 

1102 weekday = match["weekday"] 

1103 relative = match["relative"] 

1104 if weekday not in vWeekday.week_days or sign not in "+-": 

1105 raise ValueError(f"Expected weekday abbrevation, got: {self}") 

1106 self.weekday = weekday or None 

1107 self.relative = (relative and int(relative)) or None 

1108 if sign == "-" and self.relative: 

1109 self.relative *= -1 

1110 self.params = Parameters(params) 

1111 return self 

1112 

1113 def to_ical(self): 

1114 return self.encode(DEFAULT_ENCODING).upper() 

1115 

1116 @classmethod 

1117 def from_ical(cls, ical): 

1118 try: 

1119 return cls(ical.upper()) 

1120 except Exception as e: 

1121 raise ValueError(f"Expected weekday abbrevation, got: {ical}") from e 

1122 

1123 

1124class vFrequency(str): 

1125 """A simple class that catches illegal values.""" 

1126 

1127 params: Parameters 

1128 __slots__ = ("params",) 

1129 

1130 frequencies = CaselessDict( 

1131 { 

1132 "SECONDLY": "SECONDLY", 

1133 "MINUTELY": "MINUTELY", 

1134 "HOURLY": "HOURLY", 

1135 "DAILY": "DAILY", 

1136 "WEEKLY": "WEEKLY", 

1137 "MONTHLY": "MONTHLY", 

1138 "YEARLY": "YEARLY", 

1139 } 

1140 ) 

1141 

1142 def __new__( 

1143 cls, 

1144 value, 

1145 encoding=DEFAULT_ENCODING, 

1146 /, 

1147 params: Optional[dict[str, Any]] = None, 

1148 ): 

1149 if params is None: 

1150 params = {} 

1151 value = to_unicode(value, encoding=encoding) 

1152 self = super().__new__(cls, value) 

1153 if self not in vFrequency.frequencies: 

1154 raise ValueError(f"Expected frequency, got: {self}") 

1155 self.params = Parameters(params) 

1156 return self 

1157 

1158 def to_ical(self): 

1159 return self.encode(DEFAULT_ENCODING).upper() 

1160 

1161 @classmethod 

1162 def from_ical(cls, ical): 

1163 try: 

1164 return cls(ical.upper()) 

1165 except Exception as e: 

1166 raise ValueError(f"Expected frequency, got: {ical}") from e 

1167 

1168 

1169class vMonth(int): 

1170 """The number of the month for recurrence. 

1171 

1172 In :rfc:`5545`, this is just an int. 

1173 In :rfc:`7529`, this can be followed by `L` to indicate a leap month. 

1174 

1175 .. code-block:: pycon 

1176 

1177 >>> from icalendar import vMonth 

1178 >>> vMonth(1) # first month January 

1179 vMonth('1') 

1180 >>> vMonth("5L") # leap month in Hebrew calendar 

1181 vMonth('5L') 

1182 >>> vMonth(1).leap 

1183 False 

1184 >>> vMonth("5L").leap 

1185 True 

1186 

1187 Definition from RFC: 

1188 

1189 .. code-block:: text 

1190 

1191 type-bymonth = element bymonth { 

1192 xsd:positiveInteger | 

1193 xsd:string 

1194 } 

1195 """ 

1196 

1197 params: Parameters 

1198 

1199 def __new__( 

1200 cls, month: Union[str, int], /, params: Optional[dict[str, Any]] = None 

1201 ): 

1202 if params is None: 

1203 params = {} 

1204 if isinstance(month, vMonth): 

1205 return cls(month.to_ical().decode()) 

1206 if isinstance(month, str): 

1207 if month.isdigit(): 

1208 month_index = int(month) 

1209 leap = False 

1210 else: 

1211 if month[-1] != "L" and month[:-1].isdigit(): 

1212 raise ValueError(f"Invalid month: {month!r}") 

1213 month_index = int(month[:-1]) 

1214 leap = True 

1215 else: 

1216 leap = False 

1217 month_index = int(month) 

1218 self = super().__new__(cls, month_index) 

1219 self.leap = leap 

1220 self.params = Parameters(params) 

1221 return self 

1222 

1223 def to_ical(self) -> bytes: 

1224 """The ical representation.""" 

1225 return str(self).encode("utf-8") 

1226 

1227 @classmethod 

1228 def from_ical(cls, ical: str): 

1229 return cls(ical) 

1230 

1231 @property 

1232 def leap(self) -> bool: 

1233 """Whether this is a leap month.""" 

1234 return self._leap 

1235 

1236 @leap.setter 

1237 def leap(self, value: bool) -> None: 

1238 self._leap = value 

1239 

1240 def __repr__(self) -> str: 

1241 """repr(self)""" 

1242 return f"{self.__class__.__name__}({str(self)!r})" 

1243 

1244 def __str__(self) -> str: 

1245 """str(self)""" 

1246 return f"{int(self)}{'L' if self.leap else ''}" 

1247 

1248 

1249class vSkip(vText, Enum): 

1250 """Skip values for RRULE. 

1251 

1252 These are defined in :rfc:`7529`. 

1253 

1254 OMIT is the default value. 

1255 

1256 Examples: 

1257 

1258 .. code-block:: pycon 

1259 

1260 >>> from icalendar import vSkip 

1261 >>> vSkip.OMIT 

1262 vSkip('OMIT') 

1263 >>> vSkip.FORWARD 

1264 vSkip('FORWARD') 

1265 >>> vSkip.BACKWARD 

1266 vSkip('BACKWARD') 

1267 """ 

1268 

1269 OMIT = "OMIT" 

1270 FORWARD = "FORWARD" 

1271 BACKWARD = "BACKWARD" 

1272 

1273 __reduce_ex__ = Enum.__reduce_ex__ 

1274 

1275 def __repr__(self): 

1276 return f"{self.__class__.__name__}({self._name_!r})" 

1277 

1278 

1279class vRecur(CaselessDict): 

1280 """Recurrence definition. 

1281 

1282 Property Name: 

1283 RRULE 

1284 

1285 Purpose: 

1286 This property defines a rule or repeating pattern for recurring events, to-dos, 

1287 journal entries, or time zone definitions. 

1288 

1289 Value Type: 

1290 RECUR 

1291 

1292 Property Parameters: 

1293 IANA and non-standard property parameters can be specified on this property. 

1294 

1295 Conformance: 

1296 This property can be specified in recurring "VEVENT", "VTODO", and "VJOURNAL" 

1297 calendar components as well as in the "STANDARD" and "DAYLIGHT" sub-components 

1298 of the "VTIMEZONE" calendar component, but it SHOULD NOT be specified more than once. 

1299 The recurrence set generated with multiple "RRULE" properties is undefined. 

1300 

1301 Description: 

1302 The recurrence rule, if specified, is used in computing the recurrence set. 

1303 The recurrence set is the complete set of recurrence instances for a calendar component. 

1304 The recurrence set is generated by considering the initial "DTSTART" property along 

1305 with the "RRULE", "RDATE", and "EXDATE" properties contained within the 

1306 recurring component. The "DTSTART" property defines the first instance in the 

1307 recurrence set. The "DTSTART" property value SHOULD be synchronized with the 

1308 recurrence rule, if specified. The recurrence set generated with a "DTSTART" property 

1309 value not synchronized with the recurrence rule is undefined. 

1310 The final recurrence set is generated by gathering all of the start DATE-TIME 

1311 values generated by any of the specified "RRULE" and "RDATE" properties, and then 

1312 excluding any start DATE-TIME values specified by "EXDATE" properties. 

1313 This implies that start DATE- TIME values specified by "EXDATE" properties take 

1314 precedence over those specified by inclusion properties (i.e., "RDATE" and "RRULE"). 

1315 Where duplicate instances are generated by the "RRULE" and "RDATE" properties, 

1316 only one recurrence is considered. Duplicate instances are ignored. 

1317 

1318 The "DTSTART" property specified within the iCalendar object defines the first 

1319 instance of the recurrence. In most cases, a "DTSTART" property of DATE-TIME value 

1320 type used with a recurrence rule, should be specified as a date with local time 

1321 and time zone reference to make sure all the recurrence instances start at the 

1322 same local time regardless of time zone changes. 

1323 

1324 If the duration of the recurring component is specified with the "DTEND" or 

1325 "DUE" property, then the same exact duration will apply to all the members of the 

1326 generated recurrence set. Else, if the duration of the recurring component is 

1327 specified with the "DURATION" property, then the same nominal duration will apply 

1328 to all the members of the generated recurrence set and the exact duration of each 

1329 recurrence instance will depend on its specific start time. For example, recurrence 

1330 instances of a nominal duration of one day will have an exact duration of more or less 

1331 than 24 hours on a day where a time zone shift occurs. The duration of a specific 

1332 recurrence may be modified in an exception component or simply by using an 

1333 "RDATE" property of PERIOD value type. 

1334 

1335 Examples: 

1336 The following RRULE specifies daily events for 10 occurrences. 

1337 

1338 .. code-block:: text 

1339 

1340 RRULE:FREQ=DAILY;COUNT=10 

1341 

1342 Below, we parse the RRULE ical string. 

1343 

1344 .. code-block:: pycon 

1345 

1346 >>> from icalendar.prop import vRecur 

1347 >>> rrule = vRecur.from_ical('FREQ=DAILY;COUNT=10') 

1348 >>> rrule 

1349 vRecur({'FREQ': ['DAILY'], 'COUNT': [10]}) 

1350 

1351 You can choose to add an rrule to an :class:`icalendar.cal.Event` or 

1352 :class:`icalendar.cal.Todo`. 

1353 

1354 .. code-block:: pycon 

1355 

1356 >>> from icalendar import Event 

1357 >>> event = Event() 

1358 >>> event.add('RRULE', 'FREQ=DAILY;COUNT=10') 

1359 >>> event.rrules 

1360 [vRecur({'FREQ': ['DAILY'], 'COUNT': [10]})] 

1361 """ # noqa: E501 

1362 

1363 params: Parameters 

1364 

1365 frequencies = [ 

1366 "SECONDLY", 

1367 "MINUTELY", 

1368 "HOURLY", 

1369 "DAILY", 

1370 "WEEKLY", 

1371 "MONTHLY", 

1372 "YEARLY", 

1373 ] 

1374 

1375 # Mac iCal ignores RRULEs where FREQ is not the first rule part. 

1376 # Sorts parts according to the order listed in RFC 5545, section 3.3.10. 

1377 canonical_order = ( 

1378 "RSCALE", 

1379 "FREQ", 

1380 "UNTIL", 

1381 "COUNT", 

1382 "INTERVAL", 

1383 "BYSECOND", 

1384 "BYMINUTE", 

1385 "BYHOUR", 

1386 "BYDAY", 

1387 "BYWEEKDAY", 

1388 "BYMONTHDAY", 

1389 "BYYEARDAY", 

1390 "BYWEEKNO", 

1391 "BYMONTH", 

1392 "BYSETPOS", 

1393 "WKST", 

1394 "SKIP", 

1395 ) 

1396 

1397 types = CaselessDict( 

1398 { 

1399 "COUNT": vInt, 

1400 "INTERVAL": vInt, 

1401 "BYSECOND": vInt, 

1402 "BYMINUTE": vInt, 

1403 "BYHOUR": vInt, 

1404 "BYWEEKNO": vInt, 

1405 "BYMONTHDAY": vInt, 

1406 "BYYEARDAY": vInt, 

1407 "BYMONTH": vMonth, 

1408 "UNTIL": vDDDTypes, 

1409 "BYSETPOS": vInt, 

1410 "WKST": vWeekday, 

1411 "BYDAY": vWeekday, 

1412 "FREQ": vFrequency, 

1413 "BYWEEKDAY": vWeekday, 

1414 "SKIP": vSkip, 

1415 } 

1416 ) 

1417 

1418 def __init__(self, *args, params: Optional[dict[str, Any]] = None, **kwargs): 

1419 if params is None: 

1420 params = {} 

1421 if args and isinstance(args[0], str): 

1422 # we have a string as an argument. 

1423 args = (self.from_ical(args[0]),) + args[1:] 

1424 for k, v in kwargs.items(): 

1425 if not isinstance(v, SEQUENCE_TYPES): 

1426 kwargs[k] = [v] 

1427 super().__init__(*args, **kwargs) 

1428 self.params = Parameters(params) 

1429 

1430 def to_ical(self): 

1431 result = [] 

1432 for key, vals in self.sorted_items(): 

1433 typ = self.types.get(key, vText) 

1434 if not isinstance(vals, SEQUENCE_TYPES): 

1435 vals = [vals] # noqa: PLW2901 

1436 param_vals = b",".join(typ(val).to_ical() for val in vals) 

1437 

1438 # CaselessDict keys are always unicode 

1439 param_key = key.encode(DEFAULT_ENCODING) 

1440 result.append(param_key + b"=" + param_vals) 

1441 

1442 return b";".join(result) 

1443 

1444 @classmethod 

1445 def parse_type(cls, key, values): 

1446 # integers 

1447 parser = cls.types.get(key, vText) 

1448 return [parser.from_ical(v) for v in values.split(",")] 

1449 

1450 @classmethod 

1451 def from_ical(cls, ical: str): 

1452 if isinstance(ical, cls): 

1453 return ical 

1454 try: 

1455 recur = cls() 

1456 for pairs in ical.split(";"): 

1457 try: 

1458 key, vals = pairs.split("=") 

1459 except ValueError: 

1460 # E.g. incorrect trailing semicolon, like (issue #157): 

1461 # FREQ=YEARLY;BYMONTH=11;BYDAY=1SU; 

1462 continue 

1463 recur[key] = cls.parse_type(key, vals) 

1464 return cls(recur) 

1465 except ValueError: 

1466 raise 

1467 except Exception as e: 

1468 raise ValueError(f"Error in recurrence rule: {ical}") from e 

1469 

1470 

1471class vTime(TimeBase): 

1472 """Time 

1473 

1474 Value Name: 

1475 TIME 

1476 

1477 Purpose: 

1478 This value type is used to identify values that contain a 

1479 time of day. 

1480 

1481 Format Definition: 

1482 This value type is defined by the following notation: 

1483 

1484 .. code-block:: text 

1485 

1486 time = time-hour time-minute time-second [time-utc] 

1487 

1488 time-hour = 2DIGIT ;00-23 

1489 time-minute = 2DIGIT ;00-59 

1490 time-second = 2DIGIT ;00-60 

1491 ;The "60" value is used to account for positive "leap" seconds. 

1492 

1493 time-utc = "Z" 

1494 

1495 Description: 

1496 If the property permits, multiple "time" values are 

1497 specified by a COMMA-separated list of values. No additional 

1498 content value encoding (i.e., BACKSLASH character encoding, see 

1499 vText) is defined for this value type. 

1500 

1501 The "TIME" value type is used to identify values that contain a 

1502 time of day. The format is based on the [ISO.8601.2004] complete 

1503 representation, basic format for a time of day. The text format 

1504 consists of a two-digit, 24-hour of the day (i.e., values 00-23), 

1505 two-digit minute in the hour (i.e., values 00-59), and two-digit 

1506 seconds in the minute (i.e., values 00-60). The seconds value of 

1507 60 MUST only be used to account for positive "leap" seconds. 

1508 Fractions of a second are not supported by this format. 

1509 

1510 In parallel to the "DATE-TIME" definition above, the "TIME" value 

1511 type expresses time values in three forms: 

1512 

1513 The form of time with UTC offset MUST NOT be used. For example, 

1514 the following is not valid for a time value: 

1515 

1516 .. code-block:: text 

1517 

1518 230000-0800 ;Invalid time format 

1519 

1520 **FORM #1 LOCAL TIME** 

1521 

1522 The local time form is simply a time value that does not contain 

1523 the UTC designator nor does it reference a time zone. For 

1524 example, 11:00 PM: 

1525 

1526 .. code-block:: text 

1527 

1528 230000 

1529 

1530 Time values of this type are said to be "floating" and are not 

1531 bound to any time zone in particular. They are used to represent 

1532 the same hour, minute, and second value regardless of which time 

1533 zone is currently being observed. For example, an event can be 

1534 defined that indicates that an individual will be busy from 11:00 

1535 AM to 1:00 PM every day, no matter which time zone the person is 

1536 in. In these cases, a local time can be specified. The recipient 

1537 of an iCalendar object with a property value consisting of a local 

1538 time, without any relative time zone information, SHOULD interpret 

1539 the value as being fixed to whatever time zone the "ATTENDEE" is 

1540 in at any given moment. This means that two "Attendees", may 

1541 participate in the same event at different UTC times; floating 

1542 time SHOULD only be used where that is reasonable behavior. 

1543 

1544 In most cases, a fixed time is desired. To properly communicate a 

1545 fixed time in a property value, either UTC time or local time with 

1546 time zone reference MUST be specified. 

1547 

1548 The use of local time in a TIME value without the "TZID" property 

1549 parameter is to be interpreted as floating time, regardless of the 

1550 existence of "VTIMEZONE" calendar components in the iCalendar 

1551 object. 

1552 

1553 **FORM #2: UTC TIME** 

1554 

1555 UTC time, or absolute time, is identified by a LATIN CAPITAL 

1556 LETTER Z suffix character, the UTC designator, appended to the 

1557 time value. For example, the following represents 07:00 AM UTC: 

1558 

1559 .. code-block:: text 

1560 

1561 070000Z 

1562 

1563 The "TZID" property parameter MUST NOT be applied to TIME 

1564 properties whose time values are specified in UTC. 

1565 

1566 **FORM #3: LOCAL TIME AND TIME ZONE REFERENCE** 

1567 

1568 The local time with reference to time zone information form is 

1569 identified by the use the "TZID" property parameter to reference 

1570 the appropriate time zone definition. 

1571 

1572 Example: 

1573 The following represents 8:30 AM in New York in winter, 

1574 five hours behind UTC, in each of the three formats: 

1575 

1576 .. code-block:: text 

1577 

1578 083000 

1579 133000Z 

1580 TZID=America/New_York:083000 

1581 """ 

1582 

1583 def __init__(self, *args): 

1584 if len(args) == 1: 

1585 if not isinstance(args[0], (time, datetime)): 

1586 raise ValueError(f"Expected a datetime.time, got: {args[0]}") 

1587 self.dt = args[0] 

1588 else: 

1589 self.dt = time(*args) 

1590 self.params = Parameters({"value": "TIME"}) 

1591 

1592 def to_ical(self): 

1593 return self.dt.strftime("%H%M%S") 

1594 

1595 @staticmethod 

1596 def from_ical(ical): 

1597 # TODO: timezone support 

1598 try: 

1599 timetuple = (int(ical[:2]), int(ical[2:4]), int(ical[4:6])) 

1600 return time(*timetuple) 

1601 except Exception as e: 

1602 raise ValueError(f"Expected time, got: {ical}") from e 

1603 

1604 

1605class vUri(str): 

1606 """URI 

1607 

1608 Value Name: 

1609 URI 

1610 

1611 Purpose: 

1612 This value type is used to identify values that contain a 

1613 uniform resource identifier (URI) type of reference to the 

1614 property value. 

1615 

1616 Format Definition: 

1617 This value type is defined by the following notation: 

1618 

1619 .. code-block:: text 

1620 

1621 uri = scheme ":" hier-part [ "?" query ] [ "#" fragment ] 

1622 

1623 Description: 

1624 This value type might be used to reference binary 

1625 information, for values that are large, or otherwise undesirable 

1626 to include directly in the iCalendar object. 

1627 

1628 Property values with this value type MUST follow the generic URI 

1629 syntax defined in [RFC3986]. 

1630 

1631 When a property parameter value is a URI value type, the URI MUST 

1632 be specified as a quoted-string value. 

1633 

1634 Example: 

1635 The following is a URI for a network file: 

1636 

1637 .. code-block:: text 

1638 

1639 http://example.com/my-report.txt 

1640 

1641 .. code-block:: pycon 

1642 

1643 >>> from icalendar.prop import vUri 

1644 >>> uri = vUri.from_ical('http://example.com/my-report.txt') 

1645 >>> uri 

1646 'http://example.com/my-report.txt' 

1647 """ 

1648 

1649 params: Parameters 

1650 __slots__ = ("params",) 

1651 

1652 def __new__( 

1653 cls, 

1654 value, 

1655 encoding=DEFAULT_ENCODING, 

1656 /, 

1657 params: Optional[dict[str, Any]] = None, 

1658 ): 

1659 if params is None: 

1660 params = {} 

1661 value = to_unicode(value, encoding=encoding) 

1662 self = super().__new__(cls, value) 

1663 self.params = Parameters(params) 

1664 return self 

1665 

1666 def to_ical(self): 

1667 return self.encode(DEFAULT_ENCODING) 

1668 

1669 @classmethod 

1670 def from_ical(cls, ical): 

1671 try: 

1672 return cls(ical) 

1673 except Exception as e: 

1674 raise ValueError(f"Expected , got: {ical}") from e 

1675 

1676 

1677class vGeo: 

1678 """Geographic Position 

1679 

1680 Property Name: 

1681 GEO 

1682 

1683 Purpose: 

1684 This property specifies information related to the global 

1685 position for the activity specified by a calendar component. 

1686 

1687 Value Type: 

1688 FLOAT. The value MUST be two SEMICOLON-separated FLOAT values. 

1689 

1690 Property Parameters: 

1691 IANA and non-standard property parameters can be specified on 

1692 this property. 

1693 

1694 Conformance: 

1695 This property can be specified in "VEVENT" or "VTODO" 

1696 calendar components. 

1697 

1698 Description: 

1699 This property value specifies latitude and longitude, 

1700 in that order (i.e., "LAT LON" ordering). The longitude 

1701 represents the location east or west of the prime meridian as a 

1702 positive or negative real number, respectively. The longitude and 

1703 latitude values MAY be specified up to six decimal places, which 

1704 will allow for accuracy to within one meter of geographical 

1705 position. Receiving applications MUST accept values of this 

1706 precision and MAY truncate values of greater precision. 

1707 

1708 Example: 

1709 

1710 .. code-block:: text 

1711 

1712 GEO:37.386013;-122.082932 

1713 

1714 Parse vGeo: 

1715 

1716 .. code-block:: pycon 

1717 

1718 >>> from icalendar.prop import vGeo 

1719 >>> geo = vGeo.from_ical('37.386013;-122.082932') 

1720 >>> geo 

1721 (37.386013, -122.082932) 

1722 

1723 Add a geo location to an event: 

1724 

1725 .. code-block:: pycon 

1726 

1727 >>> from icalendar import Event 

1728 >>> event = Event() 

1729 >>> latitude = 37.386013 

1730 >>> longitude = -122.082932 

1731 >>> event.add('GEO', (latitude, longitude)) 

1732 >>> event['GEO'] 

1733 vGeo((37.386013, -122.082932)) 

1734 """ 

1735 

1736 params: Parameters 

1737 

1738 def __init__( 

1739 self, 

1740 geo: tuple[float | str | int, float | str | int], 

1741 /, 

1742 params: Optional[dict[str, Any]] = None, 

1743 ): 

1744 """Create a new vGeo from a tuple of (latitude, longitude). 

1745 

1746 Raises: 

1747 ValueError: if geo is not a tuple of (latitude, longitude) 

1748 """ 

1749 if params is None: 

1750 params = {} 

1751 try: 

1752 latitude, longitude = (geo[0], geo[1]) 

1753 latitude = float(latitude) 

1754 longitude = float(longitude) 

1755 except Exception as e: 

1756 raise ValueError( 

1757 "Input must be (float, float) for latitude and longitude" 

1758 ) from e 

1759 self.latitude = latitude 

1760 self.longitude = longitude 

1761 self.params = Parameters(params) 

1762 

1763 def to_ical(self): 

1764 return f"{self.latitude};{self.longitude}" 

1765 

1766 @staticmethod 

1767 def from_ical(ical): 

1768 try: 

1769 latitude, longitude = ical.split(";") 

1770 return (float(latitude), float(longitude)) 

1771 except Exception as e: 

1772 raise ValueError(f"Expected 'float;float' , got: {ical}") from e 

1773 

1774 def __eq__(self, other): 

1775 return self.to_ical() == other.to_ical() 

1776 

1777 def __repr__(self): 

1778 """repr(self)""" 

1779 return f"{self.__class__.__name__}(({self.latitude}, {self.longitude}))" 

1780 

1781 

1782class vUTCOffset: 

1783 """UTC Offset 

1784 

1785 Value Name: 

1786 UTC-OFFSET 

1787 

1788 Purpose: 

1789 This value type is used to identify properties that contain 

1790 an offset from UTC to local time. 

1791 

1792 Format Definition: 

1793 This value type is defined by the following notation: 

1794 

1795 .. code-block:: text 

1796 

1797 utc-offset = time-numzone 

1798 

1799 time-numzone = ("+" / "-") time-hour time-minute [time-second] 

1800 

1801 Description: 

1802 The PLUS SIGN character MUST be specified for positive 

1803 UTC offsets (i.e., ahead of UTC). The HYPHEN-MINUS character MUST 

1804 be specified for negative UTC offsets (i.e., behind of UTC). The 

1805 value of "-0000" and "-000000" are not allowed. The time-second, 

1806 if present, MUST NOT be 60; if absent, it defaults to zero. 

1807 

1808 Example: 

1809 The following UTC offsets are given for standard time for 

1810 New York (five hours behind UTC) and Geneva (one hour ahead of 

1811 UTC): 

1812 

1813 .. code-block:: text 

1814 

1815 -0500 

1816 

1817 +0100 

1818 

1819 .. code-block:: pycon 

1820 

1821 >>> from icalendar.prop import vUTCOffset 

1822 >>> utc_offset = vUTCOffset.from_ical('-0500') 

1823 >>> utc_offset 

1824 datetime.timedelta(days=-1, seconds=68400) 

1825 >>> utc_offset = vUTCOffset.from_ical('+0100') 

1826 >>> utc_offset 

1827 datetime.timedelta(seconds=3600) 

1828 """ 

1829 

1830 params: Parameters 

1831 

1832 ignore_exceptions = False # if True, and we cannot parse this 

1833 

1834 # component, we will silently ignore 

1835 # it, rather than let the exception 

1836 # propagate upwards 

1837 

1838 def __init__(self, td, /, params: Optional[dict[str, Any]] = None): 

1839 if params is None: 

1840 params = {} 

1841 if not isinstance(td, timedelta): 

1842 raise TypeError("Offset value MUST be a timedelta instance") 

1843 self.td = td 

1844 self.params = Parameters(params) 

1845 

1846 def to_ical(self): 

1847 if self.td < timedelta(0): 

1848 sign = "-%s" 

1849 td = timedelta(0) - self.td # get timedelta relative to 0 

1850 else: 

1851 # Google Calendar rejects '0000' but accepts '+0000' 

1852 sign = "+%s" 

1853 td = self.td 

1854 

1855 days, seconds = td.days, td.seconds 

1856 

1857 hours = abs(days * 24 + seconds // 3600) 

1858 minutes = abs((seconds % 3600) // 60) 

1859 seconds = abs(seconds % 60) 

1860 if seconds: 

1861 duration = f"{hours:02}{minutes:02}{seconds:02}" 

1862 else: 

1863 duration = f"{hours:02}{minutes:02}" 

1864 return sign % duration 

1865 

1866 @classmethod 

1867 def from_ical(cls, ical): 

1868 if isinstance(ical, cls): 

1869 return ical.td 

1870 try: 

1871 sign, hours, minutes, seconds = ( 

1872 ical[0:1], 

1873 int(ical[1:3]), 

1874 int(ical[3:5]), 

1875 int(ical[5:7] or 0), 

1876 ) 

1877 offset = timedelta(hours=hours, minutes=minutes, seconds=seconds) 

1878 except Exception as e: 

1879 raise ValueError(f"Expected utc offset, got: {ical}") from e 

1880 if not cls.ignore_exceptions and offset >= timedelta(hours=24): 

1881 raise ValueError(f"Offset must be less than 24 hours, was {ical}") 

1882 if sign == "-": 

1883 return -offset 

1884 return offset 

1885 

1886 def __eq__(self, other): 

1887 if not isinstance(other, vUTCOffset): 

1888 return False 

1889 return self.td == other.td 

1890 

1891 def __hash__(self): 

1892 return hash(self.td) 

1893 

1894 def __repr__(self): 

1895 return f"vUTCOffset({self.td!r})" 

1896 

1897 

1898class vInline(str): 

1899 """This is an especially dumb class that just holds raw unparsed text and 

1900 has parameters. Conversion of inline values are handled by the Component 

1901 class, so no further processing is needed. 

1902 """ 

1903 

1904 params: Parameters 

1905 __slots__ = ("params",) 

1906 

1907 def __new__( 

1908 cls, 

1909 value, 

1910 encoding=DEFAULT_ENCODING, 

1911 /, 

1912 params: Optional[dict[str, Any]] = None, 

1913 ): 

1914 if params is None: 

1915 params = {} 

1916 value = to_unicode(value, encoding=encoding) 

1917 self = super().__new__(cls, value) 

1918 self.params = Parameters(params) 

1919 return self 

1920 

1921 def to_ical(self): 

1922 return self.encode(DEFAULT_ENCODING) 

1923 

1924 @classmethod 

1925 def from_ical(cls, ical): 

1926 return cls(ical) 

1927 

1928 

1929class TypesFactory(CaselessDict): 

1930 """All Value types defined in RFC 5545 are registered in this factory 

1931 class. 

1932 

1933 The value and parameter names don't overlap. So one factory is enough for 

1934 both kinds. 

1935 """ 

1936 

1937 def __init__(self, *args, **kwargs): 

1938 """Set keys to upper for initial dict""" 

1939 super().__init__(*args, **kwargs) 

1940 self.all_types = ( 

1941 vBinary, 

1942 vBoolean, 

1943 vCalAddress, 

1944 vDDDLists, 

1945 vDDDTypes, 

1946 vDate, 

1947 vDatetime, 

1948 vDuration, 

1949 vFloat, 

1950 vFrequency, 

1951 vGeo, 

1952 vInline, 

1953 vInt, 

1954 vPeriod, 

1955 vRecur, 

1956 vText, 

1957 vTime, 

1958 vUTCOffset, 

1959 vUri, 

1960 vWeekday, 

1961 vCategory, 

1962 ) 

1963 self["binary"] = vBinary 

1964 self["boolean"] = vBoolean 

1965 self["cal-address"] = vCalAddress 

1966 self["date"] = vDDDTypes 

1967 self["date-time"] = vDDDTypes 

1968 self["duration"] = vDDDTypes 

1969 self["float"] = vFloat 

1970 self["integer"] = vInt 

1971 self["period"] = vPeriod 

1972 self["recur"] = vRecur 

1973 self["text"] = vText 

1974 self["time"] = vTime 

1975 self["uri"] = vUri 

1976 self["utc-offset"] = vUTCOffset 

1977 self["geo"] = vGeo 

1978 self["inline"] = vInline 

1979 self["date-time-list"] = vDDDLists 

1980 self["categories"] = vCategory 

1981 

1982 ################################################# 

1983 # Property types 

1984 

1985 # These are the default types 

1986 types_map = CaselessDict( 

1987 { 

1988 #################################### 

1989 # Property value types 

1990 # Calendar Properties 

1991 "calscale": "text", 

1992 "method": "text", 

1993 "prodid": "text", 

1994 "version": "text", 

1995 # Descriptive Component Properties 

1996 "attach": "uri", 

1997 "categories": "categories", 

1998 "class": "text", 

1999 "comment": "text", 

2000 "description": "text", 

2001 "geo": "geo", 

2002 "location": "text", 

2003 "percent-complete": "integer", 

2004 "priority": "integer", 

2005 "resources": "text", 

2006 "status": "text", 

2007 "summary": "text", 

2008 # Date and Time Component Properties 

2009 "completed": "date-time", 

2010 "dtend": "date-time", 

2011 "due": "date-time", 

2012 "dtstart": "date-time", 

2013 "duration": "duration", 

2014 "freebusy": "period", 

2015 "transp": "text", 

2016 # Time Zone Component Properties 

2017 "tzid": "text", 

2018 "tzname": "text", 

2019 "tzoffsetfrom": "utc-offset", 

2020 "tzoffsetto": "utc-offset", 

2021 "tzurl": "uri", 

2022 # Relationship Component Properties 

2023 "attendee": "cal-address", 

2024 "contact": "text", 

2025 "organizer": "cal-address", 

2026 "recurrence-id": "date-time", 

2027 "related-to": "text", 

2028 "url": "uri", 

2029 "uid": "text", 

2030 # Recurrence Component Properties 

2031 "exdate": "date-time-list", 

2032 "exrule": "recur", 

2033 "rdate": "date-time-list", 

2034 "rrule": "recur", 

2035 # Alarm Component Properties 

2036 "action": "text", 

2037 "repeat": "integer", 

2038 "trigger": "duration", 

2039 "acknowledged": "date-time", 

2040 # Change Management Component Properties 

2041 "created": "date-time", 

2042 "dtstamp": "date-time", 

2043 "last-modified": "date-time", 

2044 "sequence": "integer", 

2045 # Miscellaneous Component Properties 

2046 "request-status": "text", 

2047 #################################### 

2048 # parameter types (luckily there is no name overlap) 

2049 "altrep": "uri", 

2050 "cn": "text", 

2051 "cutype": "text", 

2052 "delegated-from": "cal-address", 

2053 "delegated-to": "cal-address", 

2054 "dir": "uri", 

2055 "encoding": "text", 

2056 "fmttype": "text", 

2057 "fbtype": "text", 

2058 "language": "text", 

2059 "member": "cal-address", 

2060 "partstat": "text", 

2061 "range": "text", 

2062 "related": "text", 

2063 "reltype": "text", 

2064 "role": "text", 

2065 "rsvp": "boolean", 

2066 "sent-by": "cal-address", 

2067 "value": "text", 

2068 } 

2069 ) 

2070 

2071 def for_property(self, name): 

2072 """Returns a the default type for a property or parameter""" 

2073 return self[self.types_map.get(name, "text")] 

2074 

2075 def to_ical(self, name, value): 

2076 """Encodes a named value from a primitive python type to an icalendar 

2077 encoded string. 

2078 """ 

2079 type_class = self.for_property(name) 

2080 return type_class(value).to_ical() 

2081 

2082 def from_ical(self, name, value): 

2083 """Decodes a named property or parameter value from an icalendar 

2084 encoded string to a primitive python type. 

2085 """ 

2086 type_class = self.for_property(name) 

2087 return type_class.from_ical(value) 

2088 

2089 

2090__all__ = [ 

2091 "DURATION_REGEX", 

2092 "WEEKDAY_RULE", 

2093 "TimeBase", 

2094 "TypesFactory", 

2095 "tzid_from_dt", 

2096 "tzid_from_tzinfo", 

2097 "vBinary", 

2098 "vBoolean", 

2099 "vCalAddress", 

2100 "vCategory", 

2101 "vDDDLists", 

2102 "vDDDTypes", 

2103 "vDate", 

2104 "vDatetime", 

2105 "vDuration", 

2106 "vFloat", 

2107 "vFrequency", 

2108 "vGeo", 

2109 "vInline", 

2110 "vInt", 

2111 "vMonth", 

2112 "vPeriod", 

2113 "vRecur", 

2114 "vSkip", 

2115 "vText", 

2116 "vTime", 

2117 "vUTCOffset", 

2118 "vUri", 

2119 "vWeekday", 

2120]