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

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

1226 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 

50import uuid 

51from datetime import date, datetime, time, timedelta, timezone 

52from typing import Any, ClassVar, Optional, Tuple, Union 

53 

54from icalendar.caselessdict import CaselessDict 

55from icalendar.enums import Enum 

56from icalendar.error import InvalidCalendar, JCalParsingError 

57from icalendar.parser import Parameters, escape_char, unescape_char 

58from icalendar.parser_tools import ( 

59 DEFAULT_ENCODING, 

60 ICAL_TYPE, 

61 SEQUENCE_TYPES, 

62 from_unicode, 

63 to_unicode, 

64) 

65from icalendar.timezone import tzid_from_dt, tzid_from_tzinfo, tzp 

66from icalendar.timezone.tzid import is_utc 

67from icalendar.tools import is_date, is_datetime, normalize_pytz, to_datetime 

68 

69try: 

70 from typing import TypeAlias 

71except ImportError: 

72 from typing_extensions import TypeAlias 

73try: 

74 from typing import Self 

75except ImportError: 

76 from typing_extensions import Self 

77 

78DURATION_REGEX = re.compile( 

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

80) 

81 

82WEEKDAY_RULE = re.compile( 

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

84) 

85 

86 

87class vBinary: 

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

89 

90 default_value: ClassVar[str] = "BINARY" 

91 params: Parameters 

92 obj: str 

93 

94 def __init__(self, obj, params: dict[str, str] | None = None): 

95 self.obj = to_unicode(obj) 

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

97 if params: 

98 self.params.update(params) 

99 

100 def __repr__(self): 

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

102 

103 def to_ical(self): 

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

105 

106 @staticmethod 

107 def from_ical(ical): 

108 try: 

109 return base64.b64decode(ical) 

110 except ValueError as e: 

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

112 

113 def __eq__(self, other): 

114 """self == other""" 

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

116 

117 @classmethod 

118 def examples(cls) -> list[vBinary]: 

119 """Examples of vBinary.""" 

120 return [cls("VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4")] 

121 

122 from icalendar.param import VALUE 

123 

124 def to_jcal(self, name: str) -> list: 

125 """The jCal representation of this property according to :rfc:`7265`.""" 

126 params = self.params.to_jcal() 

127 if params.get("encoding") == "BASE64": 

128 # BASE64 is the only allowed encoding 

129 del params["encoding"] 

130 return [name, params, self.VALUE.lower(), self.obj] 

131 

132 @classmethod 

133 def from_jcal(cls, jcal_property: list) -> vBinary: 

134 """Parse jCal from :rfc:`7265` to a vBinary. 

135 

136 Args: 

137 jcal_property: The jCal property to parse. 

138 

139 Raises: 

140 JCalParsingError: If the provided jCal is invalid. 

141 """ 

142 JCalParsingError.validate_property(jcal_property, cls) 

143 JCalParsingError.validate_value_type(jcal_property[3], str, cls, 3) 

144 return cls( 

145 jcal_property[3], 

146 params=Parameters.from_jcal_property(jcal_property), 

147 ) 

148 

149 

150class vBoolean(int): 

151 """Boolean 

152 

153 Value Name: BOOLEAN 

154 

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

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

157 

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

159 notation: 

160 

161 .. code-block:: text 

162 

163 boolean = "TRUE" / "FALSE" 

164 

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

166 content value encoding is defined for this value type. 

167 

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

169 has a BOOLEAN value type: 

170 

171 .. code-block:: python 

172 

173 TRUE 

174 

175 .. code-block:: pycon 

176 

177 >>> from icalendar.prop import vBoolean 

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

179 >>> boolean 

180 True 

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

182 >>> boolean 

183 False 

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

185 >>> boolean 

186 True 

187 """ 

188 

189 default_value: ClassVar[str] = "BOOLEAN" 

190 params: Parameters 

191 

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

193 

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

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

196 self.params = Parameters(params) 

197 return self 

198 

199 def to_ical(self): 

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

201 

202 @classmethod 

203 def from_ical(cls, ical): 

204 try: 

205 return cls.BOOL_MAP[ical] 

206 except Exception as e: 

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

208 

209 @classmethod 

210 def examples(cls) -> list[vBoolean]: 

211 """Examples of vBoolean.""" 

212 return [ 

213 cls(True), # noqa: FBT003 

214 cls(False), # noqa: FBT003 

215 ] 

216 

217 from icalendar.param import VALUE 

218 

219 def to_jcal(self, name: str) -> list: 

220 """The jCal representation of this property according to :rfc:`7265`.""" 

221 return [name, self.params.to_jcal(), self.VALUE.lower(), bool(self)] 

222 

223 @classmethod 

224 def from_jcal(cls, jcal_property: list) -> vBoolean: 

225 """Parse jCal from :rfc:`7265` to a vBoolean. 

226 

227 Args: 

228 jcal_property: The jCal property to parse. 

229 

230 Raises: 

231 JCalParsingError: If the provided jCal is invalid. 

232 """ 

233 JCalParsingError.validate_property(jcal_property, cls) 

234 JCalParsingError.validate_value_type(jcal_property[3], bool, cls, 3) 

235 return cls( 

236 jcal_property[3], 

237 params=Parameters.from_jcal_property(jcal_property), 

238 ) 

239 

240 

241class vText(str): 

242 """Simple text.""" 

243 

244 default_value: ClassVar[str] = "TEXT" 

245 params: Parameters 

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

247 

248 def __new__( 

249 cls, 

250 value, 

251 encoding=DEFAULT_ENCODING, 

252 /, 

253 params: dict[str, Any] | None = None, 

254 ): 

255 value = to_unicode(value, encoding=encoding) 

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

257 self.encoding = encoding 

258 self.params = Parameters(params) 

259 return self 

260 

261 def __repr__(self) -> str: 

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

263 

264 def to_ical(self) -> bytes: 

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

266 

267 @classmethod 

268 def from_ical(cls, ical: ICAL_TYPE): 

269 return cls(ical) 

270 

271 @property 

272 def ical_value(self) -> str: 

273 """The string value of the text.""" 

274 return str(self) 

275 

276 from icalendar.param import ALTREP, GAP, LANGUAGE, RELTYPE, VALUE 

277 

278 def to_jcal(self, name: str) -> list: 

279 """The jCal representation of this property according to :rfc:`7265`.""" 

280 if name == "request-status": # TODO: maybe add a vRequestStatus class? 

281 return [name, {}, "text", self.split(";", 2)] 

282 return [name, self.params.to_jcal(), self.VALUE.lower(), str(self)] 

283 

284 @classmethod 

285 def examples(cls): 

286 """Examples of vText.""" 

287 return [cls("Hello World!")] 

288 

289 @classmethod 

290 def from_jcal(cls, jcal_property: list) -> Self: 

291 """Parse jCal from :rfc:`7265`. 

292 

293 Args: 

294 jcal_property: The jCal property to parse. 

295 

296 Raises: 

297 JCalParsingError: If the provided jCal is invalid. 

298 """ 

299 JCalParsingError.validate_property(jcal_property, cls) 

300 name = jcal_property[0] 

301 if name == "categories": 

302 return vCategory.from_jcal(jcal_property) 

303 string = jcal_property[3] # TODO: accept list or string but join with ; 

304 if name == "request-status": # TODO: maybe add a vRequestStatus class? 

305 JCalParsingError.validate_list_type(jcal_property[3], str, cls, 3) 

306 string = ";".join(jcal_property[3]) 

307 JCalParsingError.validate_value_type(string, str, cls, 3) 

308 return cls( 

309 string, 

310 params=Parameters.from_jcal_property(jcal_property), 

311 ) 

312 

313 @classmethod 

314 def parse_jcal_value(cls, jcal_value: Any) -> vText: 

315 """Parse a jCal value into a vText.""" 

316 JCalParsingError.validate_value_type(jcal_value, (str, int, float), cls) 

317 return cls(str(jcal_value)) 

318 

319 

320class vCalAddress(str): 

321 r"""Calendar User Address 

322 

323 Value Name: 

324 CAL-ADDRESS 

325 

326 Purpose: 

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

328 calendar user address. 

329 

330 Description: 

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

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

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

334 mailto URI, as defined by [RFC2368]. 

335 

336 Example: 

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

338 

339 .. code-block:: text 

340 

341 mailto:jane_doe@example.com 

342 

343 Parsing: 

344 

345 .. code-block:: pycon 

346 

347 >>> from icalendar import vCalAddress 

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

349 >>> cal_address 

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

351 

352 Encoding: 

353 

354 .. code-block:: pycon 

355 

356 >>> from icalendar import vCalAddress, Event 

357 >>> event = Event() 

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

359 >>> jane.name = "Jane" 

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

361 >>> print(event.to_ical().decode().replace('\\r\\n', '\\n').strip()) 

362 BEGIN:VEVENT 

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

364 END:VEVENT 

365 """ 

366 

367 default_value: ClassVar[str] = "CAL-ADDRESS" 

368 params: Parameters 

369 __slots__ = ("params",) 

370 

371 def __new__( 

372 cls, 

373 value, 

374 encoding=DEFAULT_ENCODING, 

375 /, 

376 params: dict[str, Any] | None = None, 

377 ): 

378 value = to_unicode(value, encoding=encoding) 

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

380 self.params = Parameters(params) 

381 return self 

382 

383 def __repr__(self): 

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

385 

386 def to_ical(self): 

387 return self.encode(DEFAULT_ENCODING) 

388 

389 @classmethod 

390 def from_ical(cls, ical): 

391 return cls(ical) 

392 

393 @property 

394 def ical_value(self): 

395 """The ``mailto:`` part of the address.""" 

396 return str(self) 

397 

398 @property 

399 def email(self) -> str: 

400 """The email address without ``mailto:`` at the start.""" 

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

402 return self[7:] 

403 return str(self) 

404 

405 from icalendar.param import ( 

406 CN, 

407 CUTYPE, 

408 DELEGATED_FROM, 

409 DELEGATED_TO, 

410 DIR, 

411 LANGUAGE, 

412 PARTSTAT, 

413 ROLE, 

414 RSVP, 

415 SENT_BY, 

416 VALUE, 

417 ) 

418 

419 name = CN 

420 

421 @staticmethod 

422 def _get_email(email: str) -> str: 

423 """Extract email and add mailto: prefix if needed. 

424 

425 Handles case-insensitive mailto: prefix checking. 

426 

427 Args: 

428 email: Email string that may or may not have mailto: prefix 

429 

430 Returns: 

431 Email string with mailto: prefix 

432 """ 

433 if not email.lower().startswith("mailto:"): 

434 return f"mailto:{email}" 

435 return email 

436 

437 @classmethod 

438 def new( 

439 cls, 

440 email: str, 

441 /, 

442 cn: str | None = None, 

443 cutype: str | None = None, 

444 delegated_from: str | None = None, 

445 delegated_to: str | None = None, 

446 directory: str | None = None, 

447 language: str | None = None, 

448 partstat: str | None = None, 

449 role: str | None = None, 

450 rsvp: bool | None = None, 

451 sent_by: str | None = None, 

452 ): 

453 """Create a new vCalAddress with RFC 5545 parameters. 

454 

455 Creates a vCalAddress instance with automatic mailto: prefix handling 

456 and support for all standard RFC 5545 parameters. 

457 

458 Args: 

459 email: The email address (mailto: prefix added automatically if missing) 

460 cn: Common Name parameter 

461 cutype: Calendar user type (INDIVIDUAL, GROUP, RESOURCE, ROOM) 

462 delegated_from: Email of the calendar user that delegated 

463 delegated_to: Email of the calendar user that was delegated to 

464 directory: Reference to directory information 

465 language: Language for text values 

466 partstat: Participation status (NEEDS-ACTION, ACCEPTED, DECLINED, etc.) 

467 role: Role (REQ-PARTICIPANT, OPT-PARTICIPANT, NON-PARTICIPANT, CHAIR) 

468 rsvp: Whether RSVP is requested 

469 sent_by: Email of the calendar user acting on behalf of this user 

470 

471 Returns: 

472 vCalAddress: A new calendar address with specified parameters 

473 

474 Raises: 

475 TypeError: If email is not a string 

476 

477 Examples: 

478 Basic usage: 

479 

480 >>> from icalendar.prop import vCalAddress 

481 >>> addr = vCalAddress.new("test@test.com") 

482 >>> str(addr) 

483 'mailto:test@test.com' 

484 

485 With parameters: 

486 

487 >>> addr = vCalAddress.new("test@test.com", cn="Test User", role="CHAIR") 

488 >>> addr.params["CN"] 

489 'Test User' 

490 >>> addr.params["ROLE"] 

491 'CHAIR' 

492 """ 

