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 "DAYLIGHT", "STANDARD" 49 ]) 50 QUOTED_PARAMETERS = set([ 51 "ALTREP", "DELEGATED-FROM", "DELEGATED-TO", "DIR", "MEMBER", "SENT-BY" 52 ]) 53 MULTIVALUED_PARAMETERS = set([ 54 "DELEGATED-FROM", "DELEGATED-TO", "MEMBER" 55 ]) 56 QUOTED_TYPES = set(["URI"]) 57 58 unquoted_separator_regexp = re.compile(r"(?<!\\)([,;])") 59 60 # Parser classes. 61 62 class vCalendarStreamParser(vContent.StreamParser): 63 64 "A stream parser specifically for vCalendar/iCalendar." 65 66 def next(self): 67 68 """ 69 Return the next content item in the file as a tuple of the form 70 (name, parameters, value). 71 """ 72 73 name, parameters, value = vContent.StreamParser.next(self) 74 return name, self.decode_parameters(parameters), value 75 76 def decode_content(self, value): 77 78 """ 79 Decode the given 'value' (which may represent a collection of distinct 80 values), replacing quoted separator characters. 81 """ 82 83 sep = None 84 values = [] 85 86 for i, s in enumerate(unquoted_separator_regexp.split(value)): 87 if i % 2 != 0: 88 if not sep: 89 sep = s 90 continue 91 values.append(self.decode_content_value(s)) 92 93 if sep == ",": 94 return values 95 elif sep == ";": 96 return tuple(values) 97 else: 98 return values[0] 99 100 def decode_content_value(self, value): 101 102 "Decode the given 'value', replacing quoted separator characters." 103 104 # Replace quoted characters (see 4.3.11 in RFC 2445). 105 106 value = vContent.StreamParser.decode_content(self, value) 107 return value.replace(r"\,", ",").replace(r"\;", ";") 108 109 # Internal methods. 110 111 def decode_quoted_value(self, value): 112 113 "Decode the given 'value', returning a list of decoded values." 114 115 if value[0] == '"' and value[-1] == '"': 116 return value[1:-1] 117 else: 118 return value 119 120 def decode_parameters(self, parameters): 121 122 """ 123 Decode the given 'parameters' according to the vCalendar specification. 124 """ 125 126 decoded_parameters = {} 127 128 for param_name, param_value in parameters.items(): 129 if param_name in QUOTED_PARAMETERS: 130 param_value = self.decode_quoted_value(param_value) 131 separator = '","' 132 else: 133 separator = "," 134 if param_name in MULTIVALUED_PARAMETERS: 135 param_value = param_value.split(separator) 136 decoded_parameters[param_name] = param_value 137 138 return decoded_parameters 139 140 class vCalendarParser(vContent.Parser): 141 142 "A parser specifically for vCalendar/iCalendar." 143 144 def parse(self, f, parser_cls=None): 145 return vContent.Parser.parse(self, f, (parser_cls or vCalendarStreamParser)) 146 147 def makeComponent(self, name, parameters, value=None): 148 149 """ 150 Make a component object from the given 'name', 'parameters' and optional 151 'value'. 152 """ 153 154 if name in SECTION_TYPES: 155 return (name, parameters, value or []) 156 else: 157 return (name, parameters, value or None) 158 159 # Writer classes. 160 161 class vCalendarStreamWriter(vContent.StreamWriter): 162 163 "A stream writer specifically for vCalendar." 164 165 # Overridden methods. 166 167 def write(self, name, parameters, value): 168 169 """ 170 Write a content line, serialising the given 'name', 'parameters' and 171 'value' information. 172 """ 173 174 if name in SECTION_TYPES: 175 self.write_content_line("BEGIN", {}, name) 176 for n, p, v in value: 177 self.write(n, p, v) 178 self.write_content_line("END", {}, name) 179 else: 180 vContent.StreamWriter.write(self, name, parameters, value) 181 182 def encode_parameters(self, parameters): 183 184 """ 185 Encode the given 'parameters' according to the vCalendar specification. 186 """ 187 188 encoded_parameters = {} 189 190 for param_name, param_value in parameters.items(): 191 if param_name in QUOTED_PARAMETERS: 192 param_value = self.encode_quoted_parameter_value(param_value) 193 separator = '","' 194 else: 195 separator = "," 196 if param_name in MULTIVALUED_PARAMETERS: 197 param_value = separator.join(param_value) 198 encoded_parameters[param_name] = param_value 199 200 return encoded_parameters 201 202 def encode_content(self, value): 203 204 """ 205 Encode the given 'value' (which may be a list or tuple of separate 206 values), quoting characters and separating collections of values. 207 """ 208 209 if isinstance(value, list): 210 sep = "," 211 elif isinstance(value, tuple): 212 sep = ";" 213 else: 214 value = [value] 215 sep = "" 216 217 return sep.join([self.encode_content_value(v) for v in value]) 218 219 def encode_content_value(self, value): 220 221 "Encode the given 'value', quoting characters." 222 223 # Replace quoted characters (see 4.3.11 in RFC 2445). 224 225 value = vContent.StreamWriter.encode_content(self, value) 226 return value.replace(";", r"\;").replace(",", r"\,") 227 228 # Public functions. 229 230 def parse(stream_or_string, encoding=None, non_standard_newline=0): 231 232 """ 233 Parse the resource data found through the use of the 'stream_or_string', 234 which is either a stream providing Unicode data (the codecs module can be 235 used to open files or to wrap streams in order to provide Unicode data) or a 236 filename identifying a file to be parsed. 237 238 The optional 'encoding' can be used to specify the character encoding used 239 by the file to be parsed. 240 241 The optional 'non_standard_newline' can be set to a true value (unlike the 242 default) in order to attempt to process files with CR as the end of line 243 character. 244 245 As a result of parsing the resource, the root node of the imported resource 246 is returned. 247 """ 248 249 return vContent.parse(stream_or_string, encoding, non_standard_newline, vCalendarParser) 250 251 def iterparse(stream_or_string, encoding=None, non_standard_newline=0): 252 253 """ 254 Parse the resource data found through the use of the 'stream_or_string', 255 which is either a stream providing Unicode data (the codecs module can be 256 used to open files or to wrap streams in order to provide Unicode data) or a 257 filename identifying a file to be parsed. 258 259 The optional 'encoding' can be used to specify the character encoding used 260 by the file to be parsed. 261 262 The optional 'non_standard_newline' can be set to a true value (unlike the 263 default) in order to attempt to process files with CR as the end of line 264 character. 265 266 An iterator is returned which provides event tuples describing parsing 267 events of the form (name, parameters, value). 268 """ 269 270 return vContent.iterparse(stream_or_string, encoding, non_standard_newline, vCalendarStreamParser) 271 272 def iterwrite(stream_or_string=None, write=None, encoding=None, line_length=None): 273 274 """ 275 Return a writer which will either send data to the resource found through 276 the use of 'stream_or_string' or using the given 'write' operation. 277 278 The 'stream_or_string' parameter may be either a stream accepting Unicode 279 data (the codecs module can be used to open files or to wrap streams in 280 order to accept Unicode data) or a filename identifying a file to be 281 written. 282 283 The optional 'encoding' can be used to specify the character encoding used 284 by the file to be written. 285 286 The optional 'line_length' can be used to specify how long lines should be 287 in the resulting data. 288 """ 289 290 return vContent.iterwrite(stream_or_string, write, encoding, line_length, vCalendarStreamWriter) 291 292 def to_dict(node): 293 294 "Return the 'node' converted to a dictionary representation." 295 296 return vContent.to_dict(node, SECTION_TYPES) 297 298 to_node = vContent.to_node 299 300 # vim: tabstop=4 expandtab shiftwidth=4