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

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

876 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 re 

48import uuid 

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

50from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, Tuple, TypeAlias, Union 

51 

52from icalendar.caselessdict import CaselessDict 

53from icalendar.enums import Enum 

54from icalendar.error import InvalidCalendar, JCalParsingError 

55from icalendar.parser import Parameters 

56from icalendar.parser_tools import ( 

57 DEFAULT_ENCODING, 

58 ICAL_TYPE, 

59 SEQUENCE_TYPES, 

60 from_unicode, 

61 to_unicode, 

62) 

63from icalendar.timezone import tzid_from_dt, tzid_from_tzinfo, tzp 

64from icalendar.timezone.tzid import is_utc 

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

66 

67from .adr import AdrFields, vAdr 

68from .binary import vBinary 

69from .boolean import vBoolean 

70from .broken import vBrokenProperty 

71from .cal_address import vCalAddress 

72from .categories import vCategory 

73from .float import vFloat 

74from .geo import vGeo 

75from .inline import vInline 

76from .n import NFields, vN 

77from .org import vOrg 

78from .text import vText 

79from .uid import vUid 

80from .unknown import vUnknown 

81from .uri import vUri 

82from .xml_reference import vXmlReference 

83 

84if TYPE_CHECKING: 

85 from icalendar.compatibility import Self 

86 

87DURATION_REGEX = re.compile( 

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

89) 

90 

91WEEKDAY_RULE = re.compile( 

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

93) 

94 

95 

96class vInt(int): 

97 """Integer 

98 

99 Value Name: 

100 INTEGER 

101 

102 Purpose: 

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

104 signed integer value. 

105 

106 Format Definition: 

107 This value type is defined by the following notation: 

108 

109 .. code-block:: text 

110 

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

112 

113 Description: 

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

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

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

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

118 

119 The ``__new__`` method creates a vInt instance: 

120 

121 Parameters: 

122 value: Integer value to encode. Can be positive or negative within 

123 the range -2147483648 to 2147483647. 

124 params: Optional parameter dictionary for the property. 

125 

126 Returns: 

127 vInt instance 

128 

129 Examples: 

130 

131 .. code-block:: text 

132 

133 1234567890 

134 -1234567890 

135 +1234567890 

136 432109876 

137 

138 .. code-block:: pycon 

139 

140 >>> from icalendar.prop import vInt 

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

142 >>> integer 

143 1234567890 

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

145 >>> integer 

146 -1234567890 

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

148 >>> integer 

149 1234567890 

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

151 >>> integer 

152 432109876 

153 

154 Create a PRIORITY property (1 = highest priority): 

155 

156 .. code-block:: pycon 

157 

158 >>> priority = vInt(1) 

159 >>> priority 

160 1 

161 >>> priority.to_ical() 

162 b'1' 

163 

164 Create SEQUENCE property (for versioning): 

165 

166 .. code-block:: pycon 

167 

168 >>> sequence = vInt(3) 

169 >>> sequence.to_ical() 

170 b'3' 

171 """ 

172 

173 default_value: ClassVar[str] = "INTEGER" 

174 params: Parameters 

175 

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

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

178 self.params = Parameters(params) 

179 return self 

180 

181 def to_ical(self) -> bytes: 

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

183 

184 @classmethod 

185 def from_ical(cls, ical: ICAL_TYPE): 

186 try: 

187 return cls(ical) 

188 except Exception as e: 

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

190 

191 @classmethod 

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

193 """Examples of vInt.""" 

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

195 

196 from icalendar.param import VALUE 

197 

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

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

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

201 

202 @classmethod 

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

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

205 

206 Parameters: 

207 jcal_property: The jCal property to parse. 

208 

209 Raises: 

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

211 """ 

212 JCalParsingError.validate_property(jcal_property, cls) 

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

214 return cls( 

215 jcal_property[3], 

216 params=Parameters.from_jcal_property(jcal_property), 

217 ) 

218 

219 @classmethod 

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

221 """Parse a jCal value for vInt. 

222 

223 Raises: 

224 ~error.JCalParsingError: If the value is not an int. 

225 """ 

226 JCalParsingError.validate_value_type(value, int, cls) 

227 return cls(value) 

228 

229 

230class vDDDLists: 

231 """A list of vDDDTypes values.""" 

232 

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

234 params: Parameters 

235 dts: list[vDDDTypes] 

236 

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

238 if params is None: 

239 params = {} 

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

241 dt_list = [dt_list] 

242 vddd = [] 

243 tzid = None 

244 for dt_l in dt_list: 

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

246 vddd.append(dt) 

247 if "TZID" in dt.params: 

248 tzid = dt.params["TZID"] 

249 

250 if tzid: 

251 # NOTE: no support for multiple timezones here! 

252 params["TZID"] = tzid 

253 self.params = Parameters(params) 

254 self.dts = vddd 

255 

256 def to_ical(self): 

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

258 return b",".join(dts_ical) 

259 

260 @staticmethod 

261 def from_ical(ical, timezone=None): 

262 out = [] 

263 ical_dates = ical.split(",") 

264 for ical_dt in ical_dates: 

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

266 return out 

267 

268 def __eq__(self, other): 

269 if isinstance(other, vDDDLists): 

270 return self.dts == other.dts 

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

272 return self.dts == [other] 

273 return False 

274 

275 def __repr__(self): 

276 """String representation.""" 

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

278 

279 @classmethod 

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

281 """Examples of vDDDLists.""" 

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

283 

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

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

286 return [ 

287 name, 

288 self.params.to_jcal(), 

289 self.VALUE.lower(), 

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

291 ] 

292 

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

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

295 

296 from icalendar.param import VALUE 

297 

298 @classmethod 

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

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

301 

302 Parameters: 

303 jcal_property: The jCal property to parse. 

304 

305 Raises: 

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

307 """ 

308 JCalParsingError.validate_property(jcal_property, cls) 

309 values = jcal_property[3:] 

310 prop = jcal_property[:3] 

311 dts = [] 

312 for value in values: 

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

314 return cls( 

315 dts, 

316 params=Parameters.from_jcal_property(jcal_property), 

317 ) 

318 

319 

320class TimeBase: 

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

322 

323 default_value: ClassVar[str] 

324 params: Parameters 

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

326 

327 def __eq__(self, other): 

328 """self == other""" 

329 if isinstance(other, date): 

330 return self.dt == other 

331 if isinstance(other, TimeBase): 

332 default = object() 

333 for key in ( 

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

335 ) - self.ignore_for_equality: 

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

337 key, default 

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

339 return False 

340 return self.dt == other.dt 

