[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