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

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

117 statements  

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

2 

3from datetime import datetime 

4from typing import Any, ClassVar 

5from xml.etree.ElementTree import Element, SubElement 

6 

7from icalendar.compatibility import Self 

8from icalendar.error import JCalParsingError, XCalParsingError 

9from icalendar.parser import Parameters 

10from icalendar.parser.xcal.adapter import ElementAdapter 

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.parser_tools import to_unicode 

15from icalendar.timezone import tzp 

16from icalendar.timezone.tzid import is_utc 

17 

18from .base import TimeBase 

19 

20XCAL_DATETIME_REGEX = XCalRegexMatcher( 

21 r"(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(Z?)", 

22 "Expected date-time format YYYY-MM-DDTHH:MM:SS or YYYY-MM-DDTHH:MM:SSZ", 

23) 

24 

25 

26class vDatetime(TimeBase): 

27 """Date-Time 

28 

29 Value Name: 

30 DATE-TIME 

31 

32 Purpose: 

33 This value type is used to identify values that specify a 

34 precise calendar date and time of day. The format is based on 

35 the ISO.8601.2004 complete representation. 

36 

37 Format Definition: 

38 This value type is defined by the following notation: 

39 

40 .. code-block:: text 

41 

42 date-time = date "T" time 

43 

44 date = date-value 

45 date-value = date-fullyear date-month date-mday 

46 date-fullyear = 4DIGIT 

47 date-month = 2DIGIT ;01-12 

48 date-mday = 2DIGIT ;01-28, 01-29, 01-30, 01-31 

49 ;based on month/year 

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

51 time-hour = 2DIGIT ;00-23 

52 time-minute = 2DIGIT ;00-59 

53 time-second = 2DIGIT ;00-60 

54 time-utc = "Z" 

55 

56 The following is the representation of the date-time format. 

57 

58 .. code-block:: text 

59 

60 YYYYMMDDTHHMMSS 

61 

62 Description: 

63 vDatetime is timezone aware and uses a timezone library. 

64 When a vDatetime object is created from an 

65 ical string, you can pass a valid timezone identifier. When a 

66 vDatetime object is created from a Python :mod:`datetime` object, it uses the 

67 tzinfo component, if present. Otherwise a timezone-naive object is 

68 created. Be aware that there are certain limitations with timezone naive 

69 DATE-TIME components in the icalendar standard. 

70 

71 Example: 

72 The following represents March 2, 2021 at 10:15 AM with local time: 

73 

74 .. code-block:: pycon 

75 

76 >>> from icalendar import vDatetime 

77 >>> datetime = vDatetime.from_ical("20210302T101500") 

78 >>> datetime.tzname() 

79 >>> datetime.year 

80 2021 

81 >>> datetime.minute 

82 15 

83 

84 The following represents March 2, 2021 at 10:15 AM in New York: 

85 

86 .. code-block:: pycon 

87 

88 >>> datetime = vDatetime.from_ical("20210302T101500", 'America/New_York') 

89 >>> datetime.tzname() 

90 'EST' 

91 

92 The following represents March 2, 2021 at 10:15 AM in Berlin: 

93 

94 .. code-block:: pycon 

95 

96 >>> from zoneinfo import ZoneInfo 

97 >>> timezone = ZoneInfo("Europe/Berlin") 

98 >>> vDatetime.from_ical("20210302T101500", timezone) 

99 datetime.datetime(2021, 3, 2, 10, 15, tzinfo=ZoneInfo(key='Europe/Berlin')) 

100 """ 

101 

102 default_value: ClassVar[str] = "DATE-TIME" 

103 params: Parameters 

104 

105 def __init__(self, dt: datetime, /, params: dict[str, Any] | None = None) -> None: 

106 self.dt = dt 

107 self.params = Parameters(params) 

108 self.params.update_tzid_from(dt) 

109 

110 def to_ical(self): 

111 dt = self.dt 

112 

113 s = ( 

114 f"{dt.year:04}{dt.month:02}{dt.day:02}" 

115 f"T{dt.hour:02}{dt.minute:02}{dt.second:02}" 

116 ) 

117 if self.is_utc(): 

118 s += "Z" 

119 return s.encode("utf-8") 

120 

121 @staticmethod 

122 def from_ical(ical, timezone=None): 

123 """Create a datetime from the RFC string.""" 

124 ical = to_unicode(ical) 

125 tzinfo = None 

126 if isinstance(timezone, str): 

127 tzinfo = tzp.timezone(timezone) 

128 elif timezone is not None: 

129 tzinfo = timezone 

130 

131 # Extract the value part if parameters are present per 

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

133 # Form #3: TZID=America/New_York:19980119T020000 

134 ical_value = ical.rpartition(":")[2] 

135 

136 if len(ical_value) < 15 or ical_value[8] != "T": 

137 raise ValueError(f"Wrong datetime format: {ical}") 