341 if isinstance(other, vDDDLists): 

342 return other == self 

343 return False 

344 

345 def __hash__(self): 

346 return hash(self.dt) 

347 

348 from icalendar.param import RANGE, RELATED, TZID 

349 

350 def __repr__(self): 

351 """String representation.""" 

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

353 

354 

355DT_TYPE: TypeAlias = ( 

356 datetime 

357 | date 

358 | timedelta 

359 | time 

360 | tuple[datetime, datetime] 

361 | tuple[datetime, timedelta] 

362) 

363 

364 

365class vDDDTypes(TimeBase): 

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

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

368 So this is practical. 

369 """ 

370 

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

372 params: Parameters 

373 dt: DT_TYPE 

374 

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

376 if params is None: 

377 params = {} 

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

379 raise TypeError( 

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

381 ) 

382 self.dt = dt 

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

384 if is_date(dt): 

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

386 elif isinstance(dt, time): 

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

388 elif isinstance(dt, tuple): 

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

390 self.params = Parameters(params) 

391 self.params.update_tzid_from(dt) 

392 

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

394 """Convert to a property type. 

395 

396 Raises: 

397 ValueError: If the type is unknown. 

398 """ 

399 dt = self.dt 

400 if isinstance(dt, datetime): 

401 result = vDatetime(dt) 

402 elif isinstance(dt, date): 

403 result = vDate(dt) 

404 elif isinstance(dt, timedelta): 

405 result = vDuration(dt) 

406 elif isinstance(dt, time): 

407 result = vTime(dt) 

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

409 result = vPeriod(dt) 

410 else: 

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

412 result.params = self.params 

413 return result 

414 

415 def to_ical(self) -> str: 

416 """Return the ical representation.""" 

417 return self.to_property_type().to_ical() 

418 

419 @classmethod 

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

421 if isinstance(ical, cls): 

422 return ical.dt 

423 u = ical.upper() 

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

425 return vDuration.from_ical(ical) 

426 if "/" in u: 

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

428 

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

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

431 if len(ical) == 8: 

432 if timezone: 

433 tzinfo = tzp.timezone(timezone) 

434 if tzinfo is not None: 

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

436 return vDate.from_ical(ical) 

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

438 return vTime.from_ical(ical) 

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

440 

441 @property 

442 def td(self) -> timedelta: 

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

444 

445 This class is used to replace different time components. 

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

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

448 This property allows interoperability. 

449 """ 

450 return self.dt 

451 

452 @property 

453 def dts(self) -> list: 

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

455 return [self] 

456 

457 @classmethod 

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

459 """Examples of vDDDTypes.""" 

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

461 

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

463 """Determine the VALUE parameter.""" 

464 return self.to_property_type().VALUE 

465 

466 from icalendar.param import VALUE 

467 

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

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

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

471 

472 @classmethod 

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

474 """Parse a jCal value. 

475 

476 Raises: 

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

478 date-time, duration, or period. 

479 """ 

480 if isinstance(jcal, list): 

481 return vPeriod.parse_jcal_value(jcal) 

482 JCalParsingError.validate_value_type(jcal, str, cls) 

483 if "/" in jcal: 

484 return vPeriod.parse_jcal_value(jcal) 

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

486 try: 

487 return jcal_type.parse_jcal_value(jcal) 

488 except JCalParsingError: # noqa: PERF203 

489 pass 

490 raise JCalParsingError( 

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

492 ) 

493 

494 @classmethod 

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

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

497 

498 Parameters: 

499 jcal_property: The jCal property to parse. 

500 

501 Raises: 

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

503 """ 

504 JCalParsingError.validate_property(jcal_property, cls) 

505 with JCalParsingError.reraise_with_path_added(3): 

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

507 params = Parameters.from_jcal_property(jcal_property) 

508 if params.tzid: 

509 if isinstance(dt, tuple): 

510 # period 

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

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

513 dt = (start, end) 

514 else: 

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

516 return cls( 

517 dt, 

518 params=params, 

519 ) 

520 

521 

522class vDate(TimeBase): 

523 """Date 

524 

525 Value Name: 

526 DATE 

527 

528 Purpose: 

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

530 calendar date. 

531 

532 Format Definition: 

533 This value type is defined by the following notation: 

534 

535 .. code-block:: text 

536 

537 date = date-value 

538 

539 date-value = date-fullyear date-month date-mday 

540 date-fullyear = 4DIGIT 

541 date-month = 2DIGIT ;01-12 

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

543 ;based on month/year 

544 

545 Description: 

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

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

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

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

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

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

552 year, month, and day component text. 

553 

554 Example: 

555 The following represents July 14, 1997: 

556 

557 .. code-block:: text 

558 

559 19970714 

560 

561 .. code-block:: pycon 

562 

563 >>> from icalendar.prop import vDate 

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

565 >>> date.year 

566 1997 

567 >>> date.month 

568 7 

569 >>> date.day 

570 14 

571 """ 

572 

573 default_value: ClassVar[str] = "DATE" 

574 params: Parameters 

575 

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

577 if not isinstance(dt, date): 

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

579 self.dt = dt 

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

581 

582 def to_ical(self): 

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

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

585 

586 @staticmethod 

587 def from_ical(ical): 

588 try: 

589 timetuple = ( 

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

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

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

593 ) 

594 return date(*timetuple) 

595 except Exception as e: 

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

597 

598 @classmethod 

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

600 """Examples of vDate.""" 

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

602 

603 from icalendar.param import VALUE 

604 

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

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

607 return [ 

608 name, 

609 self.params.to_jcal(), 

610 self.VALUE.lower(), 

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

612 ] 

613 

614 @classmethod 

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

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

617 

618 Raises: 

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

620 """ 

621 JCalParsingError.validate_value_type(jcal, str, cls) 

622 try: 

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

624 except ValueError as e: 

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

626 

627 @classmethod 

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

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

630 

631 Parameters: 

632 jcal_property: The jCal property to parse. 

633 

634 Raises: 

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

636 """ 

637 JCalParsingError.validate_property(jcal_property, cls) 

638 with JCalParsingError.reraise_with_path_added(3): 

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

640 return cls( 

641 value, 

642 params=Parameters.from_jcal_property(jcal_property), 

643 ) 

644 

645 

646class vDatetime(TimeBase): 

647 """Date-Time 

648 

649 Value Name: 

650 DATE-TIME 

651 

652 Purpose: 

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

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

655 the ISO.8601.2004 complete representation. 

656 

657 Format Definition: 

658 This value type is defined by the following notation: 

659 

660 .. code-block:: text 

661 

662 date-time = date "T" time 

