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

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 ( 

10 ICalParsingError, 

11 InvalidCalendar, 

12 JCalParsingError, 

13 XCalParsingError, 

14) 

15from icalendar.parser import Parameters 

16from icalendar.parser.xcal.base import XCalParser 

17from icalendar.parser.xcal.wrapper import from_xcal_wrapper 

18 

19from .base import TimeBase 

20 

21DURATION_REGEX = re.compile( 

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

23) 

24 

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

26 

27 

28class vDuration(TimeBase): 

29 """Duration 

30 

31 Value Name: 

32 DURATION 

33 

34 Purpose: 

35 This value type is used to identify properties that contain 

36 a duration of time. 

37 

38 Format Definition: 

39 This value type is defined by the following notation: 

40 

41 .. code-block:: text 

42 

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

44 

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

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

47 dur-week = 1*DIGIT "W" 

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

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

50 dur-second = 1*DIGIT "S" 

51 dur-day = 1*DIGIT "D" 

52 

53 Description: 

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

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

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

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

58 represent nominal durations (weeks and days) and accurate 

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

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

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

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

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

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

65 computation of the exact duration requires the subtraction or 

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

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

68 When computing an exact duration, the greatest order time 

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

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

71 minutes, and number of seconds. 

72 

73 Example: 

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

75 

76 .. code-block:: ics 

77 

78 P15DT5H0M20S 

79 

80 A duration of 7 weeks would be: 

81 

82 .. code-block:: ics 

83 

84 P7W 

85 

86 .. code-block:: pycon 

87 

88 >>> from icalendar.prop import vDuration 

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

90 >>> duration 

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

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

93 >>> duration 

94 datetime.timedelta(days=49) 

95 """ 

96 

97 default_value: ClassVar[str] = "DURATION" 

98 params: Parameters 

99 

100 def __init__( 

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

102 ) -> None: 

103 if isinstance(td, str): 

104 td = vDuration.from_ical(td) 

105 if not isinstance(td, timedelta): 

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

107 self.td = td 

108 self.params = Parameters(params) 

109 

110 def to_ical(self) -> bytes: 

111 sign = "" 

112 td = self.td 

113 if td.days < 0: 

114 sign = "-" 

115 td = -td 

116 timepart = "" 

117 if td.seconds: 

118 timepart = "T" 

119 hours = td.seconds // 3600 

120 minutes = td.seconds % 3600 // 60 

121 seconds = td.seconds % 60 

122 if hours: 

123 timepart += f"{hours}H" 

124 if minutes or (hours and seconds): 

125 timepart += f"{minutes}M" 

126 if seconds: 

127 timepart += f"{seconds}S" 

128 if td.days == 0 and timepart: 

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

130 return ( 

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

132 + b"P" 

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

134 + b"D" 

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

136 ) 

137 

138 @staticmethod 

139 def from_ical(ical) -> timedelta: 

140 """Parse an iCalendar DURATION value. 

141 

142 Raises: 

143 ~icalendar.error.ICalParsingError: If the value is not a valid DURATION. 

144 """ 

145 match = DURATION_REGEX.match(ical) 

146 if not match: 

147 raise ICalParsingError( 

148 "Invalid iCalendar duration", 

149 value=ical, 

150 ) 

151 

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

153 

154 try: 

155 value = timedelta( 

156 weeks=int(weeks or 0), 

157 days=int(days or 0), 

158 hours=int(hours or 0), 

159 minutes=int(minutes or 0), 

160 seconds=int(seconds or 0), 

161 ) 

162 except OverflowError as e: 

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

164 # implementation. 

165 raise ICalParsingError( 

166 "Impractical iCalendar duration", 

167 value=ical, 

168 ) from e 

169 

170 if sign == "-": 

171 value = -value 

172 

173 return value 

174 

175 @property 

176 def dt(self) -> timedelta: 

177 """The time delta for compatibility.""" 

178 return self.td 

179 

180 @classmethod 

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

182 """Examples of vDuration.""" 

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

184 

185 from icalendar.param import VALUE 

186 

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

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

189 return [ 

190 name, 

191 self.params.to_jcal(), 

192 self.VALUE.lower(), 

193 self.to_ical().decode(), 

194 ] 

195 

196 @classmethod 

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

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

199 

200 Raises: 

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

202 JCalParsingError.validate_value_type(jcal, str, cls) 

203 try: 

204 return cls.from_ical(jcal) 

205 except (ValueError, InvalidCalendar) as e: 

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

207 

208 @classmethod 

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

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

211 

212 Parameters: 

213 jcal_property: The jCal property to parse. 

214 

215 Raises: 

216 ~icalendar.error.JCalParsingError: If the provided jCal is invalid. 

217 """ 

218 JCalParsingError.validate_property(jcal_property, cls) 

219 with JCalParsingError.reraise_with_path_added(3): 

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

221 return cls( 

222 duration, 

223 Parameters.from_jcal_property(jcal_property), 

224 ) 

225 

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

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

228 self.params.to_xcal(element) 

229 element = SubElement(element, "duration") 

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

231 

232 @classmethod 

233 @from_xcal_wrapper 

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

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

236 

237 Parameters: 

238 xml: The XML to parse or a parser. 

239 

240 Raises: 

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

242 """ 

243 element = parser.parse_tag(cls.default_value) 

244 try: 

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

246 except InvalidCalendar as e: 

247 if isinstance(e.__cause__, OverflowError): 

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

249 raise XCalParsingError( 

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

251 ) from e 

252 raise XCalParsingError( 

253 XCAL_DURATION_ERROR, element.get_xsd_token(), element 

254 ) from e 

255 return cls(td, params=params) 

256 

257 

258__all__ = ["vDuration"]