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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1359 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(self.cats) 

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: list[str] | str) -> list[str]: 

869 """Parse a CATEGORIES value from iCalendar format. 

870 

871 This helper is normally called by :meth:`Component.from_ical`, which 

872 already splits the CATEGORIES property into a list of unescaped 

873 category strings. New code should therefore pass a list of strings. 

874 

875 Passing a single comma-separated string is supported only for 

876 backwards compatibility with older parsing code and is considered 

877 legacy behavior. 

878 

879 Args: 

880 ical: A list of category strings (preferred, as provided by 

881 :meth:`Component.from_ical`), or a single comma-separated 

882 string from a legacy caller. 

883 

884 Returns: 

885 A list of category strings. 

886 """ 

887 if isinstance(ical, list): 

888 # Already split by Component.from_ical() 

889 return ical 

890 # Legacy: simple comma split (no escaping handled) 

891 ical = to_unicode(ical) 

892 return ical.split(",") 

893 

894 def __eq__(self, other): 

895 """self == other""" 

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

897 

898 def __repr__(self): 

899 """String representation.""" 

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

901 

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

903 """The jCal representation for categories.""" 

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

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

906 if not self.cats: 

907 result.append("") 

908 return result 

909 

910 @classmethod 

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

912 """Examples of vCategory.""" 

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

914 

915 from icalendar.param import VALUE 

916 

917 @classmethod 

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

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

920 

921 Args: 

922 jcal_property: The jCal property to parse. 

923 

924 Raises: 

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

926 """ 

927 JCalParsingError.validate_property(jcal_property, cls) 

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

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

930 return cls( 

931 jcal_property[3:], 

932 Parameters.from_jcal_property(jcal_property), 

933 ) 

934 

935 @property 

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

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

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

939 

940 

941class vAdr: 

942 """vCard ADR (Address) structured property per :rfc:`6350#section-6.3.1`. 

943 

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

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

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

947 

948 - post office box 

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

950 - street address 

951 - locality (e.g., city) 

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

953 - postal code 

954 - country name (full name) 

955 

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

957 

958 Semicolons are field separators and are NOT escaped. 

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

960 

961 Examples: 

962 .. code-block:: pycon 

963 

964 >>> from icalendar.prop import vAdr 

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

966 >>> adr.to_ical() 

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

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

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

970 """ 

971 

972 default_value: ClassVar[str] = "TEXT" 

973 params: Parameters 

974 

975 # 7 ADR fields per RFC 6350 

976 FIELDS = [ 

977 "po_box", 

978 "extended", 

979 "street", 

980 "locality", 

981 "region", 

982 "postal_code", 

983 "country", 

984 ] 

985 

986 def __init__( 

987 self, 

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

989 /, 

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

991 ): 

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

993 

994 Args: 

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

996 vCard format string with semicolon-separated fields 

997 params: Optional property parameters 

998 """ 

999 if isinstance(fields, str): 

1000 fields = self.from_ical(fields) 

1001 if len(fields) != 7: 

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

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

1004 self.params = Parameters(params) 

1005 

1006 def to_ical(self) -> bytes: 

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

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

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

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

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

1012 

1013 @staticmethod 

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

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

1016 

1017 Args: 

1018 ical: vCard format string with semicolon-separated fields 

1019 

1020 Returns: 

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

1022 """ 

1023 from icalendar.parser import split_on_unescaped_semicolon 

1024 

1025 ical = to_unicode(ical) 

1026 fields = split_on_unescaped_semicolon(ical) 

1027 if len(fields) != 7: 

1028 raise ValueError( 

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

1030 ) 

1031 return tuple(fields) 

1032 

1033 def __eq__(self, other): 

1034 """self == other""" 

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

1036 

1037 def __repr__(self): 

1038 """String representation.""" 

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

1040 

1041 from icalendar.param import VALUE 

1042 

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

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

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

1046 result.extend(self.fields) 

1047 return result 

1048 

1049 @classmethod 

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

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

1052 

1053 Args: 

1054 jcal_property: The jCal property to parse. 

1055 

1056 Raises: 

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

1058 """ 

1059 JCalParsingError.validate_property(jcal_property, cls) 

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

1061 raise JCalParsingError( 

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

1063 f"got {len(jcal_property)}" 

1064 ) 

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

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

1067 return cls( 

1068 tuple(jcal_property[3:]), 

1069 Parameters.from_jcal_property(jcal_property), 

1070 ) 

1071 

1072 @classmethod 

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

1074 """Examples of vAdr.""" 

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

1076 

1077 

1078class vN: 

1079 r"""vCard N (Name) structured property per :rfc:`6350#section-6.2.2`. 

1080 

1081 The N property represents a person's name. 

1082 It consists of a single structured text value. 

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

1084 

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

1086 

1087 - family names (also known as surnames) 

1088 - given names 

1089 - additional names 

1090 - honorific prefixes 

1091 - honorific suffixes 

1092 

1093 Semicolons are field separators and are NOT escaped. 

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

1095 

1096 Examples: 

1097 

1098 .. code-block:: pycon 

1099 

1100 >>> from icalendar.prop import vN 

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

1102 >>> n.to_ical() 

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

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

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

1106 """ 

1107 

1108 default_value: ClassVar[str] = "TEXT" 

1109 params: Parameters 

1110 

1111 # 5 N fields per RFC 6350 

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

1113 

1114 def __init__( 

1115 self, 

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

1117 /, 

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

1119 ): 

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

1121 

1122 Args: 

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

1124 vCard format string with semicolon-separated fields 

1125 params: Optional property parameters 

1126 """ 

1127 if isinstance(fields, str): 

1128 fields = self.from_ical(fields) 

1129 if len(fields) != 5: 

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

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

1132 self.params = Parameters(params) 

1133 

1134 def to_ical(self) -> bytes: 

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

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

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

1138 

1139 @staticmethod 

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

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

1142 

1143 Args: 

1144 ical: vCard format string with semicolon-separated fields 

1145 

1146 Returns: 

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

1148 """ 

1149 from icalendar.parser import split_on_unescaped_semicolon 

1150 

1151 ical = to_unicode(ical) 

1152 fields = split_on_unescaped_semicolon(ical) 

1153 if len(fields) != 5: 

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

1155 return tuple(fields) 

1156 

1157 def __eq__(self, other): 

1158 """self == other""" 

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

1160 

1161 def __repr__(self): 

1162 """String representation.""" 

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

1164 

1165 from icalendar.param import VALUE 

1166 

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

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

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

1170 result.extend(self.fields) 

1171 return result 

1172 

1173 @classmethod 

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

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

1176 

1177 Args: 

1178 jcal_property: The jCal property to parse. 

1179 

1180 Raises: 

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

1182 """ 

1183 JCalParsingError.validate_property(jcal_property, cls) 

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

1185 raise JCalParsingError( 

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

1187 f"got {len(jcal_property)}" 

1188 ) 

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

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

1191 return cls( 

1192 tuple(jcal_property[3:]), 

1193 Parameters.from_jcal_property(jcal_property), 

1194 ) 

1195 

1196 @classmethod 

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

1198 """Examples of vN.""" 

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

1200 

1201 

1202class vOrg: 

1203 r"""vCard ORG (Organization) structured property per :rfc:`6350#section-6.6.4`. 

1204 

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

1206 

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

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

1209 

1210 .. code-block:: text 

1211 

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

1213 

1214 Semicolons are field separators and are NOT escaped. 

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

1216 

1217 Examples: 

1218 A property value consisting of an organizational name, 

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

1220 

1221 .. code-block:: text 

1222 

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

1224 

1225 The same example in icalendar. 

1226 

1227 .. code-block:: pycon 

1228 

1229 >>> from icalendar.prop import vOrg 

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

1231 >>> org.to_ical() 

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

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

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

1235 """ 

1236 

1237 default_value: ClassVar[str] = "TEXT" 

1238 params: Parameters 

1239 

1240 def __init__( 

1241 self, 

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

1243 /, 

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

1245 ): 

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

1247 

1248 Args: 

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

1250 vCard format string with semicolon-separated fields 

1251 params: Optional property parameters 

1252 """ 

1253 if isinstance(fields, str): 

1254 fields = self.from_ical(fields) 