663 

664 date = date-value 

665 date-value = date-fullyear date-month date-mday 

666 date-fullyear = 4DIGIT 

667 date-month = 2DIGIT ;01-12 

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

669 ;based on month/year 

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

671 time-hour = 2DIGIT ;00-23 

672 time-minute = 2DIGIT ;00-59 

673 time-second = 2DIGIT ;00-60 

674 time-utc = "Z" 

675 

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

677 

678 .. code-block:: text 

679 

680 YYYYMMDDTHHMMSS 

681 

682 Description: 

683 vDatetime is timezone aware and uses a timezone library. 

684 When a vDatetime object is created from an 

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

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

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

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

689 DATE-TIME components in the icalendar standard. 

690 

691 Example: 

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

693 

694 .. code-block:: pycon 

695 

696 >>> from icalendar import vDatetime 

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

698 >>> datetime.tzname() 

699 >>> datetime.year 

700 2021 

701 >>> datetime.minute 

702 15 

703 

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

705 

706 .. code-block:: pycon 

707 

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

709 >>> datetime.tzname() 

710 'EST' 

711 

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

713 

714 .. code-block:: pycon 

715 

716 >>> from zoneinfo import ZoneInfo 

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

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

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

720 """ 

721 

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

723 params: Parameters 

724 

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

726 self.dt = dt 

727 self.params = Parameters(params) 

728 self.params.update_tzid_from(dt) 

729 

730 def to_ical(self): 

731 dt = self.dt 

732 

733 s = ( 

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

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

736 ) 

737 if self.is_utc(): 

738 s += "Z" 

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

740 

741 @staticmethod 

742 def from_ical(ical, timezone=None): 

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

744 tzinfo = None 

745 if isinstance(timezone, str): 

746 tzinfo = tzp.timezone(timezone) 

747 elif timezone is not None: 

748 tzinfo = timezone 

749 

750 try: 

751 timetuple = ( 

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

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

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

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

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

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

758 ) 

759 if tzinfo: 

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

761 if not ical[15:]: 

762 return datetime(*timetuple) # noqa: DTZ001 

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

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

765 except Exception as e: 

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

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

768 

769 @classmethod 

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

771 """Examples of vDatetime.""" 

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

773 

774 from icalendar.param import VALUE 

775 

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

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

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

779 if self.is_utc(): 

780 value += "Z" 

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

782 

783 def is_utc(self) -> bool: 

784 """Whether this datetime is UTC.""" 

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

786 

787 @classmethod 

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

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

790 

791 Raises: 

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

793 """ 

794 JCalParsingError.validate_value_type(jcal, str, cls) 

795 utc = jcal.endswith("Z") 

796 if utc: 

797 jcal = jcal[:-1] 

798 try: 

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

800 except ValueError as e: 

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

802 if utc: 

803 return tzp.localize_utc(dt) 

804 return dt 

805 

806 @classmethod 

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

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

809 

810 Parameters: 

811 jcal_property: The jCal property to parse. 

812 

813 Raises: 

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

815 """ 

816 JCalParsingError.validate_property(jcal_property, cls) 

817 params = Parameters.from_jcal_property(jcal_property) 

818 with JCalParsingError.reraise_with_path_added(3): 

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

820 if params.tzid: 

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

822 return cls( 

823 dt, 

824 params=params, 

825 ) 

826 

827 

828class vDuration(TimeBase): 

829 """Duration 

830 

831 Value Name: 

832 DURATION 

833 

834 Purpose: 

835 This value type is used to identify properties that contain 

836 a duration of time. 

837 

838 Format Definition: 

839 This value type is defined by the following notation: 

840 

841 .. code-block:: text 

842 

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

844 

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

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

847 dur-week = 1*DIGIT "W" 

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

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

850 dur-second = 1*DIGIT "S" 

851 dur-day = 1*DIGIT "D" 

852 

853 Description: 

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

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

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

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

858 represent nominal durations (weeks and days) and accurate 

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

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

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

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

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

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

865 computation of the exact duration requires the subtraction or 

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

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

868 When computing an exact duration, the greatest order time 

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

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

871 minutes, and number of seconds. 

872 

873 Example: 

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

875 

876 .. code-block:: text 

877 

878 P15DT5H0M20S 

879 

880 A duration of 7 weeks would be: 

881 

882 .. code-block:: text 

883 

884 P7W 

885 

886 .. code-block:: pycon 

887 

888 >>> from icalendar.prop import vDuration 

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

890 >>> duration 

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

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

893 >>> duration 

894 datetime.timedelta(days=49) 

895 """ 

896 

897 default_value: ClassVar[str] = "DURATION" 

898 params: Parameters 

899 

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

901 if isinstance(td, str): 

902 td = vDuration.from_ical(td) 

903 if not isinstance(td, timedelta): 

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

905 self.td = td 

906 self.params = Parameters(params) 

907 

908 def to_ical(self): 

909 sign = "" 

910 td = self.td 

911 if td.days < 0: 

912 sign = "-" 

913 td = -td 

914 timepart = "" 

915 if td.seconds: 

916 timepart = "T" 

917 hours = td.seconds // 3600 

918 minutes = td.seconds % 3600 // 60 

919 seconds = td.seconds % 60 

920 if hours: 

921 timepart += f"{hours}H" 

922 if minutes or (hours and seconds): 

923 timepart += f"{minutes}M" 

924 if seconds: 

925 timepart += f"{seconds}S" 

926 if td.days == 0 and timepart: 

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

928 return ( 

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

930 + b"P" 

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

932 + b"D" 

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

934 ) 

935 

936 @staticmethod 

937 def from_ical(ical): 

938 match = DURATION_REGEX.match(ical) 

939 if not match: 

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

941 

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

943 value = timedelta( 

944 weeks=int(weeks or 0), 

945 days=int(days or 0), 

946 hours=int(hours or 0), 

947 minutes=int(minutes or 0), 

948 seconds=int(seconds or 0), 

949 ) 

950 

951 if sign == "-": 

952 value = -value 

953 

954 return value 

955 

956 @property 

957 def dt(self) -> timedelta: 

958 """The time delta for compatibility.""" 

959 return self.td 

960 

961 @classmethod 

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

963 """Examples of vDuration.""" 

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

965 

966 from icalendar.param import VALUE 

967 

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

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

970 return [ 

971 name, 

972 self.params.to_jcal(), 

973 self.VALUE.lower(), 

974 self.to_ical().decode(), 

975 ] 

976 

977 @classmethod 

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

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

980 

981 Raises: 

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

983 JCalParsingError.validate_value_type(jcal, str, cls) 

984 try: 

985 return cls.from_ical(jcal) 

986 except ValueError as e: 

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

988 

989 @classmethod 

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

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

992 

993 Parameters: 

994 jcal_property: The jCal property to parse. 

995 

996 Raises: 

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

998 """ 

