[CalendarServer-changes] [7555] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Sat Jun 4 19:33:14 PDT 2011


Revision: 7555
          http://trac.macosforge.org/projects/calendarserver/changeset/7555
Author:   cdaboo at apple.com
Date:     2011-06-04 19:33:13 -0700 (Sat, 04 Jun 2011)
Log Message:
-----------
Add a "calverify" tool to check calendar and directory data consistency.

Modified Paths:
--------------
    CalendarServer/trunk/twext/enterprise/dal/syntax.py

Added Paths:
-----------
    CalendarServer/trunk/bin/calendarserver_calverify
    CalendarServer/trunk/calendarserver/tools/calverify.py
    CalendarServer/trunk/calendarserver/tools/tables.py

Added: CalendarServer/trunk/bin/calendarserver_calverify
===================================================================
--- CalendarServer/trunk/bin/calendarserver_calverify	                        (rev 0)
+++ CalendarServer/trunk/bin/calendarserver_calverify	2011-06-05 02:33:13 UTC (rev 7555)
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+##
+# Copyright (c) 2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import sys
+
+#PYTHONPATH
+
+if __name__ == "__main__":
+    if "PYTHONPATH" in globals():
+        sys.path.insert(0, PYTHONPATH)
+    else:
+        from os.path import dirname, abspath, join
+        from subprocess import Popen, PIPE
+
+        home = dirname(dirname(abspath(__file__)))
+        run = join(home, "run")
+
+        child = Popen((run, "-p"), stdout=PIPE)
+        path, stderr = child.communicate()
+
+        path = path.rstrip("\n")
+
+        if child.wait() == 0:
+            sys.path[0:0] = path.split(":")
+
+        sys.argv[1:1] = ["-f", join(home, "conf", "caldavd-dev.plist")]
+
+    from calendarserver.tools.calverify import main
+    main()


Property changes on: CalendarServer/trunk/bin/calendarserver_calverify
___________________________________________________________________
Added: svn:executable
   + *

