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