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 # Writer classes. 147 148 class vCalendarStreamWriter(vContent.StreamWriter): 149 150 "A stream writer specifically for vCalendar." 151 152 # Overridden methods. 153 154 def write(self, name, parameters, value): 155 156 """ 157 Write a content line, serialising the given 'name', 'parameters' and 158 'value' information. 159 """ 160 161 if name in SECTION_TYPES: 162 self.write_content_line("BEGIN", {}, name) 163 for n, p, v in value: 164 self.write(n, p, v) 165 self.write_content_line("END", {}, name) 166 else: 167 vContent.StreamWriter.write(self, name, parameters, value) 168 169 def encode_parameters(self, parameters): 170 171 """ 172 Encode the given 'parameters' according to the vCalendar specification. 173 """ 174 175 encoded_parameters = {} 176 177 for param_name, param_value in parameters.items(): 178 if param_name in QUOTED_PARAMETERS: 179 param_value = self.encode_quoted_parameter_value(param_value) 180 separator = '","' 181 else: 182 separator = "," 183 if param_name in MULTIVALUED_PARAMETERS: 184 param_value = separator.join(param_value) 185 encoded_parameters[param_name] = param_value 186 187 return encoded_parameters 188 189 def encode_content(self, value): 190 191 """ 192 Encode the given 'value' (which may be a list or tuple of separate 193 values), quoting characters and separating collections of values. 194 """ 195 196 if isinstance(value, list): 197 sep = "," 198 elif isinstance(value, tuple): 199 sep = ";" 200 else: 201 value = [value] 202 sep = "" 203 204 return sep.join([self.encode_content_value(v) for v in value]) 205 206 def encode_content_value(self, value): 207 208 "Encode the given 'value', quoting characters." 209 210 # Replace quoted characters (see 4.3.11 in RFC 2445). 211 212 value = vContent.StreamWriter.encode_content(self, value) 213 return value.replace(";", r"\;").replace(",", r"\,") 214 215 # Public functions. 216 217 def parse(stream_or_string, encoding=None, non_standard_newline=0): 218 219 """ 220 Parse the resource data found through the use of the 'stream_or_string', 221 which is either a stream providing Unicode data (the codecs module can be 222 used to open files or to wrap streams in order to provide Unicode data) or a 223 filename identifying a file to be parsed. 224 225 The optional 'encoding' can be used to specify the character encoding used 226 by the file to be parsed. 227 228 The optional 'non_standard_newline' can be set to a true value (unlike the 229 default) in order to attempt to process files with CR as the end of line 230 character. 231 232 As a result of parsing the resource, the root node of the imported resource 233 is returned. 234 """ 235 236 return vContent.parse(stream_or_string, encoding, non_standard_newline, vCalendarParser) 237 238 def iterparse(stream_or_string, encoding=None, non_standard_newline=0): 239 240 """ 241 Parse the resource data found through the use of the 'stream_or_string', 242 which is either a stream providing Unicode data (the codecs module can be 243 used to open files or to wrap streams in order to provide Unicode data) or a 244 filename identifying a file to be parsed. 245 246 The optional 'encoding' can be used to specify the character encoding used 247 by the file to be parsed. 248 249 The optional 'non_standard_newline' can be set to a true value (unlike the 250 default) in order to attempt to process files with CR as the end of line 251 character. 252 253 An iterator is returned which provides event tuples describing parsing 254 events of the form (name, parameters, value). 255 """ 256 257 return vContent.iterparse(stream_or_string, encoding, non_standard_newline, vCalendarStreamParser) 258 259 def iterwrite(stream_or_string=None, write=None, encoding=None, line_length=None): 260 261 """ 262 Return a writer which will either send data to the resource found through 263 the use of 'stream_or_string' or using the given 'write' operation. 264 265 The 'stream_or_string' parameter may be either a stream accepting Unicode 266 data (the codecs module can be used to open files or to wrap streams in 267 order to accept Unicode data) or a filename identifying a file to be 268 written. 269 270 The optional 'encoding' can be used to specify the character encoding used 271 by the file to be written. 272 273 The optional 'line_length' can be used to specify how long lines should be 274 in the resulting data. 275 """ 276 277 return vContent.iterwrite(stream_or_string, write, encoding, line_length, vCalendarStreamWriter) 278 279 to_dict = vContent.to_dict 280 to_node = vContent.to_node 281 282 # vim: tabstop=4 expandtab shiftwidth=4