1 #!/usr/bin/env python 2 3 """ 4 Parsing of vCalendar and iCalendar files. 5 6 Copyright (C) 2008, 2009, 2011, 2013, 2014, 2015, 7 2016 Paul Boddie <paul@boddie.org.uk> 8 9 This program is free software; you can redistribute it and/or modify it under 10 the terms of the GNU General Public License as published by the Free Software 11 Foundation; either version 3 of the License, or (at your option) any later 12 version. 13 14 This program is distributed in the hope that it will be useful, but WITHOUT 15 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 16 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 17 details. 18 19 You should have received a copy of the GNU General Public License along with 20 this program. If not, see <http://www.gnu.org/licenses/>. 21 22 -------- 23 24 References: 25 26 RFC 5545: Internet Calendaring and Scheduling Core Object Specification 27 (iCalendar) 28 http://tools.ietf.org/html/rfc5545 29 30 RFC 2445: Internet Calendaring and Scheduling Core Object Specification 31 (iCalendar) 32 http://tools.ietf.org/html/rfc2445 33 """ 34 35 import vContent 36 import re 37 38 try: 39 set 40 except NameError: 41 from sets import Set as set 42 43 ParseError = vContent.ParseError 44 45 # Format details. 46 47 SECTION_TYPES = set([ 48 "VALARM", "VCALENDAR", "VEVENT", "VFREEBUSY", "VJOURNAL", "VTIMEZONE", "VTODO", 49 "DAYLIGHT", "STANDARD" 50 ]) 51 QUOTED_PARAMETERS = set([ 52 "ALTREP", "DELEGATED-FROM", "DELEGATED-TO", "DIR", "MEMBER", "SENT-BY" 53 ]) 54 MULTIVALUED_PARAMETERS = set([ 55 "DELEGATED-FROM", "DELEGATED-TO", "MEMBER" 56 ]) 57 QUOTED_TYPES = set(["URI"]) 58 59 unquoted_separator_regexp = re.compile(r"(?<!\\)([,;])") 60 61 # Parser classes. 62 63 class vCalendarStreamParser(vContent.StreamParser): 64 65 "A stream parser specifically for vCalendar/iCalendar." 66 67 def next(self): 68 69 """ 70 Return the next content item in the file as a tuple of the form 71 (name, parameters, value). 72 """ 73 74 name, parameters, value = vContent.StreamParser.next(self) 75 return name, self.decode_parameters(parameters), value 76 77 def decode_content(self, value): 78 79 """ 80 Decode the given 'value' (which may represent a collection of distinct 81 values), replacing quoted separator characters. 82 """ 83 84 sep = None 85 values = [] 86 87 for i, s in enumerate(unquoted_separator_regexp.split(value)): 88 if i % 2 != 0: 89 if not sep: 90 sep = s 91 continue 92 values.append(self.decode_content_value(s)) 93 94 if sep == ",": 95 return values 96 elif sep == ";": 97 return tuple(values) 98 else: 99 return values[0] 100 101 def decode_content_value(self, value): 102 103 "Decode the given 'value', replacing quoted separator characters." 104 105 # Replace quoted characters (see 4.3.11 in RFC 2445). 106 107 value = vContent.StreamParser.decode_content(self, value) 108 return value.replace(r"\,", ",").replace(r"\;", ";") 109 110 # Internal methods. 111 112 def decode_quoted_value(self, value): 113 114 "Decode the given 'value', returning a list of decoded values." 115 116 if value[0] == '"' and value[-1] == '"': 117 return value[1:-1] 118 else: 119 return value 120 121 def decode_parameters(self, parameters): 122 123 """ 124 Decode the given 'parameters' according to the vCalendar specification. 125 """ 126 127 decoded_parameters = {} 128 129 for param_name, param_value in parameters.items(): 130 if param_name in QUOTED_PARAMETERS: 131 param_value = self.decode_quoted_value(param_value) 132 separator = '","' 133 else: 134 separator = "," 135 if param_name in MULTIVALUED_PARAMETERS: 136 param_value = param_value.split(separator) 137 decoded_parameters[param_name] = param_value 138 139 return decoded_parameters 140 141 class vCalendarParser(vContent.Parser): 142 143 "A parser specifically for vCalendar/iCalendar." 144 145 def parse(self, f, parser_cls=None): 146 return vContent.Parser.parse(self, f, (parser_cls or vCalendarStreamParser)) 147 148 def makeComponent(self, name, parameters, value=None): 149 150 """ 151 Make a component object from the given 'name', 'parameters' and optional 152 'value'. 153 """ 154 155 if name in SECTION_TYPES: 156 return (name, parameters, value or []) 157 else: 158 return (name, parameters, value or None) 159 160 # Writer classes. 161 162 class vCalendarStreamWriter(vContent.StreamWriter): 163 164 "A stream writer specifically for vCalendar." 165 166 # Overridden methods. 167 168 def write(self, name, parameters, value): 169 170 """ 171 Write a content line, serialising the given 'name', 'parameters' and 172 'value' information. 173 """ 174 175 if name in SECTION_TYPES: 176 self.write_content_line("BEGIN", {}, name) 177 for n, p, v in value: 178 self.write(n, p, v) 179 self.write_content_line("END", {}, name) 180 else: 181 vContent.StreamWriter.write(self, name, parameters, value) 182 183 def encode_parameters(self, parameters): 184 185 """ 186 Encode the given 'parameters' according to the vCalendar specification. 187 """ 188 189 encoded_parameters = {} 190 191 for param_name, param_value in parameters.items(): 192 if param_name in QUOTED_PARAMETERS: 193 separator = '","' 194 else: 195 separator = "," 196 if param_name in MULTIVALUED_PARAMETERS: 197 param_value = separator.join(param_value) 198 if param_name in QUOTED_PARAMETERS: 199 param_value = self.encode_quoted_parameter_value(param_value) 200 encoded_parameters[param_name] = param_value 201 202 return encoded_parameters 203 204 def encode_content(self, value): 205 206 """ 207 Encode the given 'value' (which may be a list or tuple of separate 208 values), quoting characters and separating collections of values. 209 """ 210 211 if isinstance(value, list): 212 sep = "," 213 elif isinstance(value, tuple): 214 sep = ";" 215 else: 216 value = [value] 217 sep = "" 218 219 return sep.join([self.encode_content_value(v) for v in value]) 220 221 def encode_content_value(self, value): 222 223 "Encode the given 'value', quoting characters." 224 225 # Replace quoted characters (see 4.3.11 in RFC 2445). 226 227 value = vContent.StreamWriter.encode_content(self, value) 228 return value.replace(";", r"\;").replace(",", r"\,") 229 230 # Public functions. 231 232 def parse(stream_or_string, encoding=None, non_standard_newline=0): 233 234 """ 235 Parse the resource data found through the use of the 'stream_or_string', 236 which is either a stream providing Unicode data (the codecs module can be 237 used to open files or to wrap streams in order to provide Unicode data) or a 238 filename identifying a file to be parsed. 239 240 The optional 'encoding' can be used to specify the character encoding used 241 by the file to be parsed. 242 243 The optional 'non_standard_newline' can be set to a true value (unlike the 244 default) in order to attempt to process files with CR as the end of line 245 character. 246 247 As a result of parsing the resource, the root node of the imported resource 248 is returned. 249 """ 250 251 return vContent.parse(stream_or_string, encoding, non_standard_newline, vCalendarParser) 252 253 def iterparse(stream_or_string, encoding=None, non_standard_newline=0): 254 255 """ 256 Parse the resource data found through the use of the 'stream_or_string', 257 which is either a stream providing Unicode data (the codecs module can be 258 used to open files or to wrap streams in order to provide Unicode data) or a 259 filename identifying a file to be parsed. 260 261 The optional 'encoding' can be used to specify the character encoding used 262 by the file to be parsed. 263 264 The optional 'non_standard_newline' can be set to a true value (unlike the 265 default) in order to attempt to process files with CR as the end of line 266 character. 267 268 An iterator is returned which provides event tuples describing parsing 269 events of the form (name, parameters, value). 270 """ 271 272 return vContent.iterparse(stream_or_string, encoding, non_standard_newline, vCalendarStreamParser) 273 274 def iterwrite(stream_or_string=None, write=None, encoding=None, line_length=None): 275 276 """ 277 Return a writer which will either send data to the resource found through 278 the use of 'stream_or_string' or using the given 'write' operation. 279 280 The 'stream_or_string' parameter may be either a stream accepting Unicode 281 data (the codecs module can be used to open files or to wrap streams in 282 order to accept Unicode data) or a filename identifying a file to be 283 written. 284 285 The optional 'encoding' can be used to specify the character encoding used 286 by the file to be written. 287 288 The optional 'line_length' can be used to specify how long lines should be 289 in the resulting data. 290 """ 291 292 return vContent.iterwrite(stream_or_string, write, encoding, line_length, vCalendarStreamWriter) 293 294 def to_dict(node): 295 296 "Return the 'node' converted to a dictionary representation." 297 298 return vContent.to_dict(node, SECTION_TYPES) 299 300 to_node = vContent.to_node 301 302 # vim: tabstop=4 expandtab shiftwidth=4