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

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

1357 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 

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 ~error.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 ~error.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 ~error.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 ~error.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 ~error.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 ~error.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 ~error.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 ~error.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 ~error.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 @property 

914 def ical_value(self) -> list[str]: 

915 """The list of categories as strings.""" 

916 return [str(cat) for cat in self.cats] 

917 

918 

919class vAdr: 

920 """vCard ADR (Address) structured property per :rfc:`6350` `Section 6.3.1 <https://datatracker.ietf.org/doc/html/rfc6350.html#section-6.3.1>`_. 

921 

922 The ADR property represents a delivery address as a single text value. 

923 The structured type value consists of a sequence of seven address components. 

924 The component values must be specified in their corresponding position. 

925 

926 - post office box 

927 - extended address (e.g., apartment or suite number) 

928 - street address 

929 - locality (e.g., city) 

930 - region (e.g., state or province) 

931 - postal code 

932 - country name (full name) 

933 

934 When a component value is missing, the associated component separator MUST still be specified. 

935 

936 Semicolons are field separators and are NOT escaped. 

937 Commas and backslashes within field values ARE escaped per :rfc:`6350`. 

938 

939 Examples: 

940 .. code-block:: pycon 

941 

942 >>> from icalendar.prop import vAdr 

943 >>> adr = vAdr(("", "", "123 Main St", "Springfield", "IL", "62701", "USA")) 

944 >>> adr.to_ical() 

945 b';;123 Main St;Springfield;IL;62701;USA' 

946 >>> vAdr.from_ical(";;123 Main St;Springfield;IL;62701;USA") 

947 ('', '', '123 Main St', 'Springfield', 'IL', '62701', 'USA') 

948 """ 

949 

950 default_value: ClassVar[str] = "TEXT" 

951 params: Parameters 

952 

953 # 7 ADR fields per RFC 6350 

954 FIELDS = [ 

955 "po_box", 

956 "extended", 

957 "street", 

958 "locality", 

959 "region", 

960 "postal_code", 

961 "country", 

962 ] 

963 

964 def __init__( 

965 self, 

966 fields: tuple[str, ...] | list[str] | str, 

967 /, 

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

969 ): 

970 """Initialize ADR with seven fields or parse from vCard format string. 

971 

972 Args: 

973 fields: Either a tuple or list of seven strings, one per field, or a 

974 vCard format string with semicolon-separated fields 

975 params: Optional property parameters 

976 """ 

977 if isinstance(fields, str): 

978 fields = self.from_ical(fields) 

979 if len(fields) != 7: 

980 raise ValueError(f"ADR must have exactly 7 fields, got {len(fields)}") 

981 self.fields = tuple(str(f) for f in fields) 

982 self.params = Parameters(params) 

983 

984 def to_ical(self) -> bytes: 

985 """Generate vCard format with semicolon-separated fields.""" 

986 # Each field is vText (handles comma/backslash escaping) 

987 # but we join with unescaped semicolons (field separators) 

988 parts = [vText(f).to_ical().decode(DEFAULT_ENCODING) for f in self.fields] 

989 return ";".join(parts).encode(DEFAULT_ENCODING) 

990 

991 @staticmethod 

992 def from_ical(ical: str | bytes) -> tuple[str, ...]: 

993 """Parse vCard ADR format into a tuple of seven fields. 

994 

995 Args: 

996 ical: vCard format string with semicolon-separated fields 

997 

998 Returns: 

999 Tuple of seven field values, or the empty string if the field is empty. 

1000 """ 

1001 from icalendar.parser import split_on_unescaped_semicolon 

1002 

1003 ical = to_unicode(ical) 

1004 fields = split_on_unescaped_semicolon(ical) 

1005 if len(fields) != 7: 

1006 raise ValueError( 

1007 f"ADR must have exactly 7 fields, got {len(fields)}: {ical}" 

1008 ) 

1009 return tuple(fields) 

1010 

1011 def __eq__(self, other): 

1012 """self == other""" 

1013 return isinstance(other, vAdr) and self.fields == other.fields 

1014 

1015 def __repr__(self): 

1016 """String representation.""" 

1017 return f"{self.__class__.__name__}({self.fields}, params={self.params})" 

1018 

1019 from icalendar.param import VALUE 

1020 

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

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

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

1024 result.extend(self.fields) 

1025 return result 

1026 

1027 @classmethod 

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

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

1030 

1031 Args: 

1032 jcal_property: The jCal property to parse. 

1033 

1034 Raises: 

1035 ~error.JCalParsingError: If the provided jCal is invalid. 

1036 """ 

1037 JCalParsingError.validate_property(jcal_property, cls) 

1038 if len(jcal_property) != 10: # name, params, value_type, 7 fields 

1039 raise JCalParsingError( 

1040 f"ADR must have 10 elements (name, params, value_type, 7 fields), " 

1041 f"got {len(jcal_property)}" 

1042 ) 

1043 for i, field in enumerate(jcal_property[3:], start=3): 

1044 JCalParsingError.validate_value_type(field, str, cls, i) 

1045 return cls( 

1046 tuple(jcal_property[3:]), 

1047 Parameters.from_jcal_property(jcal_property), 

1048 ) 

1049 

1050 @classmethod 

1051 def examples(cls) -> list[vAdr]: 

1052 """Examples of vAdr.""" 

1053 return [cls(("", "", "123 Main St", "Springfield", "IL", "62701", "USA"))] 

1054 

1055 

1056class vN: 

1057 """vCard N (Name) structured property per :rfc:`6350` `Section 6.2.2 <https://datatracker.ietf.org/doc/html/rfc6350.html#section-6.2.2>`_. 

1058 

1059 The N property represents a person's name. 

1060 It consists of a single structured text value. 

1061 Each component in the structure may have multiple values, separated by commas. 

1062 

1063 The structured property value corresponds, in sequence, to the following fields: 

1064 

1065 - family names (also known as surnames) 

1066 - given names 

1067 - additional names 

1068 - honorific prefixes 

1069 - honorific suffixes 

1070 

1071 Semicolons are field separators and are NOT escaped. 

1072 Commas and backslashes within field values ARE escaped per :rfc:`6350`. 

1073 

1074 Examples: 

1075 .. code-block:: pycon 

1076 

1077 >>> from icalendar.prop import vN 

1078 >>> n = vN(("Doe", "John", "M.", "Dr.", "Jr.,M.D.,A.C.P.")) 

1079 >>> n.to_ical() 

1080 b'Doe;John;M.;Dr.;Jr.\\\\,M.D.\\\\,A.C.P.' 

1081 >>> vN.from_ical(r"Doe;John;M.;Dr.;Jr.\,M.D.\,A.C.P.") 

1082 ('Doe', 'John', 'M.', 'Dr.', 'Jr.,M.D.,A.C.P.') 

1083 """ 

1084 

1085 default_value: ClassVar[str] = "TEXT" 

1086 params: Parameters 

1087 

1088 # 5 N fields per RFC 6350 

1089 FIELDS = ["family", "given", "additional", "prefix", "suffix"] 

1090 

1091 def __init__( 

1092 self, 

1093 fields: tuple[str, ...] | list[str] | str, 

1094 /, 

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

1096 ): 

1097 """Initialize N with five fields or parse from vCard format string. 

1098 

1099 Args: 

1100 fields: Either a tuple or list of five strings, one per field, or a 

1101 vCard format string with semicolon-separated fields 

1102 params: Optional property parameters 

1103 """ 

1104 if isinstance(fields, str): 

1105 fields = self.from_ical(fields) 

1106 if len(fields) != 5: 

1107 raise ValueError(f"N must have exactly 5 fields, got {len(fields)}") 

1108 self.fields = tuple(str(f) for f in fields) 

1109 self.params = Parameters(params) 

1110 

1111 def to_ical(self) -> bytes: 

1112 """Generate vCard format with semicolon-separated fields.""" 

1113 parts = [vText(f).to_ical().decode(DEFAULT_ENCODING) for f in self.fields] 

1114 return ";".join(parts).encode(DEFAULT_ENCODING) 

1115 

1116 @staticmethod 

1117 def from_ical(ical: str | bytes) -> tuple[str, ...]: 

1118 """Parse vCard N format into a tuple of five fields. 

1119 

1120 Args: 

1121 ical: vCard format string with semicolon-separated fields 

1122 

1123 Returns: 

1124 Tuple of five field values, or the empty string if the field is empty 

1125 """ 

1126 from icalendar.parser import split_on_unescaped_semicolon 

1127 

1128 ical = to_unicode(ical) 

1129 fields = split_on_unescaped_semicolon(ical) 

1130 if len(fields) != 5: 

1131 raise ValueError(f"N must have exactly 5 fields, got {len(fields)}: {ical}") 

1132 return tuple(fields) 

1133 

1134 def __eq__(self, other): 

1135 """self == other""" 

1136 return isinstance(other, vN) and self.fields == other.fields 

1137 

1138 def __repr__(self): 

1139 """String representation.""" 

1140 return f"{self.__class__.__name__}({self.fields}, params={self.params})" 

1141 

1142 from icalendar.param import VALUE 

1143 

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

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

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

1147 result.extend(self.fields) 

1148 return result 

1149 

1150 @classmethod 

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

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

1153 

1154 Args: 

1155 jcal_property: The jCal property to parse. 

1156 

1157 Raises: 

1158 ~error.JCalParsingError: If the provided jCal is invalid. 

1159 """ 

