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

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

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__(self, *args, params: dict[str, Any] | None = None, **kwargs): 

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

167 # we have a string as an argument. 

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

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

170 if not isinstance(v, SEQUENCE_TYPES): 

171 kwargs[k] = [v] 

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

173 self.params = Parameters(params) 

174 

175 def to_ical(self): 

176 result = [] 

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

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

179 if not isinstance(vals, SEQUENCE_TYPES): 

180 vals = [vals] 

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

182 

183 # CaselessDict keys are always unicode 

184 param_key = key.encode(DEFAULT_ENCODING) 

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

186 

187 return b";".join(result) 

188 

189 @classmethod 

190 def parse_type(cls, key, values): 

191 # integers 

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

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

194 

195 @classmethod 

196 def from_ical(cls, ical: str): 

197 if isinstance(ical, cls): 

198 return ical 

199 try: 

200 recur = cls() 

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

202 try: 

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

204 except ValueError: 

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

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

207 continue 

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

209 return cls(recur) 

210 except ValueError: 

211 raise 

212 except Exception as e: 

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

214 

215 @classmethod 

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

217 """Examples of vRecur.""" 

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

219 

220 from icalendar.param import VALUE 

221 

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

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

224 recur = {} 

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

226 key = k.lower() 

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

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

229 elif not isinstance(v, list): 

230 value = [v] 

231 else: 

232 value = v 

233 recur[key] = value 

234 if "until" in recur: 

235 until = recur["until"] 

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

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

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

239 

240 @classmethod 

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

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

243 

244 Parameters: 

245 jcal_property: The jCal property to parse. 

246 

247 Raises: 

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

249 """ 

250 JCalParsingError.validate_property(jcal_property, cls) 

251 params = Parameters.from_jcal_property(jcal_property) 

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

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

254 ): 

255 raise JCalParsingError( 

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

257 cls, 

258 3, 

259 value=jcal_property[3], 

260 ) 

261 recur = {} 

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

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

264 with JCalParsingError.reraise_with_path_added(3, key): 

265 if isinstance(value, list): 

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

267 for i, v in enumerate(value): 

268 with JCalParsingError.reraise_with_path_added(i): 

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

270 else: 

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

272 until = recur.get("until") 

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

274 recur["until"] = [until] 

275 return cls(recur, params=params) 

276 

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

278 """self == other""" 

279 if not isinstance(other, vRecur): 

280 return super().__eq__(other) 

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

282 return False 

283 for key in self.keys(): 

284 v1 = self[key] 

285 v2 = other[key] 

286 if not isinstance(v1, SEQUENCE_TYPES): 

287 v1 = [v1] 

288 if not isinstance(v2, SEQUENCE_TYPES): 

289 v2 = [v2] 

290 if v1 != v2: 

291 return False 

292 return True 

293 

294 __hash__ = None 

295 

296 

297__all__ = ["vRecur"]