493 if not isinstance(email, str): 

494 raise TypeError(f"Email must be a string, not {type(email).__name__}") 

495 

496 # Handle mailto: prefix (case-insensitive) 

497 email_with_prefix = cls._get_email(email) 

498 

499 # Create the address 

500 addr = cls(email_with_prefix) 

501 

502 # Set parameters if provided 

503 if cn is not None: 

504 addr.params["CN"] = cn 

505 if cutype is not None: 

506 addr.params["CUTYPE"] = cutype 

507 if delegated_from is not None: 

508 addr.params["DELEGATED-FROM"] = cls._get_email(delegated_from) 

509 if delegated_to is not None: 

510 addr.params["DELEGATED-TO"] = cls._get_email(delegated_to) 

511 if directory is not None: 

512 addr.params["DIR"] = directory 

513 if language is not None: 

514 addr.params["LANGUAGE"] = language 

515 if partstat is not None: 

516 addr.params["PARTSTAT"] = partstat 

517 if role is not None: 

518 addr.params["ROLE"] = role 

519 if rsvp is not None: 

520 addr.params["RSVP"] = "TRUE" if rsvp else "FALSE" 

521 if sent_by is not None: 

522 addr.params["SENT-BY"] = cls._get_email(sent_by) 

523 

524 return addr 

525 

526 def to_jcal(self, name: str) -> list: 

527 """Return this property in jCal format.""" 

528 return [name, self.params.to_jcal(), self.VALUE.lower(), self.ical_value] 

529 

530 @classmethod 

531 def examples(cls) -> list[vCalAddress]: 

532 """Examples of vCalAddress.""" 

533 return [cls.new("you@example.org", cn="You There")] 

534 

535 @classmethod 

536 def from_jcal(cls, jcal_property: list) -> Self: 

537 """Parse jCal from :rfc:`7265`. 

538 

539 Args: 

540 jcal_property: The jCal property to parse. 

541 

542 Raises: 

543 JCalParsingError: If the provided jCal is invalid. 

544 """ 

545 JCalParsingError.validate_property(jcal_property, cls) 

546 JCalParsingError.validate_value_type(jcal_property[3], str, cls, 3) 

547 return cls( 

548 jcal_property[3], 

549 params=Parameters.from_jcal_property(jcal_property), 

550 ) 

551 

552 

553class vFloat(float): 

554 """Float 

555 

556 Value Name: 

557 FLOAT 

558 

559 Purpose: 

560 This value type is used to identify properties that contain 

561 a real-number value. 

562 

563 Format Definition: 

564 This value type is defined by the following notation: 

565 

566 .. code-block:: text 

567 

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

569 

570 Description: 

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

572 specified by a COMMA-separated list of values. 

573 

574 Example: 

575 

576 .. code-block:: text 

577 

578 1000000.0000001 

579 1.333 

580 -3.14 

581 

582 .. code-block:: pycon 

583 

584 >>> from icalendar.prop import vFloat 

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

586 >>> float 

587 1000000.0000001 

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

589 >>> float 

590 1.333 

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

592 >>> float 

593 1.333 

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

595 >>> float 

596 -3.14 

597 """ 

598 

599 default_value: ClassVar[str] = "FLOAT" 

600 params: Parameters 

601 

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

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

604 self.params = Parameters(params) 

605 return self 

606 

607 def to_ical(self): 

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

609 

610 @classmethod 

611 def from_ical(cls, ical): 

612 try: 

613 return cls(ical) 

614 except Exception as e: 

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

616 

617 @classmethod 

618 def examples(cls) -> list[vFloat]: 

619 """Examples of vFloat.""" 

620 return [vFloat(3.1415)] 

621 

622 from icalendar.param import VALUE 

623 

624 def to_jcal(self, name: str) -> list: 

625 """The jCal representation of this property according to :rfc:`7265`.""" 

626 return [name, self.params.to_jcal(), self.VALUE.lower(), float(self)] 

627 

628 @classmethod 

629 def from_jcal(cls, jcal_property: list) -> Self: 

630 """Parse jCal from :rfc:`7265`. 

631 

632 Args: 

633 jcal_property: The jCal property to parse. 

634 

635 Raises: 

636 JCalParsingError: If the jCal provided is invalid. 

637 """ 

638 JCalParsingError.validate_property(jcal_property, cls) 

639 if jcal_property[0].upper() == "GEO": 

640 return vGeo.from_jcal(jcal_property) 

641 JCalParsingError.validate_value_type(jcal_property[3], float, cls, 3) 

642 return cls( 

643 jcal_property[3], 

644 params=Parameters.from_jcal_property(jcal_property), 

645 ) 

646 

647 

648class vInt(int): 

649 """Integer 

650 

651 Value Name: 

652 INTEGER 

653 

654 Purpose: 

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

656 signed integer value. 

657 

658 Format Definition: 

659 This value type is defined by the following notation: 

660 

661 .. code-block:: text 

662 

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

664 

665 Description: 

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

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

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

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

670 

671 Example: 

672 

673 .. code-block:: text 

674 

675 1234567890 

676 -1234567890 

677 +1234567890 

678 432109876 

679 

680 .. code-block:: pycon 

681 

682 >>> from icalendar.prop import vInt 

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

684 >>> integer 

685 1234567890 

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

687 >>> integer 

688 -1234567890 

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

690 >>> integer 

691 1234567890 

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

693 >>> integer 

694 432109876 

695 """ 

696 

697 default_value: ClassVar[str] = "INTEGER" 

698 params: Parameters 

699 

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

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

702 self.params = Parameters(params) 

703 return self 

704 

705 def to_ical(self) -> bytes: 

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

707 

708 @classmethod 

709 def from_ical(cls, ical: ICAL_TYPE): 

710 try: 

711 return cls(ical) 

712 except Exception as e: 

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

714 

715 @classmethod 

716 def examples(cls) -> list[vInt]: 

717 """Examples of vInt.""" 

718 return [vInt(1000), vInt(-42)] 

719 

720 from icalendar.param import VALUE 

721 

722 def to_jcal(self, name: str) -> list: 

723 """The jCal representation of this property according to :rfc:`7265`.""" 

724 return [name, self.params.to_jcal(), self.VALUE.lower(), int(self)] 

725 

726 @classmethod 

727 def from_jcal(cls, jcal_property: list) -> Self: 

728 """Parse jCal from :rfc:`7265`. 

729 

730 Args: 

731 jcal_property: The jCal property to parse. 

732 

733 Raises: 

734 JCalParsingError: If the provided jCal is invalid. 

735 """ 

736 JCalParsingError.validate_property(jcal_property, cls) 

737 JCalParsingError.validate_value_type(jcal_property[3], int, cls, 3) 

738 return cls( 

739 jcal_property[3], 

740 params=Parameters.from_jcal_property(jcal_property), 

741 ) 

742 

743 @classmethod 

744 def parse_jcal_value(cls, value: Any) -> int: 

745 """Parse a jCal value for vInt. 

746 

747 Raises: 

748 JCalParsingError: If the value is not an int. 

749 """ 

750 JCalParsingError.validate_value_type(value, int, cls) 

751 return cls(value) 

752 

753 

754class vDDDLists: 

755 """A list of vDDDTypes values.""" 

756 

757 default_value: ClassVar[str] = "DATE-TIME" 

758 params: Parameters 

759 dts: list[vDDDTypes] 

760 

761 def __init__(self, dt_list, params: dict[str, Any] | None = None): 

762 if params is None: 

763 params = {} 

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

765 dt_list = [dt_list] 

766 vddd = [] 

767 tzid = None 

768 for dt_l in dt_list: 

769 dt = vDDDTypes(dt_l) if not isinstance(dt_l, vDDDTypes) else dt_l 

770 vddd.append(dt) 

771 if "TZID" in dt.params: 

772 tzid = dt.params["TZID"] 

773 

774 if tzid: 

775 # NOTE: no support for multiple timezones here! 

776 params["TZID"] = tzid 

777 self.params = Parameters(params) 

778 self.dts = vddd 

779 

780 def to_ical(self): 

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

782 return b",".join(dts_ical) 

783 

784 @staticmethod 

785 def from_ical(ical, timezone=None): 

786 out = [] 

787 ical_dates = ical.split(",") 

788 for ical_dt in ical_dates: 

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

790 return out 

791 

792 def __eq__(self, other): 

793 if isinstance(other, vDDDLists): 

794 return self.dts == other.dts 

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

796 return self.dts == [other] 

797 return False 

798 

799 def __repr__(self): 

800 """String representation.""" 

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

802 

803 @classmethod 

804 def examples(cls) -> list[vDDDLists]: 

805 """Examples of vDDDLists.""" 

806 return [vDDDLists([datetime(2025, 11, 10, 16, 50)])] # noqa: DTZ001 

807 

808 def to_jcal(self, name: str) -> list: 

809 """The jCal representation of this property according to :rfc:`7265`.""" 

810 return [ 

811 name, 

812 self.params.to_jcal(), 

813 self.VALUE.lower(), 

814 *[dt.to_jcal(name)[3] for dt in self.dts], 

815 ] 

816 

817 def _get_value(self) -> str | None: 

818 return None if not self.dts else self.dts[0].VALUE 

819 

820 from icalendar.param import VALUE 

821 

822 @classmethod 

823 def from_jcal(cls, jcal_property: list) -> Self: 

824 """Parse jCal from :rfc:`7265`. 

825 

826 Args: 

827 jcal_property: The jCal property to parse. 

828 

829 Raises: 

830 JCalParsingError: If the jCal provided is invalid. 

831 """ 

832 JCalParsingError.validate_property(jcal_property, cls) 

833 values = jcal_property[3:] 

834 prop = jcal_property[:3] 

835 dts = [] 

836 for value in values: 

837 dts.append(vDDDTypes.from_jcal(prop + [value])) 

838 return cls( 

839 dts, 

840 params=Parameters.from_jcal_property(jcal_property), 

841 ) 

842 

843 

844class vCategory: 

845 default_value: ClassVar[str] = "TEXT" 

846 params: Parameters 

847 

848 def __init__( 

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

850 ): 

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

852 c_list = [c_list] 

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

854 self.params = Parameters(params) 

855 

856 def __iter__(self): 

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

858 

859 def to_ical(self): 

860 return b",".join( 

861 [ 

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

863 for c in self.cats 

864 ] 

865 ) 

866 

867 @staticmethod 

868 def from_ical(ical): 

869 ical = to_unicode(ical) 

870 return ical.split(",") 

871 

872 def __eq__(self, other): 

873 """self == other""" 

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

875 

876 def __repr__(self): 

877 """String representation.""" 

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

879 

880 def to_jcal(self, name: str) -> list: 

881 """The jCal representation for categories.""" 

882 result = [name, self.params.to_jcal(), self.VALUE.lower()] 

883 result.extend(map(str, self.cats)) 

884 if not self.cats: 

885 result.append("") 

886 return result 

887 

888 @classmethod 

889 def examples(cls) -> list[vCategory]: 

890 """Examples of vCategory.""" 

891 return [cls(["HOME", "COSY"])] 

892 

893 from icalendar.param import VALUE 

894 

895 @classmethod 

896 def from_jcal(cls, jcal_property: list) -> Self: 

