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

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

750 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 

52from typing import Any, Union 

53 

54from icalendar.caselessdict import CaselessDict 

55from icalendar.enums import Enum 

56from icalendar.error import InvalidCalendar 

57from icalendar.parser import Parameters, escape_char, unescape_char 

58from icalendar.parser_tools import ( 

59 DEFAULT_ENCODING, 

60 ICAL_TYPE, 

61 SEQUENCE_TYPES, 

62 from_unicode, 

63 to_unicode, 

64) 

65from icalendar.timezone import tzid_from_dt, tzid_from_tzinfo, tzp 

66from icalendar.tools import to_datetime 

67 

68DURATION_REGEX = re.compile( 

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

70) 

71 

72WEEKDAY_RULE = re.compile( 

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

74) 

75 

76 

77class vBinary: 

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

79 

80 params: Parameters 

81 obj: str 

82 

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

84 self.obj = to_unicode(obj) 

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

86 if params: 

87 self.params.update(params) 

88 

89 def __repr__(self): 

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

91 

92 def to_ical(self): 

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

94 

95 @staticmethod 

96 def from_ical(ical): 

97 try: 

98 return base64.b64decode(ical) 

99 except ValueError as e: 

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

101 

102 def __eq__(self, other): 

103 """self == other""" 

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

105 

106 

107class vBoolean(int): 

108 """Boolean 

109 

110 Value Name: BOOLEAN 

111 

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

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

114 

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

116 notation: 

117 

118 .. code-block:: text 

119 

120 boolean = "TRUE" / "FALSE" 

121 

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

123 content value encoding is defined for this value type. 

124 

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

126 has a BOOLEAN value type: 

127 

128 .. code-block:: python 

129 

130 TRUE 

131 

132 .. code-block:: pycon 

133 

134 >>> from icalendar.prop import vBoolean 

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

136 >>> boolean 

137 True 

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

139 >>> boolean 

140 False 

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

142 >>> boolean 

143 True 

144 """ 

145 

146 params: Parameters 

147 

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

149 

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

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

152 self.params = Parameters(params) 

153 return self 

154 

155 def to_ical(self): 

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

157 

158 @classmethod 

159 def from_ical(cls, ical): 

160 try: 

161 return cls.BOOL_MAP[ical] 

162 except Exception as e: 

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

164 

165 

166class vText(str): 

167 """Simple text.""" 

168 

169 params: Parameters 

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

171 

172 def __new__( 

173 cls, 

174 value, 

175 encoding=DEFAULT_ENCODING, 

176 /, 

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

178 ): 

179 value = to_unicode(value, encoding=encoding) 

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

181 self.encoding = encoding 

182 self.params = Parameters(params) 

183 return self 

184 

185 def __repr__(self) -> str: 

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

187 

188 def to_ical(self) -> bytes: 

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

190 

191 @classmethod 

192 def from_ical(cls, ical: ICAL_TYPE): 

193 ical_unesc = unescape_char(ical) 

194 return cls(ical_unesc) 

195 

196 @property 

197 def ical_value(self) -> str: 

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

199 return str(self) 

200 

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

202 

203 

204class vCalAddress(str): 

205 r"""Calendar User Address 

206 

207 Value Name: 

208 CAL-ADDRESS 

209 

210 Purpose: 

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

212 calendar user address. 

213 

214 Description: 

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

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

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

218 mailto URI, as defined by [RFC2368]. 

219 

220 Example: 

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

222 

223 .. code-block:: text 

224 

225 mailto:jane_doe@example.com 

226 

227 Parsing: 

228 

229 .. code-block:: pycon 

230 

231 >>> from icalendar import vCalAddress 

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

233 >>> cal_address 

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

235 

236 Encoding: 

237 

238 .. code-block:: pycon 

239 

240 >>> from icalendar import vCalAddress, Event 

241 >>> event = Event() 

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

243 >>> jane.name = "Jane" 

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

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

246 BEGIN:VEVENT 

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

248 END:VEVENT 

249 """ 

250 

251 params: Parameters 

252 __slots__ = ("params",) 

253 

254 def __new__( 

255 cls, 

256 value, 

257 encoding=DEFAULT_ENCODING, 

258 /, 

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

260 ): 

261 value = to_unicode(value, encoding=encoding) 

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

263 self.params = Parameters(params) 

264 return self 

265 

266 def __repr__(self): 

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

268 

269 def to_ical(self): 

270 return self.encode(DEFAULT_ENCODING) 

271 

272 @classmethod 

273 def from_ical(cls, ical): 

274 return cls(ical) 

275 

276 @property 

277 def email(self) -> str: 

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

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

280 return self[7:] 

281 return str(self) 

282 

283 from icalendar.param import ( 

284 CN, 

285 CUTYPE, 

286 DELEGATED_FROM, 

287 DELEGATED_TO, 

288 DIR, 

289 LANGUAGE, 

290 PARTSTAT, 

291 ROLE, 

292 RSVP, 

293 SENT_BY, 

294 ) 

295 

296 name = CN 

297 

298 @staticmethod 

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

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

301 

302 Handles case-insensitive mailto: prefix checking. 

303 

304 Args: 

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

306 

307 Returns: 

308 Email string with mailto: prefix 

309 """ 

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

311 return f"mailto:{email}" 

312 return email 

313 

314 @classmethod 

315 def new( 

316 cls, 

317 email: str, 

318 /, 

319 cn: str | None = None, 

320 cutype: str | None = None, 

321 delegated_from: str | None = None, 

322 delegated_to: str | None = None, 

323 directory: str | None = None, 

324 language: str | None = None, 

325 partstat: str | None = None, 

326 role: str | None = None, 

327 rsvp: bool | None = None, 

328 sent_by: str | None = None, 

329 ): 

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

331 

332 Creates a vCalAddress instance with automatic mailto: prefix handling 

333 and support for all standard RFC 5545 parameters. 

334 

335 Args: 

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

337 cn: Common Name parameter 

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

339 delegated_from: Email of the calendar user that delegated 

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

341 directory: Reference to directory information 

342 language: Language for text values 

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

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

345 rsvp: Whether RSVP is requested 

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

347 

348 Returns: 

349 vCalAddress: A new calendar address with specified parameters 

350 

351 Raises: 

352 TypeError: If email is not a string 

353 

354 Examples: 

355 Basic usage: 

356 

357 >>> from icalendar.prop import vCalAddress 

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

359 >>> str(addr) 

360 'mailto:test@test.com' 

361 

362 With parameters: 

363 

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

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

366 'Test User' 

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

368 'CHAIR' 

369 """ 

