vContent

Annotated vCalendar.py

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