1255 if len(fields) < 1: 

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

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

1258 self.params = Parameters(params) 

1259 

1260 def to_ical(self) -> bytes: 

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

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

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

1264 

1265 @staticmethod 

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

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

1268 

1269 Args: 

1270 ical: vCard format string with semicolon-separated fields 

1271 

1272 Returns: 

1273 Tuple of field values with one or more fields 

1274 """ 

1275 from icalendar.parser import split_on_unescaped_semicolon 

1276 

1277 ical = to_unicode(ical) 

1278 fields = split_on_unescaped_semicolon(ical) 

1279 if len(fields) < 1: 

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

1281 return tuple(fields) 

1282 

1283 def __eq__(self, other): 

1284 """self == other""" 

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

1286 

1287 def __repr__(self): 

1288 """String representation.""" 

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

1290 

1291 from icalendar.param import VALUE 

1292 

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

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

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

1296 result.extend(self.fields) 

1297 return result 

1298 

1299 @classmethod 

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

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

1302 

1303 Args: 

1304 jcal_property: The jCal property to parse. 

1305 

1306 Raises: 

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

1308 """ 

1309 JCalParsingError.validate_property(jcal_property, cls) 

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

1311 raise JCalParsingError( 

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

1313 f"got {len(jcal_property)}" 

1314 ) 

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

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

1317 return cls( 

1318 tuple(jcal_property[3:]), 

1319 Parameters.from_jcal_property(jcal_property), 

1320 ) 

1321 

1322 @classmethod 

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

1324 """Examples of vOrg.""" 

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

1326 

1327 

1328class TimeBase: 

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

1330 

1331 default_value: ClassVar[str] 

1332 params: Parameters 

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

1334 

1335 def __eq__(self, other): 

1336 """self == other""" 

1337 if isinstance(other, date): 

1338 return self.dt == other 

1339 if isinstance(other, TimeBase): 

1340 default = object() 

1341 for key in ( 

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

1343 ) - self.ignore_for_equality: 

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

1345 key, default 

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

1347 return False 

1348 return self.dt == other.dt 

1349 if isinstance(other, vDDDLists): 

1350 return other == self 

1351 return False 

1352 

1353 def __hash__(self): 

1354 return hash(self.dt) 

1355 

1356 from icalendar.param import RANGE, RELATED, TZID 

1357 

1358 def __repr__(self): 

1359 """String representation.""" 

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

1361 

1362 

1363DT_TYPE: TypeAlias = Union[ 

1364 datetime, 

1365 date, 

1366 timedelta, 

1367 time, 

1368 Tuple[datetime, datetime], 

1369 Tuple[datetime, timedelta], 

1370] 

1371 

1372 

1373class vDDDTypes(TimeBase): 

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

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

1376 So this is practical. 

1377 """ 

1378 

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

1380 params: Parameters 

1381 dt: DT_TYPE 

1382 

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

1384 if params is None: 

1385 params = {} 

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

1387 raise TypeError( 

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

1389 ) 

1390 self.dt = dt 

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

1392 if is_date(dt): 

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

1394 elif isinstance(dt, time): 

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

1396 elif isinstance(dt, tuple): 

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

1398 self.params = Parameters(params) 

1399 self.params.update_tzid_from(dt) 

1400 

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

1402 """Convert to a property type. 

1403 

1404 Raises: 

1405 ValueError: If the type is unknown. 

1406 """ 

1407 dt = self.dt 

1408 if isinstance(dt, datetime): 

1409 result = vDatetime(dt) 

1410 elif isinstance(dt, date): 

1411 result = vDate(dt) 

1412 elif isinstance(dt, timedelta): 

1413 result = vDuration(dt) 

1414 elif isinstance(dt, time): 

1415 result = vTime(dt) 

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

1417 result = vPeriod(dt) 

1418 else: 

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

1420 result.params = self.params 

1421 return result 

1422 

1423 def to_ical(self) -> str: 

1424 """Return the ical representation.""" 

1425 return self.to_property_type().to_ical() 

1426 

1427 @classmethod 

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

1429 if isinstance(ical, cls): 

1430 return ical.dt 

1431 u = ical.upper() 

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

1433 return vDuration.from_ical(ical) 

1434 if "/" in u: 

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

1436 

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

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

1439 if len(ical) == 8: 

1440 if timezone: 

1441 tzinfo = tzp.timezone(timezone) 

1442 if tzinfo is not None: 

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

1444 return vDate.from_ical(ical) 

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

1446 return vTime.from_ical(ical) 

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

1448 

1449 @property 

1450 def td(self) -> timedelta: 

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

1452 

1453 This class is used to replace different time components. 

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

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

1456 This property allows interoperability. 

1457 """ 

1458 return self.dt 

1459 

1460 @property 

1461 def dts(self) -> list: 

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

1463 return [self] 

1464 

1465 @classmethod 

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

1467 """Examples of vDDDTypes.""" 

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

1469 

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

1471 """Determine the VALUE parameter.""" 

1472 return self.to_property_type().VALUE 

1473 

1474 from icalendar.param import VALUE 

1475 

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

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

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

1479 

1480 @classmethod 

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

1482 """Parse a jCal value. 

1483 

1484 Raises: 

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

1486 date-time, duration, or period. 

1487 """ 

1488 if isinstance(jcal, list): 

1489 return vPeriod.parse_jcal_value(jcal) 

1490 JCalParsingError.validate_value_type(jcal, str, cls) 

1491 if "/" in jcal: 

1492 return vPeriod.parse_jcal_value(jcal) 

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

1494 try: 

1495 return jcal_type.parse_jcal_value(jcal) 

1496 except JCalParsingError: # noqa: PERF203 

1497 pass 

1498 raise JCalParsingError( 

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

1500 ) 

1501 

1502 @classmethod 

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

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

1505 

1506 Args: 

1507 jcal_property: The jCal property to parse. 

1508 

1509 Raises: 

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

1511 """ 

1512 JCalParsingError.validate_property(jcal_property, cls) 

1513 with JCalParsingError.reraise_with_path_added(3): 

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

1515 params = Parameters.from_jcal_property(jcal_property) 

1516 if params.tzid: 

1517 if isinstance(dt, tuple): 

1518 # period 

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

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

1521 dt = (start, end) 

1522 else: 

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

1524 return cls( 

1525 dt, 

1526 params=params, 

1527 ) 

1528 

1529 

1530class vDate(TimeBase): 

1531 """Date 

1532 

1533 Value Name: 

1534 DATE 

1535 

1536 Purpose: 

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

1538 calendar date. 

1539 

1540 Format Definition: 

1541 This value type is defined by the following notation: 

1542 

1543 .. code-block:: text 

1544 

1545 date = date-value 

1546 

1547 date-value = date-fullyear date-month date-mday 

1548 date-fullyear = 4DIGIT 

1549 date-month = 2DIGIT ;01-12 

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

1551 ;based on month/year 

1552 

1553 Description: 

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

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

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

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

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

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

1560 year, month, and day component text. 

1561 

1562 Example: 

1563 The following represents July 14, 1997: 

1564 

1565 .. code-block:: text 

1566 

1567 19970714 

1568 

1569 .. code-block:: pycon 

1570 

1571 >>> from icalendar.prop import vDate 

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

1573 >>> date.year 

1574 1997 

1575 >>> date.month 

1576 7 

1577 >>> date.day 

1578 14 

1579 """ 

1580 

1581 default_value: ClassVar[str] = "DATE" 

1582 params: Parameters 

1583 

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

1585 if not isinstance(dt, date): 

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

1587 self.dt = dt 

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

1589 

1590 def to_ical(self): 

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

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

1593 

1594 @staticmethod 

1595 def from_ical(ical): 

1596 try: 

1597 timetuple = ( 

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

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

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

1601 ) 

1602 return date(*timetuple) 

1603 except Exception as e: 

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

1605 

1606 @classmethod 

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

1608 """Examples of vDate.""" 

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

1610 

1611 from icalendar.param import VALUE 

1612 

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

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

1615 return [ 

1616 name, 

1617 self.params.to_jcal(), 

1618 self.VALUE.lower(), 

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

1620 ] 

1621 

1622 @classmethod 

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

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

1625 

1626 Raises: 

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

1628 """ 