999 JCalParsingError.validate_property(jcal_property, cls) 

1000 with JCalParsingError.reraise_with_path_added(3): 

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

1002 return cls( 

1003 duration, 

1004 Parameters.from_jcal_property(jcal_property), 

1005 ) 

1006 

1007 

1008class vPeriod(TimeBase): 

1009 """Period of Time 

1010 

1011 Value Name: 

1012 PERIOD 

1013 

1014 Purpose: 

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

1016 precise period of time. 

1017 

1018 Format Definition: 

1019 This value type is defined by the following notation: 

1020 

1021 .. code-block:: text 

1022 

1023 period = period-explicit / period-start 

1024 

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

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

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

1028 ; be before the end. 

1029 

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

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

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

1033 ; of time. 

1034 

1035 Description: 

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

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

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

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

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

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

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

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

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

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

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

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

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

1049 

1050 Example: 

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

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

1053 

1054 .. code-block:: text 

1055 

1056 19970101T180000Z/19970102T070000Z 

1057 

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

1059 and 30 minutes would be: 

1060 

1061 .. code-block:: text 

1062 

1063 19970101T180000Z/PT5H30M 

1064 

1065 .. code-block:: pycon 

1066 

1067 >>> from icalendar.prop import vPeriod 

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

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

1070 """ 

1071 

1072 default_value: ClassVar[str] = "PERIOD" 

1073 params: Parameters 

1074 by_duration: bool 

1075 start: datetime 

1076 end: datetime 

1077 duration: timedelta 

1078 

1079 def __init__( 

1080 self, 

1081 per: tuple[datetime, datetime | timedelta], 

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

1083 ): 

1084 start, end_or_duration = per 

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

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

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

1088 raise TypeError( 

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

1090 ) 

1091 by_duration = isinstance(end_or_duration, timedelta) 

1092 if by_duration: 

1093 duration = end_or_duration 

1094 end = normalize_pytz(start + duration) 

1095 else: 

1096 end = end_or_duration 

1097 duration = normalize_pytz(end - start) 

1098 if start > end: 

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

1100 

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

1102 # set the timezone identifier 

1103 # does not support different timezones for start and end 

1104 self.params.update_tzid_from(start) 

1105 

1106 self.start = start 

1107 self.end = end 

1108 self.by_duration = by_duration 

1109 self.duration = duration 

1110 

1111 def overlaps(self, other): 

1112 if self.start > other.start: 

1113 return other.overlaps(self) 

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

1115 

1116 def to_ical(self): 

1117 if self.by_duration: 

1118 return ( 

1119 vDatetime(self.start).to_ical() 

1120 + b"/" 

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

1122 ) 

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

1124 

1125 @staticmethod 

1126 def from_ical(ical, timezone=None): 

1127 try: 

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

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

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

1131 except Exception as e: 

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

1133 return (start, end_or_duration) 

1134 

1135 def __repr__(self): 

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

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

1138 

1139 @property 

1140 def dt(self): 

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

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

1143 

1144 from icalendar.param import FBTYPE 

1145 

1146 @classmethod 

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

1148 """Examples of vPeriod.""" 

1149 return [ 

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

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

1152 ] 

1153 

1154 from icalendar.param import VALUE 

1155 

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

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

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

1159 if self.by_duration: 

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

1161 else: 

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

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

1164 

1165 @classmethod 