138 

139 try: 

140 timetuple = ( 

141 int(ical_value[:4]), # year 

142 int(ical_value[4:6]), # month 

143 int(ical_value[6:8]), # day 

144 int(ical_value[9:11]), # hour 

145 int(ical_value[11:13]), # minute 

146 int(ical_value[13:15]), # second 

147 ) 

148 if tzinfo: 

149 return tzp.localize(datetime(*timetuple), tzinfo) 

150 if not ical_value[15:]: 

151 return datetime(*timetuple) 

152 if ical_value[15:] == "Z": 

153 return tzp.localize_utc(datetime(*timetuple)) 

154 except Exception as e: 

155 raise ValueError(f"Wrong datetime format: {ical}") from e 

156 raise ValueError(f"Wrong datetime format: {ical}") 

157 

158 @classmethod 

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

160 """Examples of vDatetime.""" 

161 return [ 

162 cls(tzp.localize_utc(datetime(2025, 11, 10, 16, 52))), 

163 cls(tzp.localize(datetime(2025, 11, 10, 16, 52), "America/New_York")), 

164 cls(datetime(2025, 11, 10, 16, 52)), 

165 ] 

166 

167 from icalendar.param import VALUE 

168 

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

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

171 value = self.dt.strftime("%Y-%m-%dT%H:%M:%S") 

172 if self.is_utc(): 

173 value += "Z" 

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

175 

176 def is_utc(self) -> bool: 

177 """Whether this datetime is UTC.""" 

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

179 

180 @classmethod 

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

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

183 

184 Raises: 

185 ~error.JCalParsingError: If it can't parse a date-time value. 

186 """ 

187 JCalParsingError.validate_value_type(jcal, str, cls) 

188 utc = jcal.endswith("Z") 

189 if utc: 

190 jcal = jcal[:-1] 

191 try: 

192 dt = datetime.strptime(jcal, "%Y-%m-%dT%H:%M:%S") 

193 except ValueError as e: 

194 raise JCalParsingError("Cannot parse date-time.", cls, value=jcal) from e 

195 if utc: 

196 return tzp.localize_utc(dt) 

197 return dt 

198 

199 @classmethod 

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

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

202 

203 Parameters: 

204 jcal_property: The jCal property to parse. 

205 

206 Raises: 

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

208 """ 

209 JCalParsingError.validate_property(jcal_property, cls) 

210 params = Parameters.from_jcal_property(jcal_property) 

211 with JCalParsingError.reraise_with_path_added(3): 

212 dt = cls.parse_jcal_value(jcal_property[3]) 

213 if params.tzid: 

214 dt = tzp.localize(dt, params.tzid) 

215 return cls( 

216 dt, 

217 params=params, 

218 ) 

219 

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

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

222 

223 Parameters: 

224 element: The element to add the property to. 

225 """ 

226 self.params.to_xcal(element) 

227 self.to_xcal_value_only(element, self.default_value) 

228 

229 def to_xcal_value_only(self, element: Element, tag: str): 

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

231 

232 In contrast to :meth:`to_xcal`, this method does not add the parameters. 

233 

234 Parameters: 

235 element: The element to add the property to. 

236 tag: The tag to use. 

237 """ 

238 element = SubElement(element, tag.lower()) 

239 text = self.dt.strftime("%Y-%m-%dT%H:%M:%S") 

240 if is_utc(self.dt): 

241 text += "Z" 

242 element.text = text 

243 

244 @classmethod 

245 def from_xcal_element(cls, element: ElementAdapter) -> datetime: 

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

247 

248 Parameters: 

249 element: The xCal element to parse. 

250 

251 Returns: 

252 The datetime object, possibly with a UTC timezone. 

253 

254 Raises: 

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

256 """ 

257 year, month, day, hour, minute, second, utc = XCAL_DATETIME_REGEX.groups( 

258 element 

259 ) 

260 if second == "60": 

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

262 second = 59 

263 try: 

264 dt = datetime( 

265 int(year), int(month), int(day), int(hour), int(minute), int(second) 

266 ) 

267 except ValueError as e: 

268 raise XCalParsingError( 

269 "Date-time is out of range", element.get_xsd_token(), element 

270 ) from e 

271 if utc: 

272 dt = tzp.localize_utc(dt) 

273 return dt 

274 

275 @classmethod 

276 @from_xcal_wrapper 

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

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

279 

280 Parameters: 

281 xml: The XML to parse or a parser. 

282 

283 Raises: 

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

285 """ 

286 element = parser.parse_tag(cls.default_value) 

287 dt = cls.from_xcal_element(element) 

288 tzid = params.tzid 

289 if tzid: 

290 if is_utc(dt): 

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

292 dt = tzp.localize(dt, tzid) 

293 return cls(dt, params=params) 

294 

295 

296__all__ = ["vDatetime"]