~ubuntu-branches/ubuntu/utopic/awn-extras-applets/utopic

« back to all changes in this revision

Viewing changes to src/calendar/icalendar/parser.py

  • Committer: Bazaar Package Importer
  • Author(s): Julien Lavergne
  • Date: 2010-01-13 21:50:33 UTC
  • mfrom: (1.1.4 upstream)
  • Revision ID: james.westby@ubuntu.com-20100113215033-kd9otcdjrajmiag0
Tags: 0.3.9~bzr1944-0ubuntu1
* New upstream snapshot.
 - Catch error in weather applet (LP: #359668)
* debian/patches: Refresh.
* debian/*.install: 
 - Update to new location and new applets.
 - Disable dialect applet until python-xklavier is in the archive.
 - Disable MiMenu and Pandora applets, there are unmaintained and not stable.
* debian/awn-applets-c-core: Dropped, not needed.
* debian/control:
 - Update description with new applets.
 - Remove libawn-extras and python-awnlib, all merged in python-awn-extras.
 - Replace awn-manager by awn-settings.
 - Drop build-depends on libgnome-desktop-dev, python*-dev, python2.5,
   awn-manager, libglade2-dev and libgnomeui-dev.
 - Add build-depends on libdesktop-agnostic-bin and vala-awn.
 - Bump build-depends of libawn-dev (>= 0.3.9~bzr1890), valac (>= 0.7.7) and
   debhelper (>= 7.0.50~).
 - Bump Standards-Version to 3.8.3 (no change needed).
 - Demote gconf-editor to Suggest, it's only needed for very advanced
   settings.
 - Update Recommends for python applets with new applets.
 - Suggest python-gconf for notification-area and alacarte for YAMA.
 - Add a debug package for C applets.
* debian/libawn-extras*: Removed, libawn-extras was removed upstream.
* debian/python-awnlib*: Merged with python-awn-extras.
* debian/rules:
 - Rewrite to use overrides.
* debian/copyright:
 - Update copyright and licenses.
* debian/README.source: Added.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: latin-1 -*-
2
 
 
3
 
"""
4
 
This module parses and generates contentlines as defined in RFC 2445
5
 
(iCalendar), but will probably work for other MIME types with similar syntax.
6
 
Eg. RFC 2426 (vCard)
7
 
 
8
 
It is stupid in the sense that it treats the content purely as strings. No type
9
 
conversion is attempted.
10
 
 
11
 
Copyright, 2005: Max M <maxm@mxm.dk>
12
 
License: GPL (Just contact med if and why you would like it changed)
13
 
"""
14
 
 
15
 
# from python
16
 
from types import TupleType, ListType
17
 
SequenceTypes = [TupleType, ListType]
18
 
import re
19
 
# from this package
20
 
from icalendar.caselessdict import CaselessDict
21
 
 
22
 
 
23
 
#################################################################
24
 
# Property parameter stuff
25
 
 
26
 
def paramVal(val):
27
 
    "Returns a parameter value"
28
 
    if type(val) in SequenceTypes:
29
 
        return q_join(val)
30
 
    return dQuote(val)
31
 
 
32
 
# Could be improved
33
 
NAME = re.compile('[\w-]+')
34
 
UNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7F",:;]')
35
 
QUNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7F"]')
36
 
FOLD = re.compile('([\r]?\n)+[ \t]{1}')
37
 
 
38
 
def validate_token(name):
39
 
    match = NAME.findall(name)
40
 
    if len(match) == 1 and name == match[0]:
41
 
        return
42
 
    raise ValueError, name
43
 
 
44
 
def validate_param_value(value, quoted=True):
45
 
    validator = UNSAFE_CHAR
46
 
    if quoted:
47
 
        validator = QUNSAFE_CHAR
48
 
    if validator.findall(value):
49
 
        raise ValueError, value
50
 
 
51
 
QUOTABLE = re.compile('[,;:].')
52
 
def dQuote(val):
53
 
    """
54
 
    Parameter values containing [,;:] must be double quoted
55
 
    >>> dQuote('Max')
56
 
    'Max'
57
 
    >>> dQuote('Rasmussen, Max')
58
 
    '"Rasmussen, Max"'
59
 
    >>> dQuote('name:value')
60
 
    '"name:value"'
61
 
    """
62
 
    if QUOTABLE.search(val):
63
 
        return '"%s"' % val
64
 
    return val
65
 
 
66
 
# parsing helper
67
 
def q_split(st, sep=','):
68
 
    """
69
 
    Splits a string on char, taking double (q)uotes into considderation
70
 
    >>> q_split('Max,Moller,"Rasmussen, Max"')
71
 
    ['Max', 'Moller', '"Rasmussen, Max"']
72
 
    """
73
 
    result = []
74
 
    cursor = 0
75
 
    length = len(st)
76
 
    inquote = 0
77
 
    for i in range(length):
78
 
        ch = st[i]
79
 
        if ch == '"':
80
 
            inquote = not inquote
81
 
        if not inquote and ch == sep:
82
 
            result.append(st[cursor:i])
83
 
            cursor = i + 1
84
 
        if i + 1 == length:
85
 
            result.append(st[cursor:])
86
 
    return result
87
 
 
88
 
def q_join(lst, sep=','):
89
 
    """
90
 
    Joins a list on sep, quoting strings with QUOTABLE chars
91
 
    >>> s = ['Max', 'Moller', 'Rasmussen, Max']
92
 
    >>> q_join(s)
93
 
    'Max,Moller,"Rasmussen, Max"'
94
 
    """
95
 
    return sep.join([dQuote(itm) for itm in lst])
96
 
 
97
 
class Parameters(CaselessDict):
98
 
    """
99
 
    Parser and generator of Property parameter strings. It knows nothing of
100
 
    datatypes. It's main concern is textual structure.
101
 
 
102
 
 
103
 
    Simple parameter:value pair
104
 
    >>> p = Parameters(parameter1='Value1')
105
 
    >>> str(p)
106
 
    'PARAMETER1=Value1'
107
 
 
108
 
 
109
 
    keys are converted to upper
110
 
    >>> p.keys()
111
 
    ['PARAMETER1']
112
 
 
113
 
 
114
 
    Parameters are case insensitive
115
 
    >>> p['parameter1']
116
 
    'Value1'
117
 
    >>> p['PARAMETER1']
118
 
    'Value1'
119
 
 
120
 
 
121
 
    Parameter with list of values must be seperated by comma
122
 
    >>> p = Parameters({'parameter1':['Value1', 'Value2']})
123
 
    >>> str(p)
124
 
    'PARAMETER1=Value1,Value2'
125
 
 
126
 
 
127
 
    Multiple parameters must be seperated by a semicolon
128
 
    >>> p = Parameters({'RSVP':'TRUE', 'ROLE':'REQ-PARTICIPANT'})
129
 
    >>> str(p)
130
 
    'ROLE=REQ-PARTICIPANT;RSVP=TRUE'
131
 
 
132
 
 
133
 
    Parameter values containing ',;:' must be double quoted
134
 
    >>> p = Parameters({'ALTREP':'http://www.wiz.org'})
135
 
    >>> str(p)
136
 
    'ALTREP="http://www.wiz.org"'
137
 
 
138
 
 
139
 
    list items must be quoted seperately
140
 
    >>> p = Parameters({'MEMBER':['MAILTO:projectA@host.com', 'MAILTO:projectB@host.com', ]})
141
 
    >>> str(p)
142
 
    'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"'
143
 
 
144
 
    Now the whole sheebang
145
 
    >>> p = Parameters({'parameter1':'Value1', 'parameter2':['Value2', 'Value3'],\
146
 
                          'ALTREP':['http://www.wiz.org', 'value4']})
147
 
    >>> str(p)
148
 
    'ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;PARAMETER2=Value2,Value3'
149
 
 
150
 
    We can also parse parameter strings
151
 
    >>> Parameters.from_string('PARAMETER1=Value 1;param2=Value 2')
152
 
    Parameters({'PARAMETER1': 'Value 1', 'PARAM2': 'Value 2'})
153
 
 
154
 
    Including empty strings
155
 
    >>> Parameters.from_string('param=')
156
 
    Parameters({'PARAM': ''})
157
 
 
158
 
    We can also parse parameter strings
159
 
    >>> Parameters.from_string('MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"')
160
 
    Parameters({'MEMBER': ['MAILTO:projectA@host.com', 'MAILTO:projectB@host.com']})
161
 
 
162
 
    We can also parse parameter strings
163
 
    >>> Parameters.from_string('ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;PARAMETER2=Value2,Value3')
164
 
    Parameters({'PARAMETER1': 'Value1', 'ALTREP': ['http://www.wiz.org', 'value4'], 'PARAMETER2': ['Value2', 'Value3']})
165
 
    """
166
 
 
167
 
 
168
 
    def params(self):
169
 
        """
170
 
        in rfc2445 keys are called parameters, so this is to be consitent with
171
 
        the naming conventions
172
 
        """
173
 
        return self.keys()
174
 
 
175
 
### Later, when I get more time... need to finish this off now. The last majot thing missing.
176
 
###    def _encode(self, name, value, cond=1):
177
 
###        # internal, for conditional convertion of values.
178
 
###        if cond:
179
 
###            klass = types_factory.for_property(name)
180
 
###            return klass(value)
181
 
###        return value
182
 
###
183
 
###    def add(self, name, value, encode=0):
184
 
###        "Add a parameter value and optionally encode it."
185
 
###        if encode:
186
 
###            value = self._encode(name, value, encode)
187
 
###        self[name] = value
188
 
###
189
 
###    def decoded(self, name):
190
 
###        "returns a decoded value, or list of same"
191
 
 
192
 
    def __repr__(self):
193
 
        return 'Parameters(' + dict.__repr__(self) + ')'
194
 
 
195
 
 
196
 
    def __str__(self):
197
 
        result = []
198
 
        items = self.items()
199
 
        items.sort() # To make doctests work
200
 
        for key, value in items:
201
 
            value = paramVal(value)
202
 
            result.append('%s=%s' % (key.upper(), value))
203
 
        return ';'.join(result)
204
 
 
205
 
 
206
 
    def from_string(st, strict=False):
207
 
        "Parses the parameter format from ical text format"
208
 
        try:
209
 
            # parse into strings
210
 
            result = Parameters()
211
 
            for param in q_split(st, ';'):
212
 
                key, val =  q_split(param, '=')
213
 
                validate_token(key)
214
 
                param_values = [v for v in q_split(val, ',')]
215
 
                # Property parameter values that are not in quoted
216
 
                # strings are case insensitive.
217
 
                vals = []
218
 
                for v in param_values:
219
 
                    if v.startswith('"') and v.endswith('"'):
220
 
                        v = v.strip('"')
221
 
                        validate_param_value(v, quoted=True)
222
 
                        vals.append(v)
223
 
                    else:
224
 
                        validate_param_value(v, quoted=False)
225
 
                        if strict:
226
 
                            vals.append(v.upper())
227
 
                        else:
228
 
                            vals.append(v)
229
 
                if not vals:
230
 
                    result[key] = val
231
 
                else:
232
 
                    if len(vals) == 1:
233
 
                        result[key] = vals[0]
234
 
                    else:
235
 
                        result[key] = vals
236
 
            return result
237
 
        except:
238
 
            raise ValueError, 'Not a valid parameter string'
239
 
    from_string = staticmethod(from_string)
240
 
 
241
 
 
242
 
#########################################
243
 
# parsing and generation of content lines
244
 
 
245
 
class Contentline(str):
246
 
    """
247
 
    A content line is basically a string that can be folded and parsed into
248
 
    parts.
249
 
 
250
 
    >>> c = Contentline('Si meliora dies, ut vina, poemata reddit')
251
 
    >>> str(c)
252
 
    'Si meliora dies, ut vina, poemata reddit'
253
 
 
254
 
    A long line gets folded
255
 
    >>> c = Contentline(''.join(['123456789 ']*10))
256
 
    >>> str(c)
257
 
    '123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234\\r\\n 56789 123456789 123456789 '
258
 
 
259
 
    A folded line gets unfolded
260
 
    >>> c = Contentline.from_string(str(c))
261
 
    >>> c
262
 
    '123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 '
263
 
 
264
 
    We do not fold within a UTF-8 character:
265
 
    >>> c = Contentline('This line has a UTF-8 character where it should be folded. Make sure it g\xc3\xabts folded before that character.')
266
 
    >>> '\xc3\xab' in str(c)
267
 
    True
268
 
 
269
 
    Don't fail if we fold a line that is exactly X times 74 characters long:
270
 
    >>> c = str(Contentline(''.join(['x']*148)))
271
 
 
272
 
    It can parse itself into parts. Which is a tuple of (name, params, vals)
273
 
 
274
 
    >>> c = Contentline('dtstart:20050101T120000')
275
 
    >>> c.parts()
276
 
    ('dtstart', Parameters({}), '20050101T120000')
277
 
 
278
 
    >>> c = Contentline('dtstart;value=datetime:20050101T120000')
279
 
    >>> c.parts()
280
 
    ('dtstart', Parameters({'VALUE': 'datetime'}), '20050101T120000')
281
 
 
282
 
    >>> c = Contentline('ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com')
283
 
    >>> c.parts()
284
 
    ('ATTENDEE', Parameters({'ROLE': 'REQ-PARTICIPANT', 'CN': 'Max Rasmussen'}), 'MAILTO:maxm@example.com')
285
 
    >>> str(c)
286
 
    'ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com'
287
 
 
288
 
    and back again
289
 
    >>> parts = ('ATTENDEE', Parameters({'ROLE': 'REQ-PARTICIPANT', 'CN': 'Max Rasmussen'}), 'MAILTO:maxm@example.com')
290
 
    >>> Contentline.from_parts(parts)
291
 
    'ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com'
292
 
 
293
 
    and again
294
 
    >>> parts = ('ATTENDEE', Parameters(), 'MAILTO:maxm@example.com')
295
 
    >>> Contentline.from_parts(parts)
296
 
    'ATTENDEE:MAILTO:maxm@example.com'
297
 
 
298
 
    A value can also be any of the types defined in PropertyValues
299
 
    >>> from icalendar.prop import vText
300
 
    >>> parts = ('ATTENDEE', Parameters(), vText('MAILTO:test@example.com'))
301
 
    >>> Contentline.from_parts(parts)
302
 
    'ATTENDEE:MAILTO:test@example.com'
303
 
 
304
 
    A value can also be unicode
305
 
    >>> from icalendar.prop import vText
306
 
    >>> parts = ('SUMMARY', Parameters(), vText(u'INternational char � � �'))
307
 
    >>> Contentline.from_parts(parts)
308
 
    'SUMMARY:INternational char \\xc3\\xa6 \\xc3\\xb8 \\xc3\\xa5'
309
 
 
310
 
    Traversing could look like this.
311
 
    >>> name, params, vals = c.parts()
312
 
    >>> name
313
 
    'ATTENDEE'
314
 
    >>> vals
315
 
    'MAILTO:maxm@example.com'
316
 
    >>> for key, val in params.items():
317
 
    ...     (key, val)
318
 
    ('ROLE', 'REQ-PARTICIPANT')
319
 
    ('CN', 'Max Rasmussen')
320
 
 
321
 
    And the traditional failure
322
 
    >>> c = Contentline('ATTENDEE;maxm@example.com')
323
 
    >>> c.parts()
324
 
    Traceback (most recent call last):
325
 
        ...
326
 
    ValueError: Content line could not be parsed into parts
327
 
 
328
 
    Another failure:
329
 
    >>> c = Contentline(':maxm@example.com')
330
 
    >>> c.parts()
331
 
    Traceback (most recent call last):
332
 
        ...
333
 
    ValueError: Content line could not be parsed into parts
334
 
 
335
 
    >>> c = Contentline('key;param=:value')
336
 
    >>> c.parts()
337
 
    ('key', Parameters({'PARAM': ''}), 'value')
338
 
 
339
 
    >>> c = Contentline('key;param="pvalue":value')
340
 
    >>> c.parts()
341
 
    ('key', Parameters({'PARAM': 'pvalue'}), 'value')
342
 
 
343
 
    Should bomb on missing param:
344
 
    >>> c = Contentline.from_string("k;:no param")
345
 
    >>> c.parts()
346
 
    Traceback (most recent call last):
347
 
        ...
348
 
    ValueError: Content line could not be parsed into parts
349
 
 
350
 
    >>> c = Contentline('key;param=pvalue:value', strict=False)
351
 
    >>> c.parts()
352
 
    ('key', Parameters({'PARAM': 'pvalue'}), 'value')
353
 
 
354
 
    If strict is set to True, uppercase param values that are not
355
 
    double-quoted, this is because the spec says non-quoted params are
356
 
    case-insensitive.
357
 
 
358
 
    >>> c = Contentline('key;param=pvalue:value', strict=True)
359
 
    >>> c.parts()
360
 
    ('key', Parameters({'PARAM': 'PVALUE'}), 'value')
361
 
 
362
 
    >>> c = Contentline('key;param="pValue":value', strict=True)
363
 
    >>> c.parts()
364
 
    ('key', Parameters({'PARAM': 'pValue'}), 'value')
365
 
    
366
 
    """
367
 
 
368
 
    def __new__(cls, st, strict=False):
369
 
        self = str.__new__(cls, st)
370
 
        setattr(self, 'strict', strict)
371
 
        return self
372
 
 
373
 
    def from_parts(parts):
374
 
        "Turns a tuple of parts into a content line"
375
 
        (name, params, values) = [str(p) for p in parts]
376
 
        try:
377
 
            if params:
378
 
                return Contentline('%s;%s:%s' % (name, params, values))
379
 
            return Contentline('%s:%s' %  (name, values))
380
 
        except:
381
 
            raise ValueError(
382
 
                'Property: %s Wrong values "%s" or "%s"' % (repr(name),
383
 
                                                            repr(params),
384
 
                                                            repr(values)))
385
 
    from_parts = staticmethod(from_parts)
386
 
 
387
 
    def parts(self):
388
 
        """ Splits the content line up into (name, parameters, values) parts
389
 
        """
390
 
        try:
391
 
            name_split = None
392
 
            value_split = None
393
 
            inquotes = 0
394
 
            for i in range(len(self)):
395
 
                ch = self[i]
396
 
                if not inquotes:
397
 
                    if ch in ':;' and not name_split:
398
 
                        name_split = i
399
 
                    if ch == ':' and not value_split:
400
 
                        value_split = i
401
 
                if ch == '"':
402
 
                    inquotes = not inquotes
403
 
            name = self[:name_split]
404
 
            if not name:
405
 
                raise ValueError, 'Key name is required'
406
 
            validate_token(name)
407
 
            if name_split+1 == value_split:
408
 
                raise ValueError, 'Invalid content line'
409
 
            params = Parameters.from_string(self[name_split+1:value_split],
410
 
                                            strict=self.strict)
411
 
            values = self[value_split+1:]
412
 
            return (name, params, values)
413
 
        except:
414
 
            raise ValueError, 'Content line could not be parsed into parts'
415
 
 
416
 
    def from_string(st, strict=False):
417
 
        "Unfolds the content lines in an iCalendar into long content lines"
418
 
        try:
419
 
            # a fold is carriage return followed by either a space or a tab
420
 
            return Contentline(FOLD.sub('', st), strict=strict)
421
 
        except:
422
 
            raise ValueError, 'Expected StringType with content line'
423
 
    from_string = staticmethod(from_string)
424
 
 
425
 
    def __str__(self):
426
 
        "Long content lines are folded so they are less than 75 characters wide"
427
 
        l_line = len(self)
428
 
        new_lines = []
429
 
        start = 0
430
 
        end = 74
431
 
        while True:
432
 
            if end >= l_line:
433
 
                end = l_line
434
 
            else:
435
 
                # Check that we don't fold in the middle of a UTF-8 character:
436
 
                # http://lists.osafoundation.org/pipermail/ietf-calsify/2006-August/001126.html
437
 
                while True:
438
 
                    char_value = ord(self[end])
439
 
                    if char_value < 128 or char_value >= 192:
440
 
                        # This is not in the middle of a UTF-8 character, so we
441
 
                        # can fold here:
442
 
                        break
443
 
                    else:
444
 
                        end -= 1
445
 
 
446
 
            new_lines.append(self[start:end])
447
 
            if end == l_line:
448
 
                # Done
449
 
                break
450
 
            start = end
451
 
            end = start + 74
452
 
        return '\r\n '.join(new_lines)
453
 
 
454
 
 
455
 
 
456
 
class Contentlines(list):
457
 
    """
458
 
    I assume that iCalendar files generally are a few kilobytes in size. Then
459
 
    this should be efficient. for Huge files, an iterator should probably be
460
 
    used instead.
461
 
 
462
 
    >>> c = Contentlines([Contentline('BEGIN:VEVENT\\r\\n')])
463
 
    >>> str(c)
464
 
    'BEGIN:VEVENT\\r\\n'
465
 
 
466
 
    Lets try appending it with a 100 charater wide string
467
 
    >>> c.append(Contentline(''.join(['123456789 ']*10)+'\\r\\n'))
468
 
    >>> str(c)
469
 
    'BEGIN:VEVENT\\r\\n\\r\\n123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234\\r\\n 56789 123456789 123456789 \\r\\n'
470
 
 
471
 
    Notice that there is an extra empty string in the end of the content lines.
472
 
    That is so they can be easily joined with: '\r\n'.join(contentlines)).
473
 
    >>> Contentlines.from_string('A short line\\r\\n')
474
 
    ['A short line', '']
475
 
    >>> Contentlines.from_string('A faked\\r\\n  long line\\r\\n')
476
 
    ['A faked long line', '']
477
 
    >>> Contentlines.from_string('A faked\\r\\n  long line\\r\\nAnd another lin\\r\\n\\te that is folded\\r\\n')
478
 
    ['A faked long line', 'And another line that is folded', '']
479
 
    """
480
 
 
481
 
    def __str__(self):
482
 
        "Simply join self."
483
 
        return '\r\n'.join(map(str, self))
484
 
 
485
 
    def from_string(st):
486
 
        "Parses a string into content lines"
487
 
        try:
488
 
            # a fold is carriage return followed by either a space or a tab
489
 
            unfolded = FOLD.sub('', st)
490
 
            lines = [Contentline(line) for line in unfolded.splitlines() if line]
491
 
            lines.append('') # we need a '\r\n' in the end of every content line
492
 
            return Contentlines(lines)
493
 
        except:
494
 
            raise ValueError, 'Expected StringType with content lines'
495
 
    from_string = staticmethod(from_string)
496
 
 
497
 
 
498
 
# ran this:
499
 
#    sample = open('./samples/test.ics', 'rb').read() # binary file in windows!
500
 
#    lines = Contentlines.from_string(sample)
501
 
#    for line in lines[:-1]:
502
 
#        print line.parts()
503
 
 
504
 
# got this:
505
 
#('BEGIN', Parameters({}), 'VCALENDAR')
506
 
#('METHOD', Parameters({}), 'Request')
507
 
#('PRODID', Parameters({}), '-//My product//mxm.dk/')
508
 
#('VERSION', Parameters({}), '2.0')
509
 
#('BEGIN', Parameters({}), 'VEVENT')
510
 
#('DESCRIPTION', Parameters({}), 'This is a very long description that ...')
511
 
#('PARTICIPANT', Parameters({'CN': 'Max M'}), 'MAILTO:maxm@mxm.dk')
512
 
#('DTEND', Parameters({}), '20050107T160000')
513
 
#('DTSTART', Parameters({}), '20050107T120000')
514
 
#('SUMMARY', Parameters({}), 'A second event')
515
 
#('END', Parameters({}), 'VEVENT')
516
 
#('BEGIN', Parameters({}), 'VEVENT')
517
 
#('DTEND', Parameters({}), '20050108T235900')
518
 
#('DTSTART', Parameters({}), '20050108T230000')
519
 
#('SUMMARY', Parameters({}), 'A single event')
520
 
#('UID', Parameters({}), '42')
521
 
#('END', Parameters({}), 'VEVENT')
522
 
#('END', Parameters({}), 'VCALENDAR')