1629 JCalParsingError.validate_value_type(jcal, str, cls) 

1630 try: 

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

1632 except ValueError as e: 

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

1634 

1635 @classmethod 

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

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

1638 

1639 Args: 

1640 jcal_property: The jCal property to parse. 

1641 

1642 Raises: 

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

1644 """ 

1645 JCalParsingError.validate_property(jcal_property, cls) 

1646 with JCalParsingError.reraise_with_path_added(3): 

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

1648 return cls( 

1649 value, 

1650 params=Parameters.from_jcal_property(jcal_property), 

1651 ) 

1652 

1653 

1654class vDatetime(TimeBase): 

1655 """Date-Time 

1656 

1657 Value Name: 

1658 DATE-TIME 

1659 

1660 Purpose: 

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

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

1663 the ISO.8601.2004 complete representation. 

1664 

1665 Format Definition: 

1666 This value type is defined by the following notation: 

1667 

1668 .. code-block:: text 

1669 

1670 date-time = date "T" time 

1671 

1672 date = date-value 

1673 date-value = date-fullyear date-month date-mday 

1674 date-fullyear = 4DIGIT 

1675 date-month = 2DIGIT ;01-12 

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

1677 ;based on month/year 

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

1679 time-hour = 2DIGIT ;00-23 

1680 time-minute = 2DIGIT ;00-59 

1681 time-second = 2DIGIT ;00-60 

1682 time-utc = "Z" 

1683 

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

1685 

1686 .. code-block:: text 

1687 

1688 YYYYMMDDTHHMMSS 

1689 

1690 Description: 

1691 vDatetime is timezone aware and uses a timezone library. 

1692 When a vDatetime object is created from an 

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

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

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

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

1697 DATE-TIME components in the icalendar standard. 

1698 

1699 Example: 

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

1701 

1702 .. code-block:: pycon 

1703 

1704 >>> from icalendar import vDatetime 

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

1706 >>> datetime.tzname() 

1707 >>> datetime.year 

1708 2021 

1709 >>> datetime.minute 

1710 15 

1711 

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

1713 

1714 .. code-block:: pycon 

1715 

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

1717 >>> datetime.tzname() 

1718 'EST' 

1719 

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

1721 

1722 .. code-block:: pycon 

1723 

1724 >>> from zoneinfo import ZoneInfo 

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

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

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

1728 """ 

1729 

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

1731 params: Parameters 

1732 

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

1734 self.dt = dt 

1735 self.params = Parameters(params) 

1736 self.params.update_tzid_from(dt) 

1737 

1738 def to_ical(self): 

1739 dt = self.dt 

1740 

1741 s = ( 

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

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

1744 ) 

1745 if self.is_utc(): 

1746 s += "Z" 

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

1748 

1749 @staticmethod 

1750 def from_ical(ical, timezone=None): 

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

1752 tzinfo = None 

1753 if isinstance(timezone, str): 

1754 tzinfo = tzp.timezone(timezone) 

1755 elif timezone is not None: 

1756 tzinfo = timezone 

1757 

1758 try: 

1759 timetuple = ( 

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

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

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

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

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

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

1766 ) 

1767 if tzinfo: 

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

1769 if not ical[15:]: 

1770 return datetime(*timetuple) # noqa: DTZ001 

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

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

1773 except Exception as e: 

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

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

1776 

1777 @classmethod 

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

1779 """Examples of vDatetime.""" 

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

1781 

1782 from icalendar.param import VALUE 

1783 

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

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

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

1787 if self.is_utc(): 

1788 value += "Z" 

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

1790 

1791 def is_utc(self) -> bool: 

1792 """Whether this datetime is UTC.""" 

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

1794 

1795 @classmethod 

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

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

1798 

1799 Raises: 

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

1801 """ 

1802 JCalParsingError.validate_value_type(jcal, str, cls) 

1803 utc = jcal.endswith("Z") 

1804 if utc: 

1805 jcal = jcal[:-1] 

1806 try: 

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

1808 except ValueError as e: 

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

1810 if utc: 

1811 return tzp.localize_utc(dt) 

1812 return dt 

1813 

1814 @classmethod 

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

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

1817 

1818 Args: 

1819 jcal_property: The jCal property to parse. 

1820 

1821 Raises: 

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

1823 """ 

1824 JCalParsingError.validate_property(jcal_property, cls) 

1825 params = Parameters.from_jcal_property(jcal_property) 

1826 with JCalParsingError.reraise_with_path_added(3): 

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

1828 if params.tzid: 

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

1830 return cls( 

1831 dt, 

1832 params=params, 

1833 ) 

1834 

1835 

1836class vDuration(TimeBase): 

1837 """Duration 

1838 

1839 Value Name: 

1840 DURATION 

1841 

1842 Purpose: 

1843 This value type is used to identify properties that contain 

1844 a duration of time. 

1845 

1846 Format Definition: 

1847 This value type is defined by the following notation: 

1848 

1849 .. code-block:: text 

1850 

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

1852 

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

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

1855 dur-week = 1*DIGIT "W" 

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

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

1858 dur-second = 1*DIGIT "S" 

1859 dur-day = 1*DIGIT "D" 

1860 

1861 Description: 

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

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

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

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

1866 represent nominal durations (weeks and days) and accurate 

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

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

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

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

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

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

1873 computation of the exact duration requires the subtraction or 

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

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

1876 When computing an exact duration, the greatest order time 

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

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

1879 minutes, and number of seconds. 

1880 

1881 Example: 

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

1883 

1884 .. code-block:: text 

1885 

1886 P15DT5H0M20S 

1887 

1888 A duration of 7 weeks would be: 

1889 

1890 .. code-block:: text 

1891 

1892 P7W 

1893 

1894 .. code-block:: pycon 

1895 

1896 >>> from icalendar.prop import vDuration 

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

1898 >>> duration 

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

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

1901 >>> duration 

1902 datetime.timedelta(days=49) 

1903 """ 

1904 

1905 default_value: ClassVar[str] = "DURATION" 

1906 params: Parameters 

1907 

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

1909 if isinstance(td, str): 

1910 td = vDuration.from_ical(td) 

1911 if not isinstance(td, timedelta): 

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

1913 self.td = td 

1914 self.params = Parameters(params) 

1915 

1916 def to_ical(self): 

1917 sign = "" 

1918 td = self.td 

1919 if td.days < 0: 

1920 sign = "-" 

1921 td = -td 

1922 timepart = "" 

1923 if td.seconds: 

1924 timepart = "T" 

1925 hours = td.seconds // 3600 

1926 minutes = td.seconds % 3600 // 60 

1927 seconds = td.seconds % 60 

1928 if hours: 

1929 timepart += f"{hours}H" 

1930 if minutes or (hours and seconds): 

1931 timepart += f"{minutes}M" 

1932 if seconds: 

1933 timepart += f"{seconds}S" 

1934 if td.days == 0 and timepart: 

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

1936 return ( 

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

1938 + b"P" 

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

1940 + b"D" 

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

1942 ) 

1943 

1944 @staticmethod 

1945 def from_ical(ical): 

1946 match = DURATION_REGEX.match(ical) 

1947 if not match: 

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

1949 

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

1951 value = timedelta( 

1952 weeks=int(weeks or 0), 

1953 days=int(days or 0), 

1954 hours=int(hours or 0), 

1955 minutes=int(minutes or 0), 

1956 seconds=int(seconds or 0), 

1957 ) 

1958 

1959 if sign == "-": 

1960 value = -value 

1961 

1962 return value 

1963 

1964 @property 

1965 def dt(self) -> timedelta: 

1966 """The time delta for compatibility.""" 

1967 return self.td 

1968 

1969 @classmethod 

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

1971 """Examples of vDuration.""" 

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

1973 

1974 from icalendar.param import VALUE 

1975 

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

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

1978 return [ 

1979 name, 

1980 self.params.to_jcal(), 

1981 self.VALUE.lower(), 

1982 self.to_ical().decode(), 

1983 ] 

1984 

1985 @classmethod 

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

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

1988 

1989 Raises: 

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

1991 JCalParsingError.validate_value_type(jcal, str, cls) 

1992 try: 

1993 return cls.from_ical(jcal) 

1994 except ValueError as e: 

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

1996 

1997 @classmethod 

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

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

2000 

2001 Args: 

2002 jcal_property: The jCal property to parse. 

2003 

2004 Raises: 

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

2006 """ 

