"""Compare VTODOs and VEVENTs in two iCalendar sources.""" from base import Component, getBehavior, newFromBehavior def getSortKey(component): def getUID(component): return component.getChildValue('uid', '') # it's not quite as simple as getUID, need to account for recurrenceID and # sequence def getSequence(component): sequence = component.getChildValue('sequence', 0) return "%05d" % int(sequence) def getRecurrenceID(component): recurrence_id = component.getChildValue('recurrence_id', None) if recurrence_id is None: return '0000-00-00' else: return recurrence_id.isoformat() return getUID(component) + getSequence(component) + getRecurrenceID(component) def sortByUID(components): return sorted(components, key=getSortKey) def deleteExtraneous(component, ignore_dtstamp=False): """ Recursively walk the component's children, deleting extraneous details like X-VOBJ-ORIGINAL-TZID. """ for comp in component.components(): deleteExtraneous(comp, ignore_dtstamp) for line in component.lines(): if line.params.has_key('X-VOBJ-ORIGINAL-TZID'): del line.params['X-VOBJ-ORIGINAL-TZID'] if ignore_dtstamp and hasattr(component, 'dtstamp_list'): del component.dtstamp_list def diff(left, right): """ Take two VCALENDAR components, compare VEVENTs and VTODOs in them, return a list of object pairs containing just UID and the bits that didn't match, using None for objects that weren't present in one version or the other. When there are multiple ContentLines in one VEVENT, for instance many DESCRIPTION lines, such lines original order is assumed to be meaningful. Order is also preserved when comparing (the unlikely case of) multiple parameters of the same type in a ContentLine """ def processComponentLists(leftList, rightList): output = [] rightIndex = 0 rightListSize = len(rightList) for comp in leftList: if rightIndex >= rightListSize: output.append((comp, None)) else: leftKey = getSortKey(comp) rightComp = rightList[rightIndex] rightKey = getSortKey(rightComp) while leftKey > rightKey: output.append((None, rightComp)) rightIndex += 1 if rightIndex >= rightListSize: output.append((comp, None)) break else: rightComp = rightList[rightIndex] rightKey = getSortKey(rightComp) if leftKey < rightKey: output.append((comp, None)) elif leftKey == rightKey: rightIndex += 1 matchResult = processComponentPair(comp, rightComp) if matchResult is not None: output.append(matchResult) return output def newComponent(name, body): if body is None: return None else: c = Component(name) c.behavior = getBehavior(name) c.isNative = True return c def processComponentPair(leftComp, rightComp): """ Return None if a match, or a pair of components including UIDs and any differing children. """ leftChildKeys = leftComp.contents.keys() rightChildKeys = rightComp.contents.keys() differentContentLines = [] differentComponents = {} for key in leftChildKeys: rightList = rightComp.contents.get(key, []) if isinstance(leftComp.contents[key][0], Component): compDifference = processComponentLists(leftComp.contents[key], rightList) if len(compDifference) > 0: differentComponents[key] = compDifference elif leftComp.contents[key] != rightList: differentContentLines.append((leftComp.contents[key], rightList)) for key in rightChildKeys: if key not in leftChildKeys: if isinstance(rightComp.contents[key][0], Component): differentComponents[key] = ([], rightComp.contents[key]) else: differentContentLines.append(([], rightComp.contents[key])) if len(differentContentLines) == 0 and len(differentComponents) == 0: return None else: left = newFromBehavior(leftComp.name) right = newFromBehavior(leftComp.name) # add a UID, if one existed, despite the fact that they'll always be # the same uid = leftComp.getChildValue('uid') if uid is not None: left.add( 'uid').value = uid right.add('uid').value = uid for name, childPairList in differentComponents.iteritems(): leftComponents, rightComponents = zip(*childPairList) if len(leftComponents) > 0: # filter out None left.contents[name] = filter(None, leftComponents) if len(rightComponents) > 0: # filter out None right.contents[name] = filter(None, rightComponents) for leftChildLine, rightChildLine in differentContentLines: nonEmpty = leftChildLine or rightChildLine name = nonEmpty[0].name if leftChildLine is not None: left.contents[name] = leftChildLine if rightChildLine is not None: right.contents[name] = rightChildLine return left, right vevents = processComponentLists(sortByUID(getattr(left, 'vevent_list', [])), sortByUID(getattr(right, 'vevent_list', []))) vtodos = processComponentLists(sortByUID(getattr(left, 'vtodo_list', [])), sortByUID(getattr(right, 'vtodo_list', []))) return vevents + vtodos def prettyDiff(leftObj, rightObj): for left, right in diff(leftObj, rightObj): print "<<<<<<<<<<<<<<<" if left is not None: left.prettyPrint() print "===============" if right is not None: right.prettyPrint() print ">>>>>>>>>>>>>>>" print from optparse import OptionParser import icalendar, base import os import codecs def main(): options, args = getOptions() if args: ignore_dtstamp = options.ignore ics_file1, ics_file2 = args cal1 = base.readOne(file(ics_file1)) cal2 = base.readOne(file(ics_file2)) deleteExtraneous(cal1, ignore_dtstamp=ignore_dtstamp) deleteExtraneous(cal2, ignore_dtstamp=ignore_dtstamp) prettyDiff(cal1, cal2) version = "0.1" def getOptions(): ##### Configuration options ##### usage = "usage: %prog [options] ics_file1 ics_file2" parser = OptionParser(usage=usage, version=version) parser.set_description("ics_diff will print a comparison of two iCalendar files ") parser.add_option("-i", "--ignore-dtstamp", dest="ignore", action="store_true", default=False, help="ignore DTSTAMP lines [default: False]") (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"