1160 JCalParsingError.validate_property(jcal_property, cls) 

1161 if len(jcal_property) != 8: # name, params, value_type, 5 fields 

1162 raise JCalParsingError( 

1163 f"N must have 8 elements (name, params, value_type, 5 fields), " 

1164 f"got {len(jcal_property)}" 

1165 ) 

1166 for i, field in enumerate(jcal_property[3:], start=3): 

1167 JCalParsingError.validate_value_type(field, str, cls, i) 

1168 return cls( 

1169 tuple(jcal_property[3:]), 

1170 Parameters.from_jcal_property(jcal_property), 

1171 ) 

1172 

1173 @classmethod 

1174 def examples(cls) -> list[vN]: 

1175 """Examples of vN.""" 

1176 return [cls(("Doe", "John", "M.", "Dr.", "Jr."))] 

1177 

1178 

1179class vOrg: 

1180 r"""vCard ORG (Organization) structured property per :rfc:`6350` `Section 6.6.4 <https://datatracker.ietf.org/doc/html/rfc6350.html#section-6.6.4>`_. 

1181 

1182 The ORG property specifies the organizational name and units associated with the vCard. 

1183 

1184 Its value is a structured type consisting of components separated by semicolons. 

1185 The components are the organization name, followed by zero or more levels of organizational unit names: 

1186 

1187 .. code-block:: text 

1188 

1189 organization-name; organizational-unit-1; organizational-unit-2; ... 

1190 

1191 Semicolons are field separators and are NOT escaped. 

1192 Commas and backslashes within field values ARE escaped per :rfc:`6350`. 

1193 

1194 Examples: 

1195 A property value consisting of an organizational name, 

1196 organizational unit #1 name, and organizational unit #2 name. 

1197 

1198 .. code-block:: text 

1199 

1200 ORG:ABC\, Inc.;North American Division;Marketing 

1201 

1202 The same example in icalendar. 

1203 

1204 .. code-block:: pycon 

1205 

1206 >>> from icalendar.prop import vOrg 

1207 >>> org = vOrg(("ABC, Inc.", "North American Division", "Marketing")) 

1208 >>> org.to_ical() 

1209 b'ABC\\, Inc.;North American Division;Marketing' 

1210 >>> vOrg.from_ical(r"ABC\, Inc.;North American Division;Marketing") 

1211 ('ABC, Inc.', 'North American Division', 'Marketing') 

1212 """ 

1213 

1214 default_value: ClassVar[str] = "TEXT" 

1215 params: Parameters 

1216 

1217 def __init__( 

1218 self, 

1219 fields: tuple[str, ...] | list[str] | str, 

1220 /, 

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

1222 ): 

1223 """Initialize ORG with variable fields or parse from vCard format string. 

1224 

1225 Args: 

1226 fields: Either a tuple or list of one or more strings, or a 

1227 vCard format string with semicolon-separated fields 

1228 params: Optional property parameters 

1229 """ 

1230 if isinstance(fields, str): 

1231 fields = self.from_ical(fields) 

1232 if len(fields) < 1: 

1233 raise ValueError("ORG must have at least 1 field (organization name)") 

1234 self.fields = tuple(str(f) for f in fields) 

1235 self.params = Parameters(params) 

1236 

1237 def to_ical(self) -> bytes: 

1238 """Generate vCard format with semicolon-separated fields.""" 

1239 parts = [vText(f).to_ical().decode(DEFAULT_ENCODING) for f in self.fields] 

1240 return ";".join(parts).encode(DEFAULT_ENCODING) 

1241 

1242 @staticmethod 

1243 def from_ical(ical: str | bytes) -> tuple[str, ...]: 

1244 """Parse vCard ORG format into a tuple of fields. 

1245 

1246 Args: 

1247 ical: vCard format string with semicolon-separated fields 

1248 

1249 Returns: 

1250 Tuple of field values with one or more fields 

1251 """ 

1252 from icalendar.parser import split_on_unescaped_semicolon 

1253 

1254 ical = to_unicode(ical) 

1255 fields = split_on_unescaped_semicolon(ical) 

1256 if len(fields) < 1: 

1257 raise ValueError(f"ORG must have at least 1 field: {ical}") 

1258 return tuple(fields) 

1259 

1260 def __eq__(self, other): 

1261 """self == other""" 

1262 return isinstance(other, vOrg) and self.fields == other.fields 

1263 

1264 def __repr__(self): 

1265 """String representation.""" 

1266 return f"{self.__class__.__name__}({self.fields}, params={self.params})" 

1267 

1268 from icalendar.param import VALUE 

1269 

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

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

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

1273 result.extend(self.fields) 

1274 return result 

1275 

1276 @classmethod 

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

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

1279 

1280 Args: 

1281 jcal_property: The jCal property to parse. 

1282 

1283 Raises: 

1284 ~error.JCalParsingError: If the provided jCal is invalid. 

1285 """ 

1286 JCalParsingError.validate_property(jcal_property, cls) 

1287 if len(jcal_property) < 4: # name, params, value_type, at least 1 field 

1288 raise JCalParsingError( 

1289 f"ORG must have at least 4 elements (name, params, value_type, org name), " 

1290 f"got {len(jcal_property)}" 

1291 ) 

1292 for i, field in enumerate(jcal_property[3:], start=3): 

1293 JCalParsingError.validate_value_type(field, str, cls, i) 

1294 return cls( 

1295 tuple(jcal_property[3:]), 

1296 Parameters.from_jcal_property(jcal_property), 

1297 ) 

1298 

1299 @classmethod 

1300 def examples(cls) -> list[vOrg]: 

1301 """Examples of vOrg.""" 

1302 return [cls(("ABC Inc.", "North American Division", "Marketing"))] 

1303 

1304 

1305class TimeBase: 

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

1307 

1308 default_value: ClassVar[str] 

1309 params: Parameters 

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

1311 

1312 def __eq__(self, other): 

1313 """self == other""" 

1314 if isinstance(other, date): 

1315 return self.dt == other 

1316 if isinstance(other, TimeBase): 

1317 default = object() 

1318 for key in ( 

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

1320 ) - self.ignore_for_equality: 

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

1322 key, default 

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

1324 return False 

1325 return self.dt == other.dt 

1326 if isinstance(other, vDDDLists): 

1327 return other == self 

1328 return False 

1329 

1330 def __hash__(self): 

1331 return hash(self.dt) 

1332 

1333 from icalendar.param import RANGE, RELATED, TZID 

1334 

1335 def __repr__(self): 

1336 """String representation.""" 

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

1338 

1339 

1340DT_TYPE: TypeAlias = Union[ 

1341 datetime, 

1342 date, 

1343 timedelta, 

1344 time, 

1345 Tuple[datetime, datetime], 

1346 Tuple[datetime, timedelta], 

1347] 

1348 

1349 

1350class vDDDTypes(TimeBase): 

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

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

1353 So this is practical. 

1354 """ 

1355 

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

1357 params: Parameters 

1358 dt: DT_TYPE 

1359 

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

1361 if params is None: 

1362 params = {} 

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

1364 raise TypeError( 

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

1366 ) 

1367 self.dt = dt 

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

1369 if is_date(dt): 

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

1371 elif isinstance(dt, time): 

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

1373 elif isinstance(dt, tuple): 

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

1375 self.params = Parameters(params) 

1376 self.params.update_tzid_from(dt) 

1377 

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

1379 """Convert to a property type. 

1380 

1381 Raises: 

1382 ValueError: If the type is unknown. 

1383 """ 

1384 dt = self.dt 

1385 if isinstance(dt, datetime): 

1386 result = vDatetime(dt) 

1387 elif isinstance(dt, date): 

1388 result = vDate(dt) 

1389 elif isinstance(dt, timedelta): 

1390 result = vDuration(dt) 

1391 elif isinstance(dt, time): 

1392 result = vTime(dt) 

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

1394 result = vPeriod(dt) 

1395 else: 

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

1397 result.params = self.params 

1398 return result 

1399 

1400 def to_ical(self) -> str: 

1401 """Return the ical representation.""" 

1402 return self.to_property_type().to_ical() 

1403 

1404 @classmethod 

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

1406 if isinstance(ical, cls): 

1407 return ical.dt 

1408 u = ical.upper() 

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

1410 return vDuration.from_ical(ical) 

1411 if "/" in u: 

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

1413 

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

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

1416 if len(ical) == 8: 

1417 if timezone: 

1418 tzinfo = tzp.timezone(timezone) 

1419 if tzinfo is not None: 

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

1421 return vDate.from_ical(ical) 

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

1423 return vTime.from_ical(ical) 

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

1425 

1426 @property 

1427 def td(self) -> timedelta: 

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

1429 

1430 This class is used to replace different time components. 

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

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