2007 JCalParsingError.validate_property(jcal_property, cls) 

2008 with JCalParsingError.reraise_with_path_added(3): 

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

2010 return cls( 

2011 duration, 

2012 Parameters.from_jcal_property(jcal_property), 

2013 ) 

2014 

2015 

2016class vPeriod(TimeBase): 

2017 """Period of Time 

2018 

2019 Value Name: 

2020 PERIOD 

2021 

2022 Purpose: 

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

2024 precise period of time. 

2025 

2026 Format Definition: 

2027 This value type is defined by the following notation: 

2028 

2029 .. code-block:: text 

2030 

2031 period = period-explicit / period-start 

2032 

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

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

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

2036 ; be before the end. 

2037 

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

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

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

2041 ; of time. 

2042 

2043 Description: 

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

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

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

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

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

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

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

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

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

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

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

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

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

2057 

2058 Example: 

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

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

2061 

2062 .. code-block:: text 

2063 

2064 19970101T180000Z/19970102T070000Z 

2065 

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

2067 and 30 minutes would be: 

2068 

2069 .. code-block:: text 

2070 

2071 19970101T180000Z/PT5H30M 

2072 

2073 .. code-block:: pycon 

2074 

2075 >>> from icalendar.prop import vPeriod 

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

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

2078 """ 

2079 

2080 default_value: ClassVar[str] = "PERIOD" 

2081 params: Parameters 

2082 by_duration: bool 

2083 start: datetime 

2084 end: datetime 

2085 duration: timedelta 

2086 

2087 def __init__( 

2088 self, 

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

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

2091 ): 

2092 start, end_or_duration = per 

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

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

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

2096 raise TypeError( 

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

2098 ) 

2099 by_duration = isinstance(end_or_duration, timedelta) 

2100 if by_duration: 

2101 duration = end_or_duration 

2102 end = normalize_pytz(start + duration) 

2103 else: 

2104 end = end_or_duration 

2105 duration = normalize_pytz(end - start) 

2106 if start > end: 

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

2108 

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

2110 # set the timezone identifier 

2111 # does not support different timezones for start and end 

2112 self.params.update_tzid_from(start) 

2113 

2114 self.start = start 

2115 self.end = end 

2116 self.by_duration = by_duration 

2117 self.duration = duration 

2118 

2119 def overlaps(self, other): 

2120 if self.start > other.start: 

2121 return other.overlaps(self) 

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

2123 

2124 def to_ical(self): 

2125 if self.by_duration: 

2126 return ( 

2127 vDatetime(self.start).to_ical() 

2128 + b"/" 

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

2130 ) 

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

2132 

2133 @staticmethod 

2134 def from_ical(ical, timezone=None): 

2135 try: 

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

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

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

2139 except Exception as e: 

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

2141 return (start, end_or_duration) 

2142 

2143 def __repr__(self): 

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

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

2146 

2147 @property 

2148 def dt(self): 

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

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

2151 

2152 from icalendar.param import FBTYPE 

2153 

2154 @classmethod 

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

2156 """Examples of vPeriod.""" 

2157 return [ 

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

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

2160 ] 

2161 

2162 from icalendar.param import VALUE 

2163 

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

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

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

2167 if self.by_duration: 

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

2169 else: 

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

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

2172 

2173 @classmethod 