370 if not isinstance(email, str): 

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

372 

373 # Handle mailto: prefix (case-insensitive) 

374 email_with_prefix = cls._get_email(email) 

375 

376 # Create the address 

377 addr = cls(email_with_prefix) 

378 

379 # Set parameters if provided 

380 if cn is not None: 

381 addr.params["CN"] = cn 

382 if cutype is not None: 

383 addr.params["CUTYPE"] = cutype 

384 if delegated_from is not None: 

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

386 if delegated_to is not None: 

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

388 if directory is not None: 

389 addr.params["DIR"] = directory 

390 if language is not None: 

391 addr.params["LANGUAGE"] = language 

392 if partstat is not None: 

393 addr.params["PARTSTAT"] = partstat 

394 if role is not None: 

395 addr.params["ROLE"] = role 

396 if rsvp is not None: 

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

398 if sent_by is not None: 

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

400 

401 return addr 

402 

403 

404class vFloat(float): 

405 """Float 

406 

407 Value Name: 

408 FLOAT 

409 

410 Purpose: 

411 This value type is used to identify properties that contain 

412 a real-number value. 

413 

414 Format Definition: 

415 This value type is defined by the following notation: 

416 

417 .. code-block:: text 

418 

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

420 

421 Description: 

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

423 specified by a COMMA-separated list of values. 

424 

425 Example: 

426 

427 .. code-block:: text 

428 

429 1000000.0000001 

430 1.333 

431 -3.14 

432 

433 .. code-block:: pycon 

434 

435 >>> from icalendar.prop import vFloat 

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

437 >>> float 

438 1000000.0000001 

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

440 >>> float 

441 1.333 

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

443 >>> float 

444 1.333 

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

446 >>> float 

447 -3.14 

448 """ 

449 

450 params: Parameters 

451 

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

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

454 self.params = Parameters(params) 

455 return self 

456 

457 def to_ical(self): 

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

459 

460 @classmethod 

461 def from_ical(cls, ical): 

462 try: 

463 return cls(ical) 

464 except Exception as e: 

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

466 

467 

468class vInt(int): 

469 """Integer 

470 

471 Value Name: 

472 INTEGER 

473 

474 Purpose: 

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

476 signed integer value. 

477 

478 Format Definition: 

479 This value type is defined by the following notation: 

480 

481 .. code-block:: text 

482 

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

484 

485 Description: 

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

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

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

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

490 

491 Example: 

492 

493 .. code-block:: text 

494 

495 1234567890 

496 -1234567890 

497 +1234567890 

498 432109876 

499 

500 .. code-block:: pycon 

501 

502 >>> from icalendar.prop import vInt 

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

504 >>> integer 

505 1234567890 

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

507 >>> integer 

508 -1234567890 

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

510 >>> integer 

511 1234567890 

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

513 >>> integer 

514 432109876 

515 """ 

516 

517 params: Parameters 

518 

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

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

521 self.params = Parameters(params) 

522 return self 

523 

524 def to_ical(self) -> bytes: 

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

526 

527 @classmethod 

528 def from_ical(cls, ical: ICAL_TYPE): 

529 try: 

530 return cls(ical) 

531 except Exception as e: 

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

533 

534 

535class vDDDLists: 

536 """A list of vDDDTypes values.""" 

537 

538 params: Parameters 

539 dts: list 

540 

541 def __init__(self, dt_list): 

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

543 dt_list = [dt_list] 

544 vddd = [] 

545 tzid = None 

546 for dt_l in dt_list: 

547 dt = vDDDTypes(dt_l) 

548 vddd.append(dt) 

549 if "TZID" in dt.params: 

550 tzid = dt.params["TZID"] 

551 

552 params = {} 

553 if tzid: 

554 # NOTE: no support for multiple timezones here! 

555 params["TZID"] = tzid 

556 self.params = Parameters(params) 

557 self.dts = vddd 

558 

559 def to_ical(self): 

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

561 return b",".join(dts_ical) 

562 

563 @staticmethod 

564 def from_ical(ical, timezone=None): 

565 out = [] 

566 ical_dates = ical.split(",") 

567 for ical_dt in ical_dates: 

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

569 return out 

570 

571 def __eq__(self, other): 

572 if isinstance(other, vDDDLists): 

573 return self.dts == other.dts 

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

575 return self.dts == [other] 

576 return False 

577 

578 def __repr__(self): 

579 """String representation.""" 

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

581 

582 

583class vCategory: 

584 params: Parameters 

585 

586 def __init__( 

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

588 ): 

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

590 c_list = [c_list] 

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

592 self.params = Parameters(params) 

593 

594 def __iter__(self): 

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

596 

597 def to_ical(self): 

598 return b",".join( 

599 [ 

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

601 for c in self.cats 

602 ] 

603 ) 

604 

605 @staticmethod 

606 def from_ical(ical): 

607 ical = to_unicode(ical) 

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

609 

610 def __eq__(self, other): 

611 """self == other""" 

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

613 

614 def __repr__(self): 

615 """String representation.""" 

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

617 

618 

619class TimeBase: 

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

621 

622 params: Parameters 

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

624 

625 def __eq__(self, other): 

626 """self == other""" 

627 if isinstance(other, date): 

628 return self.dt == other 

629 if isinstance(other, TimeBase): 

630 default = object() 

631 for key in ( 

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

633 ) - self.ignore_for_equality: 

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

635 key, default 

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

637 return False 

638 return self.dt == other.dt 

639 if isinstance(other, vDDDLists): 

640 return other == self 

641 return False 

642 

643 def __hash__(self): 

644 return hash(self.dt) 

645 

646 from icalendar.param import RANGE, RELATED, TZID 

647 

648 def __repr__(self): 

649 """String representation.""" 

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

651 

652 

653class vDDDTypes(TimeBase): 

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

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

656 So this is practical. 

