Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/prop/recur/recur.py: 53%

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"""RECUR property type from :rfc:`5545`.""" 

2 

3from typing import Any, ClassVar 

4 

5from icalendar.caselessdict import CaselessDict 

6from icalendar.compatibility import Self 

7from icalendar.error import JCalParsingError 

8from icalendar.parser import Parameters 

9from icalendar.parser_tools import DEFAULT_ENCODING, SEQUENCE_TYPES 

10from icalendar.prop.dt import vDDDTypes 

11from icalendar.prop.integer import vInt 

12from icalendar.prop.recur.frequency import vFrequency 

13from icalendar.prop.recur.month import vMonth 

14from icalendar.prop.recur.skip import vSkip 

15from icalendar.prop.recur.weekday import vWeekday 

16from icalendar.prop.text import vText 

17 

18 

19class vRecur(CaselessDict): 

20 """Recurrence definition. 

21 

22 Property Name: 

23 RRULE 

24 

25 Purpose: 

26 This property defines a rule or repeating pattern for recurring events, to-dos, 

27 journal entries, or time zone definitions. 

28 

29 Value Type: 

30 RECUR 

31 

32 Property Parameters: 

33 IANA and non-standard property parameters can be specified on this property. 

34 

35 Conformance: 

36 This property can be specified in recurring "VEVENT", "VTODO", and "VJOURNAL" 

37 calendar components as well as in the "STANDARD" and "DAYLIGHT" sub-components 

38 of the "VTIMEZONE" calendar component, but it SHOULD NOT be specified more than once. 

39 The recurrence set generated with multiple "RRULE" properties is undefined. 

40 

41 Description: 

42 The recurrence rule, if specified, is used in computing the recurrence set. 

43 The recurrence set is the complete set of recurrence instances for a calendar component. 

44 The recurrence set is generated by considering the initial "DTSTART" property along 

45 with the "RRULE", "RDATE", and "EXDATE" properties contained within the 

46 recurring component. The "DTSTART" property defines the first instance in the 

47 recurrence set. The "DTSTART" property value SHOULD be synchronized with the 

48 recurrence rule, if specified. The recurrence set generated with a "DTSTART" property 

49 value not synchronized with the recurrence rule is undefined. 

50 The final recurrence set is generated by gathering all of the start DATE-TIME 

51 values generated by any of the specified "RRULE" and "RDATE" properties, and then 

52 excluding any start DATE-TIME values specified by "EXDATE" properties. 

53 This implies that start DATE- TIME values specified by "EXDATE" properties take 

54 precedence over those specified by inclusion properties (i.e., "RDATE" and "RRULE"). 

55 Where duplicate instances are generated by the "RRULE" and "RDATE" properties, 

56 only one recurrence is considered. Duplicate instances are ignored. 

57 

58 The "DTSTART" property specified within the iCalendar object defines the first 

59 instance of the recurrence. In most cases, a "DTSTART" property of DATE-TIME value 

60 type used with a recurrence rule, should be specified as a date with local time 

61 and time zone reference to make sure all the recurrence instances start at the 

62 same local time regardless of time zone changes. 

63 

64 If the duration of the recurring component is specified with the "DTEND" or 

65 "DUE" property, then the same exact duration will apply to all the members of the 

66 generated recurrence set. Else, if the duration of the recurring component is 

67 specified with the "DURATION" property, then the same nominal duration will apply 

68 to all the members of the generated recurrence set and the exact duration of each 

69 recurrence instance will depend on its specific start time. For example, recurrence 

70 instances of a nominal duration of one day will have an exact duration of more or less 

71 than 24 hours on a day where a time zone shift occurs. The duration of a specific 

72 recurrence may be modified in an exception component or simply by using an 

73 "RDATE" property of PERIOD value type. 

74 

75 Examples: 

76 The following RRULE specifies daily events for 10 occurrences. 

77 

78 .. code-block:: ics 

79 

80 RRULE:FREQ=DAILY;COUNT=10 

81 

82 Below, we parse the RRULE ical string. 

83 

84 .. code-block:: pycon 

85 

86 >>> from icalendar.prop import vRecur 

87 >>> rrule = vRecur.from_ical('FREQ=DAILY;COUNT=10') 

88 >>> rrule 

89 vRecur({'FREQ': ['DAILY'], 'COUNT': [10]}) 

90 

91 You can choose to add an rrule to an :class:`icalendar.cal.Event` or 

92 :class:`icalendar.cal.Todo`. 

93 

94 .. code-block:: pycon 

95 

96 >>> from icalendar import Event 

97 >>> event = Event() 

98 >>> event.add('RRULE', 'FREQ=DAILY;COUNT=10') 

99 >>> event.rrules 

100 [vRecur({'FREQ': ['DAILY'], 'COUNT': [10]})] 

101 """ 

