imip-agent

Annotated vCalendar.py

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