657 """ 

658 

659 params: Parameters 

660 

661 def __init__(self, dt): 

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

663 raise TypeError( 

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

665 ) 

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

667 self.params = Parameters() 

668 elif isinstance(dt, date): 

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

670 elif isinstance(dt, time): 

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

672 else: # isinstance(dt, tuple) 

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

674 

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

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

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

678 

679 self.dt = dt 

680 

681 def to_ical(self): 

682 dt = self.dt 

683 if isinstance(dt, datetime): 

684 return vDatetime(dt).to_ical() 

685 if isinstance(dt, date): 

686 return vDate(dt).to_ical() 

687 if isinstance(dt, timedelta): 

688 return vDuration(dt).to_ical() 

689 if isinstance(dt, time): 

690 return vTime(dt).to_ical() 

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

692 return vPeriod(dt).to_ical() 

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

694 

695 @classmethod 

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

697 if isinstance(ical, cls): 

698 return ical.dt 

699 u = ical.upper() 

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

701 return vDuration.from_ical(ical) 

702 if "/" in u: 

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

704 

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

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

707 if len(ical) == 8: 

708 if timezone: 

709 tzinfo = tzp.timezone(timezone) 

710 if tzinfo is not None: 

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

712 return vDate.from_ical(ical) 

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

714 return vTime.from_ical(ical) 

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

716 

717 @property 

718 def td(self) -> timedelta: 

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

720 

721 This class is used to replace different time components. 

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

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

724 This property allows interoperability. 

725 """ 

726 return self.dt 

727 

728 

729class vDate(TimeBase): 

730 """Date 

731 

732 Value Name: 

733 DATE 

734 

735 Purpose: 

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

737 calendar date. 

738 

739 Format Definition: 

740 This value type is defined by the following notation: 

741 

742 .. code-block:: text 

743 

744 date = date-value 

745 

746 date-value = date-fullyear date-month date-mday 

747 date-fullyear = 4DIGIT 

748 date-month = 2DIGIT ;01-12 

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

750 ;based on month/year 

751 

752 Description: 

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

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

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

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

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

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

759 year, month, and day component text. 

760 

761 Example: 

762 The following represents July 14, 1997: 

763 

764 .. code-block:: text 

765 

766 19970714 

767 

768 .. code-block:: pycon 

769 

770 >>> from icalendar.prop import vDate 

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

772 >>> date.year 

773 1997 

774 >>> date.month 

775 7 

776 >>> date.day 

777 14 

778 """ 

779 

780 params: Parameters 

781 

782 def __init__(self, dt): 

783 if not isinstance(dt, date): 

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

785 self.dt = dt 

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

787 

788 def to_ical(self): 

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

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

791 

792 @staticmethod 

793 def from_ical(ical): 

794 try: 

795 timetuple = ( 

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

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

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

799 ) 

800 return date(*timetuple) 

801 except Exception as e: 

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

803 

804 

805class vDatetime(TimeBase): 

806 """Date-Time 

807 

808 Value Name: 

809 DATE-TIME 

810 

811 Purpose: 

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

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

814 the ISO.8601.2004 complete representation. 

815 

816 Format Definition: 

817 This value type is defined by the following notation: 

818 

819 .. code-block:: text 

820 

821 date-time = date "T" time 

822 

823 date = date-value 

824 date-value = date-fullyear date-month date-mday 

825 date-fullyear = 4DIGIT 

826 date-month = 2DIGIT ;01-12 

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

828 ;based on month/year 

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

830 time-hour = 2DIGIT ;00-23 

831 time-minute = 2DIGIT ;00-59 

832 time-second = 2DIGIT ;00-60 

833 time-utc = "Z" 

834 

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

836 

837 .. code-block:: text 

838 

839 YYYYMMDDTHHMMSS 

840 

841 Description: 

842 vDatetime is timezone aware and uses a timezone library. 

843 When a vDatetime object is created from an 

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

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

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

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

848 DATE-TIME components in the icalendar standard. 

849 

850 Example: 

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

852 

853 .. code-block:: pycon 

854 

855 >>> from icalendar import vDatetime 

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

857 >>> datetime.tzname() 

858 >>> datetime.year 

859 2021 

860 >>> datetime.minute 

861 15 

862 

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

864 

865 .. code-block:: pycon 

866 

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

868 >>> datetime.tzname() 

869 'EST' 

870 

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

872 

873 .. code-block:: pycon 

874 

875 >>> from zoneinfo import ZoneInfo 

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

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

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

879 """ 

880 

881 params: Parameters 

882 

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

884 self.dt = dt 

885 self.params = Parameters(params) 

886 

887 def to_ical(self): 

888 dt = self.dt 

889 tzid = tzid_from_dt(dt) 

890 

891 s = ( 

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

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

894 ) 

895 if tzid == "UTC": 

896 s += "Z" 

897 elif tzid: 

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

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

900 

901 @staticmethod 

902 def from_ical(ical, timezone=None): 

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

904 tzinfo = None 

905 if isinstance(timezone, str): 

906 tzinfo = tzp.timezone(timezone) 

907 elif timezone is not None: 

908 tzinfo = timezone 

909 

910 try: 

911 timetuple = ( 

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

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

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

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

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

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

918 ) 

919 if tzinfo: 

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

921 if not ical[15:]: 

922 return datetime(*timetuple) # noqa: DTZ001 

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

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

925 except Exception as e: 

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

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

928 

929 

930class vDuration(TimeBase): 

931 """Duration 

932 

933 Value Name: 

934 DURATION 

935 

936 Purpose: 

937 This value type is used to identify properties that contain 

938 a duration of time. 

939 

940 Format Definition: 

941 This value type is defined by the following notation: 

942 

943 .. code-block:: text 

944 

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

946 

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

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

949 dur-week = 1*DIGIT "W" 

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

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

952 dur-second = 1*DIGIT "S" 

953 dur-day = 1*DIGIT "D" 

954 

955 Description: 

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

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

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

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

960 represent nominal durations (weeks and days) and accurate 

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

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

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

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

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

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

967 computation of the exact duration requires the subtraction or 

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

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

970 When computing an exact duration, the greatest order time 

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

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

973 minutes, and number of seconds. 

974 

975 Example: 

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

977 

978 .. code-block:: text 

979 

980 P15DT5H0M20S 

981 

982 A duration of 7 weeks would be: 

983 

984 .. code-block:: text 

985 

986 P7W 

987 

988 .. code-block:: pycon 

989 

990 >>> from icalendar.prop import vDuration 

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

992 >>> duration 

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

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

995 >>> duration 

996 datetime.timedelta(days=49) 

997 """ 

