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