897 """Parse jCal from :rfc:`7265`. 

898 

899 Args: 

900 jcal_property: The jCal property to parse. 

901 

902 Raises: 

903 JCalParsingError: If the provided jCal is invalid. 

904 """ 

905 JCalParsingError.validate_property(jcal_property, cls) 

906 for i, category in enumerate(jcal_property[3:], start=3): 

907 JCalParsingError.validate_value_type(category, str, cls, i) 

908 return cls( 

909 jcal_property[3:], 

910 Parameters.from_jcal_property(jcal_property), 

911 ) 

912 

913 

914class TimeBase: 

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

916 

917 default_value: ClassVar[str] 

918 params: Parameters 

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

920 

921 def __eq__(self, other): 

922 """self == other""" 

923 if isinstance(other, date): 

924 return self.dt == other 

925 if isinstance(other, TimeBase): 

926 default = object() 

927 for key in ( 

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

929 ) - self.ignore_for_equality: 

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

931 key, default 

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

933 return False 

934 return self.dt == other.dt 

935 if isinstance(other, vDDDLists): 

936 return other == self 

937 return False 

938 

939 def __hash__(self): 

940 return hash(self.dt) 

941 

942 from icalendar.param import RANGE, RELATED, TZID 

943 

944 def __repr__(self): 

945 """String representation.""" 

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

947 

948 

949DT_TYPE: TypeAlias = Union[ 

950 datetime, 

951 date, 

952 timedelta, 

953 time, 

954 Tuple[datetime, datetime], 

955 Tuple[datetime, timedelta], 

956] 

957 

958 

959class vDDDTypes(TimeBase): 

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

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

962 So this is practical. 

963 """ 

964 

965 default_value: ClassVar[str] = "DATE-TIME" 

966 params: Parameters 

967 dt: DT_TYPE 

968 

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

970 if params is None: 

971 params = {} 

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

973 raise TypeError( 

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

975 ) 

976 self.dt = dt 

977 # if isinstance(dt, (datetime, timedelta)): pass 

978 if is_date(dt): 

979 params.update({"value": "DATE"}) 

980 elif isinstance(dt, time): 

981 params.update({"value": "TIME"}) 

982 elif isinstance(dt, tuple): 

983 params.update({"value": "PERIOD"}) 

984 self.params = Parameters(params) 

985 self.params.update_tzid_from(dt) 

986 

987 def to_property_type(self) -> vDatetime | vDate | vDuration | vTime | vPeriod: 

988 """Convert to a property type. 

989 

990 Raises: 

991 ValueError: If the type is unknown. 

992 """ 

993 dt = self.dt 

994 if isinstance(dt, datetime): 

995 result = vDatetime(dt) 

996 elif isinstance(dt, date): 

997 result = vDate(dt) 

998 elif isinstance(dt, timedelta): 

999 result = vDuration(dt) 

1000 elif isinstance(dt, time): 

1001 result = vTime(dt) 

1002 elif isinstance(dt, tuple) and len(dt) == 2: 

1003 result = vPeriod(dt) 

1004 else: 

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

1006 result.params = self.params 

1007 return result 

1008 

1009 def to_ical(self) -> str: 

1010 """Return the ical representation.""" 

1011 return self.to_property_type().to_ical() 

1012 

1013 @classmethod 

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

1015 if isinstance(ical, cls): 

1016 return ical.dt 

1017 u = ical.upper() 

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

1019 return vDuration.from_ical(ical) 

1020 if "/" in u: 

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

1022 

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

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

1025 if len(ical) == 8: 

1026 if timezone: 

1027 tzinfo = tzp.timezone(timezone) 

1028 if tzinfo is not None: 

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

1030 return vDate.from_ical(ical) 

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

1032 return vTime.from_ical(ical) 

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

1034 

1035 @property 

1036 def td(self) -> timedelta: 

1037 """Compatibility property returning ``self.dt``. 

1038 

1039 This class is used to replace different time components. 

1040 Some of them contain a datetime or date (``.dt``). 

1041 Some of them contain a timedelta (``.td``). 

1042 This property allows interoperability. 

1043 """ 

1044 return self.dt 

1045 

1046 @property 

1047 def dts(self) -> list: 

1048 """Compatibility method to return a list of datetimes.""" 

1049 return [self] 

1050 

1051 @classmethod 

1052 def examples(cls) -> list[vDDDTypes]: 

1053 """Examples of vDDDTypes.""" 

1054 return [cls(date(2025, 11, 10))] 

1055 

1056 def _get_value(self) -> str | None: 

1057 """Determine the VALUE parameter.""" 

1058 return self.to_property_type().VALUE 

1059 

1060 from icalendar.param import VALUE 

1061 

1062 def to_jcal(self, name: str) -> list: 

1063 """The jCal representation of this property according to :rfc:`7265`.""" 

1064 return self.to_property_type().to_jcal(name) 

1065 

1066 @classmethod 

1067 def parse_jcal_value(cls, jcal: str | list) -> timedelta: 

1068 """Parse a jCal value. 

1069 

1070 Raises: 

1071 JCalParsingError: If the value can't be parsed as either a date, time, 

1072 date-time, duration, or period. 

1073 """ 

1074 if isinstance(jcal, list): 

1075 return vPeriod.parse_jcal_value(jcal) 

1076 JCalParsingError.validate_value_type(jcal, str, cls) 

1077 if "/" in jcal: 

1078 return vPeriod.parse_jcal_value(jcal) 

1079 for jcal_type in (vDatetime, vDate, vTime, vDuration): 

1080 try: 

1081 return jcal_type.parse_jcal_value(jcal) 

1082 except JCalParsingError: # noqa: PERF203 

1083 pass 

1084 raise JCalParsingError( 

1085 "Cannot parse date, time, date-time, duration, or period.", cls, value=jcal 

1086 ) 

1087 

1088 @classmethod 

1089 def from_jcal(cls, jcal_property: list) -> Self: 

1090 """Parse jCal from :rfc:`7265`. 

1091 

1092 Args: 

1093 jcal_property: The jCal property to parse. 

1094 

1095 Raises: 

1096 JCalParsingError: If the provided jCal is invalid. 

1097 """ 

1098 JCalParsingError.validate_property(jcal_property, cls) 

1099 with JCalParsingError.reraise_with_path_added(3): 

1100 dt = cls.parse_jcal_value(jcal_property[3]) 

1101 params = Parameters.from_jcal_property(jcal_property) 

1102 if params.tzid: 

1103 if isinstance(dt, tuple): 

1104 # period 

1105 start = tzp.localize(dt[0], params.tzid) 

1106 end = tzp.localize(dt[1], params.tzid) if is_datetime(dt[1]) else dt[1] 

1107 dt = (start, end) 

1108 else: 

1109 dt = tzp.localize(dt, params.tzid) 

1110 return cls( 

1111 dt, 

1112 params=params, 

1113 ) 

1114 

1115 

1116class vDate(TimeBase): 

1117 """Date 

1118 

1119 Value Name: 

1120 DATE 

1121 

1122 Purpose: 

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

1124 calendar date. 

1125 

1126 Format Definition: 

1127 This value type is defined by the following notation: 

1128 

1129 .. code-block:: text 

1130 

1131 date = date-value 

1132 

1133 date-value = date-fullyear date-month date-mday 

1134 date-fullyear = 4DIGIT 

1135 date-month = 2DIGIT ;01-12 

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

1137 ;based on month/year 

1138 

1139 Description: 

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

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

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

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

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

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

1146 year, month, and day component text. 

1147 

1148 Example: 

1149 The following represents July 14, 1997: 

1150 

1151 .. code-block:: text 

1152 

1153 19970714 

1154 

1155 .. code-block:: pycon 

1156 

1157 >>> from icalendar.prop import vDate 

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

1159 >>> date.year 

1160 1997 

1161 >>> date.month 

1162 7 

1163 >>> date.day 

1164 14 

1165 """ 

1166 

1167 default_value: ClassVar[str] = "DATE" 

1168 params: Parameters 

1169 

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

1171 if not isinstance(dt, date): 

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

1173 self.dt = dt 

1174 self.params = Parameters(params or {}) 

1175 

1176 def to_ical(self): 

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

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

1179 

1180 @staticmethod 

1181 def from_ical(ical): 

1182 try: 

1183 timetuple = ( 

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

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

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

1187 ) 

1188 return date(*timetuple) 

1189 except Exception as e: 

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

1191 

1192 @classmethod 

1193 def examples(cls) -> list[vDate]: 

1194 """Examples of vDate.""" 

1195 return [cls(date(2025, 11, 10))] 

1196 

1197 from icalendar.param import VALUE 

1198 

1199 def to_jcal(self, name: str) -> list: 

1200 """The jCal representation of this property according to :rfc:`7265`.""" 

1201 return [ 

1202 name, 

1203 self.params.to_jcal(), 

1204 self.VALUE.lower(), 

1205 self.dt.strftime("%Y-%m-%d"), 

1206 ] 

1207 

1208 @classmethod 

1209 def parse_jcal_value(cls, jcal: str) -> datetime: 

1210 """Parse a jCal string to a :py:class:`datetime.datetime`. 

1211 

1212 Raises: 

1213 JCalParsingError: If it can't parse a date. 

1214 """ 

1215 JCalParsingError.validate_value_type(jcal, str, cls) 

1216 try: 

1217 return datetime.strptime(jcal, "%Y-%m-%d").date() # noqa: DTZ007 

1218 except ValueError as e: 

1219 raise JCalParsingError("Cannot parse date.", cls, value=jcal) from e 

1220 

1221 @classmethod 

1222 def from_jcal(cls, jcal_property: list) -> Self: 

1223 """Parse jCal from :rfc:`7265`. 

1224 

1225 Args: 

1226 jcal_property: The jCal property to parse. 

1227 

1228 Raises: 

1229 JCalParsingError: If the provided jCal is invalid. 

1230 """ 

1231 JCalParsingError.validate_property(jcal_property, cls) 

1232 with JCalParsingError.reraise_with_path_added(3): 

1233 value = cls.parse_jcal_value(jcal_property[3]) 

1234 return cls( 

1235 value, 

1236 params=Parameters.from_jcal_property(jcal_property), 

1237 ) 

1238 

1239 

1240class vDatetime(TimeBase): 

1241 """Date-Time 

1242 

1243 Value Name: 

1244 DATE-TIME 

1245 

1246 Purpose: 

1247 This value type is used to identify values that specify a 

1248 precise calendar date and time of day. The format is based on 

1249 the ISO.8601.2004 complete representation. 

1250 

1251 Format Definition: 

1252 This value type is defined by the following notation: 

1253 

1254 .. code-block:: text 

1255 

1256 date-time = date "T" time 

1257 

1258 date = date-value 

1259 date-value = date-fullyear date-month date-mday 

1260 date-fullyear = 4DIGIT 

1261 date-month = 2DIGIT ;01-12 

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

1263 ;based on month/year 

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

1265 time-hour = 2DIGIT ;00-23 

1266 time-minute = 2DIGIT ;00-59 

1267 time-second = 2DIGIT ;00-60 

1268 time-utc = "Z" 

1269 

1270 The following is the representation of the date-time format. 

1271 

1272 .. code-block:: text 

1273 

1274 YYYYMMDDTHHMMSS 

1275 

1276 Description: 

1277 vDatetime is timezone aware and uses a timezone library. 

1278 When a vDatetime object is created from an 

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

1280 vDatetime object is created from a Python :py:mod:`datetime` object, it uses the 

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

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

1283 DATE-TIME components in the icalendar standard. 

1284 

1285 Example: 

1286 The following represents March 2, 2021 at 10:15 AM with local time: 

1287 

1288 .. code-block:: pycon 

1289 

1290 >>> from icalendar import vDatetime 

1291 >>> datetime = vDatetime.from_ical("20210302T101500") 

1292 >>> datetime.tzname() 

1293 >>> datetime.year 

1294 2021 

1295 >>> datetime.minute 

1296 15 

1297 

1298 The following represents March 2, 2021 at 10:15 AM in New York: 

1299 

1300 .. code-block:: pycon 

1301 