2174 def parse_jcal_value( 

2175 cls, jcal: str | list 

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

2177 """Parse a jCal value. 

2178 

2179 Raises: 

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

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

2182 """ 

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

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

2185 jcal = jcal.split("/") 

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

2187 raise JCalParsingError( 

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

2189 ) 

2190 with JCalParsingError.reraise_with_path_added(0): 

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

2192 with JCalParsingError.reraise_with_path_added(1): 

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

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

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

2196 else: 

2197 try: 

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

2199 except JCalParsingError as e: 

2200 raise JCalParsingError( 

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

2202 cls, 

2203 value=jcal[1], 

2204 ) from e 

2205 return start, end_or_duration 

2206 

2207 @classmethod 

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

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

2210 

2211 Args: 

2212 jcal_property: The jCal property to parse. 

2213 

2214 Raises: 

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

2216 """ 

2217 JCalParsingError.validate_property(jcal_property, cls) 

2218 with JCalParsingError.reraise_with_path_added(3): 

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

2220 params = Parameters.from_jcal_property(jcal_property) 

2221 tzid = params.tzid 

2222 

2223 if tzid: 

2224 start = tzp.localize(start, tzid) 

2225 if is_datetime(end_or_duration): 

2226 end_or_duration = tzp.localize(end_or_duration, tzid) 

2227 

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

2229 

2230 

2231class vWeekday(str): 

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

2233 

2234 .. code-block:: pycon 

2235 

2236 >>> from icalendar import vWeekday 

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

2238 'MO' 

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

2240 2 

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

2242 'FR' 

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

2244 -1 

2245 

2246 Definition from :rfc:`5545#section-3.3.10`: 

2247 

2248 .. code-block:: text 

2249 

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

2251 plus = "+" 

2252 minus = "-" 

2253 ordwk = 1*2DIGIT ;1 to 53 

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

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

2256 ;FRIDAY, and SATURDAY days of the week. 

2257 

2258 """ 

2259 

2260 params: Parameters 

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

2262 

2263 week_days = CaselessDict( 

2264 { 

2265 "SU": 0, 

2266 "MO": 1, 

2267 "TU": 2, 

2268 "WE": 3, 

2269 "TH": 4, 

2270 "FR": 5, 

2271 "SA": 6, 

2272 } 

2273 ) 

2274 

2275 def __new__( 

2276 cls, 

2277 value, 

2278 encoding=DEFAULT_ENCODING, 

2279 /, 

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

2281 ): 

2282 value = to_unicode(value, encoding=encoding) 

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

2284 match = WEEKDAY_RULE.match(self) 

2285 if match is None: 

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

2287 match = match.groupdict() 

2288 sign = match["signal"] 

2289 weekday = match["weekday"] 

2290 relative = match["relative"] 

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

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

2293 self.weekday = weekday or None 

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

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

2296 self.relative *= -1 

2297 self.params = Parameters(params) 

2298 return self 

2299 

2300 def to_ical(self): 

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

2302 

2303 @classmethod 

2304 def from_ical(cls, ical): 

2305 try: 

2306 return cls(ical.upper()) 

2307 except Exception as e: 

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

2309 

2310 @classmethod 

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

2312 """Parse a jCal value for vWeekday. 

2313 

2314 Raises: 

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

2316 """ 

2317 JCalParsingError.validate_value_type(value, str, cls) 

2318 try: 

2319 return cls(value) 

2320 except ValueError as e: 

2321 raise JCalParsingError( 

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

2323 ) from e 

2324 

2325 

2326class vFrequency(str): 

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

2328 

2329 params: Parameters 

2330 __slots__ = ("params",) 

2331 

2332 frequencies = CaselessDict( 

2333 { 

2334 "SECONDLY": "SECONDLY", 

2335 "MINUTELY": "MINUTELY", 

2336 "HOURLY": "HOURLY", 

2337 "DAILY": "DAILY", 

2338 "WEEKLY": "WEEKLY", 

2339 "MONTHLY": "MONTHLY", 

2340 "YEARLY": "YEARLY", 

2341 } 

2342 ) 

2343 

2344 def __new__( 

2345 cls, 

2346 value, 

2347 encoding=DEFAULT_ENCODING, 

2348 /, 

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

2350 ): 

2351 value = to_unicode(value, encoding=encoding) 

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

2353 if self not in vFrequency.frequencies: 

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

2355 self.params = Parameters(params) 

2356 return self 

2357 

2358 def to_ical(self): 

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

2360 

2361 @classmethod 

2362 def from_ical(cls, ical): 

2363 try: 

2364 return cls(ical.upper()) 

2365 except Exception as e: 

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

2367 

2368 @classmethod 

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

2370 """Parse a jCal value for vFrequency. 

2371 

2372 Raises: 

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

2374 """ 

2375 JCalParsingError.validate_value_type(value, str, cls) 

2376 try: 

2377 return cls(value) 

2378 except ValueError as e: 

2379 raise JCalParsingError( 

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

2381 ) from e 

2382 

2383 

2384class vMonth(int): 

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

2386 

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

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

2389 

2390 .. code-block:: pycon 

2391 

2392 >>> from icalendar import vMonth 

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

2394 vMonth('1') 

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

2396 vMonth('5L') 

2397 >>> vMonth(1).leap 

2398 False 

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

2400 True 

2401 

2402 Definition from RFC: 

2403 

2404 .. code-block:: text 

2405 

2406 type-bymonth = element bymonth { 

2407 xsd:positiveInteger | 

2408 xsd:string 

2409 } 

2410 """ 

2411 

2412 params: Parameters 

2413 

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

2415 if isinstance(month, vMonth): 

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

2417 if isinstance(month, str): 

2418 if month.isdigit(): 

2419 month_index = int(month) 

2420 leap = False 

2421 else: 

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

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

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

2425 leap = True 

2426 else: 

2427 leap = False 

2428 month_index = int(month) 

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

2430 self.leap = leap 

2431 self.params = Parameters(params) 

2432 return self 

2433 

2434 def to_ical(self) -> bytes: 

2435 """The ical representation.""" 

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

2437 

2438 @classmethod 

2439 def from_ical(cls, ical: str): 

2440 return cls(ical) 

2441 

2442 @property 

2443 def leap(self) -> bool: 

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

2445 return self._leap 

2446 

2447 @leap.setter 

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

2449 self._leap = value 

2450 

2451 def __repr__(self) -> str: 

2452 """repr(self)""" 

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

2454 

2455 def __str__(self) -> str: 

2456 """str(self)""" 

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

2458 

2459 @classmethod 

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

2461 """Parse a jCal value for vMonth. 

2462 

2463 Raises: 

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

2465 """ 

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

2467 try: 

2468 return cls(value) 

2469 except ValueError as e: 

2470 raise JCalParsingError( 

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

2472 ) from e 

2473 

2474 

2475class vSkip(vText, Enum): 

2476 """Skip values for RRULE. 

2477 

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

2479 

2480 OMIT is the default value. 

2481 

2482 Examples: 

2483 

2484 .. code-block:: pycon 

2485 

2486 >>> from icalendar import vSkip 

2487 >>> vSkip.OMIT 

2488 vSkip('OMIT') 

2489 >>> vSkip.FORWARD 

2490 vSkip('FORWARD') 

2491 >>> vSkip.BACKWARD 

2492 vSkip('BACKWARD') 

2493 """ 

2494 

2495 OMIT = "OMIT" 

2496 FORWARD = "FORWARD" 

2497 BACKWARD = "BACKWARD" 

2498 

2499 __reduce_ex__ = Enum.__reduce_ex__ 

2500 

2501 def __repr__(self): 

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

2503 

2504 @classmethod 

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

2506 """Parse a jCal value for vSkip. 

2507 

2508 Raises: 

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

2510 """ 

2511 JCalParsingError.validate_value_type(value, str, cls) 

2512 try: 

2513 return cls[value.upper()] 

2514 except KeyError as e: 

2515 raise JCalParsingError( 

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

2517 ) from e 

2518 

2519 

2520class vRecur(CaselessDict): 

2521 """Recurrence definition. 

2522 

2523 Property Name: 

2524 RRULE 

2525 

2526 Purpose: 

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

2528 journal entries, or time zone definitions. 

2529 

2530 Value Type: 

2531 RECUR 

2532 

2533 Property Parameters: 

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

2535 

2536 Conformance: 

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

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

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

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

2541 

2542 Description: 

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

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

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

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

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

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

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

2550 value not synchronized with the recurrence rule is undefined. 

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

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

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

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

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

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

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

2558 

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

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

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

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

2563 same local time regardless of time zone changes. 

2564 

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

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

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

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

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

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

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

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

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

2574 "RDATE" property of PERIOD value type. 

2575 

2576 Examples: 

2577 The following RRULE specifies daily events for 10 occurrences. 

2578 

2579 .. code-block:: text 

2580 

2581 RRULE:FREQ=DAILY;COUNT=10 

2582 

2583 Below, we parse the RRULE ical string. 

2584 

2585 .. code-block:: pycon 

2586 

2587 >>> from icalendar.prop import vRecur 

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

2589 >>> rrule 

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

2591 

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

2593 :class:`icalendar.cal.Todo`. 

2594 

2595 .. code-block:: pycon 

2596 

2597 >>> from icalendar import Event 

2598 >>> event = Event() 

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

2600 >>> event.rrules 

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

2602 """ # noqa: E501 

2603 

2604 default_value: ClassVar[str] = "RECUR" 

2605 params: Parameters 

2606 

2607 frequencies = [ 

2608 "SECONDLY", 

2609 "MINUTELY", 

2610 "HOURLY", 

2611 "DAILY", 

2612 "WEEKLY", 

2613 "MONTHLY", 

2614 "YEARLY", 

2615 ] 

2616 

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

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

2619 canonical_order = ( 

2620 "RSCALE", 

2621 "FREQ", 

2622 "UNTIL", 

2623 "COUNT", 

2624 "INTERVAL", 

2625 "BYSECOND", 

2626 "BYMINUTE", 

2627 "BYHOUR", 

2628 "BYDAY", 

2629 "BYWEEKDAY", 

2630 "BYMONTHDAY", 

2631 "BYYEARDAY", 

2632 "BYWEEKNO", 

2633 "BYMONTH", 

2634 "BYSETPOS", 

2635 "WKST", 

2636 "SKIP", 

2637 ) 

2638 

2639 types = CaselessDict( 

2640 { 

2641 "COUNT": vInt, 

2642 "INTERVAL": vInt, 

2643 "BYSECOND": vInt, 

2644 "BYMINUTE": vInt, 

2645 "BYHOUR": vInt, 

2646 "BYWEEKNO": vInt, 

2647 "BYMONTHDAY": vInt, 

2648 "BYYEARDAY": vInt, 

2649 "BYMONTH": vMonth, 

2650 "UNTIL": vDDDTypes, 

2651 "BYSETPOS": vInt, 

2652 "WKST": vWeekday, 

2653 "BYDAY": vWeekday, 

2654 "FREQ": vFrequency, 

2655 "BYWEEKDAY": vWeekday, 

2656 "SKIP": vSkip, # RFC 7529 

2657 "RSCALE": vText, # RFC 7529 

2658 } 

2659 ) 

2660 

2661 # for reproducible serialization: 

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

2663 # look up in RFC 

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

2665 

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

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

2668 # we have a string as an argument. 

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

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

2671 if not isinstance(v, SEQUENCE_TYPES): 

2672 kwargs[k] = [v] 

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

2674 self.params = Parameters(params) 

2675 

2676 def to_ical(self): 

2677 result = [] 

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

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

2680 if not isinstance(vals, SEQUENCE_TYPES): 

2681 vals = [vals] # noqa: PLW2901 

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

2683 

2684 # CaselessDict keys are always unicode 

2685 param_key = key.encode(DEFAULT_ENCODING) 

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

2687 

2688 return b";".join(result) 

2689 

2690 @classmethod 

2691 def parse_type(cls, key, values): 

2692 # integers 

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

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

2695 

2696 @classmethod 

2697 def from_ical(cls, ical: str): 

2698 if isinstance(ical, cls): 

2699 return ical 

2700 try: 

2701 recur = cls() 

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

2703 try: 

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

2705 except ValueError: 

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

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

2708 continue 

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

2710 return cls(recur) 

2711 except ValueError: 

2712 raise 

2713 except Exception as e: 

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

2715 

2716 @classmethod 

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

2718 """Examples of vRecur.""" 

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

2720 

2721 from icalendar.param import VALUE 

2722 

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

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

2725 recur = {} 

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

2727 key = k.lower() 

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

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

2730 elif not isinstance(v, list): 

2731 value = [v] 

2732 else: 

2733 value = v 

2734 recur[key] = value 

2735 if "until" in recur: 

2736 until = recur["until"] 

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

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

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

2740 

2741 @classmethod 

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

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

2744 

2745 Args: 

2746 jcal_property: The jCal property to parse. 

2747 

2748 Raises: 

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

2750 """ 

2751 JCalParsingError.validate_property(jcal_property, cls) 

2752 params = Parameters.from_jcal_property(jcal_property) 

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

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

2755 ): 

