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

110 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 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 method supports parsing the three forms of time values defined in :rfc:`5545#section-3.3.12`: 

169 - Local time (floating) 

170 - UTC time 

171 - Local time with time zone reference 

172 

173 Returns: 

174 A :class:`datetime.time` object representing the parsed time, with timezone information if applicable. 

175 

176 Raises: 

177 ValueError: if the provided string cannot be parsed as a time. 

178 """ 

179 tzinfo = None 

180 if isinstance(timezone, str): 

181 tzinfo = tzp.timezone(timezone) 

182 elif timezone is not None: 

183 tzinfo = timezone 

184 

185 if isinstance(ical, bytes): 

186 ical = ical.decode() 

187 # Extract the value part if parameters are present per 

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

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

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

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

192 ical = ical[:-1] 

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

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

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

196 raise ValueError(f"Expected time, got: {ical}") 

197 try: 

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

199 if tzinfo: 

200 return tzp.localize(time(*timetuple), tzinfo) 

201 if utc: 

202 return tzp.localize_utc(time(*timetuple)) 

203 return time(*timetuple) 

204 except Exception as e: 

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

206 

207 @classmethod 

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

209 """Examples of vTime.""" 

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

211 

212 from icalendar.param import VALUE 

213 

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

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

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

217 if self.is_utc(): 

218 value += "Z" 

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

220 

221 @classmethod 

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

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

224 

225 Raises: 

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

227 """ 

228 JCalParsingError.validate_value_type(jcal, str, cls) 

229 match = TIME_JCAL_REGEX.match(jcal) 

230 if match is None: 

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

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

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

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

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

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

237 

238 @classmethod 

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

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

241 

242 Parameters: 

243 jcal_property: The jCal property to parse. 

244 

245 Raises: 

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

247 """ 

248 JCalParsingError.validate_property(jcal_property, cls) 

249 with JCalParsingError.reraise_with_path_added(3): 

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

251 return cls( 

252 value, 

253 params=Parameters.from_jcal_property(jcal_property), 

254 ) 

255 

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

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

258 self.params.to_xcal(element) 

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

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

261 if is_utc(self.dt): 

262 text += "Z" 

263 element.text = text 

264 

265 @classmethod 

266 @from_xcal_wrapper 

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

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

269 

270 Parameters: 

271 xml: The XML to parse or a parser. 

272 

273 Raises: 

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

275 """ 

276 element = parser.parse_tag(cls.default_value) 

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

278 if second == "60": 

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

280 second = 59 

281 try: 

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

283 except ValueError as e: 

284 raise XCalParsingError( 

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

286 ) from e 

287 tzid = params.tzid 

288 if utc: 

289 if tzid: 

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

291 dt = tzp.localize_utc(dt) 

292 elif tzid: 

293 dt = tzp.localize(dt, tzid) 

294 return cls(dt, params=params) 

295 

296 

297__all__ = ["vTime"]