1302 >>> datetime = vDatetime.from_ical("20210302T101500", 'America/New_York') 

1303 >>> datetime.tzname() 

1304 'EST' 

1305 

1306 The following represents March 2, 2021 at 10:15 AM in Berlin: 

1307 

1308 .. code-block:: pycon 

1309 

1310 >>> from zoneinfo import ZoneInfo 

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

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

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

1314 """ 

1315 

1316 default_value: ClassVar[str] = "DATE-TIME" 

1317 params: Parameters 

1318 

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

1320 self.dt = dt 

1321 self.params = Parameters(params) 

1322 self.params.update_tzid_from(dt) 

1323 

1324 def to_ical(self): 

1325 dt = self.dt 

1326 

1327 s = ( 

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

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

1330 ) 

1331 if self.is_utc(): 

1332 s += "Z" 

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

1334 

1335 @staticmethod 

1336 def from_ical(ical, timezone=None): 

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

1338 tzinfo = None 

1339 if isinstance(timezone, str): 

1340 tzinfo = tzp.timezone(timezone) 

1341 elif timezone is not None: 

1342 tzinfo = timezone 

1343 

1344 try: 

1345 timetuple = ( 

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

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

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

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

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

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

1352 ) 

1353 if tzinfo: 

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

1355 if not ical[15:]: 

1356 return datetime(*timetuple) # noqa: DTZ001 

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

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

1359 except Exception as e: 

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

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

1362 

1363 @classmethod 

1364 def examples(cls) -> list[vDatetime]: 

1365 """Examples of vDatetime.""" 

1366 return [cls(datetime(2025, 11, 10, 16, 52))] # noqa: DTZ001 

1367 

1368 from icalendar.param import VALUE 

1369 

1370 def to_jcal(self, name: str) -> list: 

1371 """The jCal representation of this property according to :rfc:`7265`.""" 

1372 value = self.dt.strftime("%Y-%m-%dT%H:%M:%S") 

1373 if self.is_utc(): 

1374 value += "Z" 

1375 return [name, self.params.to_jcal(exclude_utc=True), self.VALUE.lower(), value] 

1376 

1377 def is_utc(self) -> bool: 

1378 """Whether this datetime is UTC.""" 

1379 return self.params.is_utc() or is_utc(self.dt) 

1380 

1381 @classmethod 

1382 def parse_jcal_value(cls, jcal: str) -> datetime: 

1383 """Parse a jCal string to a :py:class:`datetime.datetime`. 

1384 

1385 Raises: 

1386 JCalParsingError: If it can't parse a date-time value. 

1387 """ 

1388 JCalParsingError.validate_value_type(jcal, str, cls) 

1389 utc = jcal.endswith("Z") 

1390 if utc: 

1391 jcal = jcal[:-1] 

1392 try: 

1393 dt = datetime.strptime(jcal, "%Y-%m-%dT%H:%M:%S") # noqa: DTZ007 

1394 except ValueError as e: 

1395 raise JCalParsingError("Cannot parse date-time.", cls, value=jcal) from e 

1396 if utc: 

1397 return tzp.localize_utc(dt) 

1398 return dt 

1399 

1400 @classmethod 

1401 def from_jcal(cls, jcal_property: list) -> Self: 

1402 """Parse jCal from :rfc:`7265`. 

1403 

1404 Args: 

1405 jcal_property: The jCal property to parse. 

1406 

1407 Raises: 

1408 JCalParsingError: If the provided jCal is invalid. 

1409 """ 

1410 JCalParsingError.validate_property(jcal_property, cls) 

1411 params = Parameters.from_jcal_property(jcal_property) 

1412 with JCalParsingError.reraise_with_path_added(3): 

1413 dt = cls.parse_jcal_value(jcal_property[3]) 

1414 if params.tzid: 

1415 dt = tzp.localize(dt, params.tzid) 

1416 return cls( 

1417 dt, 

1418 params=params, 

1419 ) 

1420 

1421 

1422class vDuration(TimeBase): 

1423 """Duration 

1424 

1425 Value Name: 

1426 DURATION 

1427 

1428 Purpose: 

1429 This value type is used to identify properties that contain 

1430 a duration of time. 

1431 

1432 Format Definition: 

1433 This value type is defined by the following notation: 

1434 

1435 .. code-block:: text 

1436 

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

1438 

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

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

1441 dur-week = 1*DIGIT "W" 

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

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

1444 dur-second = 1*DIGIT "S" 

1445 dur-day = 1*DIGIT "D" 

1446 

1447 Description: 

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

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

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

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

1452 represent nominal durations (weeks and days) and accurate 

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

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

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

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

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

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

1459 computation of the exact duration requires the subtraction or 

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

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

1462 When computing an exact duration, the greatest order time 

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

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

1465 minutes, and number of seconds. 

1466 

1467 Example: 

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

1469 

1470 .. code-block:: text 

1471 

1472 P15DT5H0M20S 

1473 

1474 A duration of 7 weeks would be: 

1475 

1476 .. code-block:: text 

1477 

1478 P7W 

1479 

1480 .. code-block:: pycon 

1481 

1482 >>> from icalendar.prop import vDuration 

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

1484 >>> duration 

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

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

1487 >>> duration 

1488 datetime.timedelta(days=49) 

1489 """ 

1490 

1491 default_value: ClassVar[str] = "DURATION" 

1492 params: Parameters 

1493 

1494 def __init__(self, td: timedelta | str, /, params: dict[str, Any] | None = None): 

1495 if isinstance(td, str): 

1496 td = vDuration.from_ical(td) 

1497 if not isinstance(td, timedelta): 

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

1499 self.td = td 

1500 self.params = Parameters(params) 

1501 

1502 def to_ical(self): 

1503 sign = "" 

1504 td = self.td 

1505 if td.days < 0: 

1506 sign = "-" 

1507 td = -td 

1508 timepart = "" 

1509 if td.seconds: 

1510 timepart = "T" 

1511 hours = td.seconds // 3600 

1512 minutes = td.seconds % 3600 // 60 

1513 seconds = td.seconds % 60 

1514 if hours: 

1515 timepart += f"{hours}H" 

1516 if minutes or (hours and seconds): 

1517 timepart += f"{minutes}M" 

1518 if seconds: 

1519 timepart += f"{seconds}S" 

1520 if td.days == 0 and timepart: 

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

1522 return ( 

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

1524 + b"P" 

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

1526 + b"D" 

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

1528 ) 

1529 

1530 @staticmethod 

1531 def from_ical(ical): 

1532 match = DURATION_REGEX.match(ical) 

1533 if not match: 

1534 raise InvalidCalendar(f"Invalid iCalendar duration: {ical}") 

1535 

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

1537 value = timedelta( 

1538 weeks=int(weeks or 0), 

1539 days=int(days or 0), 

1540 hours=int(hours or 0), 

1541 minutes=int(minutes or 0), 

1542 seconds=int(seconds or 0), 

1543 ) 

1544 

1545 if sign == "-": 

1546 value = -value 

1547 

1548 return value 

1549 

1550 @property 

1551 def dt(self) -> timedelta: 

1552 """The time delta for compatibility.""" 

1553 return self.td 

1554 

1555 @classmethod 

1556 def examples(cls) -> list[vDuration]: 

1557 """Examples of vDuration.""" 

1558 return [cls(timedelta(1, 99))] 

1559 

1560 from icalendar.param import VALUE 

1561 

1562 def to_jcal(self, name: str) -> list: 

1563 """The jCal representation of this property according to :rfc:`7265`.""" 

1564 return [ 

1565 name, 

1566 self.params.to_jcal(), 

1567 self.VALUE.lower(), 

1568 self.to_ical().decode(), 

1569 ] 

1570 

1571 @classmethod 

1572 def parse_jcal_value(cls, jcal: str) -> timedelta | None: 

1573 """Parse a jCal string to a :py:class:`datetime.timedelta`. 

1574 

1575 Raises: 

1576 JCalParsingError: If it can't parse a duration.""" 

1577 JCalParsingError.validate_value_type(jcal, str, cls) 

1578 try: 

1579 return cls.from_ical(jcal) 

1580 except ValueError as e: 

1581 raise JCalParsingError("Cannot parse duration.", cls, value=jcal) from e 

1582 

1583 @classmethod 

1584 def from_jcal(cls, jcal_property: list) -> Self: 

1585 """Parse jCal from :rfc:`7265`. 

1586 

1587 Args: 

1588 jcal_property: The jCal property to parse. 

1589 

1590 Raises: 

1591 JCalParsingError: If the provided jCal is invalid. 

1592 """ 

1593 JCalParsingError.validate_property(jcal_property, cls) 

1594 with JCalParsingError.reraise_with_path_added(3): 

1595 duration = cls.parse_jcal_value(jcal_property[3]) 

1596 return cls( 

1597 duration, 

1598 Parameters.from_jcal_property(jcal_property), 

1599 ) 

1600 

1601 

1602class vPeriod(TimeBase): 

1603 """Period of Time 

1604 

1605 Value Name: 

1606 PERIOD 

1607 

1608 Purpose: 

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

1610 precise period of time. 

1611 

1612 Format Definition: 

1613 This value type is defined by the following notation: 

1614 

1615 .. code-block:: text 

1616 

1617 period = period-explicit / period-start 

1618 

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

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

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

1622 ; be before the end. 

1623 

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

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

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

1627 ; of time. 

1628 

1629 Description: 

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

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

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

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

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

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

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

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

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

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

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

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

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

1643 

1644 Example: 

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

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

1647 

1648 .. code-block:: text 

1649 

1650 19970101T180000Z/19970102T070000Z 

1651 

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

1653 and 30 minutes would be: 

1654 

1655 .. code-block:: text 

1656 

1657 19970101T180000Z/PT5H30M 

1658 

1659 .. code-block:: pycon 

1660 

1661 >>> from icalendar.prop import vPeriod 

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

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

1664 """ 

1665 

1666 default_value: ClassVar[str] = "PERIOD" 

1667 params: Parameters 

1668 by_duration: bool 

1669 start: datetime 

1670 end: datetime 

1671 duration: timedelta 

1672 

1673 def __init__( 

1674 self, 

1675 per: tuple[datetime, Union[datetime, timedelta]], 

1676 params: dict[str, Any] | None = None, 

1677 ): 

1678 start, end_or_duration = per 

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

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

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

1682 raise TypeError( 

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

1684 ) 

1685 by_duration = isinstance(end_or_duration, timedelta) 

1686 if by_duration: 

1687 duration = end_or_duration 

1688 end = normalize_pytz(start + duration) 

1689 else: 

1690 end = end_or_duration 

1691 duration = normalize_pytz(end - start) 

1692 if start > end: 

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

1694 

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

1696 # set the timezone identifier 

1697 # does not support different timezones for start and end 

1698 self.params.update_tzid_from(start) 

1699 

1700 self.start = start 

1701 self.end = end 

1702 self.by_duration = by_duration 

1703 self.duration = duration 

1704 

1705 def overlaps(self, other): 

1706 if self.start > other.start: 

1707 return other.overlaps(self) 

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

1709 

1710 def to_ical(self): 

1711 if self.by_duration: 

1712 return ( 

1713 vDatetime(self.start).to_ical() 

1714 + b"/" 

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

1716 ) 

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

1718 

1719 @staticmethod 

1720 def from_ical(ical, timezone=None): 

1721 try: 

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

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

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

1725 except Exception as e: 

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

1727 return (start, end_or_duration) 

1728 

1729 def __repr__(self): 

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

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

1732 

1733 @property 

1734 def dt(self): 

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

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

1737 

1738 from icalendar.param import FBTYPE 

1739 

1740 @classmethod 

1741 def examples(cls) -> list[vPeriod]: 

1742 """Examples of vPeriod.""" 

1743 return [ 

1744 vPeriod((datetime(2025, 11, 10, 16, 35), timedelta(hours=1, minutes=30))), # noqa: DTZ001 

1745 vPeriod((datetime(2025, 11, 10, 16, 35), datetime(2025, 11, 10, 18, 5))), # noqa: DTZ001 

1746 ] 