Added: CalendarServer/trunk/calendarserver/tools/calverify.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/calverify.py	                        (rev 0)
+++ CalendarServer/trunk/calendarserver/tools/calverify.py	2011-06-05 02:33:13 UTC (rev 7555)
@@ -0,0 +1,635 @@
+#!/usr/bin/env python
+# -*- test-case-name: calendarserver.tools.test.test_calverify -*-
+##
+# Copyright (c) 2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+This tool scans the calendar store to analyze organizer/attendee event
+states to verify that the organizer's view of attendee state matches up
+with the attendees' views. It can optionally apply a fix to bring the two
+views back into line.
+
+In theory the implicit scheduling model should eliminate the possibility
+of mismatches, however, because we store separate resources for organizer
+and attendee events, there is a possibility of mismatch. This is greatly
+lessened via the new transaction model of database changes, but it is
+possible there are edge cases or actual implicit processing errors we have
+missed. This tool will allow us to track mismatches to help determine these
+errors and get them fixed.
+
+Even in the long term if we move to a "single instance" store where the
+organizer event resource is the only one we store (with attendee views
+derived from that), in a situation where we have server-to-server scheduling
+it is possible for mismatches to creep in. In that case having a way to analyze
+multiple DBs for inconsistency would be good too. 
+
+"""
+
+from calendarserver.tap.util import directoryFromConfig
+from calendarserver.tools import tables
+from calendarserver.tools.cmdline import utilityMain
+from pycalendar import definitions
+from pycalendar.calendar import PyCalendar
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.period import PyCalendarPeriod
+from twext.enterprise.dal.syntax import Select, Parameter, Count
+from twisted.application.service import Service
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from twisted.python import log
+from twisted.python.text import wordWrap
+from twisted.python.usage import Options
+from twistedcaldav.dateops import pyCalendarTodatetime
+from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
+from txdav.common.datastore.sql_tables import schema, _BIND_MODE_OWN
+import collections
+import os
+import sys
+
+def usage(e=None):
+    if e:
+        print e
+        print ""
+    try:
+        CalVerifyOptions().opt_help()
+    except SystemExit:
+        pass
+    if e:
+        sys.exit(64)
+    else:
+        sys.exit(0)
+
+
+description = '\n'.join(
+    wordWrap(
+        """
+        Usage: calendarserver_calverify [options] [input specifiers]\n
+        """,
+        int(os.environ.get('COLUMNS', '80'))
+    )
+)
+
+class CalVerifyOptions(Options):
+    """
+    Command-line options for 'calendarserver_calverify'
+    """
+
+    synopsis = description
+
+    optFlags = [
+        ['fix', 'x', "Fix mismatches."],
+        ['missing', 'v', "Show 'orphaned' homes."],
+        ['verbose', 'v', "Verbose logging."],
+    ]
+
+    optParameters = [
+        ['config', 'f', DEFAULT_CONFIG_FILE, "Specify caldavd.plist configuration path."],
+        ['data', 'd', "./calverify-data", "Path where ancillary data is stored."],
+    ]
+
+    def __init__(self):
+        super(CalVerifyOptions, self).__init__()
+        self.outputName = '-'
+
+    def opt_output(self, filename):
+        """
+        Specify output file path (default: '-', meaning stdout).
+        """
+        self.outputName = filename
+
+    opt_o = opt_output
+
+    def openOutput(self):
+        """
+        Open the appropriate output file based on the '--output' option.
+        """
+        if self.outputName == '-':
+            return sys.stdout
+        else:
+            return open(self.outputName, 'wb')
+
+
+
+class CalVerifyService(Service, object):
+    """
+    Service which runs, exports the appropriate records, then stops the reactor.
+    """
+
+    def __init__(self, store, options, output, reactor, config):
+        super(CalVerifyService, self).__init__()
+        self.store   = store
+        self.options = options
+        self.output  = output
+        self.reactor = reactor
+        self.config = config
+        self._directory = None
+
+
+    def startService(self):
+        """
+        Start the service.
+        """
+        super(CalVerifyService, self).startService()
+        self.doCalVerify()
+
+
+    @inlineCallbacks
+    def doCalVerify(self):
+        """
+        Do the export, stopping the reactor when done.
+        """
+        self.txn = self.store.newTransaction()
+        try:
+            if self.options["missing"]:
+                yield self.doOrphans()
+            yield self.doScan(self.options["fix"])
+            yield self.txn.commit()
+            self.txn = None
+
+            self.output.close()
+        except:
+            log.err()
+
+        self.reactor.stop()
+
+
+    @inlineCallbacks
+    def doOrphans(self):
+        """
+        Report on home collections for which there are no directory records. 
+        """
+        print "\n---- Finding calendar homes with no directory record ----"
+
+        uids = yield self.getAllHomeUIDs()
+        missing = []
+        uids_len = len(uids)
+        uids_div = 1 if uids_len < 100 else uids_len / 100
+
+        for ctr, uid in enumerate(uids):
+            if self.options["verbose"] and divmod(ctr+1, uids_div)[1] == 0:
+                print "%d of %d (%d%%)" % (
+                    ctr+1,
+                    uids_len,
+                    ((ctr+1) * 100 / uids_len),
+                )
+
+            if self.directoryService().recordWithGUID(uid[0]) is None:
+                contents = yield self.countHomeContents(uid)
+                missing.append((uid[0], contents,))
+        
+        # Print table of results
+        table = tables.Table()
+        table.addHeader(("Owner UID", "Calendar Objects"))
+        for uid, count in missing:
+            table.addRow((
+                uid,
+                count,
+            ))
+        
+        self.output.write("\n")
+        self.output.write("Homes without a matching directory record (total=%d):\n" % (len(missing),))
+        table.printTable(os=self.output)
+        
+
+    @inlineCallbacks
+    def getAllHomeUIDs(self):
+        ch = schema.CALENDAR_HOME
+        rows = (yield Select(
+            [ch.OWNER_UID,],
+            From=ch,
+        ).on(self.txn))
+        returnValue(tuple(rows))
+
+    @inlineCallbacks
+    def countHomeContents(self, uid):
+        ch = schema.CALENDAR_HOME
+        cb = schema.CALENDAR_BIND
+        co = schema.CALENDAR_OBJECT
+        kwds = { "UID" : uid }
+        rows = (yield Select(
+            [Count(co.RESOURCE_ID),],
+            From=ch.join(cb.join(co)),
+            Where=(
+                ch.OWNER_UID == Parameter("UID")).And(
+                ch.RESOURCE_ID == cb.CALENDAR_HOME_RESOURCE_ID).And(
+                cb.BIND_MODE == _BIND_MODE_OWN).And(
+                cb.CALENDAR_RESOURCE_ID == co.CALENDAR_RESOURCE_ID
+            )
+        ).on(self.txn, **kwds))
+        returnValue(int(rows[0][0]) if rows else 0)
+
+    @inlineCallbacks
+    def doScan(self, fix):
+        
+        print "\n---- Scanning calendar data ----"
+
+        self.start = PyCalendarDateTime.getToday()
+        self.start.setDateOnly(False)
+        self.end = self.start.duplicate()
+        self.end.offsetYear(1)
+        self.fix = fix
+
+        rows = yield self.getAllResourceInfo(self.start)
+        print "Number of events to process: %s" % (len(rows,))
+        
+        # Split into organizer events and attendee events
+        self.organized = []
+        self.organized_byuid = {}
+        self.attended = []
+        self.attended_byuid = collections.defaultdict(list)
+        self.matched_attendee_to_organizer = collections.defaultdict(set)
+        for owner, resid, uid, md5, organizer in rows:
+            if organizer.startswith("urn:uuid:") and owner == organizer[9:]:
+                self.organized.append((owner, resid, uid, md5, organizer,))
+                self.organized_byuid[uid] = (owner, resid, uid, md5, organizer,)
+            else:
+                self.attended.append((owner, resid, uid, md5, organizer,))
+                self.attended_byuid[uid].append((owner, resid, uid, md5, organizer,))
+                
+        print "Number of organizer events to process: %s" % (len(self.organized),)
+        print "Number of attendee events to process: %s" % (len(self.attended,))
+
+        yield self.verifyAllAttendeesForOrganizer()
+        yield self.verifyAllOrganizersForAttendee()
+        
+        yield succeed(None)
+
+    @inlineCallbacks
+    def getAllResourceInfo(self, start):
+        co = schema.CALENDAR_OBJECT
+        cb = schema.CALENDAR_BIND
+        ch = schema.CALENDAR_HOME
+        tr = schema.TIME_RANGE
+        kwds = { "Start" : pyCalendarTodatetime(start) }
+        rows = (yield Select(
+            [ch.OWNER_UID, co.RESOURCE_ID, co.ICALENDAR_UID, co.MD5, co.ORGANIZER,],
+            From=ch.join(cb).join(co).join(tr, type='left', on=(co.RESOURCE_ID == tr.CALENDAR_OBJECT_RESOURCE_ID)),
+            Where=(
+                ch.RESOURCE_ID == cb.CALENDAR_HOME_RESOURCE_ID     ).And(
+                cb.CALENDAR_RESOURCE_ID == co.CALENDAR_RESOURCE_ID ).And(
+                cb.BIND_MODE == _BIND_MODE_OWN).And(
+                (tr.START_DATE >= Parameter("Start")).Or(co.RECURRANCE_MAX == '1900-01-01')).And(
+                cb.CALENDAR_RESOURCE_NAME != "inbox").And(
+                co.ORGANIZER != ""
+            ),
+            GroupBy=(ch.OWNER_UID, co.RESOURCE_ID, co.ICALENDAR_UID, co.MD5, co.ORGANIZER,),
+        ).on(self.txn, **kwds))
+        returnValue(tuple(rows))
+
+    @inlineCallbacks
+    def verifyAllAttendeesForOrganizer(self):
+        """
+        Make sure that for each organizer, each referenced attendee has a consistent view of the organizer's event.
+        We will look for events that an organizer has and are missing for the attendee, and events that an organizer's
+        view of attendee status does not match the attendee's view of their own status.
+        """
+        
+        print "\n---- Verifying Organizer events against Attendee copies ----"
+
+        results_missing = []
+        results_mismatch = []
+        organized_len = len(self.organized)
+        organizer_div = 1 if organized_len < 100 else organized_len / 100
+
+        # Test organized events
+        for ctr, organizerEvent in enumerate(self.organized):
+            
+            if self.options["verbose"] and divmod(ctr+1, organizer_div)[1] == 0:
+                print "%d of %d (%d%%) Missing: %d  Mismatched: %s" % (
+                    ctr+1,
+                    organized_len,
+                    ((ctr+1) * 100 / organized_len),
+                    len(results_missing),
+                    len(results_mismatch),
+                )
+
+            # Get the organizer's view of attendee states            
+            organizer, resid, uid, _ignore_md5, _ignore_organizer = organizerEvent
+            calendar = yield self.getCalendar(resid)
+            organizerViewOfAttendees = self.buildAttendeeStates(calendar, self.start, self.end)
+            try:
+                del organizerViewOfAttendees[organizer]
+            except KeyError:
+                # Odd - the organizer is not an attendee - this usually does not happen
+                pass
+            if len(organizerViewOfAttendees) == 0:
+                continue
+            
+            # Get attendee states for matching UID
+            eachAttendeesOwnStatus = {}
+            for attendeeEvent in self.attended_byuid.get(uid, ()):
+                owner, resid, uid, _ignore_md5, _ignore_organizer = attendeeEvent
+                calendar = yield self.getCalendar(resid)
+                eachAttendeesOwnStatus[owner] = self.buildAttendeeStates(calendar, self.start, self.end, attendee_only=owner)
+            
+            # Look at each attendee in the organizer's meeting
+            for organizerAttendee, organizerViewOfStatus in organizerViewOfAttendees.iteritems():
+                broken = False
+
+                self.matched_attendee_to_organizer[uid].add(organizerAttendee)
+
+                # If an entry for the attendee exists, then check whether attendee status matches
+                if organizerAttendee in eachAttendeesOwnStatus:
+                    attendeeOwnStatus = eachAttendeesOwnStatus[organizerAttendee].get(organizerAttendee, set())
+
+                    if organizerViewOfStatus != attendeeOwnStatus:
+                        # Check that the difference is only cancelled or declined on the organizers side
+                        for _ignore_organizerInstance, partstat in organizerViewOfStatus.difference(attendeeOwnStatus):
+                            if partstat not in ("DECLINED", "CANCELLED"):
+                                results_mismatch.append((uid, organizer, organizerAttendee))
+                                broken = True
+                                break
+                        # Check that the difference is only cancelled on the attendees side
+                        for _ignore_attendeeInstance, partstat in attendeeOwnStatus.difference(organizerViewOfStatus):
+                            if partstat not in ("CANCELLED",):
+                                if not broken:
+                                    results_mismatch.append((uid, organizer, organizerAttendee))
+                                broken = True
+                                break
+
+                # Check that the status for this attendee is always declined which means a missing copy of the event is OK
+                else:
+                    for _ignore_instance_id, partstat in organizerViewOfStatus:
+                        if partstat not in ("DECLINED", "CANCELLED"):
+                            results_missing.append((uid, organizer, organizerAttendee,))
+                            broken = True
+                            break
+                
+                # If there was a problem we can fix it
+                if broken and self.fix:
+                    # TODO: This is where we attempt a fix
+                    #self.fixEvent(organizer, organizerAttendee, eventpath, attendeePaths.get(organizerAttendee, None))
+                    pass
+                
+        # Print table of results
+        table = tables.Table()
+        table.addHeader(("Organizer", "Attendee", "Event UID",))
+        for item in results_missing:
+            uid, organizer, attendee = item
+            organizer_record = self.directoryService().recordWithGUID(organizer)
+            attendee_record = self.directoryService().recordWithGUID(attendee)
+            table.addRow((
+                "%s/%s (%s)" % (organizer_record.recordType if organizer_record else "-", organizer_record.shortNames[0] if organizer_record else "-", organizer,),
+                "%s/%s (%s)" % (attendee_record.recordType if attendee_record else "-", attendee_record.shortNames[0] if attendee_record else "-", attendee,),
+                uid,
+            ))
+        
+        self.output.write("\n")
+        self.output.write("Events missing from Attendee's calendars (total=%d):\n" % (len(results_missing),))
+        table.printTable(os=self.output)
+            
+        # Print table of results
+        table = tables.Table()
+        table.addHeader(("Organizer", "Attendee", "Event UID",))
+        for item in results_mismatch:
+            uid, organizer, attendee = item
+            organizer_record = self.directoryService().recordWithGUID(organizer)
+            attendee_record = self.directoryService().recordWithGUID(attendee)
+            table.addRow((
+                "%s/%s (%s)" % (organizer_record.recordType if organizer_record else "-", organizer_record.shortNames[0] if organizer_record else "-", organizer,),
+                "%s/%s (%s)" % (attendee_record.recordType if attendee_record else "-", attendee_record.shortNames[0] if attendee_record else "-", attendee,),
+                uid,
+            ))
+        
+        self.output.write("\n")
+        self.output.write("Events mismatched between Organizer's and Attendee's calendars (total=%d):\n" % (len(results_mismatch),))
+        table.printTable(os=self.output)
+
+    @inlineCallbacks
+    def verifyAllOrganizersForAttendee(self):
+        """
+        Make sure that for each attendee, there is a matching event for the organizer.
+        """
+
+        print "\n---- Verifying Attendee events against Organizer copies ----"
+
+        # Now try to match up each attendee event
+        missing = []
+        mismatched = []
+        attended_len = len(self.attended)
+        attended_div = 1 if attended_len < 100 else attended_len / 100
+
+        for ctr, attendeeEvent in enumerate(self.attended):
+            
+            if self.options["verbose"] and divmod(ctr+1, attended_div)[1] == 0:
+                print "%d of %d (%d%%) Missing: %d  Mismatched: %s" % (
+                    ctr+1,
+                    attended_len,
+                    ((ctr+1) * 100 / attended_len),
+                    len(missing),
+                    len(mismatched),
+                )
+
+            attendee, resid, uid, _ignore_md5, organizer = attendeeEvent
+            calendar = yield self.getCalendar(resid)
+            eachAttendeesOwnStatus = self.buildAttendeeStates(calendar, self.start, self.end, attendee_only=attendee)
+            if attendee not in eachAttendeesOwnStatus:
+                continue
+
+            # Only care about data for hosted organizers                
+            if not organizer.startswith("urn:uuid:"):
+                continue
+            organizer = organizer[9:]
+
+            if uid not in self.organized_byuid:
+
+                # Check whether attendee has all instances cancelled
+                if self.allCancelled(eachAttendeesOwnStatus):
+                    continue
+                
+                missing.append((uid, attendee, organizer,))
+                
+                # If there is a miss we fix by removing the attendee data
+                if self.fix:
+                    # TODO: This is where we attempt a fix
+                    pass
+
+            elif attendee not in self.matched_attendee_to_organizer[uid]:
+                # Check whether attendee has all instances cancelled
+                if self.allCancelled(eachAttendeesOwnStatus):
+                    continue
+
+                mismatched.append((uid, attendee, organizer,))
+                
+                # If there is a mismatch we fix by re-inviting the attendee
+                if self.fix:
+                    # TODO: This is where we attempt a fix
+                    pass
+
+        # Print table of results
+        table = tables.Table()
+        table.addHeader(("UID", "Owner", "Organizer",))
+        missing.sort()
+        unique_set = set()
+        for item in missing:
+            uid, attendee, organizer = item
+            unique_set.add(uid)
+            if organizer:
+                organizerRecord = self.directoryService().recordWithGUID(organizer)
+                organizer = "%s/%s (%s)" % (organizerRecord.recordType if organizerRecord else "-", organizerRecord.shortNames[0] if organizerRecord else "-", organizer,)
+            attendeeRecord = self.directoryService().recordWithGUID(attendee)
+            table.addRow((
+                uid,
+                "%s/%s (%s)" % (attendeeRecord.recordType if attendeeRecord else "-", attendeeRecord.shortNames[0] if attendeeRecord else "-", attendee,),
+                organizer,
+            ))
+        
+        self.output.write("\n")
+        self.output.write("Attendee events missing in Organizer's calendar (total=%d, unique=%d):\n" % (len(missing), len(unique_set),))
+        table.printTable(os=self.output)
+
+        # Print table of results
+        table = tables.Table()
+        table.addHeader(("UID", "Owner", "Organizer",))
+        mismatched.sort()
+        for item in mismatched:
+            uid, attendee, organizer = item
+            if organizer:
+                organizerRecord = self.directoryService().recordWithGUID(organizer)
+                organizer = "%s/%s (%s)" % (organizerRecord.recordType if organizerRecord else "-", organizerRecord.shortNames[0] if organizerRecord else "-", organizer,)
+            attendeeRecord = self.directoryService().recordWithGUID(attendee)
+            table.addRow((
+                uid,
+                "%s/%s (%s)" % (attendeeRecord.recordType if attendeeRecord else "-", attendeeRecord.shortNames[0] if attendeeRecord else "-", attendee,),
+                organizer,
+            ))
+        
+        self.output.write("\n")
+        self.output.write("Attendee events mismatched in Organizer's calendar (total=%d):\n" % (len(mismatched),))
+        table.printTable(os=self.output)
+
+
+    @inlineCallbacks
+    def getCalendar(self, resid):
+        co = schema.CALENDAR_OBJECT
+        kwds = { "ResourceID" : resid }
+        rows = (yield Select(
+            [co.ICALENDAR_TEXT],
+            From=co,
+            Where=(
+                co.RESOURCE_ID == Parameter("ResourceID")
+            ),
+        ).on(self.txn, **kwds))
+        returnValue(PyCalendar.parseText(rows[0][0]) if rows else None)
+
+    def buildAttendeeStates(self, calendar, start, end, attendee_only=None):
+        # Expand events into instances in the start/end range
+        results = []
+        calendar.getVEvents(
+            PyCalendarPeriod(
+                start=start,
+                end=end,
+            ),
+            results
+        )
+    
+        # Need to do iCal fake master fixup
+        overrides = len(calendar.getComponents(definitions.cICalComponent_VEVENT)) > 1
+    
+        # Create map of each attendee's instances with the instance id (start time) and attendee part-stat
+        attendees = {}
+        for item in results:
+            
+            # Fake master fixup
+            if overrides:
+                if not item.getOwner().isRecurrenceInstance():
+                    if item.getOwner().getRecurrenceSet() is None or not item.getOwner().getRecurrenceSet().hasRecurrence():
+                        continue
+    
+            # Get Status - ignore cancelled events
+            status = item.getOwner().loadValueString(definitions.cICalProperty_STATUS)
+            cancelled = status == definitions.cICalProperty_STATUS_CANCELLED
+    
+            # Get instance start
+            item.getInstanceStart().adjustToUTC()
+            instance_id = item.getInstanceStart().getText()
+            
+            props = item.getOwner().getProperties().get(definitions.cICalProperty_ATTENDEE, [])
+            for prop in props:
+                caladdr = prop.getCalAddressValue().getValue()
+                if caladdr.startswith("urn:uuid:"):
+                    caladdr = caladdr[9:]
+                else:
+                    continue
+                if attendee_only is not None and attendee_only != caladdr:
+                    continue
+                if cancelled:
+                    partstat = "CANCELLED"
+                else:
+                    if not prop.hasAttribute(definitions.cICalAttribute_PARTSTAT):
+                        partstat = definitions.cICalAttribute_PARTSTAT_NEEDSACTION
+                    else:
+                        partstat = prop.getAttributeValue(definitions.cICalAttribute_PARTSTAT)
+    
+                attendees.setdefault(caladdr, set()).add((instance_id, partstat))
+                    
+        return attendees
+    
+    
+    def allCancelled(self, attendeesStatus):
+        # Check whether attendees have all instances cancelled
+        all_cancelled = True
+        for _ignore_guid, states in attendeesStatus.iteritems():
+            for _ignore_instance_id, partstat in states:
+                if partstat not in ("CANCELLED",):
+                    all_cancelled = False
+                    break
+            if not all_cancelled:
+                break
+        return all_cancelled
+       
+
+    def directoryService(self):
+        """
+        Get an appropriate directory service for this L{CalVerifyService}'s
+        configuration, creating one first if necessary.
+        """
+        if self._directory is None:
+            self._directory = directoryFromConfig(self.config)
+        return self._directory
+
+
+    def stopService(self):
+        """
+        Stop the service.  Nothing to do; everything should be finished by this
+        time.
+        """
+        # TODO: stopping this service mid-export should really stop the export
+        # loop, but this is not implemented because nothing will actually do it
+        # except hitting ^C (which also calls reactor.stop(), so that will exit
+        # anyway).
+
+
+
+def main(argv=sys.argv, stderr=sys.stderr, reactor=None):
+    """
+    Do the export.
+    """
+    if reactor is None:
+        from twisted.internet import reactor
+    options = CalVerifyOptions()
+    options.parseOptions(argv[1:])
+    try:
+        output = options.openOutput()
+    except IOError, e:
+        stderr.write("Unable to open output file for writing: %s\n" % (e))
+        sys.exit(1)
+    def makeService(store):
+        from twistedcaldav.config import config
+        return CalVerifyService(store, options, output, reactor, config)
+    utilityMain(options['config'], makeService, reactor)
+
+if __name__ == '__main__':
+    main()


Property changes on: CalendarServer/trunk/calendarserver/tools/calverify.py
___________________________________________________________________
Added: svn:executable
   + *

Added: CalendarServer/trunk/calendarserver/tools/tables.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/tables.py	                        (rev 0)
+++ CalendarServer/trunk/calendarserver/tools/tables.py	2011-06-05 02:33:13 UTC (rev 7555)
@@ -0,0 +1,275 @@
+##
+# Copyright (c) 2009-2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from sys import stdout
+import types
+
+class Table(object):
+    """
+    Class that allows pretty printing ascii tables.
+    
+    The table supports multiline headers and footers, independent
+    column formatting by row, alternative tab-delimited output. 
+    """
+    
+    class ColumnFormat(object):
+        """
+        Defines the format string, justification and span for a column.
+        """
+        
+        LEFT_JUSTIFY = 0
+        RIGHT_JUSTIFY = 1
+        CENTER_JUSTIFY = 2
+
+        def __init__(self, strFormat="%s", justify=LEFT_JUSTIFY, span=1):
+            
+            self.format = strFormat
+            self.justify = justify
+            self.span = span
+
+    def __init__(self, table=None):
+        
+        self.headers = []
+        self.headerColumnFormats = []
+        self.rows = []
+        self.footers = []
+        self.footerColumnFormats = []
+        self.columnCount = 0
+        self.defaultColumnFormats = []
+        self.columnFormatsByRow = {}
+
+        if table:
+            self.setData(table)
+
+    def setData(self, table):
+        
+        self.hasTitles = True
+        self.headers.append(table[0])
+        self.rows = table[1:]
+        self._getMaxColumnCount()
+
+    def setDefaultColumnFormats(self, columnFormats):
+        
+        self.defaultColumnFormats = columnFormats
+
+    def addDefaultColumnFormat(self, columnFormat):
+        
+        self.defaultColumnFormats.append(columnFormat)
+
+    def setHeaders(self, rows, columnFormats=None):
+        
+        self.headers = rows
+        self.headerColumnFormats = columnFormats if columnFormats else [None,] * len(self.headers)
+        self._getMaxColumnCount()
+
+    def addHeader(self, row, columnFormats=None):
+        
+        self.headers.append(row)
+        self.headerColumnFormats.append(columnFormats)
+        self._getMaxColumnCount()
+
+    def addHeaderDivider(self, skipColumns=()):
+        
+        self.headers.append((None, skipColumns,))
+        self.headerColumnFormats.append(None)
+
+    def setFooters(self, row, columnFormats=None):
+        
+        self.footers = row
+        self.footerColumnFormats = columnFormats if columnFormats else [None,] * len(self.footers)
+        self._getMaxColumnCount()
+
+    def addFooter(self, row, columnFormats=None):
+        
+        self.footers.append(row)
+        self.footerColumnFormats.append(columnFormats)
+        self._getMaxColumnCount()
+
+    def addRow(self, row=None, columnFormats=None):
+        
+        self.rows.append(row)
+        if columnFormats:
+            self.columnFormatsByRow[len(self.rows) - 1] = columnFormats
+        self._getMaxColumnCount()
+    
+    def addDivider(self, skipColumns=()):
+        
+        self.rows.append((None, skipColumns,))
+
+    def printTable(self, os=stdout):
+        
+        maxWidths = self._getMaxWidths()
+        
+        self.printDivider(os, maxWidths, False)
+        if self.headers:
+            for header, format in zip(self.headers, self.headerColumnFormats):
+                self.printRow(os, header, self._getHeaderColumnFormat(format), maxWidths)
+            self.printDivider(os, maxWidths)
+        for ctr, row in enumerate(self.rows):
+            self.printRow(os, row, self._getColumnFormatForRow(ctr), maxWidths)
+        if self.footers:
+            self.printDivider(os, maxWidths, double=True)
+            for footer, format in zip(self.footers, self.footerColumnFormats):
+                self.printRow(os, footer, self._getFooterColumnFormat(format), maxWidths)
+        self.printDivider(os, maxWidths, False)
+    
+    def printRow(self, os, row, format, maxWidths):
+        
+        if row is None or type(row) is tuple and row[0] is None:
+            self.printDivider(os, maxWidths, skipColumns=row[1] if type(row) is tuple else ())
+        else:
+            if len(row) != len(maxWidths):
+                row = list(row)
+                row.extend([""] * (len(maxWidths) - len(row)))
+
+            t = "|"
+            ctr = 0
+            while ctr < len(row):
+                startCtr = ctr
+                maxWidth = 0
+                for _ignore_span in xrange(format[startCtr].span if format else 1):
+                    maxWidth += maxWidths[ctr]
+                    ctr += 1
+                maxWidth += 3 * ((format[startCtr].span - 1) if format else 0)
+                text = self._columnText(row, startCtr, format, width=maxWidth)
+                t += " " + text + " |"
+            t += "\n"
+            os.write(t)
+            
+
+    def printDivider(self, os, maxWidths, intermediate=True, double=False, skipColumns=()):
+        t = "|" if intermediate else "+"
+        for widthctr, width in enumerate(maxWidths):
+            if widthctr in skipColumns:
+                c = " "
+            else:
+                c = "=" if double else "-"
+            t += c * (width + 2)
+            t += "+" if widthctr < len(maxWidths) - 1 else ("|" if intermediate else "+")
+        t += "\n"
+        os.write(t)
+
+    def printTabDelimitedData(self, os=stdout, footer=True):
+        
+        if self.headers:
+            titles = [""] * len(self.headers[0])
+            for row, header in enumerate(self.headers):
+                for col, item in enumerate(header):
+                    titles[col] += (" " if row and item else "") + item
+            self.printTabDelimitedRow(os, titles, self._getHeaderColumnFormat(self.headerColumnFormats[0]))
+        for ctr, row in enumerate(self.rows):
+            self.printTabDelimitedRow(os, row, self._getColumnFormatForRow(ctr))
+        if self.footers and footer:
+            for footer in self.footers:
+                self.printTabDelimitedRow(os, footer, self._getFooterColumnFormat(self.footerColumnFormats[0]))
+
+    def printTabDelimitedRow(self, os, row, format):
+        
+        if row is None:
+            row = [""] * self.columnCount
+        
+        if len(row) != self.columnCount:
+            row = list(row)
+            row.extend([""] * (self.columnCount - len(row)))
+
+        textItems = [self._columnText(row, ctr, format) for ctr in xrange((len(row)))]
+        os.write("\t".join(textItems) + "\n")
+        
+    def _getMaxColumnCount(self):
+        
+        self.columnCount = 0
+        if self.headers:
+            for header in self.headers:
+                self.columnCount = max(self.columnCount, len(header) if header else 0)
+        for row in self.rows:
+            self.columnCount = max(self.columnCount, len(row) if row else 0)
+        if self.footers:
+            for footer in self.footers:
+                self.columnCount = max(self.columnCount, len(footer) if footer else 0)
+
+    def _getMaxWidths(self):
+
+        maxWidths = [0] * self.columnCount
+
+        if self.headers:
+            for header, format in zip(self.headers, self.headerColumnFormats):
+                self._updateMaxWidthsFromRow(header, self._getHeaderColumnFormat(format), maxWidths)
+            
+        for ctr, row in enumerate(self.rows):
+            self._updateMaxWidthsFromRow(row, self._getColumnFormatForRow(ctr), maxWidths)
+
+        if self.footers:
+            for footer, format in zip(self.footers, self.footerColumnFormats):
+                self._updateMaxWidthsFromRow(footer, self._getFooterColumnFormat(format), maxWidths)
+            
+        return maxWidths
+
+    def _updateMaxWidthsFromRow(self, row, format, maxWidths):
+        
+        if row and (type(row) is not tuple or row[0] is not None):
+            ctr = 0
+            while ctr < len(row):
+                
+                text = self._columnText(row, ctr, format)       
+                startCtr = ctr
+                for _ignore_span in xrange(format[startCtr].span if format else 1):
+                    maxWidths[ctr] = max(maxWidths[ctr], len(text) / (format[startCtr].span if format else 1))
+                    ctr += 1
+    
+    def _getHeaderColumnFormat(self, format):
+        
+        if format:
+            return format
+        else:
+            justify = Table.ColumnFormat.CENTER_JUSTIFY if len(self.headers) == 1 else Table.ColumnFormat.LEFT_JUSTIFY
+            return [Table.ColumnFormat(justify = justify)] * self.columnCount
+
+    def _getFooterColumnFormat(self, format):
+        
+        if format:
+            return format
+        else:
+            return self.defaultColumnFormats
+
+    def _getColumnFormatForRow(self, ctr):
+        
+        if ctr in self.columnFormatsByRow:
+            return self.columnFormatsByRow[ctr]
+        else:
+            return self.defaultColumnFormats
+
+    def _columnText(self, row, column, format, width=0):
+        
+        if row is None or column >= len(row):
+            return ""
+        
+        colData = row[column]
+        if colData is None:
+            colData = ""
+
+        columnFormat = format[column] if format and column < len(format) else Table.ColumnFormat()
+        if type(colData) in types.StringTypes:
+            text = colData
+        else:
+            text = columnFormat.format % colData
+        if width:
+            if columnFormat.justify == Table.ColumnFormat.LEFT_JUSTIFY:
+                text = text.ljust(width)
+            elif columnFormat.justify == Table.ColumnFormat.RIGHT_JUSTIFY:
+                text = text.rjust(width)
+            elif columnFormat.justify == Table.ColumnFormat.CENTER_JUSTIFY:
+                text = text.center(width)
+        return text


Property changes on: CalendarServer/trunk/calendarserver/tools/tables.py
___________________________________________________________________
Added: svn:executable
   + *

Modified: CalendarServer/trunk/twext/enterprise/dal/syntax.py
===================================================================
--- CalendarServer/trunk/twext/enterprise/dal/syntax.py	2011-06-03 20:27:30 UTC (rev 7554)
+++ CalendarServer/trunk/twext/enterprise/dal/syntax.py	2011-06-05 02:33:13 UTC (rev 7555)
@@ -377,6 +377,7 @@
 
 
 
+Count = Function("count")
 Max = Function("max")
 Len = Function("character_length", "length")
 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20110604/d4b32e9d/attachment-0001.html>


More information about the calendarserver-changes mailing list