998 

999 params: Parameters 

1000 

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

1002 if isinstance(td, str): 

1003 td = vDuration.from_ical(td) 

1004 if not isinstance(td, timedelta): 

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

1006 self.td = td 

1007 self.params = Parameters(params) 

1008 

1009 def to_ical(self): 

1010 sign = "" 

1011 td = self.td 

1012 if td.days < 0: 

1013 sign = "-" 

1014 td = -td 

1015 timepart = "" 

1016 if td.seconds: 

1017 timepart = "T" 

1018 hours = td.seconds // 3600 

1019 minutes = td.seconds % 3600 // 60 

1020 seconds = td.seconds % 60 

1021 if hours: 

1022 timepart += f"{hours}H" 

1023 if minutes or (hours and seconds): 

1024 timepart += f"{minutes}M" 

1025 if seconds: 

1026 timepart += f"{seconds}S" 

1027 if td.days == 0 and timepart: 

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

1029 return ( 

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

1031 + b"P" 

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

1033 + b"D" 

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

1035 ) 

1036 

1037 @staticmethod 

1038 def from_ical(ical): 

1039 match = DURATION_REGEX.match(ical) 

1040 if not match: 

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

1042 

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

1044 value = timedelta( 

1045 weeks=int(weeks or 0), 

1046 days=int(days or 0), 

1047 hours=int(hours or 0), 

1048 minutes=int(minutes or 0), 

1049 seconds=int(seconds or 0), 

1050 ) 

1051 

1052 if sign == "-": 

1053 value = -value 

1054 

1055 return value 

1056 

1057 @property 

1058 def dt(self) -> timedelta: 

1059 """The time delta for compatibility.""" 

1060 return self.td 

1061 

1062 

1063class vPeriod(TimeBase): 

1064 """Period of Time 

1065 

1066 Value Name: 

1067 PERIOD 

1068 

1069 Purpose: 

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

1071 precise period of time. 

1072 

1073 Format Definition: 

1074 This value type is defined by the following notation: 

1075 

1076 .. code-block:: text 

1077 

1078 period = period-explicit / period-start 

1079 

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

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

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

1083 ; be before the end. 

1084 

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

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

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

1088 ; of time. 

1089 

1090 Description: 

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

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

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

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

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

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

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

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

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

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

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

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

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

1104 

1105 Example: 

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

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

1108 

1109 .. code-block:: text 

1110 

1111 19970101T180000Z/19970102T070000Z 

1112 

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

1114 and 30 minutes would be: 

1115 

1116 .. code-block:: text 

1117 

1118 19970101T180000Z/PT5H30M 

1119 

1120 .. code-block:: pycon 

1121 

1122 >>> from icalendar.prop import vPeriod 

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

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

1125 """ 

1126 

1127 params: Parameters 

1128 

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

1130 start, end_or_duration = per 

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

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

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

1134 raise TypeError( 

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

1136 ) 

1137 by_duration = 0 

1138 if isinstance(end_or_duration, timedelta): 

1139 by_duration = 1 

1140 duration = end_or_duration 

1141 end = start + duration 

1142 else: 

1143 end = end_or_duration 

1144 duration = end - start 

1145 if start > end: 

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

1147 

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

1149 # set the timezone identifier 

1150 # does not support different timezones for start and end 

1151 tzid = tzid_from_dt(start) 

1152 if tzid: 

1153 self.params["TZID"] = tzid 

1154 

1155 self.start = start 

1156 self.end = end 

1157 self.by_duration = by_duration 

1158 self.duration = duration 

1159 

1160 def overlaps(self, other): 

1161 if self.start > other.start: 

1162 return other.overlaps(self) 

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

1164 

1165 def to_ical(self): 

1166 if self.by_duration: 

1167 return ( 

1168 vDatetime(self.start).to_ical() 

1169 + b"/" 

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

1171 ) 

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

1173 

1174 @staticmethod 

1175 def from_ical(ical, timezone=None): 

1176 try: 

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

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

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

1180 except Exception as e: 

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

1182 return (start, end_or_duration) 

1183 

1184 def __repr__(self): 

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

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

1187 

1188 @property 

1189 def dt(self): 

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

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

1192 

1193 from icalendar.param import FBTYPE 

1194 

1195 

1196class vWeekday(str): 

1197 """Either a ``weekday`` or a ``weekdaynum`` 

1198 

1199 .. code-block:: pycon 

1200 

1201 >>> from icalendar import vWeekday 

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

1203 'MO' 

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

1205 2 

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

1207 'FR' 

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

1209 -1 

1210 

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

1212 

1213 .. code-block:: text 

1214 

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

1216 plus = "+" 

1217 minus = "-" 

1218 ordwk = 1*2DIGIT ;1 to 53 

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

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

1221 ;FRIDAY, and SATURDAY days of the week. 

1222 

1223 """ 

1224 

1225 params: Parameters 

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

1227 

1228 week_days = CaselessDict( 

1229 { 

1230 "SU": 0, 

1231 "MO": 1, 

1232 "TU": 2, 

1233 "WE": 3, 

1234 "TH": 4, 

1235 "FR": 5, 

1236 "SA": 6, 

1237 } 

1238 ) 

1239 

1240 def __new__( 

1241 cls, 

1242 value, 

1243 encoding=DEFAULT_ENCODING, 

1244 /, 

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

1246 ): 

1247 value = to_unicode(value, encoding=encoding) 

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

1249 match = WEEKDAY_RULE.match(self) 

1250 if match is None: 

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

1252 match = match.groupdict() 

1253 sign = match["signal"] 

1254 weekday = match["weekday"] 

1255 relative = match["relative"] 

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

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

1258 self.weekday = weekday or None 

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

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

1261 self.relative *= -1 

1262 self.params = Parameters(params) 

1263 return self 

1264 

1265 def to_ical(self): 

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

1267 

1268 @classmethod 

1269 def from_ical(cls, ical): 

1270 try: 

1271 return cls(ical.upper()) 

1272 except Exception as e: 

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

1274 

1275 

1276class vFrequency(str): 

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

1278 

1279 params: Parameters 

1280 __slots__ = ("params",) 

1281 