102 

103 default_value: ClassVar[str] = "RECUR" 

104 params: Parameters 

105 

106 frequencies = [ 

107 "SECONDLY", 

108 "MINUTELY", 

109 "HOURLY", 

110 "DAILY", 

111 "WEEKLY", 

112 "MONTHLY", 

113 "YEARLY", 

114 ] 

115 

116 # Mac iCal ignores RRULEs where FREQ is not the first rule part. 

117 # Sorts parts according to the order listed in RFC 5545, section 3.3.10. 

118 canonical_order = ( 

119 "RSCALE", 

120 "FREQ", 

121 "UNTIL", 

122 "COUNT", 

123 "INTERVAL", 

124 "BYSECOND", 

125 "BYMINUTE", 

126 "BYHOUR", 

127 "BYDAY", 

128 "BYWEEKDAY", 

129 "BYMONTHDAY", 

130 "BYYEARDAY", 

131 "BYWEEKNO", 

132 "BYMONTH", 

133 "BYSETPOS", 

134 "WKST", 

135 "SKIP", 

136 ) 

137 

138 types = CaselessDict( 

139 { 

140 "COUNT": vInt, 

141 "INTERVAL": vInt, 

142 "BYSECOND": vInt, 

143 "BYMINUTE": vInt, 

144 "BYHOUR": vInt, 

145 "BYWEEKNO": vInt, 

146 "BYMONTHDAY": vInt, 

147 "BYYEARDAY": vInt, 

148 "BYMONTH": vMonth, 

149 "UNTIL": vDDDTypes, 

150 "BYSETPOS": vInt, 

151 "WKST": vWeekday, 

152 "BYDAY": vWeekday, 

153 "FREQ": vFrequency, 

154 "BYWEEKDAY": vWeekday, 

155 "SKIP": vSkip, # RFC 7529 

156 "RSCALE": vText, # RFC 7529 

157 } 

158 ) 

159 

160 # for reproducible serialization: 

161 # RULE: if and only if it can be a list it will be a list 

162 # look up in RFC 

163 jcal_not_a_list = {"FREQ", "UNTIL", "COUNT", "INTERVAL", "WKST", "SKIP", "RSCALE"} 

164 

165 def __init__( 

166 self, *args: Any, params: dict[str, Any] | None = None, **kwargs: Any 

167 ) -> None: 

168 if args and isinstance(args[0], str): 

169 # we have a string as an argument. 

170 args = (self.from_ical(args[0]),) + args[1:] 

171 for k, v in kwargs.items(): 

172 if not isinstance(v, SEQUENCE_TYPES): 

173 kwargs[k] = [v] 

174 super().__init__(*args, **kwargs) 

175 self.params = Parameters(params) 

176 

177 def to_ical(self): 

178 result = [] 

179 for key, vals in self.sorted_items(): 

180 typ = self.types.get(key, vText) 

181 if not isinstance(vals, SEQUENCE_TYPES): 

182 vals = [vals] 

183 param_vals = b",".join(typ(val).to_ical() for val in vals) 

184 

185 # CaselessDict keys are always unicode 