1166 def parse_jcal_value( 

1167 cls, jcal: str | list 

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

1169 """Parse a jCal value. 

1170 

1171 Raises: 

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

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

1174 """ 

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

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

1177 jcal = jcal.split("/") 

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

1179 raise JCalParsingError( 

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

1181 ) 

1182 with JCalParsingError.reraise_with_path_added(0): 

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

1184 with JCalParsingError.reraise_with_path_added(1): 

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

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

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

1188 else: 

1189 try: 

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

1191 except JCalParsingError as e: 

1192 raise JCalParsingError( 

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

1194 cls, 

1195 value=jcal[1], 

1196 ) from e 

1197 return start, end_or_duration 

1198 

1199 @classmethod 

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

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

1202 

1203 Parameters: 

1204 jcal_property: The jCal property to parse. 

1205 

1206 Raises: 

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

1208 """ 

1209 JCalParsingError.validate_property(jcal_property, cls) 

1210 with JCalParsingError.reraise_with_path_added(3): 

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

1212 params = Parameters.from_jcal_property(jcal_property) 

1213 tzid = params.tzid 

1214 

1215 if tzid: 

1216 start = tzp.localize(start, tzid) 

1217 if is_datetime(end_or_duration): 

1218 end_or_duration = tzp.localize(end_or_duration, tzid) 

1219 

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

1221 

1222 

1223class vWeekday(str): 

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

1225 

1226 .. code-block:: pycon 

1227 

1228 >>> from icalendar import vWeekday 

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

1230 'MO' 

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

1232 2 

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

1234 'FR' 

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

1236 -1 

1237 

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

1239 

1240 .. code-block:: text 

1241 

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

1243 plus = "+" 

1244 minus = "-" 

1245 ordwk = 1*2DIGIT ;1 to 53 

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

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

1248 ;FRIDAY, and SATURDAY days of the week. 

1249 

1250 """ 

1251 

1252 params: Parameters 

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

1254 

1255 week_days = CaselessDict( 

1256 { 

1257 "SU": 0, 

1258 "MO": 1, 

1259 "TU": 2, 

1260 "WE": 3, 

1261 "TH": 4, 

1262 "FR": 5, 

1263 "SA": 6, 

1264 } 

1265 ) 

1266 

1267 def __new__( 

1268 cls, 

1269 value, 

1270 encoding=DEFAULT_ENCODING, 

1271 /, 

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

1273 ): 

1274 value = to_unicode(value, encoding=encoding) 

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

1276 match = WEEKDAY_RULE.match(self) 

1277 if match is None: 

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

1279 match = match.groupdict() 

1280 sign = match["signal"] 

1281 weekday = match["weekday"] 

1282 relative = match["relative"] 

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

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

1285 self.weekday = weekday or None 

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

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

1288 self.relative *= -1 

1289 self.params = Parameters(params) 

1290 return self 

1291 

1292 def to_ical(self): 

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

1294 

1295 @classmethod 

1296 def from_ical(cls, ical): 

1297 try: 

1298 return cls(ical.upper()) 

1299 except Exception as e: 

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

1301 

1302 @classmethod 

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

1304 """Parse a jCal value for vWeekday. 

1305 

1306 Raises: 

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

1308 """ 

1309 JCalParsingError.validate_value_type(value, str, cls) 

1310 try: 

1311 return cls(value) 

1312 except ValueError as e: 

1313 raise JCalParsingError( 

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

1315 ) from e 

1316 

1317 

1318class vFrequency(str): 

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

1320 

1321 params: Parameters 

1322 __slots__ = ("params",) 

1323 

1324 frequencies = CaselessDict( 

1325 { 

1326 "SECONDLY": "SECONDLY", 

1327 "MINUTELY": "MINUTELY", 

1328 "HOURLY": "HOURLY", 

1329 "DAILY": "DAILY", 

1330 "WEEKLY": "WEEKLY", 

1331 "MONTHLY": "MONTHLY", 

1332 "YEARLY": "YEARLY", 

1333 } 

1334 ) 

1335 

1336 def __new__( 

1337 cls, 

1338 value, 

1339 encoding=DEFAULT_ENCODING, 

1340 /, 

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

1342 ): 

1343 value = to_unicode(value, encoding=encoding) 

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

1345 if self not in vFrequency.frequencies: 

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

1347 self.params = Parameters(params) 

1348 return self 

1349 

1350 def to_ical(self): 

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

1352 

1353 @classmethod 

1354 def from_ical(cls, ical): 

1355 try: 

1356 return cls(ical.upper()) 

1357 except Exception as e: 

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

1359 

1360 @classmethod 

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

1362 """Parse a jCal value for vFrequency. 

1363 

1364 Raises: 

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

1366 """ 

1367 JCalParsingError.validate_value_type(value, str, cls) 

1368 try: 

1369 return cls(value) 

1370 except ValueError as e: 

1371 raise JCalParsingError( 

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

1373 ) from e 

1374 

1375 

1376class vMonth(int): 

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

1378 

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

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

1381 

1382 .. code-block:: pycon 

1383 

1384 >>> from icalendar import vMonth 

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

1386 vMonth('1') 

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

1388 vMonth('5L') 

1389 >>> vMonth(1).leap 

1390 False 

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

1392 True 

1393 

1394 Definition from RFC: 

1395 

1396 .. code-block:: text 

1397 

1398 type-bymonth = element bymonth { 

1399 xsd:positiveInteger | 

1400 xsd:string 

1401 } 

1402 """ 

1403 

1404 params: Parameters 

1405 

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

1407 if isinstance(month, vMonth): 

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

1409 if isinstance(month, str): 

1410 if month.isdigit(): 

1411 month_index = int(month) 

1412 leap = False 

1413 else: 

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

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

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

1417 leap = True 

1418 else: 

1419 leap = False 

1420 month_index = int(month) 

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

1422 self.leap = leap 

1423 self.params = Parameters(params) 

1424 return self 

1425 

1426 def to_ical(self) -> bytes: 

1427 """The ical representation.""" 

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

1429 

1430 @classmethod 

1431 def from_ical(cls, ical: str): 

1432 return cls(ical) 

1433 

1434 @property 

1435 def leap(self) -> bool: 

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

1437 return self._leap 

1438 

1439 @leap.setter 

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

1441 self._leap = value 

1442 

1443 def __repr__(self) -> str: 

1444 """repr(self)""" 

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

1446 

1447 def __str__(self) -> str: 

1448 """str(self)""" 

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

1450 

1451 @classmethod 

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

1453 """Parse a jCal value for vMonth. 

1454 

1455 Raises: 

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

1457 """ 

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

1459 try: 

1460 return cls(value) 

1461 except ValueError as e: 

1462 raise JCalParsingError( 

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

1464 ) from e 

1465 

1466 

1467class vSkip(vText, Enum): 

1468 """Skip values for RRULE. 

1469 

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

1471 

1472 OMIT is the default value. 

1473 

1474 Examples: 

1475 

1476 .. code-block:: pycon 

1477 

1478 >>> from icalendar import vSkip 

1479 >>> vSkip.OMIT 

1480 vSkip('OMIT') 

1481 >>> vSkip.FORWARD 

1482 vSkip('FORWARD') 

1483 >>> vSkip.BACKWARD 

1484 vSkip('BACKWARD') 

1485 """ 

1486 

1487 OMIT = "OMIT" 

1488 FORWARD = "FORWARD" 

1489 BACKWARD = "BACKWARD" 

1490 

1491 __reduce_ex__ = Enum.__reduce_ex__ 

1492 

1493 def __repr__(self): 

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

1495 

1496 @classmethod 

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

1498 """Parse a jCal value for vSkip. 

1499 

1500 Raises: 

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

1502 """ 

1503 JCalParsingError.validate_value_type(value, str, cls) 

1504 try: 

1505 return cls[value.upper()] 

1506 except KeyError as e: 

1507 raise JCalParsingError( 

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

1509 ) from e 

1510 

1511 

1512class vRecur(CaselessDict): 

1513 """Recurrence definition. 

1514 

1515 Property Name: 

1516 RRULE 

1517 

1518 Purpose: 

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

1520 journal entries, or time zone definitions. 

1521 

1522 Value Type: 

1523 RECUR 

1524 

1525 Property Parameters: 

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

1527 

1528 Conformance: 

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

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

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

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

1533 

1534 Description: 

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

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

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

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

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

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

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

1542 value not synchronized with the recurrence rule is undefined. 

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

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

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

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

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

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

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

1550 

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

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

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

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

1555 same local time regardless of time zone changes. 

1556 

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

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

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

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

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

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

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

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

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

1566 "RDATE" property of PERIOD value type. 

1567 

1568 Examples: 

1569 The following RRULE specifies daily events for 10 occurrences. 

1570 

1571 .. code-block:: text 

1572 

1573 RRULE:FREQ=DAILY;COUNT=10 

1574 

1575 Below, we parse the RRULE ical string. 

1576 

1577 .. code-block:: pycon 

1578 

1579 >>> from icalendar.prop import vRecur 

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

1581 >>> rrule 

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

1583 

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

1585 :class:`icalendar.cal.Todo`. 

1586 

1587 .. code-block:: pycon 

1588 

1589 >>> from icalendar import Event 

1590 >>> event = Event() 

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

1592 >>> event.rrules 

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

1594 """ # noqa: E501 

1595 

1596 default_value: ClassVar[str] = "RECUR" 

1597 params: Parameters 

1598 

1599 frequencies = [ 

1600 "SECONDLY", 

1601 "MINUTELY", 

1602 "HOURLY", 

1603 "DAILY", 

1604 "WEEKLY", 

1605 "MONTHLY", 

1606 "YEARLY", 

1607 ] 

1608 

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

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

1611 canonical_order = ( 

1612 "RSCALE", 

1613 "FREQ", 

1614 "UNTIL", 

1615 "COUNT", 

1616 "INTERVAL", 

1617 "BYSECOND", 

1618 "BYMINUTE", 

1619 "BYHOUR", 

1620 "BYDAY", 

1621 "BYWEEKDAY", 

1622 "BYMONTHDAY", 

1623 "BYYEARDAY", 

1624 "BYWEEKNO", 

1625 "BYMONTH", 

1626 "BYSETPOS", 

1627 "WKST", 

1628 "SKIP", 

1629 ) 

1630 

1631 types = CaselessDict( 

1632 { 

1633 "COUNT": vInt, 

1634 "INTERVAL": vInt, 

1635 "BYSECOND": vInt, 

1636 "BYMINUTE": vInt, 

1637 "BYHOUR": vInt, 

1638 "BYWEEKNO": vInt, 

1639 "BYMONTHDAY": vInt, 

1640 "BYYEARDAY": vInt, 

1641 "BYMONTH": vMonth, 

1642 "UNTIL": vDDDTypes, 

1643 "BYSETPOS": vInt, 

1644 "WKST": vWeekday, 

1645 "BYDAY": vWeekday, 

1646 "FREQ": vFrequency, 

1647 "BYWEEKDAY": vWeekday, 

1648 "SKIP": vSkip, # RFC 7529 

1649 "RSCALE": vText, # RFC 7529 

1650 } 

1651 ) 

1652 

1653 # for reproducible serialization: 

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

1655 # look up in RFC 

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

1657 

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

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

1660 # we have a string as an argument. 

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

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

1663 if not isinstance(v, SEQUENCE_TYPES): 

1664 kwargs[k] = [v] 

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

1666 self.params = Parameters(params) 

1667 

1668 def to_ical(self): 

1669 result = [] 

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

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

1672 if not isinstance(vals, SEQUENCE_TYPES): 

1673 vals = [vals] # noqa: PLW2901 

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

1675 

1676 # CaselessDict keys are always unicode 

1677 param_key = key.encode(DEFAULT_ENCODING) 

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

1679 

1680 return b";".join(result) 

1681 

1682 @classmethod 

1683 def parse_type(cls, key, values): 

1684 # integers 

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

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

1687 

1688 @classmethod 

1689 def from_ical(cls, ical: str): 

1690 if isinstance(ical, cls): 

1691 return ical 

1692 try: 

1693 recur = cls() 

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

1695 try: 

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

1697 except ValueError: 

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

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

1700 continue 

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

1702 return cls(recur) 

1703 except ValueError: 

1704 raise 

1705 except Exception as e: 

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

1707 

1708 @classmethod 

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

1710 """Examples of vRecur.""" 

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

1712 

1713 from icalendar.param import VALUE 

1714 

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

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

1717 recur = {} 

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

1719 key = k.lower() 

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

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

1722 elif not isinstance(v, list): 

1723 value = [v] 

1724 else: 

1725 value = v 

1726 recur[key] = value 

1727 if "until" in recur: 

1728 until = recur["until"] 

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

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

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

1732 

1733 @classmethod 

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

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

1736 

1737 Parameters: 

1738 jcal_property: The jCal property to parse. 

1739 

1740 Raises: 

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

1742 """ 

1743 JCalParsingError.validate_property(jcal_property, cls) 

1744 params = Parameters.from_jcal_property(jcal_property) 

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

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

1747 ): 

