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

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

106 statements  

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

2 

3from datetime import date, datetime, timedelta 

4from typing import Any, ClassVar 

5 

6from icalendar.compatibility import Self 

7from icalendar.error import JCalParsingError 

8from icalendar.parser import Parameters 

9from icalendar.timezone import tzp 

10from icalendar.tools import is_datetime, normalize_pytz 

11 

12from .base import TimeBase 

13from .datetime import vDatetime 

14from .duration import vDuration 

15 

16 

17class vPeriod(TimeBase): 

18 """Period of Time 

19 

20 Value Name: 

21 PERIOD 

22 

23 Purpose: 

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

25 precise period of time. 

26 

27 Format Definition: 

28 This value type is defined by the following notation: 

29 

30 .. code-block:: text 

31 

32 period = period-explicit / period-start 

33 

34 period-explicit = date-time "/" date-time 

35 ; [ISO.8601.2004] complete representation basic format for a 

36 ; period of time consisting of a start and end. The start MUST 

37 ; be before the end. 

38 

39 period-start = date-time "/" dur-value 

40 ; [ISO.8601.2004] complete representation basic format for a 

41 ; period of time consisting of a start and positive duration 

42 ; of time. 

43 

44 Description: 

45 If the property permits, multiple "period" values are 

46 specified by a COMMA-separated list of values. There are two 

47 forms of a period of time. First, a period of time is identified 

48 by its start and its end. This format is based on the 

49 [ISO.8601.2004] complete representation, basic format for "DATE- 

50 TIME" start of the period, followed by a SOLIDUS character 

51 followed by the "DATE-TIME" of the end of the period. The start 

52 of the period MUST be before the end of the period. Second, a 

53 period of time can also be defined by a start and a positive 

54 duration of time. The format is based on the [ISO.8601.2004] 

55 complete representation, basic format for the "DATE-TIME" start of 

56 the period, followed by a SOLIDUS character, followed by the 

57 [ISO.8601.2004] basic format for "DURATION" of the period. 

58 

59 Example: 

60 The period starting at 18:00:00 UTC, on January 1, 1997 and 

61 ending at 07:00:00 UTC on January 2, 1997 would be: 

62 

63 .. code-block:: ics 

64 

65 19970101T180000Z/19970102T070000Z 

66 

67 The period start at 18:00:00 on January 1, 1997 and lasting 5 hours 

68 and 30 minutes would be: 

69 

70 .. code-block:: ics 

71 

72 19970101T180000Z/PT5H30M 

73 

74 .. code-block:: pycon 

75 

76 >>> from icalendar.prop import vPeriod 

77 >>> period = vPeriod.from_ical('19970101T180000Z/19970102T070000Z') 

78 >>> period = vPeriod.from_ical('19970101T180000Z/PT5H30M') 

79 """ 

80 

81 default_value: ClassVar[str] = "PERIOD" 

82 params: Parameters 

83 by_duration: bool 

84 start: datetime 

85 end: datetime 

86 duration: timedelta 

87 

88 def __init__( 

89 self, 

90 per: tuple[datetime, datetime | timedelta], 

91 params: dict[str, Any] | None = None, 

92 ): 

93 start, end_or_duration = per 

94 if not (isinstance(start, (datetime, date))): 

95 raise TypeError("Start value MUST be a datetime or date instance") 

96 if not (isinstance(end_or_duration, (datetime, date, timedelta))): 

97 raise TypeError( 

98 "end_or_duration MUST be a datetime, date or timedelta instance" 

99 ) 

100 by_duration = isinstance(end_or_duration, timedelta) 

101 if by_duration: 

102 duration = end_or_duration 

103 end = normalize_pytz(start + duration) 

104 else: 

105 end = end_or_duration 

106 duration = normalize_pytz(end - start) 

107 if start > end: 

108 raise ValueError("Start time is greater than end time") 

109 

110 self.params = Parameters(params or {"value": "PERIOD"}) 

111 # set the timezone identifier 

112 # does not support different timezones for start and end 

113 self.params.update_tzid_from(start) 

114 

115 self.start = start 

116 self.end = end 

117 self.by_duration = by_duration 

118 self.duration = duration 

119 

120 def overlaps(self, other): 

121 if self.start > other.start: 

122 return other.overlaps(self) 

123 return self.start <= other.start < self.end 

124 

125 def to_ical(self): 