1282 frequencies = CaselessDict( 

1283 { 

1284 "SECONDLY": "SECONDLY", 

1285 "MINUTELY": "MINUTELY", 

1286 "HOURLY": "HOURLY", 

1287 "DAILY": "DAILY", 

1288 "WEEKLY": "WEEKLY", 

1289 "MONTHLY": "MONTHLY", 

1290 "YEARLY": "YEARLY", 

1291 } 

1292 ) 

1293 

1294 def __new__( 

1295 cls, 

1296 value, 

1297 encoding=DEFAULT_ENCODING, 

1298 /, 

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

1300 ): 

1301 value = to_unicode(value, encoding=encoding) 

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

1303 if self not in vFrequency.frequencies: 

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

1305 self.params = Parameters(params) 

1306 return self 

1307 

1308 def to_ical(self): 

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

1310 

1311 @classmethod 

1312 def from_ical(cls, ical): 

1313 try: 

1314 return cls(ical.upper()) 

1315 except Exception as e: 

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

1317 

1318 

1319class vMonth(int): 

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

1321 

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

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

1324 

1325 .. code-block:: pycon 

1326 

1327 >>> from icalendar import vMonth 

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

1329 vMonth('1') 

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

1331 vMonth('5L') 

1332 >>> vMonth(1).leap 

1333 False 

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

1335 True 

1336 

1337 Definition from RFC: 

1338 

1339 .. code-block:: text 

1340 

1341 type-bymonth = element bymonth { 

1342 xsd:positiveInteger | 

1343 xsd:string 

1344 } 

1345 """ 

1346 

1347 params: Parameters 

1348 

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

1350 if isinstance(month, vMonth): 

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

1352 if isinstance(month, str): 

1353 if month.isdigit(): 

1354 month_index = int(month) 

1355 leap = False 

1356 else: 

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

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

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

1360 leap = True 

1361 else: 

1362 leap = False 

1363 month_index = int(month) 

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

1365 self.leap = leap 

1366 self.params = Parameters(params) 

1367 return self 

1368 

1369 def to_ical(self) -> bytes: 

1370 """The ical representation.""" 

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

1372 

1373 @classmethod 

1374 def from_ical(cls, ical: str): 

1375 return cls(ical) 

1376 

1377 @property 

1378 def leap(self) -> bool: 

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

1380 return self._leap 

1381 

1382 @leap.setter 

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

1384 self._leap = value 

1385 

1386 def __repr__(self) -> str: 

1387 """repr(self)""" 

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

1389 

1390 def __str__(self) -> str: 

1391 """str(self)""" 

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

1393 

1394 

1395class vSkip(vText, Enum): 

1396 """Skip values for RRULE. 

1397 

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

1399 

1400 OMIT is the default value. 

1401 

1402 Examples: 

1403 

1404 .. code-block:: pycon 

1405 

1406 >>> from icalendar import vSkip 

1407 >>> vSkip.OMIT 

1408 vSkip('OMIT') 

1409 >>> vSkip.FORWARD 

1410 vSkip('FORWARD') 

1411 >>> vSkip.BACKWARD 

1412 vSkip('BACKWARD') 

1413 """ 

1414 

1415 OMIT = "OMIT" 

1416 FORWARD = "FORWARD" 

1417 BACKWARD = "BACKWARD" 

1418 

1419 __reduce_ex__ = Enum.__reduce_ex__ 

1420 

1421 def __repr__(self): 

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

1423 

1424 

1425class vRecur(CaselessDict): 

1426 """Recurrence definition. 

1427 

1428 Property Name: 

1429 RRULE 

1430 

1431 Purpose: 

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

1433 journal entries, or time zone definitions. 

1434 

1435 Value Type: 

1436 RECUR 

1437 

1438 Property Parameters: 

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

1440 

1441 Conformance: 

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

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

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

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

1446 

1447 Description: 

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

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

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

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

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

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

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

1455 value not synchronized with the recurrence rule is undefined. 

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

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

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

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

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

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

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

1463 

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

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

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

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

1468 same local time regardless of time zone changes. 

1469 

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

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

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

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

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

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

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

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

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

1479 "RDATE" property of PERIOD value type. 

1480 

1481 Examples: 

1482 The following RRULE specifies daily events for 10 occurrences. 

1483 

1484 .. code-block:: text 

1485 

1486 RRULE:FREQ=DAILY;COUNT=10 

1487 

1488 Below, we parse the RRULE ical string. 

1489 

1490 .. code-block:: pycon 

1491 

1492 >>> from icalendar.prop import vRecur 

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

1494 >>> rrule 

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

1496 

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

1498 :class:`icalendar.cal.Todo`. 

1499 

1500 .. code-block:: pycon 

1501 

1502 >>> from icalendar import Event 

1503 >>> event = Event() 

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

1505 >>> event.rrules 

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

1507 """ # noqa: E501 

1508 

1509 params: Parameters 

1510 

1511 frequencies = [ 

1512 "SECONDLY", 

1513 "MINUTELY", 

1514 "HOURLY", 

1515 "DAILY", 

1516 "WEEKLY", 

1517 "MONTHLY", 

1518 "YEARLY", 

1519 ] 

1520 

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

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

1523 canonical_order = ( 

1524 "RSCALE", 

1525 "FREQ", 

1526 "UNTIL", 

1527 "COUNT", 

1528 "INTERVAL", 

1529 "BYSECOND", 

1530 "BYMINUTE", 

1531 "BYHOUR", 

1532 "BYDAY", 

1533 "BYWEEKDAY", 

1534 "BYMONTHDAY", 

1535 "BYYEARDAY", 

1536 "BYWEEKNO", 

1537 "BYMONTH", 

1538 "BYSETPOS", 

1539 "WKST", 

1540 "SKIP", 

1541 ) 

1542 

1543 types = CaselessDict( 

1544 { 

1545 "COUNT": vInt, 

1546 "INTERVAL": vInt, 

1547 "BYSECOND": vInt, 

1548 "BYMINUTE": vInt, 

1549 "BYHOUR": vInt, 

1550 "BYWEEKNO": vInt, 

1551 "BYMONTHDAY": vInt, 

1552 "BYYEARDAY": vInt, 

1553 "BYMONTH": vMonth, 

1554 "UNTIL": vDDDTypes, 

1555 "BYSETPOS": vInt, 

1556 "WKST": vWeekday, 

1557 "BYDAY": vWeekday, 

1558 "FREQ": vFrequency, 

1559 "BYWEEKDAY": vWeekday, 

1560 "SKIP": vSkip, 

1561 } 

1562 ) 

1563 

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

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

1566 # we have a string as an argument. 

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

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

1569 if not isinstance(v, SEQUENCE_TYPES): 

1570 kwargs[k] = [v] 

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

1572 self.params = Parameters(params) 

1573 

1574 def to_ical(self): 

1575 result = [] 

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

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

1578 if not isinstance(vals, SEQUENCE_TYPES): 

1579 vals = [vals] # noqa: PLW2901 

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

1581 

1582 # CaselessDict keys are always unicode 

1583 param_key = key.encode(DEFAULT_ENCODING) 

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

1585 

1586 return b";".join(result) 

1587 

1588 @classmethod 

1589 def parse_type(cls, key, values): 

1590 # integers 

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

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

1593 

1594 @classmethod 

1595 def from_ical(cls, ical: str): 

1596 if isinstance(ical, cls): 

1597 return ical 

1598 try: 

1599 recur = cls() 

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

1601 try: 

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

1603 except ValueError: 

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

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

1606 continue 

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

1608 return cls(recur) 

1609 except ValueError: 

1610 raise 

1611 except Exception as e: 

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

1613 

1614 

1615class vTime(TimeBase): 

1616 """Time 