1748 raise JCalParsingError( 

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

1750 cls, 

1751 3, 

1752 value=jcal_property[3], 

1753 ) 

1754 recur = {} 

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

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

1757 with JCalParsingError.reraise_with_path_added(3, key): 

1758 if isinstance(value, list): 

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

1760 for i, v in enumerate(value): 

1761 with JCalParsingError.reraise_with_path_added(i): 

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

1763 else: 

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

1765 until = recur.get("until") 

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

1767 recur["until"] = [until] 

1768 return cls(recur, params=params) 

1769 

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

1771 """self == other""" 

1772 if not isinstance(other, vRecur): 

1773 return super().__eq__(other) 

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

1775 return False 

1776 for key in self.keys(): 

1777 v1 = self[key] 

1778 v2 = other[key] 

1779 if not isinstance(v1, SEQUENCE_TYPES): 

1780 v1 = [v1] 

1781 if not isinstance(v2, SEQUENCE_TYPES): 

1782 v2 = [v2] 

1783 if v1 != v2: 

1784 return False 

1785 return True 

1786 

1787 

1788TIME_JCAL_REGEX = re.compile( 

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

1790) 

1791 

1792 

1793class vTime(TimeBase): 

1794 """Time 

1795 

1796 Value Name: 

1797 TIME 

1798 

1799 Purpose: 

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

1801 time of day. 

1802 

1803 Format Definition: 

1804 This value type is defined by the following notation: 

1805 

1806 .. code-block:: text 

1807 

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

1809 

1810 time-hour = 2DIGIT ;00-23 

1811 time-minute = 2DIGIT ;00-59 

1812 time-second = 2DIGIT ;00-60 

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

1814 

1815 time-utc = "Z" 

1816 

1817 Description: 

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

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

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

1821 vText) is defined for this value type. 

1822 

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

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

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

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

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

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

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

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

1831 

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

1833 type expresses time values in three forms: 

1834 

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

1836 the following is not valid for a time value: 

1837 

1838 .. code-block:: text 

1839 

1840 230000-0800 ;Invalid time format 

1841 

1842 **FORM #1 LOCAL TIME** 

1843 

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

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

1846 example, 11:00 PM: 

1847 

1848 .. code-block:: text 

1849 

1850 230000 

1851 

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

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

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

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

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

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

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

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

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

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

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

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

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

1865 

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

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

1868 time zone reference MUST be specified. 

1869 

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

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

1872 existence of "VTIMEZONE" calendar components in the iCalendar 

1873 object. 

1874 

1875 **FORM #2: UTC TIME** 

1876 

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

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

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

1880 

1881 .. code-block:: text 

1882 

1883 070000Z 

1884 

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

1886 properties whose time values are specified in UTC. 

1887 

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

1889 

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

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

1892 the appropriate time zone definition. 

1893 

1894 Example: 

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

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

1897 

1898 .. code-block:: text 

1899 

1900 083000 

1901 133000Z 

1902 TZID=America/New_York:083000 

1903 """ 

