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]