1747 

1748 from icalendar.param import VALUE 

1749 

1750 def to_jcal(self, name: str) -> list: 

1751 """The jCal representation of this property according to :rfc:`7265`.""" 

1752 value = [vDatetime(self.start).to_jcal(name)[-1]] 

1753 if self.by_duration: 

1754 value.append(vDuration(self.duration).to_jcal(name)[-1]) 

1755 else: 

1756 value.append(vDatetime(self.end).to_jcal(name)[-1]) 

1757 return [name, self.params.to_jcal(exclude_utc=True), self.VALUE.lower(), value] 

1758 

1759 @classmethod 

1760 def parse_jcal_value( 

1761 cls, jcal: str | list 

1762 ) -> tuple[datetime, datetime] | tuple[datetime, timedelta]: 

1763 """Parse a jCal value. 

1764 

1765 Raises: 

1766 JCalParsingError: If the period is not a list with exactly two items, 

1767 or it can't parse a date-time or duration. 

1768 """ 

1769 if isinstance(jcal, str) and "/" in jcal: 

1770 # only occurs in the example of RFC7265, Section B.2.2. 

1771 jcal = jcal.split("/") 

1772 if not isinstance(jcal, list) or len(jcal) != 2: 

1773 raise JCalParsingError( 

1774 "A period must be a list with exactly 2 items.", cls, value=jcal 

1775 ) 

1776 with JCalParsingError.reraise_with_path_added(0): 

1777 start = vDatetime.parse_jcal_value(jcal[0]) 

1778 with JCalParsingError.reraise_with_path_added(1): 

1779 JCalParsingError.validate_value_type(jcal[1], str, cls) 

1780 if jcal[1].startswith(("P", "-P", "+P")): 

1781 end_or_duration = vDuration.parse_jcal_value(jcal[1]) 

1782 else: 

1783 try: 

1784 end_or_duration = vDatetime.parse_jcal_value(jcal[1]) 

1785 except JCalParsingError as e: 

1786 raise JCalParsingError( 

1787 "Cannot parse date-time or duration.", 

1788 cls, 

1789 value=jcal[1], 

1790 ) from e 

1791 return start, end_or_duration 

1792 

1793 @classmethod 

1794 def from_jcal(cls, jcal_property: list) -> Self: 

1795 """Parse jCal from :rfc:`7265`. 

1796 

1797 Args: 

1798 jcal_property: The jCal property to parse. 

1799 

1800 Raises: 

1801 JCalParsingError: If the provided jCal is invalid. 

1802 """ 

1803 JCalParsingError.validate_property(jcal_property, cls) 

1804 with JCalParsingError.reraise_with_path_added(3): 

1805 start, end_or_duration = cls.parse_jcal_value(jcal_property[3]) 

1806 params = Parameters.from_jcal_property(jcal_property) 

1807 tzid = params.tzid 

1808 

1809 if tzid: 

1810 start = tzp.localize(start, tzid) 

1811 if is_datetime(end_or_duration): 

1812 end_or_duration = tzp.localize(end_or_duration, tzid) 

1813 

1814 return cls((start, end_or_duration), params=params) 

1815 

1816 

1817class vWeekday(str): 

1818 """Either a ``weekday`` or a ``weekdaynum``. 

1819 

1820 .. code-block:: pycon 

1821 

1822 >>> from icalendar import vWeekday 

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

1824 'MO' 

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

1826 2 

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

1828 'FR' 

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

1830 -1 

1831 

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

1833 

1834 .. code-block:: text 

1835 

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

1837 plus = "+" 

1838 minus = "-" 

1839 ordwk = 1*2DIGIT ;1 to 53 

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

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

1842 ;FRIDAY, and SATURDAY days of the week. 

1843 

1844 """ 

1845 

1846 params: Parameters 

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

1848 

1849 week_days = CaselessDict( 

1850 { 

1851 "SU": 0, 

1852 "MO": 1, 

1853 "TU": 2, 

1854 "WE": 3, 

1855 "TH": 4, 

1856 "FR": 5, 

1857 "SA": 6, 

1858 } 

1859 ) 

1860 

1861 def __new__( 

1862 cls, 

1863 value, 

1864 encoding=DEFAULT_ENCODING, 

1865 /, 

1866 params: dict[str, Any] | None = None, 

1867 ): 

1868 value = to_unicode(value, encoding=encoding) 

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

1870 match = WEEKDAY_RULE.match(self) 

1871 if match is None: 

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

1873 match = match.groupdict() 

1874 sign = match["signal"] 

1875 weekday = match["weekday"] 

1876 relative = match["relative"] 

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

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

1879 self.weekday = weekday or None 

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

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

1882 self.relative *= -1 

1883 self.params = Parameters(params) 

1884 return self 

1885 

1886 def to_ical(self): 

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

1888 

1889 @classmethod 

1890 def from_ical(cls, ical): 

1891 try: 

1892 return cls(ical.upper()) 

1893 except Exception as e: 

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

1895 

1896 @classmethod 

1897 def parse_jcal_value(cls, value: Any) -> vWeekday: 

1898 """Parse a jCal value for vWeekday. 

1899 

1900 Raises: 

1901 JCalParsingError: If the value is not a valid weekday. 

1902 """ 

1903 JCalParsingError.validate_value_type(value, str, cls) 

1904 try: 

1905 return cls(value) 

1906 except ValueError as e: 

1907 raise JCalParsingError( 

1908 "The value must be a valid weekday.", cls, value=value 

1909 ) from e 

1910 

1911 

1912class vFrequency(str): 

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

1914 

1915 params: Parameters 

1916 __slots__ = ("params",) 

1917 

1918 frequencies = CaselessDict( 

1919 { 

1920 "SECONDLY": "SECONDLY", 

1921 "MINUTELY": "MINUTELY", 

1922 "HOURLY": "HOURLY", 

1923 "DAILY": "DAILY", 

1924 "WEEKLY": "WEEKLY", 

1925 "MONTHLY": "MONTHLY", 

1926 "YEARLY": "YEARLY", 

1927 } 

1928 ) 

1929 

1930 def __new__( 

1931 cls, 

1932 value, 

1933 encoding=DEFAULT_ENCODING, 

1934 /, 

1935 params: dict[str, Any] | None = None, 

1936 ): 

1937 value = to_unicode(value, encoding=encoding) 

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

1939 if self not in vFrequency.frequencies: 

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

1941 self.params = Parameters(params) 

1942 return self 

1943 

1944 def to_ical(self): 

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

1946 

1947 @classmethod 

1948 def from_ical(cls, ical): 

1949 try: 

1950 return cls(ical.upper()) 

1951 except Exception as e: 

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

1953 

1954 @classmethod 

1955 def parse_jcal_value(cls, value: Any) -> vFrequency: 

1956 """Parse a jCal value for vFrequency. 

1957 

1958 Raises: 

1959 JCalParsingError: If the value is not a valid frequency. 

1960 """ 

1961 JCalParsingError.validate_value_type(value, str, cls) 

1962 try: 

1963 return cls(value) 

1964 except ValueError as e: 

1965 raise JCalParsingError( 

1966 "The value must be a valid frequency.", cls, value=value 

1967 ) from e 

1968 

1969 

1970class vMonth(int): 

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

1972 

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

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

1975 

1976 .. code-block:: pycon 

1977 

1978 >>> from icalendar import vMonth 

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

1980 vMonth('1') 

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

1982 vMonth('5L') 

1983 >>> vMonth(1).leap 

1984 False 

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

1986 True 

1987 

1988 Definition from RFC: 

1989 

1990 .. code-block:: text 

1991 

1992 type-bymonth = element bymonth { 

1993 xsd:positiveInteger | 

1994 xsd:string 

1995 } 

1996 """ 

1997 

1998 params: Parameters 

1999 

2000 def __new__(cls, month: Union[str, int], /, params: dict[str, Any] | None = None): 

2001 if isinstance(month, vMonth): 

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

2003 if isinstance(month, str): 

2004 if month.isdigit(): 

2005 month_index = int(month) 

2006 leap = False 

2007 else: 

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

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

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

2011 leap = True 

2012 else: 

2013 leap = False 

2014 month_index = int(month) 

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

2016 self.leap = leap 

2017 self.params = Parameters(params) 

2018 return self 

2019 

2020 def to_ical(self) -> bytes: 

2021 """The ical representation.""" 

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

2023 

2024 @classmethod 

2025 def from_ical(cls, ical: str): 

2026 return cls(ical) 

2027 

2028 @property 

2029 def leap(self) -> bool: 

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

2031 return self._leap 

2032 

2033 @leap.setter 

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

2035 self._leap = value 

2036 

2037 def __repr__(self) -> str: 

2038 """repr(self)""" 

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

2040 

2041 def __str__(self) -> str: 

2042 """str(self)""" 

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

2044 

2045 @classmethod 

2046 def parse_jcal_value(cls, value: Any) -> vMonth: 

2047 """Parse a jCal value for vMonth. 

2048 

2049 Raises: 

2050 JCalParsingError: If the value is not a valid month. 

2051 """ 

2052 JCalParsingError.validate_value_type(value, (str, int), cls) 

2053 try: 

2054 return cls(value) 

2055 except ValueError as e: 

2056 raise JCalParsingError( 

2057 "The value must be a string or an integer.", cls, value=value 

2058 ) from e 

2059 

2060 

2061class vSkip(vText, Enum): 

2062 """Skip values for RRULE. 

2063 

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

2065 

2066 OMIT is the default value. 

2067 

2068 Examples: 

2069 

2070 .. code-block:: pycon 

2071 

2072 >>> from icalendar import vSkip 

2073 >>> vSkip.OMIT 

2074 vSkip('OMIT') 

2075 >>> vSkip.FORWARD 

2076 vSkip('FORWARD') 

2077 >>> vSkip.BACKWARD 

2078 vSkip('BACKWARD') 

2079 """ 

2080 

2081 OMIT = "OMIT" 

2082 FORWARD = "FORWARD" 

2083 BACKWARD = "BACKWARD" 

2084 

2085 __reduce_ex__ = Enum.__reduce_ex__ 

2086 

2087 def __repr__(self): 

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

2089 

2090 @classmethod 

2091 def parse_jcal_value(cls, value: Any) -> vSkip: 

2092 """Parse a jCal value for vSkip. 

2093 

2094 Raises: 

2095 JCalParsingError: If the value is not a valid skip value. 

2096 """ 

2097 JCalParsingError.validate_value_type(value, str, cls) 

2098 try: 

2099 return cls[value.upper()] 

2100 except KeyError as e: 

2101 raise JCalParsingError( 

2102 "The value must be a valid skip value.", cls, value=value 

2103 ) from e 

2104 

2105 

2106class vRecur(CaselessDict): 

2107 """Recurrence definition. 

2108 

2109 Property Name: 

2110 RRULE 

2111 

2112 Purpose: 

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

2114 journal entries, or time zone definitions. 

2115 

2116 Value Type: 

2117 RECUR 

2118 

2119 Property Parameters: 

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

2121 

2122 Conformance: 

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

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

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

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

2127 

2128 Description: 

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

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

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

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

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

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

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

2136 value not synchronized with the recurrence rule is undefined. 

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

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

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

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

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

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

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

2144 

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

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

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

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

2149 same local time regardless of time zone changes. 

2150 

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

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

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

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

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

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

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

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

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

2160 "RDATE" property of PERIOD value type. 

2161 

2162 Examples: 

2163 The following RRULE specifies daily events for 10 occurrences. 

2164 

2165 .. code-block:: text 

2166 

2167 RRULE:FREQ=DAILY;COUNT=10 

2168 

2169 Below, we parse the RRULE ical string. 

2170 

2171 .. code-block:: pycon 

2172 

2173 >>> from icalendar.prop import vRecur 

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

2175 >>> rrule 

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

2177 

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

2179 :class:`icalendar.cal.Todo`. 

2180 

2181 .. code-block:: pycon 

2182 

2183 >>> from icalendar import Event 

