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

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

111 statements  

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

2 

3import re 

4from datetime import datetime, time, timezone, tzinfo 

5from typing import Any, ClassVar 

6from xml.etree.ElementTree import Element, SubElement 

7 

8from icalendar.compatibility import Self 

9from icalendar.error import ICalParsingError, JCalParsingError, XCalParsingError 

10from icalendar.parser import Parameters 

11from icalendar.parser.xcal.base import XCalParser 

12from icalendar.parser.xcal.match import XCalRegexMatcher 

13from icalendar.parser.xcal.wrapper import from_xcal_wrapper 

14from icalendar.timezone import tzp 

15from icalendar.timezone.tzid import is_utc 

16 

17from .base import TimeBase 

18 

19TIME_JCAL_REGEX = re.compile( 

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

21) 

22 

23XCAL_TIME_REGEX = XCalRegexMatcher( 

24 TIME_JCAL_REGEX, 

25 "Expected time format HH:MM:SS or HH:MM:SSZ", 

26) 

27 

28 

29class vTime(TimeBase): 

30 """Time 

31 

32 Value Name: 

33 TIME 

34 

35 Purpose: 

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

37 time of day. 

38 

39 Format Definition: 

40 This value type is defined by the following notation: 

41 

42 .. code-block:: text 

43 

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

45 

46 time-hour = 2DIGIT ;00-23 

47 time-minute = 2DIGIT ;00-59 

48 time-second = 2DIGIT ;00-60 

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

50 

51 time-utc = "Z" 

52 

53 Description: 

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

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

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

57 vText) is defined for this value type. 

58 

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

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

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

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

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

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

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

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

67 

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

69 type expresses time values in three forms: 

70 

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

72 the following is not valid for a time value: 

73 

74 .. code-block:: ics 

75 

76 230000-0800 ;Invalid time format 

77 

78 **FORM #1 LOCAL TIME** 

79 

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

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

82 example, 11:00 PM: 

83 

84 .. code-block:: ics 

85 

86 230000 

87 

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

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

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

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

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

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

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

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

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

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

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

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

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

101 

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

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

104 time zone reference MUST be specified. 

105 

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

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

108 existence of "VTIMEZONE" calendar components in the iCalendar 

109 object. 

110 

111 **FORM #2: UTC TIME** 

112 

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

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

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

116 

117 .. code-block:: ics 

118 

119 070000Z 

120 

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

122 properties whose time values are specified in UTC. 

123 

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

125 

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

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

128 the appropriate time zone definition. 

129 

130 Example: 

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

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

133 

134 .. code-block:: ics 

135 

136 083000 

137 133000Z 

138 TZID=America/New_York:083000 

139 """ 

140 

141 default_value: ClassVar[str] = "TIME" 

142 params: Parameters 

143 

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

145 if len(args) == 1: 

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

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

148 self.dt = args[0] 

149 else: 

150 self.dt = time(*args) 

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

152 self.params.update_tzid_from(self.dt) 

153 

154 def to_ical(self): 

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

156 if self.is_utc(): 

157 value += "Z" 

158 return value 

159 

160 def is_utc(self) -> bool: 

161 """Whether this time is UTC.""" 

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

163 

164 @staticmethod 

165 def from_ical(ical: str, timezone: str | None | tzinfo = None) -> time: 

166 """Convert an ical string into a time. 

167 

168 This parses the time forms defined in :rfc:`5545#section-3.3.12`: 

169 

170 - Local time (floating) 

171 - UTC time 

172 - Local time with time zone reference 

173 

174 Returns: 

175 A :class:`datetime.time` object representing the parsed time, with 

176 timezone information if applicable. 

177 

178 Raises: 

179 ~icalendar.error.ICalParsingError: If the time cannot be parsed. 

180 """ 

181 tzinfo = None 

182 if isinstance(timezone, str): 

183 tzinfo = tzp.timezone(timezone) 

184 elif timezone is not None: 

185 tzinfo = timezone 

186 

187 if isinstance(ical, bytes): 

188 ical = ical.decode() 

189 

190 original_ical = ical 

191 

192 # Extract the value part if parameters are present per 

193 # https://datatracker.ietf.org/doc/html/rfc5545.html#section-3.3.5 

194 # Form #3: TZID=America/New_York:083000 

195 ical = ical.rpartition(":")[2] 

196 

197 if utc := ical.endswith("Z"): 

198 ical = ical[:-1] 

199 

200 # time = time-hour time-minute time-second [time-utc], six digits, 

201 # per https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.12 

202 if len(ical) != 6 or not ical.isascii() or not ical.isdigit(): 

203 raise ICalParsingError( 

204 "Expected time", 

205 value=original_ical, 

206 ) 

207 

208 try: 

209 result = time(int(ical[:2]), int(ical[2:4]), int(ical[4:6])) 

210 except Exception as e: 

211 raise ICalParsingError( 

212 "Expected time", 

213 value=original_ical, 

214 ) from e 

215 

216 if tzinfo: 

217 return tzp.localize(result, tzinfo) 

218 

219 if utc: 

220 return tzp.localize_utc(result) 

221 

222 return result 

223 

224 @classmethod 

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

226 """Examples of vTime.""" 

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

228 

229 from icalendar.param import VALUE 

230 

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

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

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

234 if self.is_utc(): 

235 value += "Z" 

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

237 

238 @classmethod 

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

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

241 

242 Raises: 

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

244 """ 

245 JCalParsingError.validate_value_type(jcal, str, cls) 

246 match = TIME_JCAL_REGEX.match(jcal) 

247 if match is None: 

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

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

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

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

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

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

254 

255 @classmethod 

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

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

258 

259 Parameters: 

260 jcal_property: The jCal property to parse. 

261 

262 Raises: 

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

264 """ 

265 JCalParsingError.validate_property(jcal_property, cls) 

266 with JCalParsingError.reraise_with_path_added(3): 

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

268 return cls( 

269 value, 

270 params=Parameters.from_jcal_property(jcal_property), 

271 ) 

272 

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

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

275 self.params.to_xcal(element) 

276 element = SubElement(element, self.default_value.lower()) 

277 text = self.dt.strftime("%H:%M:%S") 

278 if is_utc(self.dt): 

279 text += "Z" 

280 element.text = text 

281 

282 @classmethod 

283 @from_xcal_wrapper 

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

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

286 

287 Parameters: 

288 xml: The XML to parse or a parser. 

289 

290 Raises: 

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

292 """ 

293 element = parser.parse_tag(cls.default_value) 

294 hour, minute, second, utc = XCAL_TIME_REGEX.groups(element) 

295 if second == "60": 

296 # This is wrong but we have no way to represent this 

297 second = 59 

298 try: 

299 dt = time(int(hour), int(minute), int(second)) 

300 except ValueError as e: 

301 raise XCalParsingError( 

302 "Time is out of range", element.get_xsd_token(), element 

303 ) from e 

304 tzid = params.tzid 

305 if utc: 

306 if tzid: 

307 raise XCalParsingError(f"Cannot mix {tzid} with UTC", None, element) 

308 dt = tzp.localize_utc(dt) 

309 elif tzid: 

310 dt = tzp.localize(dt, tzid) 

311 return cls(dt, params=params) 

312 

313 

314__all__ = ["vTime"]