186 param_key = key.encode(DEFAULT_ENCODING) 

187 result.append(param_key + b"=" + param_vals) 

188 

189 return b";".join(result) 

190 

191 @classmethod 

192 def parse_type(cls, key, values): 

193 # integers 

194 parser = cls.types.get(key, vText) 

195 return [parser.from_ical(v) for v in values.split(",")] 

196 

197 @classmethod 

198 def from_ical(cls, ical: str): 

199 if isinstance(ical, cls): 

200 return ical 

201 try: 

202 recur = cls() 

203 for pairs in ical.split(";"): 

204 try: 

205 key, vals = pairs.split("=") 

206 except ValueError: 

207 # E.g. incorrect trailing semicolon, like (issue #157): 

208 # FREQ=YEARLY;BYMONTH=11;BYDAY=1SU; 

209 continue 

210 recur[key] = cls.parse_type(key, vals) 

211 return cls(recur) 

212 except ValueError: 

213 raise 

214 except Exception as e: 

215 raise ValueError(f"Error in recurrence rule: {ical}") from e 

216 

217 @classmethod 

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

219 """Examples of vRecur.""" 

220 return [cls.from_ical("FREQ=DAILY;COUNT=10")] 

221 

222 from icalendar.param import VALUE 

223 

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

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

226 recur = {} 

227 for k, v in self.items(): 

228 key = k.lower() 

229 if key.upper() in self.jcal_not_a_list: 

230 value = v[0] if isinstance(v, list) and len(v) == 1 else v 

231 elif not isinstance(v, list): 

232 value = [v] 

233 else: 

234 value = v 

235 recur[key] = value 

236 if "until" in recur: 

237 until = recur["until"] 

238 until_jcal = vDDDTypes(until).to_jcal("until") 

239 recur["until"] = until_jcal[-1] 

240 return [name, self.params.to_jcal(), self.VALUE.lower(), recur] 

241 

242 @classmethod 

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

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

245 

246 Parameters: 

247 jcal_property: The jCal property to parse. 

248 

249 Raises: 

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

251 """ 

252 JCalParsingError.validate_property(jcal_property, cls) 

253 params = Parameters.from_jcal_property(jcal_property) 

254 if not isinstance(jcal_property[3], dict) or not all( 

255 isinstance(k, str) for k in jcal_property[3] 

256 ): 

257 raise JCalParsingError( 

258 "The recurrence rule must be a mapping with string keys.", 

259 cls, 

260 3, 

261 value=jcal_property[3], 

262 ) 

263 recur = {} 

264 for key, value in jcal_property[3].items(): 

265 JCalParsingError.validate_jcal_token( 

266 key, "recurrence rule part name", cls, path=[3, key] 

267 ) 

268 value_type = cls.types.get(key, vText) 

269 with JCalParsingError.reraise_with_path_added(3, key): 

270 if isinstance(value, list): 

271 recur[key.lower()] = values = [] 

272 for i, v in enumerate(value): 

273 with JCalParsingError.reraise_with_path_added(i): 

274 values.append(value_type.parse_jcal_value(v)) 

275 else: 

276 recur[key] = value_type.parse_jcal_value(value) 

277 until = recur.get("until") 

278 if until is not None and not isinstance(until, list): 

279 recur["until"] = [until] 

280 return cls(recur, params=params) 

281 

282 def __eq__(self, other: object) -> bool: 

283 """self == other""" 

284 if not isinstance(other, vRecur): 

285 return super().__eq__(other) 

286 if self.keys() != other.keys(): 

287 return False 

288 for key in self.keys(): 

289 v1 = self[key] 

290 v2 = other[key] 

291 if not isinstance(v1, SEQUENCE_TYPES): 

292 v1 = [v1] 

293 if not isinstance(v2, SEQUENCE_TYPES): 

294 v2 = [v2] 

295 if v1 != v2: 

296 return False 

297 return True 

298 

299 __hash__ = None 

300 

301 

302__all__ = ["vRecur"]