2756 raise JCalParsingError( 

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

2758 cls, 

2759 3, 

2760 value=jcal_property[3], 

2761 ) 

2762 recur = {} 

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

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

2765 with JCalParsingError.reraise_with_path_added(3, key): 

2766 if isinstance(value, list): 

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

2768 for i, v in enumerate(value): 

2769 with JCalParsingError.reraise_with_path_added(i): 

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

2771 else: 

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

2773 until = recur.get("until") 

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

2775 recur["until"] = [until] 

2776 return cls(recur, params=params) 

2777 

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

2779 """self == other""" 

2780 if not isinstance(other, vRecur): 

2781 return super().__eq__(other) 

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

2783 return False 

2784 for key in self.keys(): 

2785 v1 = self[key] 

2786 v2 = other[key] 

2787 if not isinstance(v1, SEQUENCE_TYPES): 

2788 v1 = [v1] 

2789 if not isinstance(v2, SEQUENCE_TYPES): 

2790 v2 = [v2] 

2791 if v1 != v2: 

2792 return False 

2793 return True 

2794 

2795 

2796TIME_JCAL_REGEX = re.compile( 

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

2798) 

2799 

2800 

2801class vTime(TimeBase): 

2802 """Time 

2803 

2804 Value Name: 

2805 TIME 

2806 

2807 Purpose: 

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

2809 time of day. 

2810 

2811 Format Definition: 

2812 This value type is defined by the following notation: 

2813 

2814 .. code-block:: text 

2815 

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

2817 

2818 time-hour = 2DIGIT ;00-23 

2819 time-minute = 2DIGIT ;00-59 

2820 time-second = 2DIGIT ;00-60 

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

2822 

2823 time-utc = "Z" 

2824 

2825 Description: 

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

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

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

2829 vText) is defined for this value type. 

2830 

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

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

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

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

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

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

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

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

2839 

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

2841 type expresses time values in three forms: 

2842 

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

2844 the following is not valid for a time value: 

2845 

2846 .. code-block:: text 

2847 

2848 230000-0800 ;Invalid time format 

2849 

2850 **FORM #1 LOCAL TIME** 

2851 

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

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

2854 example, 11:00 PM: 

2855 

2856 .. code-block:: text 

2857 

2858 230000 

2859 

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

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

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

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

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

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

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

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

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

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

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

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

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

2873 

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

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

2876 time zone reference MUST be specified. 

2877 

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

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

2880 existence of "VTIMEZONE" calendar components in the iCalendar 

2881 object. 

2882 

2883 **FORM #2: UTC TIME** 

2884 

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

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

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

2888 

2889 .. code-block:: text 

2890 

2891 070000Z 

2892 

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

2894 properties whose time values are specified in UTC. 

2895 

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

2897 

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

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

2900 the appropriate time zone definition. 

2901 

2902 Example: 

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

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

2905 

2906 .. code-block:: text 

2907 

2908 083000 

2909 133000Z 

2910 TZID=America/New_York:083000 

2911 """ 

2912 

2913 default_value: ClassVar[str] = "TIME" 

2914 params: Parameters 

2915 

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

2917 if len(args) == 1: 

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

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

2920 self.dt = args[0] 

2921 else: 

2922 self.dt = time(*args) 

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

2924 self.params.update_tzid_from(self.dt) 

2925 

2926 def to_ical(self): 

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

2928 if self.is_utc(): 

2929 value += "Z" 

2930 return value 

2931 

2932 def is_utc(self) -> bool: 

2933 """Whether this time is UTC.""" 

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

2935 

2936 @staticmethod 

2937 def from_ical(ical): 

2938 # TODO: timezone support 

2939 try: 

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

2941 return time(*timetuple) 

2942 except Exception as e: 

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

2944 

2945 @classmethod 

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

2947 """Examples of vTime.""" 

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

2949 

2950 from icalendar.param import VALUE 

2951 

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

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

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

2955 if self.is_utc(): 

2956 value += "Z" 

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

2958 

2959 @classmethod 

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

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

2962 

2963 Raises: 

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

2965 """ 

2966 JCalParsingError.validate_value_type(jcal, str, cls) 

2967 match = TIME_JCAL_REGEX.match(jcal) 

2968 if match is None: 

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

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

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

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

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

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

2975 

2976 @classmethod 

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

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

2979 

2980 Args: 

2981 jcal_property: The jCal property to parse. 

2982 

2983 Raises: 

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

2985 """ 

2986 JCalParsingError.validate_property(jcal_property, cls) 

2987 with JCalParsingError.reraise_with_path_added(3): 

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

2989 return cls( 

2990 value, 

2991 params=Parameters.from_jcal_property(jcal_property), 

2992 ) 

2993 

2994 

2995class vUri(str): 

2996 """URI 

2997 

2998 Value Name: 

2999 URI 

3000 

3001 Purpose: 

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

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

3004 property value. 

3005 

3006 Format Definition: 

3007 This value type is defined by the following notation: 

3008 

3009 .. code-block:: text 

3010 

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

3012 

3013 Description: 

3014 This value type might be used to reference binary 

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

3016 to include directly in the iCalendar object. 

3017 

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

3019 syntax defined in [RFC3986]. 

3020 

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

3022 be specified as a quoted-string value. 

3023 

3024 Examples: 

3025 The following is a URI for a network file: 

3026 

3027 .. code-block:: text 

3028 

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

3030 

3031 .. code-block:: pycon 

3032 

3033 >>> from icalendar.prop import vUri 

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

3035 >>> uri 

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

3037 >>> uri.uri 

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

3039 """ 

3040 

3041 default_value: ClassVar[str] = "URI" 

3042 params: Parameters 

3043 __slots__ = ("params",) 

3044 

3045 def __new__( 

3046 cls, 

3047 value: str, 

3048 encoding: str = DEFAULT_ENCODING, 

3049 /, 

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

3051 ): 

3052 value = to_unicode(value, encoding=encoding) 

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

3054 self.params = Parameters(params) 

3055 return self 

3056 

3057 def to_ical(self): 

3058 return self.encode(DEFAULT_ENCODING) 

3059 

3060 @classmethod 

3061 def from_ical(cls, ical): 

3062 try: 

3063 return cls(ical) 

3064 except Exception as e: 

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

3066 

3067 @classmethod 

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

3069 """Examples of vUri.""" 

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

3071 

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

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

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

3075 

3076 @classmethod 

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

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

3079 

3080 Args: 

3081 jcal_property: The jCal property to parse. 

3082 

3083 Raises: 

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

3085 """ 

3086 JCalParsingError.validate_property(jcal_property, cls) 

3087 return cls( 

3088 jcal_property[3], 

3089 Parameters.from_jcal_property(jcal_property), 

3090 ) 

3091 

3092 @property 

3093 def ical_value(self) -> str: 

3094 """The URI.""" 

3095 return self.uri 

3096 

3097 @property 

3098 def uri(self) -> str: 

3099 """The URI.""" 

3100 return str(self) 

3101 

3102 def __repr__(self) -> str: 

3103 """repr(self)""" 

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

3105 

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

3107 

3108 

3109class vUid(vText): 

3110 """A UID of a component. 

3111 

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

3113 """ 

3114 

3115 default_value: ClassVar[str] = "UID" 

3116 

3117 @classmethod 

3118 def new(cls): 

3119 """Create a new UID for convenience. 

3120 

3121 .. code-block:: pycon 

3122 

3123 >>> from icalendar import vUid 

3124 >>> vUid.new() 

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

3126 