2184 >>> event = Event() 

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

2186 >>> event.rrules 

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

2188 """ # noqa: E501 

2189 

2190 default_value: ClassVar[str] = "RECUR" 

2191 params: Parameters 

2192 

2193 frequencies = [ 

2194 "SECONDLY", 

2195 "MINUTELY", 

2196 "HOURLY", 

2197 "DAILY", 

2198 "WEEKLY", 

2199 "MONTHLY", 

2200 "YEARLY", 

2201 ] 

2202 

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

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

2205 canonical_order = ( 

2206 "RSCALE", 

2207 "FREQ", 

2208 "UNTIL", 

2209 "COUNT", 

2210 "INTERVAL", 

2211 "BYSECOND", 

2212 "BYMINUTE", 

2213 "BYHOUR", 

2214 "BYDAY", 

2215 "BYWEEKDAY", 

2216 "BYMONTHDAY", 

2217 "BYYEARDAY", 

2218 "BYWEEKNO", 

2219 "BYMONTH", 

2220 "BYSETPOS", 

2221 "WKST", 

2222 "SKIP", 

2223 ) 

2224 

2225 types = CaselessDict( 

2226 { 

2227 "COUNT": vInt, 

2228 "INTERVAL": vInt, 

2229 "BYSECOND": vInt, 

2230 "BYMINUTE": vInt, 

2231 "BYHOUR": vInt, 

2232 "BYWEEKNO": vInt, 

2233 "BYMONTHDAY": vInt, 

2234 "BYYEARDAY": vInt, 

2235 "BYMONTH": vMonth, 

2236 "UNTIL": vDDDTypes, 

2237 "BYSETPOS": vInt, 

2238 "WKST": vWeekday, 

2239 "BYDAY": vWeekday, 

2240 "FREQ": vFrequency, 

2241 "BYWEEKDAY": vWeekday, 

2242 "SKIP": vSkip, # RFC 7529 

2243 "RSCALE": vText, # RFC 7529 

2244 } 

2245 ) 

2246 

2247 # for reproducible serialization: 

2248 # RULE: if and only if it can be a list it will be a list 

2249 # look up in RFC 

2250 jcal_not_a_list = {"FREQ", "UNTIL", "COUNT", "INTERVAL", "WKST", "SKIP", "RSCALE"} 

2251 

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

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

2254 # we have a string as an argument. 

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

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

2257 if not isinstance(v, SEQUENCE_TYPES): 

2258 kwargs[k] = [v] 

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

2260 self.params = Parameters(params) 

2261 

2262 def to_ical(self): 

2263 result = [] 

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

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

2266 if not isinstance(vals, SEQUENCE_TYPES): 

2267 vals = [vals] # noqa: PLW2901 

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

2269 

2270 # CaselessDict keys are always unicode 

2271 param_key = key.encode(DEFAULT_ENCODING) 

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

2273 

2274 return b";".join(result) 

2275 

2276 @classmethod 

2277 def parse_type(cls, key, values): 

2278 # integers 

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

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

2281 

2282 @classmethod 

2283 def from_ical(cls, ical: str): 

2284 if isinstance(ical, cls): 

2285 return ical 

2286 try: 

2287 recur = cls() 

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

2289 try: 

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

2291 except ValueError: 

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

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

2294 continue 

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

2296 return cls(recur) 

2297 except ValueError: 

2298 raise 

2299 except Exception as e: 

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

2301 

2302 @classmethod 

2303 def examples(cls) -> list[vRecur]: 

2304 """Examples of vRecur.""" 

2305 return [cls.from_ical("FREQ=DAILY;COUNT=10")] 

2306 

2307 from icalendar.param import VALUE 

2308 

2309 def to_jcal(self, name: str) -> list: 

2310 """The jCal representation of this property according to :rfc:`7265`.""" 

2311 recur = {} 

2312 for k, v in self.items(): 

2313 key = k.lower() 

2314 if key.upper() in self.jcal_not_a_list: 

2315 value = v[0] if isinstance(v, list) and len(v) == 1 else v 

2316 elif not isinstance(v, list): 

2317 value = [v] 

2318 else: 

2319 value = v 

2320 recur[key] = value 

2321 if "until" in recur: 

2322 until = recur["until"] 

2323 until_jcal = vDDDTypes(until).to_jcal("until") 

2324 recur["until"] = until_jcal[-1] 

2325 return [name, self.params.to_jcal(), self.VALUE.lower(), recur] 

2326 

2327 @classmethod 

2328 def from_jcal(cls, jcal_property: list) -> Self: 

2329 """Parse jCal from :rfc:`7265`. 

2330 

2331 Args: 

2332 jcal_property: The jCal property to parse. 

2333 

2334 Raises: 

2335 JCalParsingError: If the provided jCal is invalid. 

2336 """ 

2337 JCalParsingError.validate_property(jcal_property, cls) 

2338 params = Parameters.from_jcal_property(jcal_property) 

2339 if not isinstance(jcal_property[3], dict) or not all( 

2340 isinstance(k, str) for k in jcal_property[3] 

2341 ): 

2342 raise JCalParsingError( 

2343 "The recurrence rule must be a mapping with string keys.", 

2344 cls, 

2345 3, 

2346 value=jcal_property[3], 

2347 ) 

2348 recur = {} 

2349 for key, value in jcal_property[3].items(): 

2350 value_type = cls.types.get(key, vText) 

2351 with JCalParsingError.reraise_with_path_added(3, key): 

2352 if isinstance(value, list): 

2353 recur[key.lower()] = values = [] 

2354 for i, v in enumerate(value): 

2355 with JCalParsingError.reraise_with_path_added(i): 

2356 values.append(value_type.parse_jcal_value(v)) 

2357 else: 

2358 recur[key] = value_type.parse_jcal_value(value) 

2359 until = recur.get("until") 

2360 if until is not None and not isinstance(until, list): 

2361 recur["until"] = [until] 

2362 return cls(recur, params=params) 

2363 

2364 def __eq__(self, other: object) -> bool: 

2365 """self == other""" 

2366 if not isinstance(other, vRecur): 

2367 return super().__eq__(other) 

2368 if self.keys() != other.keys(): 

2369 return False 

2370 for key in self.keys(): 

2371 v1 = self[key] 

2372 v2 = other[key] 

2373 if not isinstance(v1, SEQUENCE_TYPES): 

2374 v1 = [v1] 

2375 if not isinstance(v2, SEQUENCE_TYPES): 

2376 v2 = [v2] 

2377 if v1 != v2: 

2378 return False 

2379 return True 

2380 

2381 

2382TIME_JCAL_REGEX = re.compile( 

2383 r"^(?P<hour>[0-9]{2}):(?P<minute>[0-9]{2}):(?P<second>[0-9]{2})(?P<utc>Z)?$" 

2384) 

2385 

2386 

2387class vTime(TimeBase): 

2388 """Time 

2389 

2390 Value Name: 

2391 TIME 

2392 

2393 Purpose: 

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

2395 time of day. 

2396 

2397 Format Definition: 

2398 This value type is defined by the following notation: 

2399 

2400 .. code-block:: text 

2401 

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

2403 

2404 time-hour = 2DIGIT ;00-23 

2405 time-minute = 2DIGIT ;00-59 

2406 time-second = 2DIGIT ;00-60 

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

2408 

2409 time-utc = "Z" 

2410 

2411 Description: 

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

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

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

2415 vText) is defined for this value type. 

2416 

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

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

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

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

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

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

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

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

2425 

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

2427 type expresses time values in three forms: 

2428 

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

2430 the following is not valid for a time value: 

2431 

2432 .. code-block:: text 

2433 

2434 230000-0800 ;Invalid time format 

2435 

2436 **FORM #1 LOCAL TIME** 

2437 

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

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

2440 example, 11:00 PM: 

2441 

2442 .. code-block:: text 

2443 

2444 230000 

2445 

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

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

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

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

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

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

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

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

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

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

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

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

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

2459 

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

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

2462 time zone reference MUST be specified. 

2463 

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

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

2466 existence of "VTIMEZONE" calendar components in the iCalendar 

2467 object. 

2468 

2469 **FORM #2: UTC TIME** 

2470 

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

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

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

2474 

2475 .. code-block:: text 

2476 

2477 070000Z 

2478 

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

2480 properties whose time values are specified in UTC. 

2481 

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

2483 

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

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

2486 the appropriate time zone definition. 

2487 

2488 Example: 

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

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

2491 

2492 .. code-block:: text 

2493 

2494 083000 

2495 133000Z 

2496 TZID=America/New_York:083000 

2497 """ 

2498 

2499 default_value: ClassVar[str] = "TIME" 

2500 params: Parameters 

2501 

2502 def __init__(self, *args, params: dict[str, Any] | None = None): 

2503 if len(args) == 1: 

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

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

2506 self.dt = args[0] 

2507 else: 

2508 self.dt = time(*args) 

2509 self.params = Parameters(params or {}) 

2510 self.params.update_tzid_from(self.dt) 

2511 

2512 def to_ical(self): 

2513 value = self.dt.strftime("%H%M%S") 

2514 if self.is_utc(): 

2515 value += "Z" 

2516 return value 

2517 

2518 def is_utc(self) -> bool: 

2519 """Whether this time is UTC.""" 

2520 return self.params.is_utc() or is_utc(self.dt) 

2521 

2522 @staticmethod 

2523 def from_ical(ical): 

2524 # TODO: timezone support 

2525 try: 

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

2527 return time(*timetuple) 

2528 except Exception as e: 

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

2530 

2531 @classmethod 

2532 def examples(cls) -> list[vTime]: 

2533 """Examples of vTime.""" 

2534 return [cls(time(12, 30))] 

2535 

2536 from icalendar.param import VALUE 

2537 

2538 def to_jcal(self, name: str) -> list: 

2539 """The jCal representation of this property according to :rfc:`7265`.""" 

2540 value = self.dt.strftime("%H:%M:%S") 

2541 if self.is_utc(): 

2542 value += "Z" 

2543 return [name, self.params.to_jcal(exclude_utc=True), self.VALUE.lower(), value] 

2544 

2545 @classmethod 

2546 def parse_jcal_value(cls, jcal: str) -> time: 

2547 """Parse a jCal string to a :py:class:`datetime.time`. 

2548 

2549 Raises: 

2550 JCalParsingError: If it can't parse a time. 

2551 """ 

2552 JCalParsingError.validate_value_type(jcal, str, cls) 

2553 match = TIME_JCAL_REGEX.match(jcal) 

2554 if match is None: 

2555 raise JCalParsingError("Cannot parse time.", cls, value=jcal) 

2556 hour = int(match.group("hour")) 

2557 minute = int(match.group("minute")) 

2558 second = int(match.group("second")) 

2559 utc = bool(match.group("utc")) 

2560 return time(hour, minute, second, tzinfo=timezone.utc if utc else None) 

2561 

2562 @classmethod 

2563 def from_jcal(cls, jcal_property: list) -> Self: 

2564 """Parse jCal from :rfc:`7265`. 

2565 

2566 Args: 

2567 jcal_property: The jCal property to parse. 

2568 

2569 Raises: 

2570 JCalParsingError: If the provided jCal is invalid. 

2571 """ 

2572 JCalParsingError.validate_property(jcal_property, cls) 

2573 with JCalParsingError.reraise_with_path_added(3): 

2574 value = cls.parse_jcal_value(jcal_property[3]) 

2575 return cls( 

2576 value, 

2577 params=Parameters.from_jcal_property(jcal_property), 

2578 ) 

2579 

2580 

2581class vUri(str): 

2582 """URI 

2583 

2584 Value Name: 

2585 URI 

2586 

2587 Purpose: 

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

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

2590 property value. 

2591 

2592 Format Definition: 

2593 This value type is defined by the following notation: 

2594 

2595 .. code-block:: text 

2596 

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

2598 

2599 Description: 

2600 This value type might be used to reference binary 

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

2602 to include directly in the iCalendar object. 

2603 

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

2605 syntax defined in [RFC3986]. 