1617 

1618 Value Name: 

1619 TIME 

1620 

1621 Purpose: 

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

1623 time of day. 

1624 

1625 Format Definition: 

1626 This value type is defined by the following notation: 

1627 

1628 .. code-block:: text 

1629 

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

1631 

1632 time-hour = 2DIGIT ;00-23 

1633 time-minute = 2DIGIT ;00-59 

1634 time-second = 2DIGIT ;00-60 

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

1636 

1637 time-utc = "Z" 

1638 

1639 Description: 

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

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

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

1643 vText) is defined for this value type. 

1644 

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

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

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

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

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

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

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

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

1653 

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

1655 type expresses time values in three forms: 

1656 

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

1658 the following is not valid for a time value: 

1659 

1660 .. code-block:: text 

1661 

1662 230000-0800 ;Invalid time format 

1663 

1664 **FORM #1 LOCAL TIME** 

1665 

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

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

1668 example, 11:00 PM: 

1669 

1670 .. code-block:: text 

1671 

1672 230000 

1673 

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

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

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

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

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

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

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

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

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

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

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

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

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

1687 

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

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

1690 time zone reference MUST be specified. 

1691 

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

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

1694 existence of "VTIMEZONE" calendar components in the iCalendar 

1695 object. 

1696 

1697 **FORM #2: UTC TIME** 

1698 

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

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

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

1702 

1703 .. code-block:: text 

1704 

1705 070000Z 

1706 

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

1708 properties whose time values are specified in UTC. 

1709 

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

1711 

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

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

1714 the appropriate time zone definition. 

1715 

1716 Example: 

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

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

1719 

1720 .. code-block:: text 

1721 

1722 083000 

1723 133000Z 

1724 TZID=America/New_York:083000 

1725 """ 

1726 

1727 def __init__(self, *args): 

1728 if len(args) == 1: 

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

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

1731 self.dt = args[0] 

1732 else: 

1733 self.dt = time(*args) 

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

1735 

1736 def to_ical(self): 

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

1738 

1739 @staticmethod 

1740 def from_ical(ical): 

1741 # TODO: timezone support 

1742 try: 

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

1744 return time(*timetuple) 

1745 except Exception as e: 

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

1747 

1748 

1749class vUri(str): 

1750 """URI 

1751 

1752 Value Name: 

1753 URI 

1754 

1755 Purpose: 

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

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

1758 property value. 

1759 

1760 Format Definition: 

1761 This value type is defined by the following notation: 

1762 

1763 .. code-block:: text 

1764 

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

1766 

1767 Description: 

1768 This value type might be used to reference binary 

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

1770 to include directly in the iCalendar object. 

1771 

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

1773 syntax defined in [RFC3986]. 

1774 

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

1776 be specified as a quoted-string value. 

1777 

1778 Examples: 

1779 The following is a URI for a network file: 

1780 

1781 .. code-block:: text 

1782 

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

1784 

1785 .. code-block:: pycon 

1786 

1787 >>> from icalendar.prop import vUri 

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

1789 >>> uri 

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

1791 >>> uri.uri 

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

1793 """ 

1794 

1795 params: Parameters 

1796 __slots__ = ("params",) 

1797 

1798 def __new__( 

1799 cls, 

1800 value: str, 

1801 encoding: str = DEFAULT_ENCODING, 

1802 /, 

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

1804 ): 

1805 value = to_unicode(value, encoding=encoding) 

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

1807 self.params = Parameters(params) 

1808 return self 

1809 

1810 def to_ical(self): 

1811 return self.encode(DEFAULT_ENCODING) 

1812 

1813 @classmethod 

1814 def from_ical(cls, ical): 

1815 try: 

1816 return cls(ical) 

1817 except Exception as e: 

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

1819 

1820 @property 

1821 def ical_value(self) -> str: 

1822 """The URI.""" 

1823 return self.uri 

1824 

1825 @property 

1826 def uri(self) -> str: 

1827 """The URI.""" 

1828 return str(self) 

1829 

1830 def __repr__(self) -> str: 

1831 """repr(self)""" 

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

1833 

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

1835 

1836 

1837class vUid(vText): 

1838 """A UID of a component. 

1839 

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

1841 """ 

1842 

1843 @classmethod 

1844 def new(cls): 

1845 """Create a new UID for convenience. 

1846 

1847 .. code-block:: pycon 

1848 

1849 >>> from icalendar import vUid 

1850 >>> vUid.new() 

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

1852 

1853 """ 

1854 return vUid(uuid.uuid4()) 

1855 

1856 def __init__(self, uid: str): 

1857 """Create a vUid.""" 

1858 super().__init__() 

1859 self.params.setdefault("VALUE", "UID") 

1860 

1861 @property 

1862 def uid(self) -> str: 

1863 """The uid of this property.""" 

1864 return str(self) 

1865 

1866 @property 

1867 def ical_value(self) -> str: 

1868 """The uid of this property.""" 

1869 return self.uid 

1870 

1871 def __repr__(self) -> str: 

1872 """repr(self)""" 

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

1874 

1875 from icalendar.param import FMTTYPE, LABEL, LINKREL 

1876 

1877 

1878class vXmlReference(vUri): 

1879 """An XML-REFERENCE. 

