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 ICalParsingError, 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) -> bytes: 

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 

125 Parameters: 

126 ical (str | bytes): Required. The iCalendar DATE-TIME value to parse. 

127 timezone (str | datetime.tzinfo | None): The time zone name or object 

128 used to localize the result. If ``None``, a trailing ``Z`` selects 

129 UTC and other values remain floating. 

130 

131 Raises: 

132 ~icalendar.error.ICalParsingError: If the value is not a valid DATE-TIME. 

133 """ 

134 ical = to_unicode(ical) 

135 tzinfo = None 

136 if isinstance(timezone, str): 

137 tzinfo = tzp.timezone(timezone) 

138 elif timezone is not None: 

139 tzinfo = timezone 

140 

141 # Extract the value part if parameters are present per 

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

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

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

145 

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

147 raise ICalParsingError( 

148 "Wrong datetime format", 

149 value=ical, 

150 ) 

151 

152 try: 

153 timetuple = ( 

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

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

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

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

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

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

160 ) 

161 if tzinfo: 

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

163 if not ical_value[15:]: 

164 return datetime(*timetuple) 

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

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

167 except Exception as e: 

168 raise ICalParsingError( 

169 "Wrong datetime format", 

170 value=ical, 

171 ) from e 

172 raise ICalParsingError( 

173 "Wrong datetime format", 

174 value=ical, 

175 ) 

176 

177 @classmethod 

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

179 """Examples of vDatetime.""" 

180 return [ 

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

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

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

184 ] 

185 

186 from icalendar.param import VALUE 

187 

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

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

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

191 if self.is_utc(): 

192 value += "Z" 

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

194 

195 def is_utc(self) -> bool: 

196 """Whether this datetime is UTC.""" 

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

198 

199 @classmethod 

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

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

202 

203 Raises: 

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

205 """ 

206 JCalParsingError.validate_value_type(jcal, str, cls) 

207 utc = jcal.endswith("Z") 

208 if utc: 

209 jcal = jcal[:-1] 

210 try: 

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

212 except ValueError as e: 

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

214 if utc: 

215 return tzp.localize_utc(dt) 

216 return dt 

217 

218 @classmethod 

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

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

221 

222 Parameters: 

223 jcal_property: The jCal property to parse. 

224 

225 Raises: 

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

227 """ 

228 JCalParsingError.validate_property(jcal_property, cls) 

229 params = Parameters.from_jcal_property(jcal_property) 

230 with JCalParsingError.reraise_with_path_added(3): 

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

232 if params.tzid: 

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

234 return cls( 

235 dt, 

236 params=params, 

237 ) 

238 

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

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

241 

242 Parameters: 

243 element: The element to add the property to. 

244 """ 

245 self.params.to_xcal(element) 

246 self.to_xcal_value_only(element, self.default_value) 

247 

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

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

250 

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

252 

253 Parameters: 

254 element: The element to add the property to. 

255 tag: The tag to use. 

256 """ 

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

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

259 if is_utc(self.dt): 

260 text += "Z" 

261 element.text = text 

262 

263 @classmethod 

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

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

266 

267 Parameters: 

268 element: The xCal element to parse. 

269 

270 Returns: 

271 The datetime object, possibly with a UTC timezone. 

272 

273 Raises: 

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

275 """ 

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

277 element 

278 ) 

279 if second == "60": 

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

281 second = 59 

282 try: 

283 dt = datetime( 

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

285 ) 

286 except ValueError as e: 

287 raise XCalParsingError( 

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

289 ) from e 

290 if utc: 

291 dt = tzp.localize_utc(dt) 

292 return dt 

293 

294 @classmethod 

295 @from_xcal_wrapper 

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

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

298 

299 Parameters: 

300 xml: The XML to parse or a parser. 

301 

302 Raises: 

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

304 """ 

305 element = parser.parse_tag(cls.default_value) 

306 dt = cls.from_xcal_element(element) 

307 tzid = params.tzid 

308 if tzid: 

309 if is_utc(dt): 

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

311 dt = tzp.localize(dt, tzid) 

312 return cls(dt, params=params) 

313 

314 

315__all__ = ["vDatetime"]