2606 

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

2608 be specified as a quoted-string value. 

2609 

2610 Examples: 

2611 The following is a URI for a network file: 

2612 

2613 .. code-block:: text 

2614 

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

2616 

2617 .. code-block:: pycon 

2618 

2619 >>> from icalendar.prop import vUri 

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

2621 >>> uri 

2622 vUri('http://example.com/my-report.txt') 

2623 >>> uri.uri 

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

2625 """ 

2626 

2627 default_value: ClassVar[str] = "URI" 

2628 params: Parameters 

2629 __slots__ = ("params",) 

2630 

2631 def __new__( 

2632 cls, 

2633 value: str, 

2634 encoding: str = DEFAULT_ENCODING, 

2635 /, 

2636 params: dict[str, Any] | None = None, 

2637 ): 

2638 value = to_unicode(value, encoding=encoding) 

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

2640 self.params = Parameters(params) 

2641 return self 

2642 

2643 def to_ical(self): 

2644 return self.encode(DEFAULT_ENCODING) 

2645 

2646 @classmethod 

2647 def from_ical(cls, ical): 

2648 try: 

2649 return cls(ical) 

2650 except Exception as e: 

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

2652 

2653 @classmethod 

2654 def examples(cls) -> list[vUri]: 

2655 """Examples of vUri.""" 

2656 return [cls("http://example.com/my-report.txt")] 

2657 

2658 def to_jcal(self, name: str) -> list: 

2659 """The jCal representation of this property according to :rfc:`7265`.""" 

2660 return [name, self.params.to_jcal(), self.VALUE.lower(), str(self)] 

2661 

2662 @classmethod 

2663 def from_jcal(cls, jcal_property: list) -> Self: 

2664 """Parse jCal from :rfc:`7265`. 

2665 

2666 Args: 

2667 jcal_property: The jCal property to parse. 

2668 

2669 Raises: 

2670 JCalParsingError: If the provided jCal is invalid. 

2671 """ 

2672 JCalParsingError.validate_property(jcal_property, cls) 

2673 return cls( 

2674 jcal_property[3], 

2675 Parameters.from_jcal_property(jcal_property), 

2676 ) 

2677 

2678 @property 

2679 def ical_value(self) -> str: 

2680 """The URI.""" 

2681 return self.uri 

2682 

2683 @property 

2684 def uri(self) -> str: 

2685 """The URI.""" 

2686 return str(self) 

2687 

2688 def __repr__(self) -> str: 

2689 """repr(self)""" 

2690 return f"{self.__class__.__name__}({self.uri!r})" 

2691 

2692 from icalendar.param import FMTTYPE, GAP, LABEL, LANGUAGE, LINKREL, RELTYPE, VALUE 

2693 

2694 

2695class vUid(vText): 

2696 """A UID of a component. 

2697 

2698 This is defined in :rfc:`9253`, Section 7. 

2699 """ 

2700 

2701 default_value: ClassVar[str] = "UID" 

2702 

2703 @classmethod 

2704 def new(cls): 

2705 """Create a new UID for convenience. 

2706 

2707 .. code-block:: pycon 

2708 

2709 >>> from icalendar import vUid 

2710 >>> vUid.new() 

2711 vUid('d755cef5-2311-46ed-a0e1-6733c9e15c63') 

2712 

2713 """ 

2714 return vUid(uuid.uuid4()) 

2715 

2716 @property 

2717 def uid(self) -> str: 

2718 """The UID of this property.""" 

2719 return str(self) 

2720 

2721 @property 

2722 def ical_value(self) -> str: 

2723 """The UID of this property.""" 

2724 return self.uid 

2725 

2726 def __repr__(self) -> str: 

2727 """repr(self)""" 

2728 return f"{self.__class__.__name__}({self.uid!r})" 

2729 

2730 from icalendar.param import FMTTYPE, LABEL, LINKREL 

2731 

2732 @classmethod 

2733 def examples(cls) -> list[vUid]: 

2734 """Examples of vUid.""" 

2735 return [cls("d755cef5-2311-46ed-a0e1-6733c9e15c63")] 

2736 

2737 

2738class vXmlReference(vUri): 

2739 """An XML-REFERENCE. 

2740 

2741 The associated value references an associated XML artifact and 

2742 is a URI with an XPointer anchor value. 

2743 

2744 This is defined in :rfc:`9253`, Section 7. 

2745 """ 

2746 

2747 default_value: ClassVar[str] = "XML-REFERENCE" 

2748 

2749 @property 

2750 def xml_reference(self) -> str: 

2751 """The XML reference URI of this property.""" 

2752 return self.uri 

2753 

2754 @property 

2755 def x_pointer(self) -> str | None: 

2756 """The XPointer of the URI. 

2757 

2758 The XPointer is defined in `W3C.WD-xptr-xpointer-20021219 

2759 <https://www.rfc-editor.org/rfc/rfc9253.html#W3C.WD-xptr-xpointer-20021219>`_, 

2760 and its use as an anchor is defined in `W3C.REC-xptr-framework-20030325 

2761 <https://www.rfc-editor.org/rfc/rfc9253.html#W3C.REC-xptr-framework-20030325>`_. 

2762 

2763 Returns: 

2764 The decoded x-pointer or ``None`` if no valid x-pointer is found. 

2765 """ 

2766 from urllib.parse import unquote, urlparse 

2767 

2768 parsed = urlparse(self.xml_reference) 

2769 fragment = unquote(parsed.fragment) 

2770 if not fragment.startswith("xpointer(") or not fragment.endswith(")"): 

2771 return None 

2772 return fragment[9:-1] 

2773 

2774 @classmethod 

2775 def examples(cls) -> list[vXmlReference]: 

2776 """Examples of vXmlReference.""" 

2777 return [cls("http://example.com/doc.xml#xpointer(/doc/element)")] 

2778 

2779 

2780class vGeo: 

2781 """Geographic Position 

2782 

2783 Property Name: 

2784 GEO 

2785 

2786 Purpose: 

2787 This property specifies information related to the global 

2788 position for the activity specified by a calendar component. 

2789 

2790 Value Type: 

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

2792 

2793 Property Parameters: 

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

2795 this property. 

2796 

2797 Conformance: 

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

2799 calendar components. 

2800 

2801 Description: 

2802 This property value specifies latitude and longitude, 

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

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

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

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

2807 will allow for accuracy to within one meter of geographical 

2808 position. Receiving applications MUST accept values of this 

2809 precision and MAY truncate values of greater precision. 

2810 

2811 Example: 

2812 

2813 .. code-block:: text 

2814 

2815 GEO:37.386013;-122.082932 

2816 

2817 Parse vGeo: 

2818 

2819 .. code-block:: pycon 

2820 

2821 >>> from icalendar.prop import vGeo 

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

2823 >>> geo 

2824 (37.386013, -122.082932) 

2825 

2826 Add a geo location to an event: 

2827 

2828 .. code-block:: pycon 

2829 

2830 >>> from icalendar import Event 

2831 >>> event = Event() 

2832 >>> latitude = 37.386013 

2833 >>> longitude = -122.082932 

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

2835 >>> event['GEO'] 

2836 vGeo((37.386013, -122.082932)) 

2837 """ 

2838 

2839 default_value: ClassVar[str] = "FLOAT" 

2840 params: Parameters 

2841 

2842 def __init__( 

2843 self, 

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

2845 /, 

2846 params: dict[str, Any] | None = None, 

2847 ): 

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

2849 

2850 Raises: 

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

2852 """ 

2853 try: 

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

2855 latitude = float(latitude) 

2856 longitude = float(longitude) 

2857 except Exception as e: 

2858 raise ValueError( 

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

2860 ) from e 

2861 self.latitude = latitude 

2862 self.longitude = longitude 

2863 self.params = Parameters(params) 

2864 

2865 def to_ical(self): 

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

2867 

2868 @staticmethod 

2869 def from_ical(ical): 

2870 try: 

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

2872 return (float(latitude), float(longitude)) 

2873 except Exception as e: 

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

2875 

2876 def __eq__(self, other): 

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

2878 

2879 def __repr__(self): 

2880 """repr(self)""" 

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

2882 

2883 def to_jcal(self, name: str) -> list: 

2884 """Convert to jCal object.""" 

2885 return [ 

2886 name, 

2887 self.params.to_jcal(), 

2888 self.VALUE.lower(), 

2889 [self.latitude, self.longitude], 

2890 ] 

2891 

2892 @classmethod 

2893 def examples(cls) -> list[vGeo]: 

2894 """Examples of vGeo.""" 

2895 return [cls((37.386013, -122.082932))] 

2896 

2897 from icalendar.param import VALUE 

2898 

2899 @classmethod 

2900 def from_jcal(cls, jcal_property: list) -> Self: 

2901 """Parse jCal from :rfc:`7265`. 

2902 

2903 Args: 

2904 jcal_property: The jCal property to parse. 

2905 

2906 Raises: 

2907 JCalParsingError: If the provided jCal is invalid. 

2908 """ 

2909 JCalParsingError.validate_property(jcal_property, cls) 

2910 return cls( 

2911 jcal_property[3], 

2912 Parameters.from_jcal_property(jcal_property), 

2913 ) 

2914 

2915 

2916UTC_OFFSET_JCAL_REGEX = re.compile( 

2917 r"^(?P<sign>[+-])?(?P<hours>\d\d):(?P<minutes>\d\d)(?::(?P<seconds>\d\d))?$" 

2918) 

2919 

2920 

2921class vUTCOffset: 

2922 """UTC Offset 

2923 

2924 Value Name: 

2925 UTC-OFFSET 

2926 

2927 Purpose: 

2928 This value type is used to identify properties that contain 

2929 an offset from UTC to local time. 

2930 

2931 Format Definition: 

2932 This value type is defined by the following notation: 

2933 

2934 .. code-block:: text 

2935 

2936 utc-offset = time-numzone 

2937 

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

2939 

2940 Description: 

2941 The PLUS SIGN character MUST be specified for positive 

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

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

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

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

2946 

2947 Example: 

2948 The following UTC offsets are given for standard time for 

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

2950 UTC): 

2951 

2952 .. code-block:: text 

2953 

2954 -0500 

2955 

2956 +0100 

2957 

2958 .. code-block:: pycon 

2959 

2960 >>> from icalendar.prop import vUTCOffset 

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

2962 >>> utc_offset 

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

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

2965 >>> utc_offset 

2966 datetime.timedelta(seconds=3600) 

2967 """ 

2968 

2969 default_value: ClassVar[str] = "UTC-OFFSET" 

2970 params: Parameters 

2971 

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

2973 

2974 # component, we will silently ignore 

2975 # it, rather than let the exception 

2976 # propagate upwards 

2977 

2978 def __init__(self, td: timedelta, /, params: dict[str, Any] | None = None): 

2979 if not isinstance(td, timedelta): 

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

2981 self.td = td 

2982 self.params = Parameters(params) 

2983 

2984 def to_ical(self) -> str: 

2985 """Return the ical representation.""" 

2986 return self.format("") 

2987 

2988 def format(self, divider: str = "") -> str: 

2989 """Represent the value with a possible divider. 

2990 

2991 .. code-block:: pycon 

2992 

2993 >>> from icalendar import vUTCOffset 

2994 >>> from datetime import timedelta 

2995 >>> utc_offset = vUTCOffset(timedelta(hours=-5)) 

2996 >>> utc_offset.format() 

2997 '-0500' 

2998 >>> utc_offset.format(divider=':') 

2999 '-05:00' 

3000 """ 

3001 if self.td < timedelta(0): 

3002 sign = "-%s" 

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

3004 else: 

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

3006 sign = "+%s" 

3007 td = self.td 

3008 

3009 days, seconds = td.days, td.seconds 

3010 

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

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

3013 seconds = abs(seconds % 60) 

3014 if seconds: 

3015 duration = f"{hours:02}{divider}{minutes:02}{divider}{seconds:02}" 

3016 else: 

3017 duration = f"{hours:02}{divider}{minutes:02}" 