1904 

1905 default_value: ClassVar[str] = "TIME" 

1906 params: Parameters 

1907 

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

1909 if len(args) == 1: 

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

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

1912 self.dt = args[0] 

1913 else: 

1914 self.dt = time(*args) 

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

1916 self.params.update_tzid_from(self.dt) 

1917 

1918 def to_ical(self): 

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

1920 if self.is_utc(): 

1921 value += "Z" 

1922 return value 

1923 

1924 def is_utc(self) -> bool: 

1925 """Whether this time is UTC.""" 

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

1927 

1928 @staticmethod 

1929 def from_ical(ical): 

1930 # TODO: timezone support 

1931 try: 

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

1933 return time(*timetuple) 

1934 except Exception as e: 

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

1936 

1937 @classmethod 

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

1939 """Examples of vTime.""" 

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

1941 

1942 from icalendar.param import VALUE 

1943 

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

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

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

1947 if self.is_utc(): 

1948 value += "Z" 

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

1950 

1951 @classmethod 

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

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

1954 

1955 Raises: 

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

1957 """ 

1958 JCalParsingError.validate_value_type(jcal, str, cls) 

1959 match = TIME_JCAL_REGEX.match(jcal) 

1960 if match is None: 

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

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

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

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

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

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

1967 

1968 @classmethod 

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

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

1971 

1972 Parameters: 

1973 jcal_property: The jCal property to parse. 

1974 

1975 Raises: 

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

1977 """ 

1978 JCalParsingError.validate_property(jcal_property, cls) 

1979 with JCalParsingError.reraise_with_path_added(3): 

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

1981 return cls( 

1982 value, 

1983 params=Parameters.from_jcal_property(jcal_property), 

1984 ) 

1985 

1986 

1987UTC_OFFSET_JCAL_REGEX = re.compile( 

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

1989) 

1990 

1991 

1992class vUTCOffset: 

1993 """UTC Offset 

1994 

1995 Value Name: 

1996 UTC-OFFSET 

1997 

1998 Purpose: 

1999 This value type is used to identify properties that contain 

2000 an offset from UTC to local time. 

2001 

2002 Format Definition: 

2003 This value type is defined by the following notation: 

2004 

2005 .. code-block:: text 

2006 

2007 utc-offset = time-numzone 

2008 

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

2010 

2011 Description: 

2012 The PLUS SIGN character MUST be specified for positive 

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

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

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

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

2017 

2018 Example: 

2019 The following UTC offsets are given for standard time for 

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

2021 UTC): 

2022 

2023 .. code-block:: text 

2024 

2025 -0500 

2026 

2027 +0100 

2028 

2029 .. code-block:: pycon 

2030 

2031 >>> from icalendar.prop import vUTCOffset 

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

2033 >>> utc_offset 

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

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

2036 >>> utc_offset 

2037 datetime.timedelta(seconds=3600) 

2038 """ 

2039 

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

2041 params: Parameters 

2042 

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

2044 

2045 # component, we will silently ignore 

2046 # it, rather than let the exception 

2047 # propagate upwards 

2048 

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

2050 if not isinstance(td, timedelta): 

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

2052 self.td = td 

2053 self.params = Parameters(params) 

2054 

2055 def to_ical(self) -> str: 

2056 """Return the ical representation.""" 

2057 return self.format("") 

2058 

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

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

2061 

2062 .. code-block:: pycon 

2063 

2064 >>> from icalendar import vUTCOffset 

2065 >>> from datetime import timedelta 

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

2067 >>> utc_offset.format() 

2068 '-0500' 

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

2070 '-05:00' 

2071 """ 

2072 if self.td < timedelta(0): 

2073 sign = "-%s" 

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

2075 else: 

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

2077 sign = "+%s" 

2078 td = self.td 

2079 

2080 days, seconds = td.days, td.seconds 

2081 

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

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

2084 seconds = abs(seconds % 60) 

2085 if seconds: 

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

2087 else: 

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

2089 return sign % duration 

2090 

2091 @classmethod 

2092 def from_ical(cls, ical): 

2093 if isinstance(ical, cls): 

2094 return ical.td 

2095 try: 

2096 sign, hours, minutes, seconds = ( 

2097 ical[0:1], 

2098 int(ical[1:3]), 

2099 int(ical[3:5]), 

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

2101 ) 

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

2103 except Exception as e: 

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

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

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

2107 if sign == "-": 

2108 return -offset 

2109 return offset 

2110 

2111 def __eq__(self, other): 

2112 if not isinstance(other, vUTCOffset): 

2113 return False 

2114 return self.td == other.td 

2115 

2116 def __hash__(self): 

2117 return hash(self.td) 

2118 

2119 def __repr__(self): 

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

2121 

2122 @classmethod 

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

2124 """Examples of vUTCOffset.""" 

2125 return [ 

2126 cls(timedelta(hours=3)), 

2127 cls(timedelta(0)), 

2128 ] 

2129 

2130 from icalendar.param import VALUE 

2131 

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

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

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

2135 

2136 @classmethod 

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

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

2139 

2140 Parameters: 

2141 jcal_property: The jCal property to parse. 

2142 

2143 Raises: 

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

2145 """ 

2146 JCalParsingError.validate_property(jcal_property, cls) 

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

2148 if match is None: 

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

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

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

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

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

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

2155 if negative: 

2156 t = -t 

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

2158 

2159 

2160class TypesFactory(CaselessDict): 

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

2162 class. 

2163 

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

2165 both kinds. 