1433 This property allows interoperability. 

1434 """ 

1435 return self.dt 

1436 

1437 @property 

1438 def dts(self) -> list: 

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

1440 return [self] 

1441 

1442 @classmethod 

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

1444 """Examples of vDDDTypes.""" 

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

1446 

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

1448 """Determine the VALUE parameter.""" 

1449 return self.to_property_type().VALUE 

1450 

1451 from icalendar.param import VALUE 

1452 

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

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

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

1456 

1457 @classmethod 

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

1459 """Parse a jCal value. 

1460 

1461 Raises: 

1462 ~error.JCalParsingError: If the value can't be parsed as either a date, time, 

1463 date-time, duration, or period. 

1464 """ 

1465 if isinstance(jcal, list): 

1466 return vPeriod.parse_jcal_value(jcal) 

1467 JCalParsingError.validate_value_type(jcal, str, cls) 

1468 if "/" in jcal: 

1469 return vPeriod.parse_jcal_value(jcal) 

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

1471 try: 

1472 return jcal_type.parse_jcal_value(jcal) 

1473 except JCalParsingError: # noqa: PERF203 

1474 pass 

1475 raise JCalParsingError( 

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

1477 ) 

1478 

1479 @classmethod 

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

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

1482 

1483 Args: 

1484 jcal_property: The jCal property to parse. 

1485 

1486 Raises: 

1487 ~error.JCalParsingError: If the provided jCal is invalid. 

1488 """ 

1489 JCalParsingError.validate_property(jcal_property, cls) 

1490 with JCalParsingError.reraise_with_path_added(3): 

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

1492 params = Parameters.from_jcal_property(jcal_property) 

1493 if params.tzid: 

1494 if isinstance(dt, tuple): 

1495 # period 

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

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

1498 dt = (start, end) 

1499 else: 

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

1501 return cls( 

1502 dt, 

1503 params=params, 

1504 ) 

1505 

1506 

1507class vDate(TimeBase): 

1508 """Date 

1509 

1510 Value Name: 

1511 DATE 

1512 

1513 Purpose: 

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

1515 calendar date. 

1516 

1517 Format Definition: 

1518 This value type is defined by the following notation: 

1519 

1520 .. code-block:: text 

1521 

1522 date = date-value 

1523 

1524 date-value = date-fullyear date-month date-mday 

1525 date-fullyear = 4DIGIT 

1526 date-month = 2DIGIT ;01-12 

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

1528 ;based on month/year 

1529 

1530 Description: 

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

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

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

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

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

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

1537 year, month, and day component text. 

1538 

1539 Example: 

1540 The following represents July 14, 1997: 

1541 

1542 .. code-block:: text 

1543 

1544 19970714 

1545 

1546 .. code-block:: pycon 

1547 

1548 >>> from icalendar.prop import vDate 

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

1550 >>> date.year 

1551 1997 

1552 >>> date.month 

1553 7 

1554 >>> date.day 

1555 14 

1556 """ 

1557 

1558 default_value: ClassVar[str] = "DATE" 

1559 params: Parameters 

1560 

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

1562 if not isinstance(dt, date): 

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

1564 self.dt = dt 

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

1566 

1567 def to_ical(self): 

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

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

1570 

1571 @staticmethod 

1572 def from_ical(ical): 

1573 try: 

1574 timetuple = ( 

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

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

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

1578 ) 

1579 return date(*timetuple) 

1580 except Exception as e: 

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

1582 

1583 @classmethod 

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

1585 """Examples of vDate.""" 

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

1587 

1588 from icalendar.param import VALUE 

1589 

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

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

1592 return [ 

1593 name, 

1594 self.params.to_jcal(), 

1595 self.VALUE.lower(), 

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

1597 ] 

1598 

1599 @classmethod 

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

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

1602 

1603 Raises: 

1604 ~error.JCalParsingError: If it can't parse a date. 

1605 """ 

1606 JCalParsingError.validate_value_type(jcal, str, cls) 

1607 try: 

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

1609 except ValueError as e: 

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

1611 

1612 @classmethod 

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

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

1615 

1616 Args: 

1617 jcal_property: The jCal property to parse. 

1618 

1619 Raises: 

1620 ~error.JCalParsingError: If the provided jCal is invalid. 

1621 """ 

1622 JCalParsingError.validate_property(jcal_property, cls) 

1623 with JCalParsingError.reraise_with_path_added(3): 

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

1625 return cls( 

1626 value, 

1627 params=Parameters.from_jcal_property(jcal_property), 

1628 ) 

1629 

1630 

1631class vDatetime(TimeBase): 

1632 """Date-Time 

1633 

1634 Value Name: 

1635 DATE-TIME 

1636 

1637 Purpose: 

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

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

1640 the ISO.8601.2004 complete representation. 

1641 

1642 Format Definition: 

1643 This value type is defined by the following notation: 

1644 

1645 .. code-block:: text 

1646 

1647 date-time = date "T" time 

1648 

1649 date = date-value 

1650 date-value = date-fullyear date-month date-mday 

1651 date-fullyear = 4DIGIT 

1652 date-month = 2DIGIT ;01-12 

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

1654 ;based on month/year 

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

1656 time-hour = 2DIGIT ;00-23 

1657 time-minute = 2DIGIT ;00-59 

1658 time-second = 2DIGIT ;00-60 

1659 time-utc = "Z" 

1660 

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

1662 

1663 .. code-block:: text 

1664 

1665 YYYYMMDDTHHMMSS 

1666 

1667 Description: 

1668 vDatetime is timezone aware and uses a timezone library. 

1669 When a vDatetime object is created from an 

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

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

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

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

1674 DATE-TIME components in the icalendar standard. 

1675 

1676 Example: 

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

1678 

1679 .. code-block:: pycon 

1680 

1681 >>> from icalendar import vDatetime 

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

1683 >>> datetime.tzname() 

1684 >>> datetime.year 

1685 2021 

1686 >>> datetime.minute 

1687 15 

1688 

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

1690 

1691 .. code-block:: pycon 

1692 

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

1694 >>> datetime.tzname() 

1695 'EST' 

1696 

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

1698 

1699 .. code-block:: pycon 

1700 

1701 >>> from zoneinfo import ZoneInfo 

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

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

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

1705 """ 

1706 

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

1708 params: Parameters 

1709 

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

1711 self.dt = dt 

1712 self.params = Parameters(params) 

1713 self.params.update_tzid_from(dt) 

1714 

1715 def to_ical(self): 

1716 dt = self.dt 

1717 

1718 s = ( 

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

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

1721 ) 

1722 if self.is_utc(): 

1723 s += "Z" 

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

1725 

1726 @staticmethod 

1727 def from_ical(ical, timezone=None): 

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

1729 tzinfo = None 

1730 if isinstance(timezone, str): 

1731 tzinfo = tzp.timezone(timezone) 

1732 elif timezone is not None: 

1733 tzinfo = timezone 

1734 

1735 try: 

1736 timetuple = ( 

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

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

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

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

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

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

1743 ) 

1744 if tzinfo: 

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

1746 if not ical[15:]: 

1747 return datetime(*timetuple) # noqa: DTZ001 

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

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

1750 except Exception as e: 

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

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

1753 

1754 @classmethod 

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

1756 """Examples of vDatetime.""" 

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

1758 

1759 from icalendar.param import VALUE 

1760 

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

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

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

1764 if self.is_utc(): 

1765 value += "Z" 

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

1767 

1768 def is_utc(self) -> bool: 

1769 """Whether this datetime is UTC.""" 

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

1771 

1772 @classmethod 

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

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

1775 

1776 Raises: 

1777 ~error.JCalParsingError: If it can't parse a date-time value. 

1778 """ 

1779 JCalParsingError.validate_value_type(jcal, str, cls) 

1780 utc = jcal.endswith("Z") 

1781 if utc: 

1782 jcal = jcal[:-1] 

1783 try: 

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

1785 except ValueError as e: 

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

1787 if utc: 

1788 return tzp.localize_utc(dt) 

1789 return dt 

1790 

1791 @classmethod 

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

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

1794 

1795 Args: 

1796 jcal_property: The jCal property to parse. 

1797 

1798 Raises: 

1799 ~error.JCalParsingError: If the provided jCal is invalid. 

1800 """ 

1801 JCalParsingError.validate_property(jcal_property, cls) 

1802 params = Parameters.from_jcal_property(jcal_property) 

1803 with JCalParsingError.reraise_with_path_added(3): 

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

1805 if params.tzid: 

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

1807 return cls( 

1808 dt, 

1809 params=params, 

1810 ) 

1811 

1812 

1813class vDuration(TimeBase): 