3127 """ 

3128 return vUid(uuid.uuid4()) 

3129 

3130 @property 

3131 def uid(self) -> str: 

3132 """The UID of this property.""" 

3133 return str(self) 

3134 

3135 @property 

3136 def ical_value(self) -> str: 

3137 """The UID of this property.""" 

3138 return self.uid 

3139 

3140 def __repr__(self) -> str: 

3141 """repr(self)""" 

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

3143 

3144 from icalendar.param import FMTTYPE, LABEL, LINKREL 

3145 

3146 @classmethod 

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

3148 """Examples of vUid.""" 

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

3150 

3151 

3152class vXmlReference(vUri): 

3153 """An XML-REFERENCE. 

3154 

3155 The associated value references an associated XML artifact and 

3156 is a URI with an XPointer anchor value. 

3157 

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

3159 """ 

3160 

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

3162 

3163 @property 

3164 def xml_reference(self) -> str: 

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

3166 return self.uri 

3167 

3168 @property 

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

3170 """The XPointer of the URI. 

3171 

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

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

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

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

3176 

3177 Returns: 

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

3179 """ 

3180 from urllib.parse import unquote, urlparse 

3181 

3182 parsed = urlparse(self.xml_reference) 

3183 fragment = unquote(parsed.fragment) 

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

3185 return None 

3186 return fragment[9:-1] 

3187 

3188 @classmethod 

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

3190 """Examples of vXmlReference.""" 

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

3192 

3193 

3194class vGeo: 

3195 """Geographic Position 

3196 

3197 Property Name: 

3198 GEO 

3199 

3200 Purpose: 

3201 This property specifies information related to the global 

3202 position for the activity specified by a calendar component. 

3203 

3204 Value Type: 

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

3206 

3207 Property Parameters: 

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

3209 this property. 

3210 

3211 Conformance: 

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

3213 calendar components. 

3214 

3215 Description: 

3216 This property value specifies latitude and longitude, 

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

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

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

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

3221 will allow for accuracy to within one meter of geographical 

3222 position. Receiving applications MUST accept values of this 

3223 precision and MAY truncate values of greater precision. 

3224 

3225 Example: 

3226 

3227 .. code-block:: text 

3228 

3229 GEO:37.386013;-122.082932 

3230 

3231 Parse vGeo: 

3232 

3233 .. code-block:: pycon 

3234 

3235 >>> from icalendar.prop import vGeo 

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

3237 >>> geo 

3238 (37.386013, -122.082932) 

3239 

3240 Add a geo location to an event: 

3241 

3242 .. code-block:: pycon 

3243 

3244 >>> from icalendar import Event 

3245 >>> event = Event() 

3246 >>> latitude = 37.386013 

3247 >>> longitude = -122.082932 

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

3249 >>> event['GEO'] 

3250 vGeo((37.386013, -122.082932)) 

3251 """ 

3252 

3253 default_value: ClassVar[str] = "FLOAT" 

3254 params: Parameters 

3255 

3256 def __init__( 

3257 self, 

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

3259 /, 

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

3261 ): 

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

3263 

3264 Raises: 

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

3266 """ 

3267 try: 

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

3269 latitude = float(latitude) 

3270 longitude = float(longitude) 

3271 except Exception as e: 

3272 raise ValueError( 

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

3274 ) from e 

3275 self.latitude = latitude 

3276 self.longitude = longitude 

3277 self.params = Parameters(params) 

3278 

3279 def to_ical(self): 

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

3281 

3282 @staticmethod 

3283 def from_ical(ical): 

3284 try: 

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

3286 return (float(latitude), float(longitude)) 

3287 except Exception as e: 

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

3289 

3290 def __eq__(self, other): 

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

3292 

3293 def __repr__(self): 

3294 """repr(self)""" 

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

3296 

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

3298 """Convert to jCal object.""" 

3299 return [ 

3300 name, 

3301 self.params.to_jcal(), 

3302 self.VALUE.lower(), 

3303 [self.latitude, self.longitude], 

3304 ] 

3305 

3306 @classmethod 

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

3308 """Examples of vGeo.""" 

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

3310 

3311 from icalendar.param import VALUE 

3312 

3313 @classmethod 

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

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

3316 

3317 Args: 

3318 jcal_property: The jCal property to parse. 

3319 

3320 Raises: 

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

3322 """ 

3323 JCalParsingError.validate_property(jcal_property, cls) 

3324 return cls( 

3325 jcal_property[3], 

3326 Parameters.from_jcal_property(jcal_property), 

3327 ) 

3328 

3329 

3330UTC_OFFSET_JCAL_REGEX = re.compile( 

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

3332) 

3333 

3334 

3335class vUTCOffset: 

3336 """UTC Offset 

3337 

3338 Value Name: 

3339 UTC-OFFSET 

3340 

3341 Purpose: 

3342 This value type is used to identify properties that contain 

3343 an offset from UTC to local time. 

3344 

3345 Format Definition: 

3346 This value type is defined by the following notation: 

3347 

3348 .. code-block:: text 

3349 

3350 utc-offset = time-numzone 

3351 

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

3353 

3354 Description: 

3355 The PLUS SIGN character MUST be specified for positive 

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

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

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

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

3360 

3361 Example: 

3362 The following UTC offsets are given for standard time for 

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

3364 UTC): 

3365 

3366 .. code-block:: text 

3367 

3368 -0500 

3369 

3370 +0100 

3371 

3372 .. code-block:: pycon 

3373 

3374 >>> from icalendar.prop import vUTCOffset 

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

3376 >>> utc_offset 

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

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

3379 >>> utc_offset 

3380 datetime.timedelta(seconds=3600) 

3381 """ 

3382 

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

3384 params: Parameters 

3385 

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

3387 

3388 # component, we will silently ignore 

3389 # it, rather than let the exception 

3390 # propagate upwards 

3391 

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

3393 if not isinstance(td, timedelta): 

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

3395 self.td = td 

3396 self.params = Parameters(params) 

3397 

3398 def to_ical(self) -> str: 

3399 """Return the ical representation.""" 

3400 return self.format("") 

3401 

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

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

3404 

3405 .. code-block:: pycon 

3406 

3407 >>> from icalendar import vUTCOffset 

3408 >>> from datetime import timedelta 

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

3410 >>> utc_offset.format() 

3411 '-0500' 

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

3413 '-05:00' 

3414 """ 

3415 if self.td < timedelta(0): 

3416 sign = "-%s" 

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

3418 else: 

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

3420 sign = "+%s" 

3421 td = self.td 

3422 

3423 days, seconds = td.days, td.seconds 

3424 

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

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

3427 seconds = abs(seconds % 60) 

3428 if seconds: 

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

3430 else: 

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

3432 return sign % duration 

3433 

3434 @classmethod 

3435 def from_ical(cls, ical): 

3436 if isinstance(ical, cls): 

3437 return ical.td 

3438 try: 

3439 sign, hours, minutes, seconds = ( 

3440 ical[0:1], 

3441 int(ical[1:3]), 

3442 int(ical[3:5]), 

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

3444 ) 

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

3446 except Exception as e: 

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

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

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

3450 if sign == "-": 

3451 return -offset 

3452 return offset 

3453 

3454 def __eq__(self, other): 

3455 if not isinstance(other, vUTCOffset): 

3456 return False 

3457 return self.td == other.td 

3458 

3459 def __hash__(self): 

3460 return hash(self.td) 

3461 

3462 def __repr__(self): 

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

3464 

3465 @classmethod 

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

3467 """Examples of vUTCOffset.""" 

3468 return [ 

3469 cls(timedelta(hours=3)), 

3470 cls(timedelta(0)), 

3471 ] 

3472 

3473 from icalendar.param import VALUE 

3474 

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

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

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

3478 

3479 @classmethod 

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

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

3482 

3483 Args: 

3484 jcal_property: The jCal property to parse. 

3485 

3486 Raises: 

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

3488 """ 

3489 JCalParsingError.validate_property(jcal_property, cls) 

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

3491 if match is None: 

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

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

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

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

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

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

3498 if negative: 

3499 t = -t 

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

3501 

3502 

3503class vInline(str): 

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

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

3506 class, so no further processing is needed. 

3507 """ 

3508 

3509 params: Parameters 

3510 __slots__ = ("params",) 

3511 

3512 def __new__( 

3513 cls, 

3514 value, 

3515 encoding=DEFAULT_ENCODING, 

3516 /, 

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

3518 ): 

3519 value = to_unicode(value, encoding=encoding) 

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

3521 self.params = Parameters(params) 

