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

103 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:: text 

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:: text 

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 from icalendar.param import FBTYPE 

156 

157 @classmethod 

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

159 """Examples of vPeriod.""" 

160 return [ 

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

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

163 ] 

164 

165 from icalendar.param import VALUE 

166 

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

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

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

170 if self.by_duration: 

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

172 else: 

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

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

175 

176 @classmethod 

177 def parse_jcal_value( 

178 cls, jcal: str | list 

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

180 """Parse a jCal value. 

181 

182 Raises: 

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

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

185 """ 

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

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

188 jcal = jcal.split("/") 

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

190 raise JCalParsingError( 

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

192 ) 

193 with JCalParsingError.reraise_with_path_added(0): 

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

195 with JCalParsingError.reraise_with_path_added(1): 

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

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

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

199 else: 

200 try: 

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

202 except JCalParsingError as e: 

203 raise JCalParsingError( 

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

205 cls, 

206 value=jcal[1], 

207 ) from e 

208 return start, end_or_duration 

209 

210 @classmethod 

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

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

213 

214 Parameters: 

215 jcal_property: The jCal property to parse. 

216 

217 Raises: 

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

219 """ 

220 JCalParsingError.validate_property(jcal_property, cls) 

221 with JCalParsingError.reraise_with_path_added(3): 

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

223 params = Parameters.from_jcal_property(jcal_property) 

224 tzid = params.tzid 

225 

226 if tzid: 

227 start = tzp.localize(start, tzid) 

228 if is_datetime(end_or_duration): 

229 end_or_duration = tzp.localize(end_or_duration, tzid) 

230 

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

232 

233 

234__all__ = ["vPeriod"]