1814 """Duration 

1815 

1816 Value Name: 

1817 DURATION 

1818 

1819 Purpose: 

1820 This value type is used to identify properties that contain 

1821 a duration of time. 

1822 

1823 Format Definition: 

1824 This value type is defined by the following notation: 

1825 

1826 .. code-block:: text 

1827 

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

1829 

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

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

1832 dur-week = 1*DIGIT "W" 

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

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

1835 dur-second = 1*DIGIT "S" 

1836 dur-day = 1*DIGIT "D" 

1837 

1838 Description: 

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

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

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

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

1843 represent nominal durations (weeks and days) and accurate 

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

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

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

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

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

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

1850 computation of the exact duration requires the subtraction or 

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

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

1853 When computing an exact duration, the greatest order time 

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

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

1856 minutes, and number of seconds. 

1857 

1858 Example: 

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

1860 

1861 .. code-block:: text 

1862 

1863 P15DT5H0M20S 

1864 

1865 A duration of 7 weeks would be: 

1866 

1867 .. code-block:: text 

1868 

1869 P7W 

1870 

1871 .. code-block:: pycon 

1872 

1873 >>> from icalendar.prop import vDuration 

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

1875 >>> duration 

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

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

1878 >>> duration 

1879 datetime.timedelta(days=49) 

1880 """ 

1881 

1882 default_value: ClassVar[str] = "DURATION" 

1883 params: Parameters 

1884 

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

1886 if isinstance(td, str): 

1887 td = vDuration.from_ical(td) 

1888 if not isinstance(td, timedelta): 

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

1890 self.td = td 

1891 self.params = Parameters(params) 

1892 

1893 def to_ical(self): 

1894 sign = "" 

1895 td = self.td 

1896 if td.days < 0: 

1897 sign = "-" 

1898 td = -td 

1899 timepart = "" 

1900 if td.seconds: 

1901 timepart = "T" 

1902 hours = td.seconds // 3600 

1903 minutes = td.seconds % 3600 // 60 

1904 seconds = td.seconds % 60 

1905 if hours: 

1906 timepart += f"{hours}H" 

1907 if minutes or (hours and seconds): 

1908 timepart += f"{minutes}M" 

1909 if seconds: 

1910 timepart += f"{seconds}S" 

1911 if td.days == 0 and timepart: 

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

1913 return ( 

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

1915 + b"P" 

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

1917 + b"D" 

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

1919 ) 

1920 

1921 @staticmethod 

1922 def from_ical(ical): 

1923 match = DURATION_REGEX.match(ical) 

1924 if not match: 

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

1926 

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

1928 value = timedelta( 

1929 weeks=int(weeks or 0), 

1930 days=int(days or 0), 

1931 hours=int(hours or 0), 

1932 minutes=int(minutes or 0), 

1933 seconds=int(seconds or 0), 

1934 ) 

1935 

1936 if sign == "-": 

1937 value = -value 

1938 

1939 return value 

1940 

1941 @property 

1942 def dt(self) -> timedelta: 

1943 """The time delta for compatibility.""" 

1944 return self.td 

1945 

1946 @classmethod 

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

1948 """Examples of vDuration.""" 

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

1950 

1951 from icalendar.param import VALUE 

1952 

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

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

1955 return [ 

1956 name, 

1957 self.params.to_jcal(), 

1958 self.VALUE.lower(), 

1959 self.to_ical().decode(), 

1960 ] 

1961 

1962 @classmethod 

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

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

1965 

1966 Raises: 

1967 ~error.JCalParsingError: If it can't parse a duration.""" 

1968 JCalParsingError.validate_value_type(jcal, str, cls) 

1969 try: 

1970 return cls.from_ical(jcal) 

1971 except ValueError as e: 

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

1973 

1974 @classmethod 

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

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

1977 

1978 Args: 

1979 jcal_property: The jCal property to parse. 

1980 

1981 Raises: 

1982 ~error.JCalParsingError: If the provided jCal is invalid. 

1983 """ 

1984 JCalParsingError.validate_property(jcal_property, cls) 

1985 with JCalParsingError.reraise_with_path_added(3): 

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

1987 return cls( 

1988 duration, 

1989 Parameters.from_jcal_property(jcal_property), 

1990 ) 

1991 

1992 

1993class vPeriod(TimeBase): 

1994 """Period of Time 

1995 

1996 Value Name: 

1997 PERIOD 

1998 

1999 Purpose: 

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

2001 precise period of time. 

2002 

2003 Format Definition: 

2004 This value type is defined by the following notation: 

2005 

2006 .. code-block:: text 

2007 

2008 period = period-explicit / period-start 

2009 

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

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

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

2013 ; be before the end. 

2014 

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

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

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

2018 ; of time. 

2019 

2020 Description: 

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

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

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

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

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

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

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

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

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

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

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

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

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

2034 

2035 Example: 

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

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

2038 

2039 .. code-block:: text 

2040 

2041 19970101T180000Z/19970102T070000Z 

2042 

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

2044 and 30 minutes would be: 

2045 

2046 .. code-block:: text 

2047 

2048 19970101T180000Z/PT5H30M 

2049 

2050 .. code-block:: pycon 

2051 

2052 >>> from icalendar.prop import vPeriod 

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

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

2055 """ 

2056 

2057 default_value: ClassVar[str] = "PERIOD" 

2058 params: Parameters 

2059 by_duration: bool 

2060 start: datetime 

2061 end: datetime 

2062 duration: timedelta 

2063 

2064 def __init__( 

2065 self, 

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

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

2068 ): 

2069 start, end_or_duration = per 

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

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

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

2073 raise TypeError( 

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

2075 ) 

2076 by_duration = isinstance(end_or_duration, timedelta) 

2077 if by_duration: 

2078 duration = end_or_duration 

2079 end = normalize_pytz(start + duration) 

2080 else: 

2081 end = end_or_duration 

2082 duration = normalize_pytz(end - start) 

2083 if start > end: 

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

2085 

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

2087 # set the timezone identifier 

2088 # does not support different timezones for start and end 

2089 self.params.update_tzid_from(start) 

2090 

2091 self.start = start 

2092 self.end = end 

2093 self.by_duration = by_duration 

2094 self.duration = duration 

2095 

2096 def overlaps(self, other): 

2097 if self.start > other.start: 

2098 return other.overlaps(self) 

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

2100 

2101 def to_ical(self): 

2102 if self.by_duration: 

2103 return ( 

2104 vDatetime(self.start).to_ical() 

2105 + b"/" 

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

2107 ) 

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

2109 

2110 @staticmethod 

2111 def from_ical(ical, timezone=None): 

2112 try: 

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

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

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

2116 except Exception as e: 

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

2118 return (start, end_or_duration) 

2119 

2120 def __repr__(self): 

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

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

2123 

2124 @property 

2125 def dt(self): 

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

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

2128 

2129 from icalendar.param import FBTYPE 

2130 

2131 @classmethod 

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

2133 """Examples of vPeriod.""" 

2134 return [ 

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

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

2137 ] 

2138 

2139 from icalendar.param import VALUE 

2140 

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

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

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

2144 if self.by_duration: 

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

2146 else: 

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

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

2149 

2150 @classmethod 

2151 def parse_jcal_value( 

2152 cls, jcal: str | list 

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

2154 """Parse a jCal value. 

2155 

2156 Raises: 

2157 ~error.JCalParsingError: If the period is not a list with exactly two items, 

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

2159 """ 

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

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

2162 jcal = jcal.split("/") 

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

2164 raise JCalParsingError( 

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

2166 ) 

2167 with JCalParsingError.reraise_with_path_added(0): 

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

2169 with JCalParsingError.reraise_with_path_added(1): 

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

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

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

2173 else: 

2174 try: 

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

2176 except JCalParsingError as e: 

2177 raise JCalParsingError( 

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

2179 cls, 

2180 value=jcal[1], 

2181 ) from e 

2182 return start, end_or_duration 

2183 

2184 @classmethod 

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

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

2187 

2188 Args: 

2189 jcal_property: The jCal property to parse. 

2190 

2191 Raises: 

2192 ~error.JCalParsingError: If the provided jCal is invalid. 

2193 """ 

2194 JCalParsingError.validate_property(jcal_property, cls) 

2195 with JCalParsingError.reraise_with_path_added(3): 

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

2197 params = Parameters.from_jcal_property(jcal_property) 

2198 tzid = params.tzid 

2199 

2200 if tzid: 

2201 start = tzp.localize(start, tzid) 

2202 if is_datetime(end_or_duration): 

2203 end_or_duration = tzp.localize(end_or_duration, tzid) 

2204 

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

2206 

2207 

2208class vWeekday(str): 

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

2210 

2211 .. code-block:: pycon 

2212 

2213 >>> from icalendar import vWeekday 

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

2215 'MO' 

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

2217 2 

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

2219 'FR' 

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

2221 -1 

2222 

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

2224 

2225 .. code-block:: text 

2226 

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

2228 plus = "+" 

2229 minus = "-" 

2230 ordwk = 1*2DIGIT ;1 to 53 

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

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

2233 ;FRIDAY, and SATURDAY days of the week. 

2234 