1880 

1881 The associated value references an associated XML artifact and 

1882 is a URI with an XPointer anchor value. 

1883 

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

1885 """ 

1886 

1887 def __init__(self, xml_reference: str): 

1888 """Create a new XML reference.""" 

1889 super().__init__() 

1890 self.params.setdefault("VALUE", "XML-REFERENCE") 

1891 

1892 @property 

1893 def xml_reference(self) -> str: 

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

1895 return self.uri 

1896 

1897 @property 

1898 def x_pointer(self) -> str: 

1899 """The XPointer of the URI. 

1900 

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

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

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

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

1905 """ 

1906 from urllib.parse import unquote, urlparse 

1907 

1908 parsed = urlparse(self.xml_reference) 

1909 fragment = unquote(parsed.fragment) 

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

1911 raise ValueError(f"No valid X Pointer found in {fragment!r}.") 

1912 return fragment[9:-1] 

1913 

1914 

1915class vGeo: 

1916 """Geographic Position 

1917 

1918 Property Name: 

1919 GEO 

1920 

1921 Purpose: 

1922 This property specifies information related to the global 

1923 position for the activity specified by a calendar component. 

1924 

1925 Value Type: 

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

1927 

1928 Property Parameters: 

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

1930 this property. 

1931 

1932 Conformance: 

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

1934 calendar components. 

1935 

1936 Description: 

1937 This property value specifies latitude and longitude, 

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

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

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

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

1942 will allow for accuracy to within one meter of geographical 

1943 position. Receiving applications MUST accept values of this 

1944 precision and MAY truncate values of greater precision. 

1945 

1946 Example: 

1947 

1948 .. code-block:: text 

1949 

1950 GEO:37.386013;-122.082932 

1951 

1952 Parse vGeo: 

1953 

1954 .. code-block:: pycon 

1955 

1956 >>> from icalendar.prop import vGeo 

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

1958 >>> geo 

1959 (37.386013, -122.082932) 

1960 

1961 Add a geo location to an event: 

1962 

1963 .. code-block:: pycon 

1964 

1965 >>> from icalendar import Event 

1966 >>> event = Event() 

1967 >>> latitude = 37.386013 

1968 >>> longitude = -122.082932 

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

1970 >>> event['GEO'] 

1971 vGeo((37.386013, -122.082932)) 

1972 """ 

1973 

1974 params: Parameters 

1975 

1976 def __init__( 

1977 self, 

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

1979 /, 

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

1981 ): 

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

1983 

1984 Raises: 

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

1986 """ 

1987 try: 

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

1989 latitude = float(latitude) 

1990 longitude = float(longitude) 

1991 except Exception as e: 

1992 raise ValueError( 

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

1994 ) from e 

1995 self.latitude = latitude 

1996 self.longitude = longitude 

1997 self.params = Parameters(params) 

1998 

1999 def to_ical(self): 

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

2001 

2002 @staticmethod 

2003 def from_ical(ical): 

2004 try: 

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

2006 return (float(latitude), float(longitude)) 

2007 except Exception as e: 

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

2009 

2010 def __eq__(self, other): 

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

2012 

2013 def __repr__(self): 

2014 """repr(self)""" 

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

2016 

2017 

2018class vUTCOffset: 

2019 """UTC Offset 

2020 

2021 Value Name: 

2022 UTC-OFFSET 

2023 

2024 Purpose: 

2025 This value type is used to identify properties that contain 

2026 an offset from UTC to local time. 

2027 

2028 Format Definition: 

2029 This value type is defined by the following notation: 

2030 

2031 .. code-block:: text 

2032 

2033 utc-offset = time-numzone 

2034 

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

2036 

2037 Description: 

2038 The PLUS SIGN character MUST be specified for positive 

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

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

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

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

2043 

2044 Example: 

2045 The following UTC offsets are given for standard time for 

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

2047 UTC): 

2048 

2049 .. code-block:: text 

2050 

2051 -0500 

2052 

2053 +0100 

2054 

2055 .. code-block:: pycon 

2056 

2057 >>> from icalendar.prop import vUTCOffset 

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

2059 >>> utc_offset 

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

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

2062 >>> utc_offset 

2063 datetime.timedelta(seconds=3600) 

2064 """ 

2065 

2066 params: Parameters 

2067 

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

2069 

2070 # component, we will silently ignore 

2071 # it, rather than let the exception 

2072 # propagate upwards 

2073 

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

2075 if not isinstance(td, timedelta): 

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

2077 self.td = td 

2078 self.params = Parameters(params) 

2079 

2080 def to_ical(self): 

2081 if self.td < timedelta(0): 

2082 sign = "-%s" 

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

2084 else: 

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

2086 sign = "+%s" 

2087 td = self.td 

2088 

2089 days, seconds = td.days, td.seconds 

2090 

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

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

2093 seconds = abs(seconds % 60) 

2094 if seconds: 

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

2096 else: 

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

2098 return sign % duration 

2099 

2100 @classmethod 

2101 def from_ical(cls, ical): 

2102 if isinstance(ical, cls): 

2103 return ical.td 

2104 try: 

2105 sign, hours, minutes, seconds = ( 

2106 ical[0:1], 

2107 int(ical[1:3]), 

2108 int(ical[3:5]), 

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

2110 ) 

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

2112 except Exception as e: 

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

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

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

2116 if sign == "-": 

2117 return -offset 

2118 return offset 

2119 

2120 def __eq__(self, other): 

2121 if not isinstance(other, vUTCOffset): 

2122 return False 

2123 return self.td == other.td 

2124 

2125 def __hash__(self): 

2126 return hash(self.td) 

2127 

2128 def __repr__(self): 

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

2130 

2131 

2132class vInline(str): 

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

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

2135 class, so no further processing is needed. 

2136 """ 

2137 

2138 params: Parameters 

2139 __slots__ = ("params",) 

2140 

2141 def __new__( 

2142 cls, 

2143 value, 

2144 encoding=DEFAULT_ENCODING, 

2145 /, 

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

2147 ): 

2148 value = to_unicode(value, encoding=encoding) 

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

2150 self.params = Parameters(params) 

2151 return self 

2152 

2153 def to_ical(self): 

2154 return self.encode(DEFAULT_ENCODING) 

2155 

2156 @classmethod 

2157 def from_ical(cls, ical): 

2158 return cls(ical) 

2159 

2160 

2161class TypesFactory(CaselessDict): 

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

2163 class. 

2164 

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

2166 both kinds. 

2167 """ 