126 if self.by_duration: 

127 return ( 

128 vDatetime(self.start).to_ical() 

129 + b"/" 

130 + vDuration(self.duration).to_ical() 

131 ) 

132 return vDatetime(self.start).to_ical() + b"/" + vDatetime(self.end).to_ical() 

133 

134 @staticmethod 

135 def from_ical(ical, timezone=None): 

136 from icalendar.prop.dt.types import vDDDTypes 

137 

138 try: 

139 start, end_or_duration = ical.split("/") 

140 start = vDDDTypes.from_ical(start, timezone=timezone) 

141 end_or_duration = vDDDTypes.from_ical(end_or_duration, timezone=timezone) 

142 except Exception as e: 

143 raise ValueError(f"Expected period format, got: {ical}") from e 

144 return (start, end_or_duration) 

145 

146 def __repr__(self): 

147 p = (self.start, self.duration) if self.by_duration else (self.start, self.end) 

148 return f"vPeriod({p!r})" 

149 

150 @property 

151 def dt(self): 

152 """Make this cooperate with the other vDDDTypes.""" 

153 return (self.start, (self.duration if self.by_duration else self.end)) 

154 

155 @property 

156 def ical_value(self) -> tuple[datetime, timedelta | datetime]: 

157 """ 

158 Returns the period as a tuple of its start datetime 

159 and either its end datetime or duration. 

160 """ 

161 return self.dt 

162 

163 from icalendar.param import FBTYPE 

164 

165 @classmethod 

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

167 """Examples of vPeriod.""" 

168 return [ 

169 vPeriod((datetime(2025, 11, 10, 16, 35), timedelta(hours=1, minutes=30))), 

170 vPeriod((datetime(2025, 11, 10, 16, 35), datetime(2025, 11, 10, 18, 5))), 

171 ] 

172 

173 from icalendar.param import VALUE 

174 

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

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

177 value = [vDatetime(self.start).to_jcal(name)[-1]] 

178 if self.by_duration: 

179 value.append(vDuration(self.duration).to_jcal(name)[-1]) 

180 else: 

181 value.append(vDatetime(self.end).to_jcal(name)[-1]) 

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

183 

184 @classmethod 

185 def parse_jcal_value( 

186 cls, jcal: str | list 

187 ) -> tuple[datetime, datetime] | tuple[datetime, timedelta]: 

188 """Parse a jCal value. 

189 

190 Raises: 

191 ~error.JCalParsingError: If the period is not a list with exactly two items, 

192 or it can't parse a date-time or duration. 

193 """ 

194 if isinstance(jcal, str) and "/" in jcal: 

195 # only occurs in the example of RFC7265, Section B.2.2. 

196 jcal = jcal.split("/") 

197 if not isinstance(jcal, list) or len(jcal) != 2: 

198 raise JCalParsingError( 

199 "A period must be a list with exactly 2 items.", cls, value=jcal 

200 ) 

201 with JCalParsingError.reraise_with_path_added(0): 

202 start = vDatetime.parse_jcal_value(jcal[0]) 

203 with JCalParsingError.reraise_with_path_added(1): 

204 JCalParsingError.validate_value_type(jcal[1], str, cls) 

205 if jcal[1].startswith(("P", "-P", "+P")): 

206 end_or_duration = vDuration.parse_jcal_value(jcal[1]) 

207 else: 

208 try: 

209 end_or_duration = vDatetime.parse_jcal_value(jcal[1]) 

210 except JCalParsingError as e: 

211 raise JCalParsingError( 

212 "Cannot parse date-time or duration.", 

213 cls, 

214 value=jcal[1], 

215 ) from e 

216 return start, end_or_duration 

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 ~error.JCalParsingError: If the provided jCal is invalid. 

227 """ 

228 JCalParsingError.validate_property(jcal_property, cls) 

229 with JCalParsingError.reraise_with_path_added(3): 

230 start, end_or_duration = cls.parse_jcal_value(jcal_property[3]) 

231 params = Parameters.from_jcal_property(jcal_property) 

232 tzid = params.tzid 

233 

234 if tzid: 

235 start = tzp.localize(start, tzid) 

236 if is_datetime(end_or_duration): 

237 end_or_duration = tzp.localize(end_or_duration, tzid) 

238 

239 return cls((start, end_or_duration), params=params) 

240 

241 

242__all__ = ["vPeriod"]