2235 """ 

2236 

2237 params: Parameters 

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

2239 

2240 week_days = CaselessDict( 

2241 { 

2242 "SU": 0, 

2243 "MO": 1, 

2244 "TU": 2, 

2245 "WE": 3, 

2246 "TH": 4, 

2247 "FR": 5, 

2248 "SA": 6, 

2249 } 

2250 ) 

2251 

2252 def __new__( 

2253 cls, 

2254 value, 

2255 encoding=DEFAULT_ENCODING, 

2256 /, 

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

2258 ): 

2259 value = to_unicode(value, encoding=encoding) 

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

2261 match = WEEKDAY_RULE.match(self) 

2262 if match is None: 

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

2264 match = match.groupdict() 

2265 sign = match["signal"] 

2266 weekday = match["weekday"] 

2267 relative = match["relative"] 

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

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

2270 self.weekday = weekday or None 

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

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

2273 self.relative *= -1 

2274 self.params = Parameters(params) 

2275 return self 

2276 

2277 def to_ical(self): 

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

2279 

2280 @classmethod 

2281 def from_ical(cls, ical): 

2282 try: 

2283 return cls(ical.upper()) 

2284 except Exception as e: 

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

2286 

2287 @classmethod 

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

2289 """Parse a jCal value for vWeekday. 

2290 

2291 Raises: 

2292 ~error.JCalParsingError: If the value is not a valid weekday. 

2293 """ 

2294 JCalParsingError.validate_value_type(value, str, cls) 

2295 try: 

2296 return cls(value) 

2297 except ValueError as e: 

2298 raise JCalParsingError( 

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

2300 ) from e 

2301 

2302 

2303class vFrequency(str): 

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

2305 

2306 params: Parameters 

2307 __slots__ = ("params",) 

2308 

2309 frequencies = CaselessDict( 

2310 { 

2311 "SECONDLY": "SECONDLY", 

2312 "MINUTELY": "MINUTELY", 

2313 "HOURLY": "HOURLY", 

2314 "DAILY": "DAILY", 

2315 "WEEKLY": "WEEKLY", 

2316 "MONTHLY": "MONTHLY", 

2317 "YEARLY": "YEARLY", 

2318 } 

2319 ) 

2320 

2321 def __new__( 

2322 cls, 

2323 value, 

2324 encoding=DEFAULT_ENCODING, 

2325 /, 

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

2327 ): 

2328 value = to_unicode(value, encoding=encoding) 

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

2330 if self not in vFrequency.frequencies: 

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

2332 self.params = Parameters(params) 

2333 return self 

2334 

2335 def to_ical(self): 

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

2337 

2338 @classmethod 

2339 def from_ical(cls, ical): 

2340 try: 

2341 return cls(ical.upper()) 

2342 except Exception as e: 

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

2344 

2345 @classmethod 

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

2347 """Parse a jCal value for vFrequency. 

2348 

2349 Raises: 

2350 ~error.JCalParsingError: If the value is not a valid frequency. 

2351 """ 

2352 JCalParsingError.validate_value_type(value, str, cls) 

2353 try: 

2354 return cls(value) 

2355 except ValueError as e: 

2356 raise JCalParsingError( 

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

2358 ) from e 

2359 

2360 

2361class vMonth(int): 

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

2363 

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

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

2366 

2367 .. code-block:: pycon 

2368 

2369 >>> from icalendar import vMonth 

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

2371 vMonth('1') 

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

2373 vMonth('5L') 

2374 >>> vMonth(1).leap 

2375 False 

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

2377 True 

2378 

2379 Definition from RFC: 

2380 

2381 .. code-block:: text 

2382 

2383 type-bymonth = element bymonth { 

2384 xsd:positiveInteger | 

2385 xsd:string 

2386 } 

2387 """ 

2388 

2389 params: Parameters 

2390 

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

2392 if isinstance(month, vMonth): 

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

2394 if isinstance(month, str): 

2395 if month.isdigit(): 

2396 month_index = int(month) 

2397 leap = False 

2398 else: 

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

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

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

2402 leap = True 

2403 else: 

2404 leap = False 

2405 month_index = int(month) 

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

2407 self.leap = leap 

2408 self.params = Parameters(params) 

2409 return self 

2410 

2411 def to_ical(self) -> bytes: 

2412 """The ical representation.""" 

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

2414 

2415 @classmethod 

2416 def from_ical(cls, ical: str): 

2417 return cls(ical) 

2418 

2419 @property 

2420 def leap(self) -> bool: 

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

2422 return self._leap 

2423 

2424 @leap.setter 

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

2426 self._leap = value 

2427 

2428 def __repr__(self) -> str: 

2429 """repr(self)""" 

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

2431 

2432 def __str__(self) -> str: 

2433 """str(self)""" 

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

2435 

2436 @classmethod 

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

2438 """Parse a jCal value for vMonth. 

2439 

2440 Raises: 

2441 ~error.JCalParsingError: If the value is not a valid month. 

2442 """ 

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

2444 try: 

2445 return cls(value) 

2446 except ValueError as e: 

2447 raise JCalParsingError( 

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

2449 ) from e 

2450 

2451 

2452class vSkip(vText, Enum): 

2453 """Skip values for RRULE. 

2454 

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

2456 

2457 OMIT is the default value. 

2458 

2459 Examples: 

2460 

2461 .. code-block:: pycon 

2462 

2463 >>> from icalendar import vSkip 

2464 >>> vSkip.OMIT 

2465 vSkip('OMIT') 

2466 >>> vSkip.FORWARD 

2467 vSkip('FORWARD') 

2468 >>> vSkip.BACKWARD 

2469 vSkip('BACKWARD') 

2470 """ 

2471 

2472 OMIT = "OMIT" 

2473 FORWARD = "FORWARD" 

2474 BACKWARD = "BACKWARD" 

2475 

2476 __reduce_ex__ = Enum.__reduce_ex__ 

2477 

2478 def __repr__(self): 

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

2480 

2481 @classmethod 

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

2483 """Parse a jCal value for vSkip. 

2484 

2485 Raises: 

2486 ~error.JCalParsingError: If the value is not a valid skip value. 

2487 """ 

2488 JCalParsingError.validate_value_type(value, str, cls) 

2489 try: 

2490 return cls[value.upper()] 

2491 except KeyError as e: 

2492 raise JCalParsingError( 

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

2494 ) from e 

2495 

2496 

2497class vRecur(CaselessDict): 

2498 """Recurrence definition. 

2499 

2500 Property Name: 

2501 RRULE 

2502 

2503 Purpose: 

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

2505 journal entries, or time zone definitions. 

2506 

2507 Value Type: 

2508 RECUR 

2509 

2510 Property Parameters: 

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

2512 

2513 Conformance: 

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

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

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

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

2518 

2519 Description: 

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

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

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

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

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

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

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

2527 value not synchronized with the recurrence rule is undefined. 

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

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

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

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

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

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

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

2535 

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

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

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

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

2540 same local time regardless of time zone changes. 

2541 

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

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

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

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

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

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

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

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

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

2551 "RDATE" property of PERIOD value type. 

2552 

2553 Examples: 

2554 The following RRULE specifies daily events for 10 occurrences. 

2555 

2556 .. code-block:: text 

2557 

2558 RRULE:FREQ=DAILY;COUNT=10 

2559 

2560 Below, we parse the RRULE ical string. 

2561 

2562 .. code-block:: pycon 

2563 

2564 >>> from icalendar.prop import vRecur 

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

2566 >>> rrule 

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

2568 

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

2570 :class:`icalendar.cal.Todo`. 

2571 

2572 .. code-block:: pycon 

2573 

2574 >>> from icalendar import Event 

2575 >>> event = Event() 

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

2577 >>> event.rrules 

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

2579 """ # noqa: E501 

2580 

2581 default_value: ClassVar[str] = "RECUR" 

2582 params: Parameters 

2583 

2584 frequencies = [ 

2585 "SECONDLY", 

2586 "MINUTELY", 

2587 "HOURLY", 

2588 "DAILY", 

2589 "WEEKLY", 

2590 "MONTHLY", 

2591 "YEARLY", 

2592 ] 

2593 

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

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

2596 canonical_order = ( 

2597 "RSCALE", 

2598 "FREQ", 

2599 "UNTIL", 

2600 "COUNT", 

2601 "INTERVAL", 

2602 "BYSECOND", 

2603 "BYMINUTE", 

2604 "BYHOUR", 

2605 "BYDAY", 

2606 "BYWEEKDAY", 

2607 "BYMONTHDAY", 

2608 "BYYEARDAY", 

2609 "BYWEEKNO", 

2610 "BYMONTH", 

2611 "BYSETPOS", 

2612 "WKST", 

2613 "SKIP", 

2614 ) 

2615 

2616 types = CaselessDict( 

2617 { 

2618 "COUNT": vInt, 

2619 "INTERVAL": vInt, 

2620 "BYSECOND": vInt, 

2621 "BYMINUTE": vInt, 

2622 "BYHOUR": vInt, 

2623 "BYWEEKNO": vInt, 

2624 "BYMONTHDAY": vInt, 

2625 "BYYEARDAY": vInt, 

2626 "BYMONTH": vMonth, 

2627 "UNTIL": vDDDTypes, 

2628 "BYSETPOS": vInt, 

2629 "WKST": vWeekday, 

2630 "BYDAY": vWeekday, 

2631 "FREQ": vFrequency, 

2632 "BYWEEKDAY": vWeekday, 

2633 "SKIP": vSkip, # RFC 7529 

2634 "RSCALE": vText, # RFC 7529 

2635 } 

2636 ) 