2168 

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

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

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

2172 self.all_types = ( 

2173 vBinary, 

2174 vBoolean, 

2175 vCalAddress, 

2176 vDDDLists, 

2177 vDDDTypes, 

2178 vDate, 

2179 vDatetime, 

2180 vDuration, 

2181 vFloat, 

2182 vFrequency, 

2183 vGeo, 

2184 vInline, 

2185 vInt, 

2186 vPeriod, 

2187 vRecur, 

2188 vText, 

2189 vTime, 

2190 vUTCOffset, 

2191 vUri, 

2192 vWeekday, 

2193 vCategory, 

2194 ) 

2195 self["binary"] = vBinary 

2196 self["boolean"] = vBoolean 

2197 self["cal-address"] = vCalAddress 

2198 self["date"] = vDDDTypes 

2199 self["date-time"] = vDDDTypes 

2200 self["duration"] = vDDDTypes 

2201 self["float"] = vFloat 

2202 self["integer"] = vInt 

2203 self["period"] = vPeriod 

2204 self["recur"] = vRecur 

2205 self["text"] = vText 

2206 self["time"] = vTime 

2207 self["uri"] = vUri 

2208 self["utc-offset"] = vUTCOffset 

2209 self["geo"] = vGeo 

2210 self["inline"] = vInline 

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

2212 self["categories"] = vCategory 

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

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

2215 

2216 ################################################# 

2217 # Property types 

2218 

2219 # These are the default types 

2220 types_map = CaselessDict( 

2221 { 

2222 #################################### 

2223 # Property value types 

2224 # Calendar Properties 

2225 "calscale": "text", 

2226 "method": "text", 

2227 "prodid": "text", 

2228 "version": "text", 

2229 # Descriptive Component Properties 

2230 "attach": "uri", 

2231 "categories": "categories", 

2232 "class": "text", 

2233 "comment": "text", 

2234 "description": "text", 

2235 "geo": "geo", 

2236 "location": "text", 

2237 "percent-complete": "integer", 

2238 "priority": "integer", 

2239 "resources": "text", 

2240 "status": "text", 

2241 "summary": "text", 

2242 # RFC 9253 

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

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

2245 "link": "uri", 

2246 "concept": "uri", 

2247 "refid": "text", 

2248 # Date and Time Component Properties 

2249 "completed": "date-time", 

2250 "dtend": "date-time", 

2251 "due": "date-time", 

2252 "dtstart": "date-time", 

2253 "duration": "duration", 

2254 "freebusy": "period", 

2255 "transp": "text", 

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

2257 # Time Zone Component Properties 

2258 "tzid": "text", 

2259 "tzname": "text", 

2260 "tzoffsetfrom": "utc-offset", 

2261 "tzoffsetto": "utc-offset", 

2262 "tzurl": "uri", 

2263 # Relationship Component Properties 

2264 "attendee": "cal-address", 

2265 "contact": "text", 

2266 "organizer": "cal-address", 

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

2268 "related-to": "text", 

2269 "url": "uri", 

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

2271 "source": "uri", 

2272 "uid": "text", 

2273 # Recurrence Component Properties 

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

2275 "exrule": "recur", 

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

2277 "rrule": "recur", 

2278 # Alarm Component Properties 

2279 "action": "text", 

2280 "repeat": "integer", 

2281 "trigger": "duration", 

2282 "acknowledged": "date-time", 

2283 # Change Management Component Properties 

2284 "created": "date-time", 

2285 "dtstamp": "date-time", 

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

2287 "sequence": "integer", 

2288 # Miscellaneous Component Properties 

2289 "request-status": "text", 

2290 #################################### 

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

2292 "altrep": "uri", 

2293 "cn": "text", 

2294 "cutype": "text", 

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

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

2297 "dir": "uri", 

2298 "encoding": "text", 

2299 "fmttype": "text", 

2300 "fbtype": "text", 

2301 "language": "text", 

2302 "member": "cal-address", 

2303 "partstat": "text", 

2304 "range": "text", 

2305 "related": "text", 

2306 "reltype": "text", 

2307 "role": "text", 

2308 "rsvp": "boolean", 

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

2310 "value": "text", 

2311 # rfc 9253 parameters 

2312 "label": "text", 

2313 "linkrel": "text", 

2314 "gap": "duration", 

2315 } 

2316 ) 

2317 

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

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

2320 

2321 Args: 

2322 name: Property or parameter name 

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

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

2325 

2326 Returns: 

2327 The appropriate value type class 

2328 """ 

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

2330 # regardless of the VALUE parameter 

2331 if name.upper() in ("RDATE", "EXDATE"): 

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

2333 

2334 # Only use VALUE parameter for known properties 

2335 # that support multiple value types 

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

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

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

2339 return self[value_param] 

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

2341 

2342 def to_ical(self, name, value): 

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

2344 encoded string. 

2345 """ 

2346 type_class = self.for_property(name) 

2347 return type_class(value).to_ical() 

2348 

2349 def from_ical(self, name, value): 

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

2351 encoded string to a primitive python type. 

2352 """ 

2353 type_class = self.for_property(name) 

2354 return type_class.from_ical(value) 

2355 

2356 

2357__all__ = [ 

2358 "DURATION_REGEX", 

2359 "WEEKDAY_RULE", 

2360 "TimeBase", 

2361 "TypesFactory", 

2362 "tzid_from_dt", 

2363 "tzid_from_tzinfo", 

2364 "vBinary", 

2365 "vBoolean", 

2366 "vCalAddress", 

2367 "vCategory", 

2368 "vDDDLists", 

2369 "vDDDTypes", 

2370 "vDate", 

2371 "vDatetime", 

2372 "vDuration", 

2373 "vFloat", 

2374 "vFrequency", 

2375 "vGeo", 

2376 "vInline", 

2377 "vInt", 

2378 "vMonth", 

2379 "vPeriod", 

2380 "vRecur", 

2381 "vSkip", 

2382 "vText", 

2383 "vTime", 

2384 "vUTCOffset", 

2385 "vUid", 

2386 "vUri", 

2387 "vWeekday", 

2388 "vXmlReference", 

2389]