3018 return sign % duration 

3019 

3020 @classmethod 

3021 def from_ical(cls, ical): 

3022 if isinstance(ical, cls): 

3023 return ical.td 

3024 try: 

3025 sign, hours, minutes, seconds = ( 

3026 ical[0:1], 

3027 int(ical[1:3]), 

3028 int(ical[3:5]), 

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

3030 ) 

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

3032 except Exception as e: 

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

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

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

3036 if sign == "-": 

3037 return -offset 

3038 return offset 

3039 

3040 def __eq__(self, other): 

3041 if not isinstance(other, vUTCOffset): 

3042 return False 

3043 return self.td == other.td 

3044 

3045 def __hash__(self): 

3046 return hash(self.td) 

3047 

3048 def __repr__(self): 

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

3050 

3051 @classmethod 

3052 def examples(cls) -> list[vUTCOffset]: 

3053 """Examples of vUTCOffset.""" 

3054 return [ 

3055 cls(timedelta(hours=3)), 

3056 cls(timedelta(0)), 

3057 ] 

3058 

3059 from icalendar.param import VALUE 

3060 

3061 def to_jcal(self, name: str) -> list: 

3062 """The jCal representation of this property according to :rfc:`7265`.""" 

3063 return [name, self.params.to_jcal(), self.VALUE.lower(), self.format(":")] 

3064 

3065 @classmethod 

3066 def from_jcal(cls, jcal_property: list) -> Self: 

3067 """Parse jCal from :rfc:`7265`. 

3068 

3069 Args: 

3070 jcal_property: The jCal property to parse. 

3071 

3072 Raises: 

3073 JCalParsingError: If the provided jCal is invalid. 

3074 """ 

3075 JCalParsingError.validate_property(jcal_property, cls) 

3076 match = UTC_OFFSET_JCAL_REGEX.match(jcal_property[3]) 

3077 if match is None: 

3078 raise JCalParsingError(f"Cannot parse {jcal_property!r} as UTC-OFFSET.") 

3079 negative = match.group("sign") == "-" 

3080 hours = int(match.group("hours")) 

3081 minutes = int(match.group("minutes")) 

3082 seconds = int(match.group("seconds") or 0) 

3083 t = timedelta(hours=hours, minutes=minutes, seconds=seconds) 

3084 if negative: 

3085 t = -t 

3086 return cls(t, Parameters.from_jcal_property(jcal_property)) 

3087 

3088 

3089class vInline(str): 

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

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

3092 class, so no further processing is needed. 

3093 """ 

3094 

3095 params: Parameters 

3096 __slots__ = ("params",) 

3097 

3098 def __new__( 

3099 cls, 

3100 value, 

3101 encoding=DEFAULT_ENCODING, 

3102 /, 

3103 params: dict[str, Any] | None = None, 

3104 ): 

3105 value = to_unicode(value, encoding=encoding) 

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

3107 self.params = Parameters(params) 

3108 return self 

3109 

3110 def to_ical(self): 

3111 return self.encode(DEFAULT_ENCODING) 

3112 

3113 @classmethod 

3114 def from_ical(cls, ical): 

3115 return cls(ical) 

3116 

3117 

3118class vUnknown(vText): 

3119 """This is text but the VALUE parameter is unknown. 

3120 

3121 Since :rfc:`7265`, it is important to record if values are unknown. 

3122 For :rfc:`5545`, we could just assume TEXT. 

3123 """ 

3124 

3125 default_value: ClassVar[str] = "UNKNOWN" 

3126 

3127 @classmethod 

3128 def examples(cls) -> list[vUnknown]: 

3129 """Examples of vUnknown.""" 

3130 return [vUnknown("Some property text.")] 

3131 

3132 from icalendar.param import VALUE 

3133 

3134 

3135class TypesFactory(CaselessDict): 

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

3137 class. 

3138 

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

3140 both kinds. 

3141 """ 

3142 

3143 _instance: ClassVar[TypesFactory] = None 

3144 

3145 def instance() -> TypesFactory: 

3146 """Return a singleton instance of this class.""" 

3147 if TypesFactory._instance is None: 

3148 TypesFactory._instance = TypesFactory() 

3149 return TypesFactory._instance 

3150 

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

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

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

3154 self.all_types = ( 

3155 vBinary, 

3156 vBoolean, 

3157 vCalAddress, 

3158 vDDDLists, 

3159 vDDDTypes, 

3160 vDate, 

3161 vDatetime, 

3162 vDuration, 

3163 vFloat, 

3164 vFrequency, 

3165 vGeo, 

3166 vInline, 

3167 vInt, 

3168 vPeriod, 

3169 vRecur, 

3170 vText, 

3171 vTime, 

3172 vUTCOffset, 

3173 vUri, 

3174 vWeekday, 

3175 vCategory, 

3176 vUid, 

3177 vXmlReference, 

3178 vUnknown, 

3179 ) 

3180 self["binary"] = vBinary 

3181 self["boolean"] = vBoolean 

3182 self["cal-address"] = vCalAddress 

3183 self["date"] = vDDDTypes 

3184 self["date-time"] = vDDDTypes 

3185 self["duration"] = vDDDTypes 

3186 self["float"] = vFloat 

3187 self["integer"] = vInt 

3188 self["period"] = vPeriod 

3189 self["recur"] = vRecur 

3190 self["text"] = vText 

3191 self["time"] = vTime 

3192 self["uri"] = vUri 

3193 self["utc-offset"] = vUTCOffset 

3194 self["geo"] = vGeo 

3195 self["inline"] = vInline 

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

3197 self["categories"] = vCategory 

3198 self["unknown"] = vUnknown # RFC 7265 

3199 self["uid"] = vUid # RFC 9253 

3200 self["xml-reference"] = vXmlReference # RFC 9253 

3201 

3202 ################################################# 

3203 # Property types 

3204 

3205 # These are the default types 

3206 types_map = CaselessDict( 

3207 { 

3208 #################################### 

3209 # Property value types 

3210 # Calendar Properties 

3211 "calscale": "text", 

3212 "method": "text", 

3213 "prodid": "text", 

3214 "version": "text", 

3215 # Descriptive Component Properties 

3216 "attach": "uri", 

3217 "categories": "categories", 

3218 "class": "text", 

3219 "comment": "text", 

3220 "description": "text", 

3221 "geo": "geo", 

3222 "location": "text", 

3223 "percent-complete": "integer", 

3224 "priority": "integer", 

3225 "resources": "text", 

3226 "status": "text", 

3227 "summary": "text", 

3228 # RFC 9253 

3229 # link should be uri, xml-reference or uid 

3230 # uri is likely most helpful if people forget to set VALUE 

3231 "link": "uri", 

3232 "concept": "uri", 

3233 "refid": "text", 

3234 # Date and Time Component Properties 

3235 "completed": "date-time", 

3236 "dtend": "date-time", 

3237 "due": "date-time", 

3238 "dtstart": "date-time", 

3239 "duration": "duration", 

3240 "freebusy": "period", 

3241 "transp": "text", 

3242 "refresh-interval": "duration", # RFC 7986 

3243 # Time Zone Component Properties 

3244 "tzid": "text", 

3245 "tzname": "text", 

3246 "tzoffsetfrom": "utc-offset", 

3247 "tzoffsetto": "utc-offset", 

3248 "tzurl": "uri", 

3249 # Relationship Component Properties 

3250 "attendee": "cal-address", 

3251 "contact": "text", 

3252 "organizer": "cal-address", 

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

3254 "related-to": "text", 

3255 "url": "uri", 

3256 "conference": "uri", # RFC 7986 

3257 "source": "uri", 

3258 "uid": "text", 

3259 # Recurrence Component Properties 

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

3261 "exrule": "recur", 

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

3263 "rrule": "recur", 

3264 # Alarm Component Properties 

3265 "action": "text", 

3266 "repeat": "integer", 

3267 "trigger": "duration", 

3268 "acknowledged": "date-time", 

3269 # Change Management Component Properties 

3270 "created": "date-time", 

3271 "dtstamp": "date-time", 

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

3273 "sequence": "integer", 

3274 # Miscellaneous Component Properties 

3275 "request-status": "text", 

3276 #################################### 

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

3278 "altrep": "uri", 

3279 "cn": "text", 

3280 "cutype": "text", 

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

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

3283 "dir": "uri", 

3284 "encoding": "text", 

3285 "fmttype": "text", 

3286 "fbtype": "text", 

3287 "language": "text", 

3288 "member": "cal-address", 

3289 "partstat": "text", 

3290 "range": "text", 

3291 "related": "text", 

3292 "reltype": "text", 

3293 "role": "text", 

3294 "rsvp": "boolean", 

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

3296 "value": "text", 

3297 # rfc 9253 parameters 

3298 "label": "text", 

3299 "linkrel": "text", 

3300 "gap": "duration", 

3301 } 

3302 ) 

3303 

3304 def for_property(self, name, value_param: str | None = None) -> type: 

3305 """Returns the type class for a property or parameter. 

3306 

3307 Args: 

3308 name: Property or parameter name 

3309 value_param: Optional ``VALUE`` parameter, for example, 

3310 "DATE", "DATE-TIME", or other string. 

3311 

3312 Returns: 

3313 The appropriate value type class. 

3314 """ 

3315 # Special case: RDATE and EXDATE always use vDDDLists to support list values 

3316 # regardless of the VALUE parameter 

3317 if name.upper() in ("RDATE", "EXDATE"): # and value_param is None: 

3318 return self["date-time-list"] 

3319 

3320 # Only use VALUE parameter for known properties that support multiple value 

3321 # types (like DTSTART, DTEND, etc. which can be DATE or DATE-TIME) 

3322 # For unknown/custom properties, always use the default type from types_map 

3323 if value_param and name in self.types_map and value_param in self: 

3324 return self[value_param] 

3325 return self[self.types_map.get(name, "unknown")] 

3326 

3327 def to_ical(self, name, value): 

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

3329 encoded string. 

3330 """ 

3331 type_class = self.for_property(name) 

3332 return type_class(value).to_ical() 

3333 

3334 def from_ical(self, name, value): 

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

3336 encoded string to a primitive python type. 

3337 """ 

3338 type_class = self.for_property(name) 

3339 return type_class.from_ical(value) 

3340 

3341 

3342VPROPERTY: TypeAlias = Union[ 

3343 vBoolean, 

3344 vCalAddress, 

3345 vCategory, 

3346 vDDDLists, 

3347 vDDDTypes, 

3348 vDate, 

3349 vDatetime, 

3350 vDuration, 

3351 vFloat, 

3352 vFrequency, 

3353 vInt, 

3354 vMonth, 

3355 vPeriod, 

3356 vRecur, 

3357 vSkip, 

3358 vText, 

3359 vTime, 

3360 vUTCOffset, 

3361 vUri, 

3362 vWeekday, 

3363 vInline, 

3364 vBinary, 

3365 vGeo, 

3366 vUnknown, 

3367 vXmlReference, 

3368 vUid, 

3369] 

3370 

3371__all__ = [ 

3372 "DURATION_REGEX", 

3373 "VPROPERTY", 

3374 "WEEKDAY_RULE", 

3375 "TimeBase", 

3376 "TypesFactory", 

3377 "tzid_from_dt", 

3378 "tzid_from_tzinfo", 

3379 "vBinary", 

3380 "vBoolean", 

3381 "vCalAddress", 

3382 "vCategory", 

3383 "vDDDLists", 

3384 "vDDDTypes", 

3385 "vDate", 

3386 "vDatetime", 

3387 "vDuration", 

3388 "vFloat", 

3389 "vFrequency", 

3390 "vGeo", 

3391 "vInline", 

3392 "vInt", 

3393 "vMonth", 

3394 "vPeriod", 

3395 "vRecur", 

3396 "vSkip", 

3397 "vText", 

3398 "vTime", 

3399 "vUTCOffset", 

3400 "vUid", 

3401 "vUnknown", 

3402 "vUri", 

3403 "vWeekday", 

3404 "vXmlReference", 

3405]