2637 

2638 # for reproducible serialization: 

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

2640 # look up in RFC 

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

2642 

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

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

2645 # we have a string as an argument. 

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

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

2648 if not isinstance(v, SEQUENCE_TYPES): 

2649 kwargs[k] = [v] 

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

2651 self.params = Parameters(params) 

2652 

2653 def to_ical(self): 

2654 result = [] 

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

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

2657 if not isinstance(vals, SEQUENCE_TYPES): 

2658 vals = [vals] # noqa: PLW2901 

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

2660 

2661 # CaselessDict keys are always unicode 

2662 param_key = key.encode(DEFAULT_ENCODING) 

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

2664 

2665 return b";".join(result) 

2666 

2667 @classmethod 

2668 def parse_type(cls, key, values): 

2669 # integers 

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

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

2672 

2673 @classmethod 

2674 def from_ical(cls, ical: str): 

2675 if isinstance(ical, cls): 

2676 return ical 

2677 try: 

2678 recur = cls() 

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

2680 try: 

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

2682 except ValueError: 

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

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

2685 continue 

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

2687 return cls(recur) 

2688 except ValueError: 

2689 raise 

2690 except Exception as e: 

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

2692 

2693 @classmethod 

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

2695 """Examples of vRecur.""" 

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

2697 

2698 from icalendar.param import VALUE 

2699 

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

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

2702 recur = {} 

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

2704 key = k.lower() 

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

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

2707 elif not isinstance(v, list): 

2708 value = [v] 

2709 else: 

2710 value = v 

2711 recur[key] = value 

2712 if "until" in recur: 

2713 until = recur["until"] 

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

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

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

2717 

2718 @classmethod 

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

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

2721 

2722 Args: 

2723 jcal_property: The jCal property to parse. 

2724 

2725 Raises: 

2726 ~error.JCalParsingError: If the provided jCal is invalid. 

2727 """ 

2728 JCalParsingError.validate_property(jcal_property, cls) 

2729 params = Parameters.from_jcal_property(jcal_property) 

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

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

2732 ): 

2733 raise JCalParsingError( 

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

2735 cls, 

2736 3, 

2737 value=jcal_property[3], 

2738 ) 

2739 recur = {} 

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

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

2742 with JCalParsingError.reraise_with_path_added(3, key): 

2743 if isinstance(value, list): 

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

2745 for i, v in enumerate(value): 

2746 with JCalParsingError.reraise_with_path_added(i): 

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

2748 else: 

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

2750 until = recur.get("until") 

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

2752 recur["until"] = [until] 

2753 return cls(recur, params=params) 

2754 

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

2756 """self == other""" 

2757 if not isinstance(other, vRecur): 

2758 return super().__eq__(other) 

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

2760 return False 

2761 for key in self.keys(): 

2762 v1 = self[key] 

2763 v2 = other[key] 

2764 if not isinstance(v1, SEQUENCE_TYPES): 

2765 v1 = [v1] 

2766 if not isinstance(v2, SEQUENCE_TYPES): 

2767 v2 = [v2] 

2768 if v1 != v2: 

2769 return False 

2770 return True 

2771 

2772 

2773TIME_JCAL_REGEX = re.compile( 

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

2775) 

2776 

2777 

2778class vTime(TimeBase): 

2779 """Time 

2780 

2781 Value Name: 

2782 TIME 

2783 

2784 Purpose: 

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

2786 time of day. 

2787 

2788 Format Definition: 

2789 This value type is defined by the following notation: 

2790 

2791 .. code-block:: text 

2792 

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

2794 

2795 time-hour = 2DIGIT ;00-23 

2796 time-minute = 2DIGIT ;00-59 

2797 time-second = 2DIGIT ;00-60 

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

2799 

2800 time-utc = "Z" 

2801 

2802 Description: 

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

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

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

2806 vText) is defined for this value type. 

2807 

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

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

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

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

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

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

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

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

2816 

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

2818 type expresses time values in three forms: 

2819 

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

2821 the following is not valid for a time value: 

2822 

2823 .. code-block:: text 

2824 

2825 230000-0800 ;Invalid time format 

2826 

2827 **FORM #1 LOCAL TIME** 

2828 

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

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

2831 example, 11:00 PM: 

2832 

2833 .. code-block:: text 

2834 

2835 230000 

2836 

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

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

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

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

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

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

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

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

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

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

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

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

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

2850 

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

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

2853 time zone reference MUST be specified. 

2854 

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

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

2857 existence of "VTIMEZONE" calendar components in the iCalendar 

2858 object. 

2859 

2860 **FORM #2: UTC TIME** 

2861 

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

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

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

2865 

2866 .. code-block:: text 

2867 

2868 070000Z 

2869 

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

2871 properties whose time values are specified in UTC. 

2872 

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

2874 

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

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

2877 the appropriate time zone definition. 

2878 

2879 Example: 

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

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

2882 

2883 .. code-block:: text 

2884 

2885 083000 

2886 133000Z 

2887 TZID=America/New_York:083000 

2888 """ 

2889 

2890 default_value: ClassVar[str] = "TIME" 

2891 params: Parameters 

2892 

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

2894 if len(args) == 1: 

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

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

2897 self.dt = args[0] 

2898 else: 

2899 self.dt = time(*args) 

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

2901 self.params.update_tzid_from(self.dt) 

2902 

2903 def to_ical(self): 

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

2905 if self.is_utc(): 

2906 value += "Z" 

2907 return value 

2908 

2909 def is_utc(self) -> bool: 

2910 """Whether this time is UTC.""" 

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

2912 

2913 @staticmethod 

2914 def from_ical(ical): 

2915 # TODO: timezone support 

2916 try: 

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

2918 return time(*timetuple) 

2919 except Exception as e: 

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

2921 

2922 @classmethod 

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

2924 """Examples of vTime.""" 

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

2926 

2927 from icalendar.param import VALUE 

2928 

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

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

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

2932 if self.is_utc(): 

2933 value += "Z" 

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

2935 

2936 @classmethod 

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

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

2939 

2940 Raises: 

2941 ~error.JCalParsingError: If it can't parse a time. 

2942 """ 

2943 JCalParsingError.validate_value_type(jcal, str, cls) 

2944 match = TIME_JCAL_REGEX.match(jcal) 

2945 if match is None: 

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

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

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

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

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

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

2952 

2953 @classmethod 

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

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

2956 

2957 Args: 

2958 jcal_property: The jCal property to parse. 

2959 

2960 Raises: 

2961 ~error.JCalParsingError: If the provided jCal is invalid. 

2962 """ 

2963 JCalParsingError.validate_property(jcal_property, cls) 

2964 with JCalParsingError.reraise_with_path_added(3): 

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

2966 return cls( 

2967 value, 

2968 params=Parameters.from_jcal_property(jcal_property), 

2969 ) 

2970 

2971 

2972class vUri(str): 

2973 """URI 

2974 

2975 Value Name: 

2976 URI 

2977 

2978 Purpose: 

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

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

2981 property value. 

2982 

2983 Format Definition: 

2984 This value type is defined by the following notation: 

2985 

2986 .. code-block:: text 

2987 

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

2989 

2990 Description: 

2991 This value type might be used to reference binary 

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

2993 to include directly in the iCalendar object. 

2994 

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

2996 syntax defined in [RFC3986]. 

2997 

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

2999 be specified as a quoted-string value. 

3000 

3001 Examples: 

3002 The following is a URI for a network file: 

3003 

3004 .. code-block:: text 

3005 

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

3007 

3008 .. code-block:: pycon 

3009 

3010 >>> from icalendar.prop import vUri 

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

3012 >>> uri 

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

3014 >>> uri.uri 

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

3016 """ 

3017 

3018 default_value: ClassVar[str] = "URI" 

3019 params: Parameters 

3020 __slots__ = ("params",) 

3021 

3022 def __new__( 

3023 cls, 

3024 value: str, 

3025 encoding: str = DEFAULT_ENCODING, 

3026 /, 

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

3028 ): 

3029 value = to_unicode(value, encoding=encoding) 

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

3031 self.params = Parameters(params) 

3032 return self 

3033 

3034 def to_ical(self): 

3035 return self.encode(DEFAULT_ENCODING) 

3036 

3037 @classmethod 

3038 def from_ical(cls, ical): 

3039 try: 

3040 return cls(ical) 

3041 except Exception as e: 

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

3043 

3044 @classmethod 

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

3046 """Examples of vUri.""" 

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

3048 

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

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

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

3052 

3053 @classmethod 

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

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

3056 

3057 Args: 

3058 jcal_property: The jCal property to parse. 

3059 

3060 Raises: 

3061 ~error.JCalParsingError: If the provided jCal is invalid. 

3062 """ 

3063 JCalParsingError.validate_property(jcal_property, cls) 

3064 return cls( 

3065 jcal_property[3], 

3066 Parameters.from_jcal_property(jcal_property), 

3067 ) 