3522 return self 

3523 

3524 def to_ical(self): 

3525 return self.encode(DEFAULT_ENCODING) 

3526 

3527 @classmethod 

3528 def from_ical(cls, ical): 

3529 return cls(ical) 

3530 

3531 

3532class vUnknown(vText): 

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

3534 

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

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

3537 """ 

3538 

3539 default_value: ClassVar[str] = "UNKNOWN" 

3540 

3541 @classmethod 

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

3543 """Examples of vUnknown.""" 

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

3545 

3546 from icalendar.param import VALUE 

3547 

3548 

3549class TypesFactory(CaselessDict): 

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

3551 class. 

3552 

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

3554 both kinds. 

3555 """ 

3556 

3557 _instance: ClassVar[TypesFactory] = None 

3558 

3559 def instance() -> TypesFactory: 

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

3561 if TypesFactory._instance is None: 

3562 TypesFactory._instance = TypesFactory() 

3563 return TypesFactory._instance 

3564 

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

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

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

3568 self.all_types = ( 

3569 vBinary, 

3570 vBoolean, 

3571 vCalAddress, 

3572 vDDDLists, 

3573 vDDDTypes, 

3574 vDate, 

3575 vDatetime, 

3576 vDuration, 

3577 vFloat, 

3578 vFrequency, 

3579 vGeo, 

3580 vInline, 

3581 vInt, 

3582 vPeriod, 

3583 vRecur, 

3584 vText, 

3585 vTime, 

3586 vUTCOffset, 

3587 vUri, 

3588 vWeekday, 

3589 vCategory, 

3590 vAdr, 

3591 vN, 

3592 vOrg, 

3593 vUid, 

3594 vXmlReference, 

3595 vUnknown, 

3596 ) 

3597 self["binary"] = vBinary 

3598 self["boolean"] = vBoolean 

3599 self["cal-address"] = vCalAddress 

3600 self["date"] = vDDDTypes 

3601 self["date-time"] = vDDDTypes 

3602 self["duration"] = vDDDTypes 

3603 self["float"] = vFloat 

3604 self["integer"] = vInt 

3605 self["period"] = vPeriod 

3606 self["recur"] = vRecur 

3607 self["text"] = vText 

3608 self["time"] = vTime 

3609 self["uri"] = vUri 

3610 self["utc-offset"] = vUTCOffset 

3611 self["geo"] = vGeo 

3612 self["inline"] = vInline 

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

3614 self["categories"] = vCategory 

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

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

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

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

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

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

3621 

3622 ################################################# 

3623 # Property types 

3624 

3625 # These are the default types 

3626 types_map = CaselessDict( 

3627 { 

3628 #################################### 

3629 # Property value types 

3630 # Calendar Properties 

3631 "calscale": "text", 

3632 "method": "text", 

3633 "prodid": "text", 

3634 "version": "text", 

3635 # Descriptive Component Properties 

3636 "attach": "uri", 

3637 "categories": "categories", 

3638 "class": "text", 

3639 # vCard Properties (RFC 6350) 

3640 "adr": "adr", 

3641 "n": "n", 

3642 "org": "org", 

3643 "comment": "text", 

3644 "description": "text", 

3645 "geo": "geo", 

3646 "location": "text", 

3647 "percent-complete": "integer", 

3648 "priority": "integer", 

3649 "resources": "text", 

3650 "status": "text", 

3651 "summary": "text", 

3652 # RFC 9253 

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

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

3655 "link": "uri", 

3656 "concept": "uri", 

3657 "refid": "text", 

3658 # Date and Time Component Properties 

3659 "completed": "date-time", 

3660 "dtend": "date-time", 

3661 "due": "date-time", 

3662 "dtstart": "date-time", 

3663 "duration": "duration", 

3664 "freebusy": "period", 

3665 "transp": "text", 

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

3667 # Time Zone Component Properties 

3668 "tzid": "text", 

3669 "tzname": "text", 

3670 "tzoffsetfrom": "utc-offset", 

3671 "tzoffsetto": "utc-offset", 

3672 "tzurl": "uri", 

3673 # Relationship Component Properties 

3674 "attendee": "cal-address", 

3675 "contact": "text", 

3676 "organizer": "cal-address", 

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

3678 "related-to": "text", 

3679 "url": "uri", 

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

3681 "source": "uri", 

3682 "uid": "text", 

3683 # Recurrence Component Properties 

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

3685 "exrule": "recur", 

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

3687 "rrule": "recur", 

3688 # Alarm Component Properties 

3689 "action": "text", 

3690 "repeat": "integer", 

3691 "trigger": "duration", 

3692 "acknowledged": "date-time", 

3693 # Change Management Component Properties 

3694 "created": "date-time", 

3695 "dtstamp": "date-time", 

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

3697 "sequence": "integer", 

3698 # Miscellaneous Component Properties 

3699 "request-status": "text", 

3700 #################################### 

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

3702 "altrep": "uri", 

3703 "cn": "text", 

3704 "cutype": "text", 

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

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

3707 "dir": "uri", 

3708 "encoding": "text", 

3709 "fmttype": "text", 

3710 "fbtype": "text", 

3711 "language": "text", 

3712 "member": "cal-address", 

3713 "partstat": "text", 

3714 "range": "text", 

3715 "related": "text", 

3716 "reltype": "text", 

3717 "role": "text", 

3718 "rsvp": "boolean", 

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

3720 "value": "text", 

3721 # rfc 9253 parameters 

3722 "label": "text", 

3723 "linkrel": "text", 

3724 "gap": "duration", 

3725 } 

3726 ) 

3727 

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

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

3730 

3731 Args: 

3732 name: Property or parameter name 

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

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

3735 

3736 Returns: 

3737 The appropriate value type class. 

3738 """ 

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

3740 # regardless of the VALUE parameter 

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

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

3743 

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

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

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

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

3748 return self[value_param] 

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

3750 

3751 def to_ical(self, name, value): 

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

3753 encoded string. 

3754 """ 

3755 type_class = self.for_property(name) 

3756 return type_class(value).to_ical() 

3757 

3758 def from_ical(self, name, value): 

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

3760 encoded string to a primitive python type. 

3761 """ 

3762 type_class = self.for_property(name) 

3763 return type_class.from_ical(value) 

3764 

3765 

3766VPROPERTY: TypeAlias = Union[ 

3767 vAdr, 

3768 vBoolean, 

3769 vCalAddress, 

3770 vCategory, 

3771 vDDDLists, 

3772 vDDDTypes, 

3773 vDate, 

3774 vDatetime, 

3775 vDuration, 

3776 vFloat, 

3777 vFrequency, 

3778 vInt, 

3779 vMonth, 

3780 vN, 

3781 vOrg, 

3782 vPeriod, 

3783 vRecur, 

3784 vSkip, 

3785 vText, 

3786 vTime, 

3787 vUTCOffset, 

3788 vUri, 

3789 vWeekday, 

3790 vInline, 

3791 vBinary, 

3792 vGeo, 

3793 vUnknown, 

3794 vXmlReference, 

3795 vUid, 

3796] 

3797 

3798__all__ = [ 

3799 "DURATION_REGEX", 

3800 "VPROPERTY", 

3801 "WEEKDAY_RULE", 

3802 "TimeBase", 

3803 "TypesFactory", 

3804 "tzid_from_dt", 

3805 "tzid_from_tzinfo", 

3806 "vAdr", 

3807 "vBinary", 

3808 "vBoolean", 

3809 "vCalAddress", 

3810 "vCategory", 

3811 "vDDDLists", 

3812 "vDDDTypes", 

3813 "vDate", 

3814 "vDatetime", 

3815 "vDuration", 

3816 "vFloat", 

3817 "vFrequency", 

3818 "vGeo", 

3819 "vInline", 

3820 "vInt", 

3821 "vMonth", 

3822 "vN", 

3823 "vOrg", 

3824 "vPeriod", 

3825 "vRecur", 

3826 "vSkip", 

3827 "vText", 

3828 "vTime", 

3829 "vUTCOffset", 

3830 "vUid", 

3831 "vUnknown", 

3832 "vUri", 

3833 "vWeekday", 

3834 "vXmlReference", 

3835]