2166 """ 

2167 

2168 _instance: ClassVar[TypesFactory] = None 

2169 

2170 def instance() -> TypesFactory: 

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

2172 if TypesFactory._instance is None: 

2173 TypesFactory._instance = TypesFactory() 

2174 return TypesFactory._instance 

2175 

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

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

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

2179 self.all_types = ( 

2180 vBinary, 

2181 vBoolean, 

2182 vCalAddress, 

2183 vDDDLists, 

2184 vDDDTypes, 

2185 vDate, 

2186 vDatetime, 

2187 vDuration, 

2188 vFloat, 

2189 vFrequency, 

2190 vGeo, 

2191 vInline, 

2192 vInt, 

2193 vPeriod, 

2194 vRecur, 

2195 vText, 

2196 vTime, 

2197 vUTCOffset, 

2198 vUri, 

2199 vWeekday, 

2200 vCategory, 

2201 vAdr, 

2202 vN, 

2203 vOrg, 

2204 vUid, 

2205 vXmlReference, 

2206 vUnknown, 

2207 ) 

2208 self["binary"] = vBinary 

2209 self["boolean"] = vBoolean 

2210 self["cal-address"] = vCalAddress 

2211 self["date"] = vDDDTypes 

2212 self["date-time"] = vDDDTypes 

2213 self["duration"] = vDDDTypes 

2214 self["float"] = vFloat 

2215 self["integer"] = vInt 

2216 self["period"] = vPeriod 

2217 self["recur"] = vRecur 

2218 self["text"] = vText 

2219 self["time"] = vTime 

2220 self["uri"] = vUri 

2221 self["utc-offset"] = vUTCOffset 

2222 self["geo"] = vGeo 

2223 self["inline"] = vInline 

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

2225 self["categories"] = vCategory 

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

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

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

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

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

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

2232 

2233 ################################################# 

2234 # Property types 

2235 

2236 # These are the default types 

2237 types_map = CaselessDict( 

2238 { 

2239 #################################### 

2240 # Property value types 

2241 # Calendar Properties 

2242 "calscale": "text", 

2243 "method": "text", 

2244 "prodid": "text", 

2245 "version": "text", 

2246 # Descriptive Component Properties 

2247 "attach": "uri", 

2248 "categories": "categories", 

2249 "class": "text", 

2250 # vCard Properties (RFC 6350) 

2251 "adr": "adr", 

2252 "n": "n", 

2253 "org": "org", 

2254 "comment": "text", 

2255 "description": "text", 

2256 "geo": "geo", 

2257 "location": "text", 

2258 "percent-complete": "integer", 

2259 "priority": "integer", 

2260 "resources": "text", 

2261 "status": "text", 

2262 "summary": "text", 

2263 # RFC 9253 

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

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

2266 "link": "uri", 

2267 "concept": "uri", 

2268 "refid": "text", 

2269 # Date and Time Component Properties 

2270 "completed": "date-time", 

2271 "dtend": "date-time", 

2272 "due": "date-time", 

2273 "dtstart": "date-time", 

2274 "duration": "duration", 

2275 "freebusy": "period", 

2276 "transp": "text", 

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

2278 # Time Zone Component Properties 

2279 "tzid": "text", 

2280 "tzname": "text", 

2281 "tzoffsetfrom": "utc-offset", 

2282 "tzoffsetto": "utc-offset", 

2283 "tzurl": "uri", 

2284 # Relationship Component Properties 

2285 "attendee": "cal-address", 

2286 "contact": "text", 

2287 "organizer": "cal-address", 

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

2289 "related-to": "text", 

2290 "url": "uri", 

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

2292 "source": "uri", 

2293 "uid": "text", 

2294 # Recurrence Component Properties 

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

2296 "exrule": "recur", 

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

2298 "rrule": "recur", 

2299 # Alarm Component Properties 

2300 "action": "text", 

2301 "repeat": "integer", 

2302 "trigger": "duration", 

2303 "acknowledged": "date-time", 

2304 # Change Management Component Properties 

2305 "created": "date-time", 

2306 "dtstamp": "date-time", 

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

2308 "sequence": "integer", 

2309 # Miscellaneous Component Properties 

2310 "request-status": "text", 

2311 #################################### 

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

2313 "altrep": "uri", 

2314 "cn": "text", 

2315 "cutype": "text", 

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

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

2318 "dir": "uri", 

2319 "encoding": "text", 

2320 "fmttype": "text", 

2321 "fbtype": "text", 

2322 "language": "text", 

2323 "member": "cal-address", 

2324 "partstat": "text", 

2325 "range": "text", 

2326 "related": "text", 

2327 "reltype": "text", 

2328 "role": "text", 

2329 "rsvp": "boolean", 

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

2331 "value": "text", 

2332 # rfc 9253 parameters 

2333 "label": "text", 

2334 "linkrel": "text", 

2335 "gap": "duration", 

2336 } 

2337 ) 

2338 

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

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

2341 

2342 Parameters: 

2343 name: Property or parameter name 

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

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

2346 

2347 Returns: 

2348 The appropriate value type class. 

2349 """ 

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

2351 # regardless of the VALUE parameter 

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

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

2354 

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

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

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

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

2359 return self[value_param] 

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

2361 

2362 def to_ical(self, name, value): 

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

2364 encoded string. 

2365 """ 

2366 type_class = self.for_property(name) 

2367 return type_class(value).to_ical() 

2368 

2369 def from_ical(self, name, value): 

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

2371 encoded string to a primitive python type. 

2372 """ 

2373 type_class = self.for_property(name) 

2374 return type_class.from_ical(value) 

2375 

2376 

2377VPROPERTY: TypeAlias = ( 

2378 vAdr 

2379 | vBoolean 

2380 | vBrokenProperty 

2381 | vCalAddress 

2382 | vCategory 

2383 | vDDDLists 

2384 | vDDDTypes 

2385 | vDate 

2386 | vDatetime 

2387 | vDuration 

2388 | vFloat 

2389 | vFrequency 

2390 | vInt 

2391 | vMonth 

2392 | vN 

2393 | vOrg 

2394 | vPeriod 

2395 | vRecur 

2396 | vSkip 

2397 | vText 

2398 | vTime 

2399 | vUTCOffset 

2400 | vUri 

2401 | vWeekday 

2402 | vInline 

2403 | vBinary 

2404 | vGeo 

2405 | vUnknown 

2406 | vXmlReference 

2407 | vUid 

2408) 

2409 

2410__all__ = [ 

2411 "DURATION_REGEX", 

2412 "VPROPERTY", 

2413 "WEEKDAY_RULE", 

2414 "AdrFields", 

2415 "NFields", 

2416 "TimeBase", 

2417 "TypesFactory", 

2418 "tzid_from_dt", 

2419 "tzid_from_tzinfo", 

2420 "vAdr", 

2421 "vBinary", 

2422 "vBoolean", 

2423 "vBrokenProperty", 

2424 "vCalAddress", 

2425 "vCategory", 

2426 "vDDDLists", 

2427 "vDDDTypes", 

2428 "vDate", 

2429 "vDatetime", 

2430 "vDuration", 

2431 "vFloat", 

2432 "vFrequency", 

2433 "vGeo", 

2434 "vInline", 

2435 "vInt", 

2436 "vMonth", 

2437 "vN", 

2438 "vOrg", 

2439 "vPeriod", 

2440 "vRecur", 

2441 "vSkip", 

2442 "vText", 

2443 "vTime", 

2444 "vUTCOffset", 

2445 "vUid", 

2446 "vUnknown", 

2447 "vUri", 

2448 "vWeekday", 

2449 "vXmlReference", 

2450]