3068 

3069 @property 

3070 def ical_value(self) -> str: 

3071 """The URI.""" 

3072 return self.uri 

3073 

3074 @property 

3075 def uri(self) -> str: 

3076 """The URI.""" 

3077 return str(self) 

3078 

3079 def __repr__(self) -> str: 

3080 """repr(self)""" 

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

3082 

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

3084 

3085 

3086class vUid(vText): 

3087 """A UID of a component. 

3088 

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

3090 """ 

3091 

3092 default_value: ClassVar[str] = "UID" 

3093 

3094 @classmethod 

3095 def new(cls): 

3096 """Create a new UID for convenience. 

3097 

3098 .. code-block:: pycon 

3099 

3100 >>> from icalendar import vUid 

3101 >>> vUid.new() 

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

3103 

3104 """ 

3105 return vUid(uuid.uuid4()) 

3106 

3107 @property 

3108 def uid(self) -> str: 

3109 """The UID of this property.""" 

3110 return str(self) 

3111 

3112 @property 

3113 def ical_value(self) -> str: 

3114 """The UID of this property.""" 

3115 return self.uid 

3116 

3117 def __repr__(self) -> str: 

3118 """repr(self)""" 

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

3120 

3121 from icalendar.param import FMTTYPE, LABEL, LINKREL 

3122 

3123 @classmethod 

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

3125 """Examples of vUid.""" 

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

3127 

3128 

3129class vXmlReference(vUri): 

3130 """An XML-REFERENCE. 

3131 

3132 The associated value references an associated XML artifact and 

3133 is a URI with an XPointer anchor value. 

3134 

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

3136 """ 

3137 

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

3139 

3140 @property 

3141 def xml_reference(self) -> str: 

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

3143 return self.uri 

3144 

3145 @property 

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

3147 """The XPointer of the URI. 

3148 

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

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

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

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

3153 

3154 Returns: 

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

3156 """ 

3157 from urllib.parse import unquote, urlparse 

3158 

3159 parsed = urlparse(self.xml_reference) 

3160 fragment = unquote(parsed.fragment) 

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

3162 return None 

3163 return fragment[9:-1] 

3164 

3165 @classmethod 

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

3167 """Examples of vXmlReference.""" 

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

3169 

3170 

3171class vGeo: 

3172 """Geographic Position 

3173 

3174 Property Name: 

3175 GEO 

3176 

3177 Purpose: 

3178 This property specifies information related to the global 

3179 position for the activity specified by a calendar component. 

3180 

3181 Value Type: 

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

3183 

3184 Property Parameters: 

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

3186 this property. 

3187 

3188 Conformance: 

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

3190 calendar components. 

3191 

3192 Description: 

3193 This property value specifies latitude and longitude, 

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

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

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

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

3198 will allow for accuracy to within one meter of geographical 

3199 position. Receiving applications MUST accept values of this 

3200 precision and MAY truncate values of greater precision. 

3201 

3202 Example: 

3203 

3204 .. code-block:: text 

3205 

3206 GEO:37.386013;-122.082932 

3207 

3208 Parse vGeo: 

3209 

3210 .. code-block:: pycon 

3211 

3212 >>> from icalendar.prop import vGeo 

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

3214 >>> geo 

3215 (37.386013, -122.082932) 

3216 

3217 Add a geo location to an event: 

3218 

3219 .. code-block:: pycon 

3220 

3221 >>> from icalendar import Event 

3222 >>> event = Event() 

3223 >>> latitude = 37.386013 

3224 >>> longitude = -122.082932 

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

3226 >>> event['GEO'] 

3227 vGeo((37.386013, -122.082932)) 

3228 """ 

3229 

3230 default_value: ClassVar[str] = "FLOAT" 

3231 params: Parameters 

3232 

3233 def __init__( 

3234 self, 

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

3236 /, 

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

3238 ): 

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

3240 

3241 Raises: 

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

3243 """ 

3244 try: 

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

3246 latitude = float(latitude) 

3247 longitude = float(longitude) 

3248 except Exception as e: 

3249 raise ValueError( 

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

3251 ) from e 

3252 self.latitude = latitude 

3253 self.longitude = longitude 

3254 self.params = Parameters(params) 

3255 

3256 def to_ical(self): 

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

3258 

3259 @staticmethod 

3260 def from_ical(ical): 

3261 try: 

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

3263 return (float(latitude), float(longitude)) 

3264 except Exception as e: 

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

3266 

3267 def __eq__(self, other): 

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

3269 

3270 def __repr__(self): 

3271 """repr(self)""" 

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

3273 

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

3275 """Convert to jCal object.""" 

3276 return [ 

3277 name, 

3278 self.params.to_jcal(), 

3279 self.VALUE.lower(), 

3280 [self.latitude, self.longitude], 

3281 ] 

3282 

3283 @classmethod 

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

3285 """Examples of vGeo.""" 

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

3287 

3288 from icalendar.param import VALUE 

3289 

3290 @classmethod 

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

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

3293 

3294 Args: 

3295 jcal_property: The jCal property to parse. 

3296 

3297 Raises: 

3298 ~error.JCalParsingError: If the provided jCal is invalid. 

3299 """ 

3300 JCalParsingError.validate_property(jcal_property, cls) 

3301 return cls( 

3302 jcal_property[3], 

3303 Parameters.from_jcal_property(jcal_property), 

3304 ) 

3305 

3306 

3307UTC_OFFSET_JCAL_REGEX = re.compile( 

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

3309) 

3310 

3311 

3312class vUTCOffset: 

3313 """UTC Offset 

3314 

3315 Value Name: 

3316 UTC-OFFSET 

3317 

3318 Purpose: 

3319 This value type is used to identify properties that contain 

3320 an offset from UTC to local time. 

3321 

3322 Format Definition: 

3323 This value type is defined by the following notation: 

3324 

3325 .. code-block:: text 

3326 

3327 utc-offset = time-numzone 

3328 

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

3330 

3331 Description: 

3332 The PLUS SIGN character MUST be specified for positive 

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

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

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

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

3337 

3338 Example: 

3339 The following UTC offsets are given for standard time for 

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

3341 UTC): 

3342 

3343 .. code-block:: text 

3344 

3345 -0500 

3346 

3347 +0100 

3348 

3349 .. code-block:: pycon 

3350 

3351 >>> from icalendar.prop import vUTCOffset 

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

3353 >>> utc_offset 

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

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

3356 >>> utc_offset 

3357 datetime.timedelta(seconds=3600) 

3358 """ 

3359 

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

3361 params: Parameters 

3362 

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

3364 

3365 # component, we will silently ignore 

3366 # it, rather than let the exception 

3367 # propagate upwards 

3368 

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

3370 if not isinstance(td, timedelta): 

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

3372 self.td = td 

3373 self.params = Parameters(params) 

3374 

3375 def to_ical(self) -> str: 

3376 """Return the ical representation.""" 

3377 return self.format("") 

3378 

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

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

3381 

3382 .. code-block:: pycon 

3383 

3384 >>> from icalendar import vUTCOffset 

3385 >>> from datetime import timedelta 

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

3387 >>> utc_offset.format() 

3388 '-0500' 

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

3390 '-05:00' 

3391 """ 

3392 if self.td < timedelta(0): 

3393 sign = "-%s" 

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

3395 else: 

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

3397 sign = "+%s" 

3398 td = self.td 

3399 

3400 days, seconds = td.days, td.seconds 

3401 

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

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

3404 seconds = abs(seconds % 60) 

3405 if seconds: 

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

3407 else: 

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

3409 return sign % duration 

3410 

3411 @classmethod 

3412 def from_ical(cls, ical): 

3413 if isinstance(ical, cls): 

3414 return ical.td 

3415 try: 

3416 sign, hours, minutes, seconds = ( 

3417 ical[0:1], 

3418 int(ical[1:3]), 

3419 int(ical[3:5]), 

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

3421 ) 

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

3423 except Exception as e: 

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

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

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

3427 if sign == "-": 

3428 return -offset 

3429 return offset 

3430 

3431 def __eq__(self, other): 

3432 if not isinstance(other, vUTCOffset): 

3433 return False 

3434 return self.td == other.td 

3435 

3436 def __hash__(self): 

3437 return hash(self.td) 

3438 

3439 def __repr__(self): 

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

3441 

3442 @classmethod 

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

3444 """Examples of vUTCOffset.""" 

3445 return [ 

3446 cls(timedelta(hours=3)), 

3447 cls(timedelta(0)), 

3448 ] 

3449 

3450 from icalendar.param import VALUE 

3451 

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

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

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

3455 

3456 @classmethod 

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

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

3459 

3460 Args: 

3461 jcal_property: The jCal property to parse. 

3462 

3463 Raises: 

3464 ~error.JCalParsingError: If the provided jCal is invalid. 

3465 """ 

3466 JCalParsingError.validate_property(jcal_property, cls) 

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

3468 if match is None: 

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

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

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

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

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

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

3475 if negative: 

3476 t = -t 

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

3478 

3479 

