Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/prop/dt/duration.py: 74%

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

95 statements  

1"""DURATION property type from :rfc:`5545`.""" 

2 

3import re 

4from datetime import timedelta 

5from typing import Any, ClassVar 

6from xml.etree.ElementTree import Element, SubElement 

7 

8from icalendar.compatibility import Self 

9from icalendar.error import InvalidCalendar, JCalParsingError, XCalParsingError 

10from icalendar.parser import Parameters 

11from icalendar.parser.xcal.base import XCalParser 

12from icalendar.parser.xcal.wrapper import from_xcal_wrapper 

13 

14from .base import TimeBase 

15 

16DURATION_REGEX = re.compile( 

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

18) 

19 

20XCAL_DURATION_ERROR = "Expected duration format https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.6" 

21 

22 

23class vDuration(TimeBase): 

24 """Duration 

25 

26 Value Name: 

27 DURATION 

28 

29 Purpose: 

30 This value type is used to identify properties that contain 

31 a duration of time. 

32 

33 Format Definition: 

34 This value type is defined by the following notation: 

35 

36 .. code-block:: text 

37 

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

39 

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

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

42 dur-week = 1*DIGIT "W" 

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

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

45 dur-second = 1*DIGIT "S" 

46 dur-day = 1*DIGIT "D" 

47 

48 Description: 

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

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

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

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

53 represent nominal durations (weeks and days) and accurate 

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

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

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

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

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

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

60 computation of the exact duration requires the subtraction or 

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

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

63 When computing an exact duration, the greatest order time 

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

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

66 minutes, and number of seconds. 

67 

68 Example: 

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

70 

71 .. code-block:: ics 

72 

73 P15DT5H0M20S 

74 

75 A duration of 7 weeks would be: 

76 

77 .. code-block:: ics 

78 

79 P7W 

80 

81 .. code-block:: pycon 

82 

83 >>> from icalendar.prop import vDuration 

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

85 >>> duration 

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

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

88 >>> duration 

89 datetime.timedelta(days=49) 

90 """ 

91 

92 default_value: ClassVar[str] = "DURATION" 

93 params: Parameters 

94 

95 def __init__( 

96 self, td: timedelta | str, /, params: dict[str, Any] | None = None 

97 ) -> None: 

98 if isinstance(td, str): 

99 td = vDuration.from_ical(td) 

100 if not isinstance(td, timedelta): 

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

102 self.td = td 

103 self.params = Parameters(params) 

104 

105 def to_ical(self): 

106 sign = "" 

107 td = self.td 

108 if td.days < 0: 

109 sign = "-" 

110 td = -td 

111 timepart = "" 

112 if td.seconds: 

113 timepart = "T" 

114 hours = td.seconds // 3600 

115 minutes = td.seconds % 3600 // 60 

116 seconds = td.seconds % 60 

117 if hours: 

118 timepart += f"{hours}H" 

119 if minutes or (hours and seconds): 

120 timepart += f"{minutes}M" 

121 if seconds: 

122 timepart += f"{seconds}S" 

123 if td.days == 0 and timepart: 

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

125 return ( 

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

127 + b"P" 

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

129 + b"D" 

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

131 ) 

132 

133 @staticmethod 

134 def from_ical(ical): 

135 match = DURATION_REGEX.match(ical) 

136 if not match: 

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

138 

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

140 try: 

141 value = timedelta( 

142 weeks=int(weeks or 0), 

143 days=int(days or 0), 

144 hours=int(hours or 0), 

145 minutes=int(minutes or 0), 

146 seconds=int(seconds or 0), 

147 ) 

148 except OverflowError as e: 

149 # ``timedelta`` rejects values that are too large for its C 

150 # implementation. Raise the same error as other invalid durations 

151 # instead of leaking ``OverflowError`` to callers. 

152 raise InvalidCalendar(f"Impractical iCalendar duration: {ical}") from e 

153 

154 if sign == "-": 

155 value = -value 

156 

157 return value 

158 

159 @property 

160 def dt(self) -> timedelta: 

161 """The time delta for compatibility.""" 

162 return self.td 

163 

164 @classmethod 

165 def examples(cls) -> list[Self]: 

166 """Examples of vDuration.""" 

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

168 

169 from icalendar.param import VALUE 

170 

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

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

173 return [ 

174 name, 

175 self.params.to_jcal(), 

176 self.VALUE.lower(), 

177 self.to_ical().decode(), 

178 ] 

179 

180 @classmethod 

181 def parse_jcal_value(cls, jcal: str) -> timedelta: 

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

183 

184 Raises: 

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

186 JCalParsingError.validate_value_type(jcal, str, cls) 

187 try: 

188 return cls.from_ical(jcal) 

189 except (ValueError, InvalidCalendar) as e: 

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

191 

192 @classmethod 

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

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

195 

196 Parameters: 

197 jcal_property: The jCal property to parse. 

198 

199 Raises: 

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

201 """ 

202 JCalParsingError.validate_property(jcal_property, cls) 

203 with JCalParsingError.reraise_with_path_added(3): 

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

205 return cls( 

206 duration, 

207 Parameters.from_jcal_property(jcal_property), 

208 ) 

209 

210 def to_xcal(self, element: Element) -> None: 

211 """The xCal representation of this property according to :rfc:`6321`.""" 

212 self.params.to_xcal(element) 

213 element = SubElement(element, "duration") 

214 element.text = self.to_ical().decode() 

215 

216 @classmethod 

217 @from_xcal_wrapper 

218 def from_xcal(cls, parser: XCalParser, params: Parameters) -> Self: 

219 """Parse xCal from :rfc:`6321`. 

220 

221 Parameters: 

222 xml: The XML to parse or a parser. 

223 

224 Raises: 

225 ~error.XCalParsingError: If the provided xCal is invalid. 

226 """ 

227 element = parser.parse_tag(cls.default_value) 

228 try: 

229 td = cls.from_ical(element.get_xsd_token()) 

230 except InvalidCalendar as e: 

231 if isinstance(e.__cause__, OverflowError): 

232 # The syntax is valid but we are out of range 

233 raise XCalParsingError( 

234 "Duration is out of range", element.get_xsd_token(), element 

235 ) from e 

236 raise XCalParsingError( 

237 XCAL_DURATION_ERROR, element.get_xsd_token(), element 

238 ) from e 

239 return cls(td, params=params) 

240 

241 

242__all__ = ["vDuration"]