diff options
Diffstat (limited to 'src/vobject')
-rw-r--r-- | src/vobject/base.py | 56 | ||||
-rw-r--r-- | src/vobject/icalendar.py | 144 | ||||
-rwxr-xr-x | src/vobject/midnight.py | 59 | ||||
-rw-r--r-- | src/vobject/to_pst.py | 55 |
4 files changed, 170 insertions, 144 deletions
diff --git a/src/vobject/base.py b/src/vobject/base.py index 0fb3a28..abb6a9e 100644 --- a/src/vobject/base.py +++ b/src/vobject/base.py @@ -855,13 +855,35 @@ def dquoteEscape(param): return param def foldOneLine(outbuf, input, lineLength = 75): - if isinstance(input, basestring): input = StringIO.StringIO(input) - input.seek(0) - outbuf.write(input.read(lineLength) + CRLF) - brokenline = input.read(lineLength - 1) - while brokenline: - outbuf.write(' ' + brokenline + CRLF) - brokenline = input.read(lineLength - 1) + # Folding line procedure that ensures multi-byte utf-8 sequences are not broken + # across lines + + if len(input) < lineLength: + # Optimize for unfolded line case + outbuf.write(input) + else: + # Look for valid utf8 range and write that out + start = 0 + written = 0 + while written < len(input): + # Start max length -1 chars on from where we are + offset = start + lineLength - 1 + if offset >= len(input): + line = input[start:] + outbuf.write(line) + written = len(input) + else: + # Check whether next char is valid utf8 lead byte + while (input[offset] > 0x7F) and ((ord(input[offset]) & 0xC0) == 0x80): + # Step back until we have a valid char + offset -= 1 + + line = input[start:offset] + outbuf.write(line) + outbuf.write("\r\n ") + written += offset - start + start = offset + outbuf.write("\r\n") def defaultSerialize(obj, buf, lineLength): """Encode and fold obj and its children, write to buf or return a string.""" @@ -874,12 +896,12 @@ def defaultSerialize(obj, buf, lineLength): else: groupString = obj.group + '.' if obj.useBegin: - foldOneLine(outbuf, groupString + u"BEGIN:" + obj.name, lineLength) + foldOneLine(outbuf, str(groupString + u"BEGIN:" + obj.name), lineLength) for child in obj.getSortedChildren(): #validate is recursive, we only need to validate once child.serialize(outbuf, lineLength, validate=False) if obj.useBegin: - foldOneLine(outbuf, groupString + u"END:" + obj.name, lineLength) + foldOneLine(outbuf, str(groupString + u"END:" + obj.name), lineLength) if DEBUG: logger.debug("Finished %s" % obj.name.upper()) elif isinstance(obj, ContentLine): @@ -887,14 +909,18 @@ def defaultSerialize(obj, buf, lineLength): if obj.behavior and not startedEncoded: obj.behavior.encode(obj) s=StringIO.StringIO() #unfolded buffer if obj.group is not None: - s.write(obj.group + '.') + s.write(str(obj.group + '.')) if DEBUG: logger.debug("Serializing line" + str(obj)) - s.write(obj.name.upper()) + s.write(str(obj.name.upper())) for key, paramvals in obj.params.iteritems(): - s.write(';' + key + '=' + ','.join(map(dquoteEscape, paramvals))) - s.write(':' + obj.value) + s.write(';' + str(key) + '=' + ','.join(map(dquoteEscape, paramvals)).encode("utf-8")) + if isinstance(obj.value, unicode): + strout = obj.value.encode("utf-8") + else: + strout = obj.value + s.write(':' + strout) if obj.behavior and not startedEncoded: obj.behavior.decode(obj) - foldOneLine(outbuf, s, lineLength) + foldOneLine(outbuf, s.getvalue(), lineLength) if DEBUG: logger.debug("Finished %s line" % obj.name.upper()) return buf or outbuf.getvalue() @@ -1057,7 +1083,7 @@ def newFromBehavior(name, id=None): else: obj = ContentLine(name, [], '') obj.behavior = behavior - obj.isNative = True + obj.isNative = False return obj diff --git a/src/vobject/icalendar.py b/src/vobject/icalendar.py index e61a2af..fd18910 100644 --- a/src/vobject/icalendar.py +++ b/src/vobject/icalendar.py @@ -782,20 +782,21 @@ class MultiTextBehavior(behavior.Behavior): #------------------------ Registered Behavior subclasses ----------------------- class VCalendar2_0(VCalendarComponentBehavior): - """vCalendar 2.0 behavior.""" + """vCalendar 2.0 behavior. With added VAVAILABILITY support.""" name = 'VCALENDAR' description = 'vCalendar 2.0, also known as iCalendar.' versionString = '2.0' sortFirst = ('version', 'calscale', 'method', 'prodid', 'vtimezone') - knownChildren = {'CALSCALE': (0, 1, None),#min, max, behaviorRegistry id - 'METHOD': (0, 1, None), - 'VERSION': (0, 1, None),#required, but auto-generated - 'PRODID': (1, 1, None), - 'VTIMEZONE': (0, None, None), - 'VEVENT': (0, None, None), - 'VTODO': (0, None, None), - 'VJOURNAL': (0, None, None), - 'VFREEBUSY': (0, None, None) + knownChildren = {'CALSCALE': (0, 1, None),#min, max, behaviorRegistry id + 'METHOD': (0, 1, None), + 'VERSION': (0, 1, None),#required, but auto-generated + 'PRODID': (1, 1, None), + 'VTIMEZONE': (0, None, None), + 'VEVENT': (0, None, None), + 'VTODO': (0, None, None), + 'VJOURNAL': (0, None, None), + 'VFREEBUSY': (0, None, None), + 'VAVAILABILITY': (0, None, None), } @classmethod @@ -941,7 +942,7 @@ class VEvent(RecurringBehavior): @classmethod def validate(cls, obj, raiseException, *args): - if obj.contents.has_key('DTEND') and obj.contents.has_key('DURATION'): + if obj.contents.has_key('dtend') and obj.contents.has_key('duration'): if raiseException: m = "VEVENT components cannot contain both DTEND and DURATION\ components" @@ -996,7 +997,7 @@ class VTodo(RecurringBehavior): @classmethod def validate(cls, obj, raiseException, *args): - if obj.contents.has_key('DUE') and obj.contents.has_key('DURATION'): + if obj.contents.has_key('due') and obj.contents.has_key('duration'): if raiseException: m = "VTODO components cannot contain both DUE and DURATION\ components" @@ -1047,12 +1048,14 @@ class VFreeBusy(VCalendarComponentBehavior): >>> vfb.add('dtstart').value = datetime.datetime(2006, 2, 16, 1, tzinfo=utc) >>> vfb.add('dtend').value = vfb.dtstart.value + twoHours >>> vfb.add('freebusy').value = [(vfb.dtstart.value, twoHours / 2)] + >>> vfb.add('freebusy').value = [(vfb.dtstart.value, vfb.dtend.value)] >>> print vfb.serialize() BEGIN:VFREEBUSY UID:test DTSTART:20060216T010000Z DTEND:20060216T030000Z FREEBUSY:20060216T010000Z/PT1H + FREEBUSY:20060216T010000Z/20060216T030000Z END:VFREEBUSY """ @@ -1172,7 +1175,7 @@ class VAlarm(VCalendarComponentBehavior): ; and MUST NOT occur more than once description / - if obj.contents.has_key('DTEND') and obj.contents.has_key('DURATION'): + if obj.contents.has_key('dtend') and obj.contents.has_key('duration'): if raiseException: m = "VEVENT components cannot contain both DTEND and DURATION\ components" @@ -1185,6 +1188,117 @@ class VAlarm(VCalendarComponentBehavior): registerBehavior(VAlarm) +class VAvailability(VCalendarComponentBehavior): + """Availability state behavior. + + >>> vav = newFromBehavior('VAVAILABILITY') + >>> vav.add('uid').value = 'test' + >>> vav.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) + >>> vav.add('dtstart').value = datetime.datetime(2006, 2, 16, 0, tzinfo=utc) + >>> vav.add('dtend').value = datetime.datetime(2006, 2, 17, 0, tzinfo=utc) + >>> vav.add('busytype').value = "BUSY" + >>> av = newFromBehavior('AVAILABLE') + >>> av.add('uid').value = 'test1' + >>> av.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) + >>> av.add('dtstart').value = datetime.datetime(2006, 2, 16, 9, tzinfo=utc) + >>> av.add('dtend').value = datetime.datetime(2006, 2, 16, 12, tzinfo=utc) + >>> av.add('summary').value = "Available in the morning" + >>> ignore = vav.add(av) + >>> print vav.serialize() + BEGIN:VAVAILABILITY + UID:test + DTSTART:20060216T000000Z + DTEND:20060217T000000Z + BEGIN:AVAILABLE + UID:test1 + DTSTART:20060216T090000Z + DTEND:20060216T120000Z + DTSTAMP:20060215T000000Z + SUMMARY:Available in the morning + END:AVAILABLE + BUSYTYPE:BUSY + DTSTAMP:20060215T000000Z + END:VAVAILABILITY + + """ + name='VAVAILABILITY' + description='A component used to represent a user\'s available time slots.' + sortFirst = ('uid', 'dtstart', 'duration', 'dtend') + knownChildren = {'UID': (1, 1, None),#min, max, behaviorRegistry id + 'DTSTAMP': (1, 1, None), + 'BUSYTYPE': (0, 1, None), + 'CREATED': (0, 1, None), + 'DTSTART': (0, 1, None), + 'LAST-MODIFIED': (0, 1, None), + 'ORGANIZER': (0, 1, None), + 'SEQUENCE': (0, 1, None), + 'SUMMARY': (0, 1, None), + 'URL': (0, 1, None), + 'DTEND': (0, 1, None), + 'DURATION': (0, 1, None), + 'CATEGORIES': (0, None, None), + 'COMMENT': (0, None, None), + 'CONTACT': (0, None, None), + 'AVAILABLE': (0, None, None), + } + + @classmethod + def validate(cls, obj, raiseException, *args): + if obj.contents.has_key('dtend') and obj.contents.has_key('duration'): + if raiseException: + m = "VAVAILABILITY components cannot contain both DTEND and DURATION\ + components" + raise ValidateError(m) + return False + else: + return super(VAvailability, cls).validate(obj, raiseException, *args) + +registerBehavior(VAvailability) + +class Available(RecurringBehavior): + """Event behavior.""" + name='AVAILABLE' + sortFirst = ('uid', 'recurrence-id', 'dtstart', 'duration', 'dtend') + + description='Defines a period of time in which a user is normally available.' + knownChildren = {'DTSTAMP': (1, 1, None),#min, max, behaviorRegistry id + 'DTSTART': (1, 1, None), + 'UID': (1, 1, None), + 'DTEND': (0, 1, None), #NOTE: One of DtEnd or + 'DURATION': (0, 1, None), # Duration must appear, but not both + 'CREATED': (0, 1, None), + 'LAST-MODIFIED':(0, 1, None), + 'RECURRENCE-ID':(0, 1, None), + 'RRULE': (0, 1, None), + 'SUMMARY': (0, 1, None), + 'CATEGORIES': (0, None, None), + 'COMMENT': (0, None, None), + 'CONTACT': (0, None, None), + 'EXDATE': (0, None, None), + 'RDATE': (0, None, None), + } + + @classmethod + def validate(cls, obj, raiseException, *args): + has_dtend = obj.contents.has_key('dtend') + has_duration = obj.contents.has_key('duration') + if has_dtend and has_duration: + if raiseException: + m = "AVAILABLE components cannot contain both DTEND and DURATION\ + properties" + raise ValidateError(m) + return False + elif not (has_dtend or has_duration): + if raiseException: + m = "AVAILABLE components must contain one of DTEND or DURATION\ + properties" + raise ValidateError(m) + return False + else: + return super(Available, cls).validate(obj, raiseException, *args) + +registerBehavior(Available) + class Duration(behavior.Behavior): """Behavior for Duration ContentLines. Transform to datetime.timedelta.""" name = 'DURATION' @@ -1344,7 +1458,7 @@ registerBehavior(MultiDateBehavior, 'EXDATE') textList = ['CALSCALE', 'METHOD', 'PRODID', 'CLASS', 'COMMENT', 'DESCRIPTION', 'LOCATION', 'STATUS', 'SUMMARY', 'TRANSP', 'CONTACT', 'RELATED-TO', - 'UID', 'ACTION', 'REQUEST-STATUS', 'TZID'] + 'UID', 'ACTION', 'REQUEST-STATUS', 'TZID', 'BUSYTYPE'] map(lambda x: registerBehavior(TextBehavior, x), textList) multiTextList = ['CATEGORIES', 'RESOURCES'] @@ -1678,7 +1792,7 @@ def stringToPeriod(s, tzinfo=None): delta = stringToDurations(valEnd)[0] return (start, delta) else: - return (start, stringToDateTime(valEnd, tzinfo) - start) + return (start, stringToDateTime(valEnd, tzinfo)) def getTransition(transitionTo, year, tzinfo): diff --git a/src/vobject/midnight.py b/src/vobject/midnight.py deleted file mode 100755 index c4e7ae8..0000000 --- a/src/vobject/midnight.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python -"""Rewrite VEVENT's DTSTART and DTEND if they're midnight to midnight""" - -from optparse import OptionParser -import icalendar, base, datetime -import os -import codecs - -midnight = datetime.time(0) -one_day = datetime.timedelta(1) - -def changeVeventTimes(vevent): - dtstart = vevent.getChildValue('dtstart') - dtend = vevent.getChildValue('dtend') - if isinstance(dtstart, datetime.datetime): - if dtend is None: - if dtstart.tzinfo is None and dtstart.time() == midnight: - vevent.dtstart.value = dtstart.date() - elif (isinstance(dtend, datetime.datetime) and - dtend.tzinfo is None and dtend.time() == midnight): - vevent.dtstart.value = dtstart.date() - vevent.dtend.value = dtend.date() - - -def main(): - options, args = getOptions() - if args: - in_filename, out_filename = args - cal = base.readOne(file(in_filename)) - for vevent in cal.vevent_list: - changeVeventTimes(vevent) - out_file = codecs.open(out_filename, "w", "utf-8") - cal.serialize(out_file) - out_file.close() - - -version = "0.1" - -def getOptions(): - ##### Configuration options ##### - - usage = "usage: %prog [options] in_file out_file" - parser = OptionParser(usage=usage, version=version) - parser.set_description("convert midnight events to all") - - (cmdline_options, args) = parser.parse_args() - if len(args) < 2: - print "error: too few arguments given" - print - print parser.format_help() - return False, False - - return cmdline_options, args - -if __name__ == "__main__": - try: - main() - except KeyboardInterrupt: - print "Aborted" diff --git a/src/vobject/to_pst.py b/src/vobject/to_pst.py deleted file mode 100644 index 318f47f..0000000 --- a/src/vobject/to_pst.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python -"""Rewrite VEVENT's DTSTART and DTEND if they're midnight to midnight""" - -from optparse import OptionParser -import icalendar, base, datetime -import os -import codecs -import PyICU - -pst = PyICU.ICUtzinfo.getInstance('US/Pacific') - -def changeVeventTimes(vevent): - dtstart = vevent.getChildValue('dtstart') - dtend = vevent.getChildValue('dtend') - if isinstance(dtstart, datetime.datetime): - if dtstart.tzinfo is None: - vevent.dtstart.value = dtstart.replace(tzinfo=pst) - if dtend is not None and dtend.tzinfo is None: - vevent.dtend.value = dtend.replace(tzinfo=pst) - -def main(): - options, args = getOptions() - if args: - in_filename, out_filename = args - cal = base.readOne(file(in_filename)) - for vevent in cal.vevent_list: - changeVeventTimes(vevent) - out_file = codecs.open(out_filename, "w", "utf-8") - cal.serialize(out_file) - out_file.close() - - -version = "0.1" - -def getOptions(): - ##### Configuration options ##### - - usage = "usage: %prog [options] in_file out_file" - parser = OptionParser(usage=usage, version=version) - parser.set_description("convert midnight events to all") - - (cmdline_options, args) = parser.parse_args() - if len(args) < 2: - print "error: too few arguments given" - print - print parser.format_help() - return False, False - - return cmdline_options, args - -if __name__ == "__main__": - try: - main() - except KeyboardInterrupt: - print "Aborted" |