3480class vInline(str): 

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

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

3483 class, so no further processing is needed. 

3484 """ 

3485 

3486 params: Parameters 

3487 __slots__ = ("params",) 

3488 

3489 def __new__( 

3490 cls, 

3491 value, 

3492 encoding=DEFAULT_ENCODING, 

3493 /, 

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

3495 ): 

3496 value = to_unicode(value, encoding=encoding) 

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

3498 self.params = Parameters(params) 

3499 return self 

3500 

3501 def to_ical(self): 

3502 return self.encode(DEFAULT_ENCODING) 

3503 

3504 @classmethod 

3505 def from_ical(cls, ical): 

3506 return cls(ical) 

3507 

3508 

3509class vUnknown(vText): 

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

3511 

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

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

3514 """ 

3515 

3516 default_value: ClassVar[str] = "UNKNOWN" 

3517 

3518 @classmethod 

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

3520 """Examples of vUnknown.""" 

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

3522 

3523 from icalendar.param import VALUE 

3524 

3525 

3526class TypesFactory(CaselessDict): 

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

3528 class. 

3529 

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

3531 both kinds. 

3532 """ 

3533 

3534 _instance: ClassVar[TypesFactory] = None 

3535 

3536 def instance() -> TypesFactory: 

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

3538 if TypesFactory._instance is None: 

3539 TypesFactory._instance = TypesFactory() 

3540 return TypesFactory._instance 

3541 

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

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

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

3545 self.all_types = ( 

3546 vBinary, 

3547 vBoolean, 

3548 vCalAddress, 

3549 vDDDLists, 

3550 vDDDTypes, 

3551 vDate, 

3552 vDatetime, 

3553 vDuration, 

3554 vFloat, 

3555 vFrequency, 

3556 vGeo, 

3557 vInline, 

3558 vInt, 

3559 vPeriod, 

3560 vRecur, 

3561 vText, 

3562 vTime, 

3563 vUTCOffset, 

3564 vUri, 

3565 vWeekday, 

3566 vCategory, 

3567 vAdr, 

3568 vN, 

3569 vOrg, 

3570 vUid, 

3571 vXmlReference, 

3572 vUnknown, 

3573 ) 

3574 self["binary"] = vBinary 

3575 self["boolean"] = vBoolean 

3576 self["cal-address"] = vCalAddress 

3577 self["date"] = vDDDTypes 

3578 self["date-time"] = vDDDTypes 

3579 self["duration"] = vDDDTypes 

3580 self["float"] = vFloat 

3581 self["integer"] = vInt 

3582 self["period"] = vPeriod 

3583 self["recur"] = vRecur 

3584 self["text"] = vText 

3585 self["time"] = vTime 

3586 self["uri"] = vUri 

3587 self["utc-offset"] = vUTCOffset 

3588 self["geo"] = vGeo 

3589 self["inline"] = vInline 

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

3591 self["categories"] = vCategory 

3592 self["adr"] = vAdr # RFC 6350 vCard 

3593 self["n"] = vN # RFC 6350 vCard 

3594 self["org"] = vOrg # RFC 6350 vCard 

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

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

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

3598 

3599 ################################################# 

3600 # Property types 

3601 

3602 # These are the default types 

3603 types_map = CaselessDict( 

3604 { 

3605 #################################### 

3606 # Property value types 

3607 # Calendar Properties 

3608 "calscale": "text", 

3609 "method": "text", 

3610 "prodid": "text", 

3611 "version": "text", 

3612 # Descriptive Component Properties 

3613 "attach": "uri", 

3614 "categories": "categories", 

3615 "class": "text", 

3616 # vCard Properties (RFC 6350) 

3617 "adr": "adr", 

3618 "n": "n", 

3619 "org": "org", 

3620 "comment": "text", 

3621 "description": "text", 

3622 "geo": "geo", 

3623 "location": "text", 

3624 "percent-complete": "integer", 

3625 "priority": "integer", 

3626 "resources": "text", 

3627 "status": "text", 

3628 "summary": "text", 

3629 # RFC 9253 

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

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

3632 "link": "uri", 

3633 "concept": "uri", 

3634 "refid": "text", 

3635 # Date and Time Component Properties 

3636 "completed": "date-time", 

3637 "dtend": "date-time", 

3638 "due": "date-time", 

3639 "dtstart": "date-time", 

3640 "duration": "duration", 

3641 "freebusy": "period", 

3642 "transp": "text", 

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

3644 # Time Zone Component Properties 

3645 "tzid": "text", 

3646 "tzname": "text", 

3647 "tzoffsetfrom": "utc-offset", 

3648 "tzoffsetto": "utc-offset", 

3649 "tzurl": "uri", 

3650 # Relationship Component Properties 

3651 "attendee": "cal-address", 

3652 "contact": "text", 

3653 "organizer": "cal-address", 

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

3655 "related-to": "text", 

3656 "url": "uri", 

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

3658 "source": "uri", 

3659 "uid": "text", 

3660 # Recurrence Component Properties 

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

3662 "exrule": "recur", 

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

3664 "rrule": "recur", 

3665 # Alarm Component Properties 

3666 "action": "text", 

3667 "repeat": "integer", 

3668 "trigger": "duration", 

3669 "acknowledged": "date-time", 

3670 # Change Management Component Properties 

3671 "created": "date-time", 

3672 "dtstamp": "date-time", 

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

3674 "sequence": "integer", 

3675 # Miscellaneous Component Properties 

3676 "request-status": "text", 

3677 #################################### 

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

3679 "altrep": "uri", 

3680 "cn": "text", 

3681 "cutype": "text", 

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

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

3684 "dir": "uri", 

3685 "encoding": "text", 

3686 "fmttype": "text", 

3687 "fbtype": "text", 

3688 "language": "text", 

3689 "member": "cal-address", 

3690 "partstat": "text", 

3691 "range": "text", 

3692 "related": "text", 

3693 "reltype": "text", 

3694 "role": "text", 

3695 "rsvp": "boolean", 

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

3697 "value": "text", 

3698 # rfc 9253 parameters 

3699 "label": "text", 

3700 "linkrel": "text", 

3701 "gap": "duration", 

3702 } 

3703 ) 

3704 

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

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

3707 

3708 Args: 

3709 name: Property or parameter name 

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

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

3712 

3713 Returns: 

3714 The appropriate value type class. 

3715 """ 

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

3717 # regardless of the VALUE parameter 

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

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

3720 

3721 # Only use VALUE parameter for known properties that support multiple value 

3722 # types (like DTSTART, DTEND, etc. which can be DATE or DATE-TIME) 

3723 # For unknown/custom properties, always use the default type from types_map 

3724 if value_param and name in self.types_map and value_param in self: 

3725 return self[value_param] 

3726 return self[self.types_map.get(name, "unknown")] 

3727 

3728 def to_ical(self, name, value): 

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

3730 encoded string. 

3731 """ 

3732 type_class = self.for_property(name) 

3733 return type_class(value).to_ical() 

3734 

3735 def from_ical(self, name, value): 

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

3737 encoded string to a primitive python type. 

3738 """ 

3739 type_class = self.for_property(name) 

3740 return type_class.from_ical(value) 

3741 

3742 

3743VPROPERTY: TypeAlias = Union[ 

3744 vAdr, 

3745 vBoolean, 

3746 vCalAddress, 

3747 vCategory, 

3748 vDDDLists, 

3749 vDDDTypes, 

3750 vDate, 

3751 vDatetime, 

3752 vDuration, 

3753 vFloat, 

3754 vFrequency, 

3755 vInt, 

3756 vMonth, 

3757 vN, 

3758 vOrg, 

3759 vPeriod, 

3760 vRecur, 

3761 vSkip, 

3762 vText, 

3763 vTime, 

3764 vUTCOffset, 

3765 vUri, 

3766 vWeekday, 

3767 vInline, 

3768 vBinary, 

3769 vGeo, 

3770 vUnknown, 

3771 vXmlReference, 

3772 vUid, 

3773] 

3774 

3775__all__ = [ 

3776 "DURATION_REGEX", 

3777 "VPROPERTY", 

3778 "WEEKDAY_RULE", 

3779 "TimeBase", 

3780 "TypesFactory", 

3781 "tzid_from_dt", 

3782 "tzid_from_tzinfo", 

3783 "vAdr", 

3784 "vBinary", 

3785 "vBoolean", 

3786 "vCalAddress", 

3787 "vCategory", 

3788 "vDDDLists", 

3789 "vDDDTypes", 

3790 "vDate", 

3791 "vDatetime", 

3792 "vDuration", 

3793 "vFloat", 

3794 "vFrequency", 

3795 "vGeo", 

3796 "vInline", 

3797 "vInt", 

3798 "vMonth", 

3799 "vN", 

3800 "vOrg", 

3801 "vPeriod", 

3802 "vRecur", 

3803 "vSkip", 

3804 "vText", 

3805 "vTime", 

3806 "vUTCOffset", 

3807 "vUid", 

3808 "vUnknown", 

3809 "vUri", 

3810 "vWeekday", 

3811 "vXmlReference", 

3812]