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

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

94 statements  

1"""UTC-Offset 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 JCalParsingError 

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 

14 

15UTC_OFFSET_JCAL_REGEX = re.compile( 

16 r"^(?P<sign>[+-])?(?P<hours>\d\d):(?P<minutes>\d\d)(?::(?P<seconds>\d\d))?\Z" 

17) 

18 

19XCAL_UTC_OFFSET_REGEX = XCalRegexMatcher( 

20 UTC_OFFSET_JCAL_REGEX, 

21 "Expected utc-offset format https://datatracker.ietf.org/doc/html/rfc6321#section-3.6.14", 

22) 

23 

24 

25class vUTCOffset: 

26 """UTC Offset 

27 

28 Value Name: 

29 UTC-OFFSET 

30 

31 Purpose: 

32 This value type is used to identify properties that contain 

33 an offset from UTC to local time. 

34 

35 Format Definition: 

36 This value type is defined by the following notation: 

37 

38 .. code-block:: text 

39 

40 utc-offset = time-numzone 

41 

42 time-numzone = ("+" / "-") time-hour time-minute [time-second] 

43 

44 Description: 

45 The PLUS SIGN character MUST be specified for positive 

46 UTC offsets (i.e., ahead of UTC). The HYPHEN-MINUS character MUST 

47 be specified for negative UTC offsets (i.e., behind of UTC). The 

48 value of "-0000" and "-000000" are not allowed. The time-second, 

49 if present, MUST NOT be 60; if absent, it defaults to zero. 

50 

51 Example: 

52 The following UTC offsets are given for standard time for 

53 New York (five hours behind UTC) and Geneva (one hour ahead of 

54 UTC): 

55 

56 .. code-block:: ics 

57 

58 -0500 

59 

60 +0100 

61 

62 .. code-block:: pycon 

63 

64 >>> from icalendar.prop import vUTCOffset 

65 >>> utc_offset = vUTCOffset.from_ical('-0500') 

66 >>> utc_offset 

67 datetime.timedelta(days=-1, seconds=68400) 

68 >>> utc_offset = vUTCOffset.from_ical('+0100') 

69 >>> utc_offset 

70 datetime.timedelta(seconds=3600) 

71 """ 

72 

73 default_value: ClassVar[str] = "UTC-OFFSET" 

74 params: Parameters 

75 

76 ignore_exceptions = False # if True, and we cannot parse this 

77 

78 # component, we will silently ignore 

79 # it, rather than let the exception 

80 # propagate upwards 

81 

82 def __init__(self, td: timedelta, /, params: dict[str, Any] | None = None) -> None: 

83 if not isinstance(td, timedelta): 

84 raise TypeError("Offset value MUST be a timedelta instance") 

85 self.td = td 

86 self.params = Parameters(params) 

87 

88 def to_ical(self) -> str: 

89 """Return the ical representation.""" 

90 return self.format("") 

91 

92 def format(self, divider: str = "") -> str: 

93 """Represent the value with a possible divider. 

94 

95 .. code-block:: pycon 

96 

97 >>> from icalendar import vUTCOffset 

98 >>> from datetime import timedelta 

99 >>> utc_offset = vUTCOffset(timedelta(hours=-5)) 

100 >>> utc_offset.format() 

101 '-0500' 

102 >>> utc_offset.format(divider=':') 

103 '-05:00' 

104 """ 

105 if self.td < timedelta(0): 

106 sign = "-%s" 

107 td = timedelta(0) - self.td # get timedelta relative to 0 

108 else: 

109 # Google Calendar rejects '0000' but accepts '+0000' 

110 sign = "+%s" 

111 td = self.td 

112 

113 days, seconds = td.days, td.seconds 

114 

115 hours = abs(days * 24 + seconds // 3600) 

116 minutes = abs((seconds % 3600) // 60) 

117 seconds = abs(seconds % 60) 

118 if seconds: 

119 duration = f"{hours:02}{divider}{minutes:02}{divider}{seconds:02}" 

120 else: 

121 duration = f"{hours:02}{divider}{minutes:02}" 

122 return sign % duration 

123 

124 @classmethod 

125 def from_ical(cls, ical): 

126 if isinstance(ical, cls): 

127 return ical.td 

128 try: 

129 sign, hours, minutes, seconds = ( 

130 ical[0:1], 

131 int(ical[1:3]), 

132 int(ical[3:5]), 

133 int(ical[5:7] or 0), 

134 ) 

135 offset = timedelta(hours=hours, minutes=minutes, seconds=seconds) 

136 except Exception as e: 

137 raise ValueError(f"Expected UTC offset, got: {ical}") from e 

138 if not cls.ignore_exceptions and offset >= timedelta(hours=24): 

139 raise ValueError(f"Offset must be less than 24 hours, was {ical}") 

140 if sign == "-": 

141 return -offset 

142 return offset 

143 

144 def __eq__(self, other): 

145 if not isinstance(other, vUTCOffset): 

146 return False 

147 return self.td == other.td 

148 

149 def __hash__(self): 

150 return hash(self.td) 

151 

152 def __repr__(self): 

153 return f"vUTCOffset({self.td!r})" 

154 

155 @classmethod 

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

157 """Examples of vUTCOffset.""" 

158 return [ 

159 cls(timedelta(hours=3)), 

160 cls(timedelta(0)), 

161 ] 

162 

163 from icalendar.param import VALUE 

164 

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

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

167 return [name, self.params.to_jcal(), self.VALUE.lower(), self.format(":")] 

168 

169 @classmethod 

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

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

172 

173 Parameters: 

174 jcal_property: The jCal property to parse. 

175 

176 Raises: 

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

178 """ 

179 JCalParsingError.validate_property(jcal_property, cls) 

180 match = UTC_OFFSET_JCAL_REGEX.match(jcal_property[3]) 

181 if match is None: 

182 raise JCalParsingError(f"Cannot parse {jcal_property!r} as UTC-OFFSET.") 

183 negative = match.group("sign") == "-" 

184 hours = int(match.group("hours")) 

185 minutes = int(match.group("minutes")) 

186 seconds = int(match.group("seconds") or 0) 

187 t = timedelta(hours=hours, minutes=minutes, seconds=seconds) 

188 if negative: 

189 t = -t 

190 return cls(t, Parameters.from_jcal_property(jcal_property)) 

191 

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

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

194 self.params.to_xcal(element) 

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

196 element.text = self.format(":") 

197 

198 @classmethod 

199 @from_xcal_wrapper 

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

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

202 

203 Parameters: 

204 xml: The XML to parse or a parser. 

205 

206 Raises: 

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

208 """ 

209 element = parser.parse_tag(cls.default_value) 

210 sign, hour, minute, second = XCAL_UTC_OFFSET_REGEX.groups(element) 

211 dt = timedelta(hours=int(hour), minutes=int(minute), seconds=int(second or "0")) 

212 if sign == "-": 

213 dt = -dt 

214 return cls(dt, params=params) 

215 

216 

217__all__ = ["vUTCOffset"]