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