[CalendarServer-changes] [6702] CalendarServer/branches/users/cdaboo/batchupload-6699

source_changes at macosforge.org source_changes at macosforge.org
Mon Dec 20 07:12:54 PST 2010


Revision: 6702
          http://trac.macosforge.org/projects/calendarserver/changeset/6702
Author:   cdaboo at apple.com
Date:     2010-12-20 07:12:50 -0800 (Mon, 20 Dec 2010)
Log Message:
-----------
Batch upload requests.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/batchupload-6699/conf/caldavd-test.plist
    CalendarServer/branches/users/cdaboo/batchupload-6699/twext/web2/dav/element/base.py
    CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/caldavxml.py
    CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/carddavxml.py
    CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/customxml.py
    CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/directory/opendirectorybacker.py
    CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/ical.py
    CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/sharing.py
    CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/stdconfig.py
    CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/storebridge.py
    CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/vcard.py

Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/conf/caldavd-test.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/conf/caldavd-test.plist	2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/conf/caldavd-test.plist	2010-12-20 15:12:50 UTC (rev 6702)
@@ -705,6 +705,10 @@
     <key>EnableTimezoneService</key>
     <true/>
 
+    <!-- Batch Upload via POST -->
+    <key>EnableBatchUpload</key>
+    <true/>
+
     <!-- Shared Calendars & Address Books -->
     <key>Sharing</key>
     <dict>

Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twext/web2/dav/element/base.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twext/web2/dav/element/base.py	2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twext/web2/dav/element/base.py	2010-12-20 15:12:50 UTC (rev 6702)
@@ -483,6 +483,13 @@
         PCDATAElement: (0, None),
     }
 
+    @classmethod
+    def fromQname(cls, namespace, name):
+        child = cls()
+        child.namespace = namespace
+        child.name = name
+        return child
+
     def qname(self):
         return (self.namespace, self.name)
 

Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/caldavxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/caldavxml.py	2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/caldavxml.py	2010-12-20 15:12:50 UTC (rev 6702)
@@ -433,6 +433,8 @@
         else:
             return None
 
+    generateComponent = calendar
+
     def calendarData(self):
         """
         Returns the calendar data derived from this element.

Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/carddavxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/carddavxml.py	2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/carddavxml.py	2010-12-20 15:12:50 UTC (rev 6702)
@@ -280,6 +280,8 @@
         else:
             return None
 
+    generateComponent = address
+
     def addressData(self):
         """
         Returns an address component derived from this element.

Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/customxml.py	2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/customxml.py	2010-12-20 15:12:50 UTC (rev 6702)
@@ -978,7 +978,83 @@
     namespace = calendarserver_namespace
     name = "link"
 
+mm_namespace = "http://me.com/_namespace/"
 
+class Multiput (davxml.WebDAVElement):
+    namespace = mm_namespace
+    name = "multiput"
+
+    allowed_children = {
+        (mm_namespace, "resource")   : (1, None),
+    }
+
+class Resource (davxml.WebDAVElement):
+    namespace = mm_namespace
+    name = "resource"
+
+    allowed_children = {
+        (davxml,       "href")     : (0, 1),
+        (mm_namespace, "if-match") : (0, 1),
+        (davxml,       "set")      : (0, 1),
+        (davxml,       "remove")   : (0, 1),
+        (mm_namespace, "delete")   : (0, 1),
+    }
+
+class IfMatch (davxml.WebDAVElement):
+    namespace = mm_namespace
+    name = "if-match"
+
+    allowed_children = {
+        (davxml, "getetag")   : (1, 1),
+    }
+
+class Delete (davxml.WebDAVEmptyElement):
+    namespace = mm_namespace
+    name = "delete"
+
+
+class BulkRequests (davxml.WebDAVElement):
+    namespace = mm_namespace
+    name = "bulk-requests"
+    hidden = True
+    protected = True
+
+    allowed_children = {
+        (mm_namespace, "simple")   : (0, 1),
+        (mm_namespace, "crud")     : (0, 1),
+    }
+
+class Simple (davxml.WebDAVElement):
+    namespace = mm_namespace
+    name = "simple"
+    hidden = True
+    protected = True
+
+    allowed_children = {
+        (mm_namespace, "max-resources")   : (1, 1),
+        (mm_namespace, "max-bytes")       : (1, 1),
+    }
+
+class CRUD (davxml.WebDAVElement):
+    namespace = mm_namespace
+    name = "crud"
+    hidden = True
+    protected = True
+
+    allowed_children = {
+        (mm_namespace, "max-resources")   : (1, 1),
+        (mm_namespace, "max-bytes")       : (1, 1),
+    }
+
+class MaxResources (davxml.WebDAVTextElement):
+    namespace = mm_namespace
+    name = "max-resources"
+
+class MaxBytes (davxml.WebDAVTextElement):
+    namespace = mm_namespace
+    name = "max-bytes"
+
+
 ##
 # Extensions to davxml.ResourceType
 ##

Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/directory/opendirectorybacker.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/directory/opendirectorybacker.py	2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/directory/opendirectorybacker.py	2010-12-20 15:12:50 UTC (rev 6702)
@@ -44,7 +44,7 @@
 from twext.web2.dav.element.base import twisted_dav_namespace, dav_namespace, parse_date, twisted_private_namespace
 from twext.web2.dav.resource import DAVPropertyMixIn
 from twext.web2.dav.util import joinURL
-from twext.web2.http_headers import MimeType, generateContentType
+from twext.web2.http_headers import MimeType, generateContentType, ETag
 
 
 from twistedcaldav import customxml, carddavxml
@@ -1908,7 +1908,7 @@
                 #print("VCardResource.readProperty: qname = %s, result = %s" % (qname, result))
                 return result
             elif name == "getetag":
-                result = davxml.GETETag( hashlib.md5(self.vCardText()).hexdigest() )
+                result = davxml.GETETag( ETag(hashlib.md5(self.vCardText()).hexdigest()).generate() )
                 #print("VCardResource.readProperty: qname = %s, result = %s" % (qname, result))
                 return result
             elif name == "getcontenttype":

Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/ical.py	2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/ical.py	2010-12-20 15:12:50 UTC (rev 6702)
@@ -283,6 +283,37 @@
     extraRestrictedProperties = ("SUMMARY", "LOCATION",)
 
     @classmethod
+    def allFromString(clazz, string):
+        """
+        Construct a L{Component} from a string.
+        @param string: a string containing iCalendar data.
+        @return: a C{list} of L{Component}s representing the components described by
+            C{string}.
+        """
+        if type(string) is unicode:
+            string = string.encode("utf-8")
+        return clazz.allFromStream(StringIO.StringIO(string))
+
+    @classmethod
+    def allFromStream(clazz, stream):
+        """
+        Construct possibly multiple L{Component}s from a stream.
+        @param stream: a C{read()}able stream containing iCalendar data.
+        @return: a C{list} of L{Component}s representing the components described by
+            C{stream}.
+        """
+        
+        results = []
+        try:
+            for vobject in readComponents(stream):
+                results.append(clazz(None, vobject=vobject))
+            return results
+        except vParseError, e:
+            raise InvalidICalendarDataError(e)
+        except StopIteration, e:
+            raise InvalidICalendarDataError(e)
+
+    @classmethod
     def fromString(clazz, string):
         """
         Construct a L{Component} from a string.

Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/sharing.py	2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/sharing.py	2010-12-20 15:12:50 UTC (rev 6702)
@@ -597,170 +597,169 @@
         # Add to collections
         yield notifications.deleteNotifictionMessageByUID(request, record.inviteuid)
 
-    def xmlPOSTNoAuth(self, encoding, request):
-        def _handleErrorResponse(error):
-            if isinstance(error.value, HTTPError) and hasattr(error.value, "response"):
-                return error.value.response
-            return error
+    @inlineCallbacks
+    def _xmlHandleInvite(self, request, docroot):
+        yield self.authorize(request, (davxml.Read(), davxml.Write()))
+        result = (yield self._handleInvite(request, docroot))
+        returnValue(result)
+    
+    def _handleInvite(self, request, invitedoc):
+        def _handleInviteSet(inviteset):
+            userid = None
+            cn = None
+            access = None
+            summary = None
+            for item in inviteset.children:
+                if isinstance(item, davxml.HRef):
+                    userid = str(item)
+                    continue
+                if isinstance(item, customxml.CommonName):
+                    cn = str(item)
+                    continue
+                if isinstance(item, customxml.InviteSummary):
+                    summary = str(item)
+                    continue
+                if isinstance(item, customxml.ReadAccess) or isinstance(item, customxml.ReadWriteAccess):
+                    access = item
+                    continue
+            if userid and access and summary:
+                return (userid, cn, access, summary)
+            else:
+                error_text = []
+                if userid is None:
+                    error_text.append("missing href")
+                if access is None:
+                    error_text.append("missing access")
+                if summary is None:
+                    error_text.append("missing summary")
+                raise HTTPError(ErrorResponse(
+                    responsecode.FORBIDDEN,
+                    (customxml.calendarserver_namespace, "valid-request"),
+                    "%s: %s" % (", ".join(error_text), inviteset,),
+                ))
 
-        def _handleInvite(invitedoc):
-            def _handleInviteSet(inviteset):
-                userid = None
-                cn = None
+        def _handleInviteRemove(inviteremove):
+            userid = None
+            access = []
+            for item in inviteremove.children:
+                if isinstance(item, davxml.HRef):
+                    userid = str(item)
+                    continue
+                if isinstance(item, customxml.ReadAccess) or isinstance(item, customxml.ReadWriteAccess):
+                    access.append(item)
+                    continue
+            if userid is None:
+                raise HTTPError(ErrorResponse(
+                    responsecode.FORBIDDEN,
+                    (customxml.calendarserver_namespace, "valid-request"),
+                    "missing href: %s" % (inviteremove,),
+                ))
+            if len(access) == 0:
                 access = None
-                summary = None
-                for item in inviteset.children:
-                    if isinstance(item, davxml.HRef):
-                        userid = str(item)
-                        continue
-                    if isinstance(item, customxml.CommonName):
-                        cn = str(item)
-                        continue
-                    if isinstance(item, customxml.InviteSummary):
-                        summary = str(item)
-                        continue
-                    if isinstance(item, customxml.ReadAccess) or isinstance(item, customxml.ReadWriteAccess):
-                        access = item
-                        continue
-                if userid and access and summary:
-                    return (userid, cn, access, summary)
-                else:
-                    error_text = []
-                    if userid is None:
-                        error_text.append("missing href")
-                    if access is None:
-                        error_text.append("missing access")
-                    if summary is None:
-                        error_text.append("missing summary")
-                    raise HTTPError(ErrorResponse(
-                        responsecode.FORBIDDEN,
-                        (customxml.calendarserver_namespace, "valid-request"),
-                        "%s: %s" % (", ".join(error_text), inviteset,),
-                    ))
+            else:
+                access = set(access)
+            return (userid, access)
 
-            def _handleInviteRemove(inviteremove):
-                userid = None
-                access = []
-                for item in inviteremove.children:
-                    if isinstance(item, davxml.HRef):
-                        userid = str(item)
-                        continue
-                    if isinstance(item, customxml.ReadAccess) or isinstance(item, customxml.ReadWriteAccess):
-                        access.append(item)
-                        continue
-                if userid is None:
-                    raise HTTPError(ErrorResponse(
-                        responsecode.FORBIDDEN,
-                        (customxml.calendarserver_namespace, "valid-request"),
-                        "missing href: %s" % (inviteremove,),
-                    ))
-                if len(access) == 0:
-                    access = None
-                else:
-                    access = set(access)
-                return (userid, access)
+        def _autoShare(isShared, request):
+            if not isShared:
+                self.upgradeToShare()
 
-            def _autoShare(isShared, request):
-                if not isShared:
-                    self.upgradeToShare()
-
-            @inlineCallbacks
-            def _processInviteDoc(_, request):
-                setDict, removeDict, updateinviteDict = {}, {}, {}
-                okusers = set()
-                badusers = set()
-                for item in invitedoc.children:
-                    if isinstance(item, customxml.InviteSet):
-                        userid, cn, access, summary = _handleInviteSet(item)
-                        setDict[userid] = (cn, access, summary)
+        @inlineCallbacks
+        def _processInviteDoc(_, request):
+            setDict, removeDict, updateinviteDict = {}, {}, {}
+            okusers = set()
+            badusers = set()
+            for item in invitedoc.children:
+                if isinstance(item, customxml.InviteSet):
+                    userid, cn, access, summary = _handleInviteSet(item)
+                    setDict[userid] = (cn, access, summary)
+                
+                    # Validate each userid on add only
+                    (okusers if self.validUserIDForShare(userid) else badusers).add(userid)
+                elif isinstance(item, customxml.InviteRemove):
+                    userid, access = _handleInviteRemove(item)
+                    removeDict[userid] = access
                     
-                        # Validate each userid on add only
-                        (okusers if self.validUserIDForShare(userid) else badusers).add(userid)
-                    elif isinstance(item, customxml.InviteRemove):
-                        userid, access = _handleInviteRemove(item)
-                        removeDict[userid] = access
-                        
-                        # Treat removed userids as valid as we will fail invalid ones silently
-                        okusers.add(userid)
+                    # Treat removed userids as valid as we will fail invalid ones silently
+                    okusers.add(userid)
 
-                # Only make changes if all OK
-                if len(badusers) == 0:
-                    # Special case removing and adding the same user and treat that as an add
-                    sameUseridInRemoveAndSet = [u for u in removeDict.keys() if u in setDict]
-                    for u in sameUseridInRemoveAndSet:
-                        removeACL = removeDict[u]
-                        cn, newACL, summary = setDict[u]
-                        updateinviteDict[u] = (cn, removeACL, newACL, summary)
-                        del removeDict[u]
-                        del setDict[u]
-                    for userid, access in removeDict.iteritems():
-                        result = (yield self.uninviteUserToShare(userid, access, request))
-                        (okusers if result else badusers).add(userid)
-                    for userid, (cn, access, summary) in setDict.iteritems():
-                        result = (yield self.inviteUserToShare(userid, cn, access, summary, request))
-                        (okusers if result else badusers).add(userid)
-                    for userid, (cn, removeACL, newACL, summary) in updateinviteDict.iteritems():
-                        result = (yield self.inviteUserUpdateToShare(userid, cn, removeACL, newACL, summary, request))
-                        (okusers if result else badusers).add(userid)
+            # Only make changes if all OK
+            if len(badusers) == 0:
+                # Special case removing and adding the same user and treat that as an add
+                sameUseridInRemoveAndSet = [u for u in removeDict.keys() if u in setDict]
+                for u in sameUseridInRemoveAndSet:
+                    removeACL = removeDict[u]
+                    cn, newACL, summary = setDict[u]
+                    updateinviteDict[u] = (cn, removeACL, newACL, summary)
+                    del removeDict[u]
+                    del setDict[u]
+                for userid, access in removeDict.iteritems():
+                    result = (yield self.uninviteUserToShare(userid, access, request))
+                    (okusers if result else badusers).add(userid)
+                for userid, (cn, access, summary) in setDict.iteritems():
+                    result = (yield self.inviteUserToShare(userid, cn, access, summary, request))
+                    (okusers if result else badusers).add(userid)
+                for userid, (cn, removeACL, newACL, summary) in updateinviteDict.iteritems():
+                    result = (yield self.inviteUserUpdateToShare(userid, cn, removeACL, newACL, summary, request))
+                    (okusers if result else badusers).add(userid)
 
-                # Do a final validation of the entire set of invites
-                yield self.validateInvites()
-                
-                # Create the multistatus response - only needed if some are bad
-                if badusers:
-                    xml_responses = []
-                    xml_responses.extend([
-                        davxml.StatusResponse(davxml.HRef(userid), davxml.Status.fromResponseCode(responsecode.FAILED_DEPENDENCY))
-                        for userid in sorted(okusers)
-                    ])
-                    xml_responses.extend([
-                        davxml.StatusResponse(davxml.HRef(userid), davxml.Status.fromResponseCode(responsecode.FORBIDDEN))
-                        for userid in sorted(badusers)
-                    ])
-                
-                    #
-                    # Return response
-                    #
-                    returnValue(MultiStatusResponse(xml_responses))
-                else:
-                    returnValue(responsecode.OK)
-                    
+            # Do a final validation of the entire set of invites
+            yield self.validateInvites()
+            
+            # Create the multistatus response - only needed if some are bad
+            if badusers:
+                xml_responses = []
+                xml_responses.extend([
+                    davxml.StatusResponse(davxml.HRef(userid), davxml.Status.fromResponseCode(responsecode.FAILED_DEPENDENCY))
+                    for userid in sorted(okusers)
+                ])
+                xml_responses.extend([
+                    davxml.StatusResponse(davxml.HRef(userid), davxml.Status.fromResponseCode(responsecode.FORBIDDEN))
+                    for userid in sorted(badusers)
+                ])
+            
+                #
+                # Return response
+                #
+                returnValue(MultiStatusResponse(xml_responses))
+            else:
+                returnValue(responsecode.OK)
 
-            return self.isShared(request).addCallback(_autoShare, request).addCallback(_processInviteDoc, request)
+        return self.isShared(request).addCallback(_autoShare, request).addCallback(_processInviteDoc, request)
 
-        def _getData(data):
-            try:
-                doc = davxml.WebDAVDocument.fromString(data)
-            except ValueError, e:
-                self.log_error("Error parsing doc (%s) Doc:\n %s" % (str(e), data,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request")))
+    @inlineCallbacks
+    def _xmlHandleInviteReply(self, request, docroot):
+        yield self.authorize(request, (davxml.Read(), davxml.Write()))
+        result = (yield self._handleInviteReply(request, docroot))
+        returnValue(result)
+    
+    def _handleInviteReply(self, request, docroot):
+        raise NotImplementedError
 
-            root = doc.root_element
-            xmlDocHanders = {
-                customxml.InviteShare: _handleInvite, 
-            }
-            if type(root) in xmlDocHanders:
-                return xmlDocHanders[type(root)](root).addErrback(_handleErrorResponse)
-            else:
-                self.log_error("Unsupported XML (%s)" % (root,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request")))
+    @inlineCallbacks
+    def xmlRequestHandler(self, request):
+        
+        # Need to read the data and get the root element first
+        xmldata = (yield allDataFromStream(request.stream))
+        try:
+            doc = davxml.WebDAVDocument.fromString(xmldata)
+        except ValueError, e:
+            self.log_error("Error parsing doc (%s) Doc:\n %s" % (str(e), xmldata,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request")))
 
-        return allDataFromStream(request.stream).addCallback(_getData)
+        root = doc.root_element
+        if type(root) in self.xmlDocHanders:
+            result = (yield self.xmlDocHanders[type(root)](self, request, root))
+            returnValue(result)
+        else:
+            self.log_error("Unsupported XML (%s)" % (root,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request")))
 
-    def xmlPOSTPreconditions(self, _, request):
-        if request.headers.hasHeader("Content-Type"):
-            mimetype = request.headers.getHeader("Content-Type")
-            if mimetype.mediaType in ("application", "text",) and mimetype.mediaSubtype == "xml":
-                encoding = mimetype.params["charset"] if "charset" in mimetype.params else "utf8"
-                return succeed(encoding)
-        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request")))
+    xmlDocHanders = {
+        customxml.InviteShare: _xmlHandleInvite,
+        customxml.InviteReply: _xmlHandleInviteReply,          
+    }
 
-    def xmlPOSTAuth(self, request):
-        d = self.authorize(request, (davxml.Read(), davxml.Write()))
-        d.addCallback(self.xmlPOSTPreconditions, request)
-        d.addCallback(self.xmlPOSTNoAuth, request)
-        return d
-    
     def POST_handler_content_type(self, request, contentType):
         if self.isCollection():
             if contentType:
@@ -773,8 +772,8 @@
         return succeed(responsecode.FORBIDDEN)
 
     _postHandlers = {
-        ("application", "xml") : xmlPOSTAuth,
-        ("text", "xml") : xmlPOSTAuth,
+        ("application", "xml") : xmlRequestHandler,
+        ("text", "xml") : xmlRequestHandler,
     }
 
 inviteAccessMapToXML = {
@@ -1134,63 +1133,37 @@
         # Add to collections
         yield notifications.addNotification(request, notificationUID, xmltype, xmldata)
 
-    def xmlPOSTNoAuth(self, encoding, request):
+    def _handleInviteReply(self, request, invitereplydoc):
+        """ Handle a user accepting or declining a sharing invite """
+        hostUrl = None
+        accepted = None
+        summary = None
+        replytoUID = None
+        for item in invitereplydoc.children:
+            if isinstance(item, customxml.InviteStatusAccepted):
+                accepted = True
+            elif isinstance(item, customxml.InviteStatusDeclined):
+                accepted = False
+            elif isinstance(item, customxml.InviteSummary):
+                summary = str(item)
+            elif isinstance(item, customxml.HostURL):
+                for hosturlItem in item.children:
+                    if isinstance(hosturlItem, davxml.HRef):
+                        hostUrl = str(hosturlItem)
+            elif isinstance(item, customxml.InReplyTo):
+                replytoUID = str(item)
+        
+        if accepted is None or hostUrl is None or replytoUID is None:
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (customxml.calendarserver_namespace, "valid-request"),
+                "missing required XML elements",
+            ))
+        if accepted:
+            return self.acceptInviteShare(request, hostUrl, replytoUID, displayname=summary)
+        else:
+            return self.declineShare(request, hostUrl, replytoUID)
 
-        def _handleErrorResponse(error):
-            if isinstance(error.value, HTTPError) and hasattr(error.value, "response"):
-                return error.value.response
-            return error
-
-        def _handleInviteReply(invitereplydoc):
-            """ Handle a user accepting or declining a sharing invite """
-            hostUrl = None
-            accepted = None
-            summary = None
-            replytoUID = None
-            for item in invitereplydoc.children:
-                if isinstance(item, customxml.InviteStatusAccepted):
-                    accepted = True
-                elif isinstance(item, customxml.InviteStatusDeclined):
-                    accepted = False
-                elif isinstance(item, customxml.InviteSummary):
-                    summary = str(item)
-                elif isinstance(item, customxml.HostURL):
-                    for hosturlItem in item.children:
-                        if isinstance(hosturlItem, davxml.HRef):
-                            hostUrl = str(hosturlItem)
-                elif isinstance(item, customxml.InReplyTo):
-                    replytoUID = str(item)
-            
-            if accepted is None or hostUrl is None or replytoUID is None:
-                raise HTTPError(ErrorResponse(
-                    responsecode.FORBIDDEN,
-                    (customxml.calendarserver_namespace, "valid-request"),
-                    "missing required XML elements",
-                ))
-            if accepted:
-                return self.acceptInviteShare(request, hostUrl, replytoUID, displayname=summary)
-            else:
-                return self.declineShare(request, hostUrl, replytoUID)
-
-        def _getData(data):
-            try:
-                doc = davxml.WebDAVDocument.fromString(data)
-            except ValueError, e:
-                print "Error parsing doc (%s) Doc:\n %s" % (str(e), data,)
-                raise
-
-            root = doc.root_element
-            xmlDocHanders = {
-                customxml.InviteReply: _handleInviteReply,          
-            }
-            if type(root) in xmlDocHanders:
-                return xmlDocHanders[type(root)](root).addErrback(_handleErrorResponse)
-            else:
-                self.log_error("Unsupported XML (%s)" % (root,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request")))
-
-        return allDataFromStream(request.stream).addCallback(_getData)
-
 class SharedCollectionRecord(object):
     
     def __init__(self, shareuid, sharetype, hosturl, localname, summary):

Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/stdconfig.py	2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/stdconfig.py	2010-12-20 15:12:50 UTC (rev 6702)
@@ -354,6 +354,7 @@
     "EnableDropBox"           : False, # Calendar Drop Box
     "EnablePrivateEvents"     : False, # Private Events
     "EnableTimezoneService"   : False, # Timezone service
+    "EnableBatchUpload"       : False, # POST batch uploads
     
     "Sharing": {
         "Enabled"             : False, # Overall on/off switch

Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/storebridge.py	2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/storebridge.py	2010-12-20 15:12:50 UTC (rev 6702)
@@ -15,55 +15,48 @@
 # limitations under the License.
 ##
 
-"""
-Wrappers to translate between the APIs in L{txdav.caldav.icalendarstore} and
-L{txdav.carddav.iaddressbookstore} and those in L{twistedcaldav}.
-"""
-
-from urlparse import urlsplit
-
-from twisted.internet.defer import succeed, inlineCallbacks, returnValue,\
-    maybeDeferred
-from twisted.internet.protocol import Protocol
-from twisted.python.log import err as logDefaultException
-from twisted.python.util import FancyEqMixin
-
 from twext.python.log import Logger
-
 from twext.web2 import responsecode
 from twext.web2.dav import davxml
-from twext.web2.dav.element.base import dav_namespace
-from twext.web2.dav.http import ErrorResponse, ResponseQueue
+from twext.web2.dav.element.base import dav_namespace, WebDAVUnknownElement
+from twext.web2.dav.http import ErrorResponse, ResponseQueue, MultiStatusResponse
 from twext.web2.dav.noneprops import NonePropertyStore
 from twext.web2.dav.resource import TwistedACLInheritable, AccessDeniedError
-from twext.web2.dav.util import parentForURL, allDataFromStream, joinURL, \
-    davXMLFromStream
+from twext.web2.dav.util import parentForURL, allDataFromStream, joinURL, davXMLFromStream
 from twext.web2.http import HTTPError, StatusResponse, Response
 from twext.web2.http_headers import ETag, MimeType
-from twext.web2.responsecode import (
-    FORBIDDEN, NO_CONTENT, NOT_FOUND, CREATED, CONFLICT, PRECONDITION_FAILED,
-    BAD_REQUEST, OK,
-)
+from twext.web2.responsecode import FORBIDDEN, NO_CONTENT, NOT_FOUND, CREATED, CONFLICT, PRECONDITION_FAILED, BAD_REQUEST, OK
 from twext.web2.stream import ProducerStream, readStream, MemoryStream
-
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue, maybeDeferred
+from twisted.internet.protocol import Protocol
+from twisted.python.hashlib import md5
+from twisted.python.log import err as logDefaultException
+from twisted.python.util import FancyEqMixin
+from twistedcaldav import customxml, carddavxml, caldavxml
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.config import config
+from twistedcaldav.ical import Component as VCalendar, Property as VProperty
 from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
-from twistedcaldav.notifications import NotificationCollectionResource, \
-    NotificationResource
+from twistedcaldav.method.put_addressbook_common import StoreAddressObjectResource
+from twistedcaldav.method.put_common import StoreCalendarObjectResource
+from twistedcaldav.notifications import NotificationCollectionResource, NotificationResource
 from twistedcaldav.resource import CalDAVResource, GlobalAddressBookResource
 from twistedcaldav.schedule import ScheduleInboxResource
 from twistedcaldav.scheduling.implicit import ImplicitScheduler
-from twistedcaldav.ical import Component as VCalendar
-from twistedcaldav.ical import Property as VProperty
 from twistedcaldav.vcard import Component as VCard
-
+from twistedcaldav.vcard import Property as VCardProperty
 from txdav.base.propertystore.base import PropertyName
 from txdav.common.icommondatastore import NoSuchObjectResourceError
+from urlparse import urlsplit
+import time
 
+"""
+Wrappers to translate between the APIs in L{txdav.caldav.icalendarstore} and
+L{txdav.carddav.iaddressbookstore} and those in L{twistedcaldav}.
+"""
+
 log = Logger()
 
-
 class _NewStorePropertiesWrapper(object):
     """
     Wrap a new-style property store (a L{txdav.idav.IPropertyStore}) in the old-
@@ -465,6 +458,384 @@
         yield self._newStoreObject.rename(basename)
         returnValue(NO_CONTENT)
 
+    def liveProperties(self):
+        baseProperties = ()
+        if config.EnableBatchUpload:
+            baseProperties += (
+                customxml.BulkRequests.qname(),
+            )
+        return super(_CommonHomeChildCollectionMixin, self).liveProperties() + baseProperties
+
+    @inlineCallbacks
+    def _readGlobalProperty(self, qname, property, request):
+
+        if config.EnableBatchUpload and qname == customxml.BulkRequests.qname():
+            returnValue(customxml.BulkRequests(
+                customxml.Simple(
+                    customxml.MaxResources.fromString("100"),
+                    customxml.MaxBytes.fromString("%d" % (10*1024*1024,)),
+                ),
+                customxml.CRUD(
+                    customxml.MaxResources.fromString("100"),
+                    customxml.MaxBytes.fromString("%d" % (10*1024*1024,)),
+                ),
+            ))
+        else:
+            result = (yield super(_CommonHomeChildCollectionMixin, self)._readGlobalProperty(qname, property, request))
+            returnValue(result)
+
+    @inlineCallbacks
+    def checkCTagPrecondition(self, request):
+        if request.headers.hasHeader("If"):
+            iffy = request.headers.getRawHeaders("If")[0]
+            prefix = "<%sctag/" % (customxml.mm_namespace,)
+            if prefix in iffy:
+                testctag = iffy[iffy.find(prefix):]
+                testctag = testctag[len(prefix):]
+                testctag = testctag.split(">", 1)[0]
+                ctag = (yield self.getSyncToken())
+                if testctag != ctag:
+                    raise HTTPError(StatusResponse(responsecode.PRECONDITION_FAILED, "CTag pre-condition failure"))
+
+    def checkReturnChanged(self, request):
+        if request.headers.hasHeader("X-MobileMe-DAV-Options"):
+            return_changed = request.headers.getRawHeaders("X-MobileMe-DAV-Options")[0]
+            return ("return-changed-data" in return_changed)
+        else:
+            return False
+
+    @requiresPermissions(davxml.Bind())
+    @inlineCallbacks
+    def simpleBatchPOST(self, request):
+        
+        # If CTag precondition
+        yield self.checkCTagPrecondition(request)
+        
+        # Look for return changed data option
+        return_changed = self.checkReturnChanged(request)
+
+        # Read in all data
+        data = (yield allDataFromStream(request.stream))
+        
+        components = self.componentsFromData(data)
+        
+        # Build response
+        xmlresponses = []
+        for component in components:
+            
+            code = None
+            error = None
+            dataChanged = None
+            try:
+                componentdata = str(component)
+
+                # Create a new name if one was not provided
+                name =  md5(str(componentdata) + str(time.time()) + request.path).hexdigest() + self.resourceSuffix()
+            
+                # Get a resource for the new item
+                newchildURL = joinURL(request.path, name)
+                newchild = (yield request.locateResource(newchildURL))
+                yield self.storeResourceData(request, newchild, newchildURL, componentdata)
+
+                # FIXME: figure out return_changed behavior
+                if return_changed:
+                    pass
+#                    note = component.getProperty("NOTE")
+#                    if note is None:
+#                        component.addProperty(VCardProperty("NOTE", component.resourceUID()))
+#                    else:
+#                        note.setValue(component.resourceUID())
+#                    dataChanged = str(component)
+
+            except HTTPError, e:
+                # Extract the pre-condition
+                code = e.response.code
+                if isinstance(e.response, ErrorResponse):
+                    error = e.response.error
+                    error = (error.namespace, error.name,)
+            except Exception:
+                code = responsecode.BAD_REQUEST
+            
+            if code is None:
+                
+                if not return_changed or not dataChanged:
+                    xmlresponses.append(
+                        davxml.PropertyStatusResponse(
+                            davxml.HRef.fromString(newchildURL),
+                            davxml.PropertyStatus(
+                                davxml.PropertyContainer(
+                                    davxml.GETETag.fromString(newchild.etag().generate()),
+                                    customxml.UID.fromString(component.resourceUID()),
+                                ),
+                                davxml.Status.fromResponseCode(responsecode.OK),
+                            )
+                        )
+                    )
+                else:
+                    xmlresponses.append(
+                        davxml.PropertyStatusResponse(
+                            davxml.HRef.fromString(newchildURL),
+                            davxml.PropertyStatus(
+                                davxml.PropertyContainer(
+                                    davxml.GETETag.fromString(newchild.etag().generate()),
+                                    self.xmlDataElementType().fromAddressData(dataChanged),
+                                ),
+                                davxml.Status.fromResponseCode(responsecode.OK),
+                            )
+                        )
+                    )
+                
+            else:
+                xmlresponses.append(
+                    davxml.StatusResponse(
+                        davxml.HRef.fromString(""),
+                        davxml.Status.fromResponseCode(code),
+                    davxml.Error(
+                        WebDAVUnknownElement.fromQname(*error),
+                        customxml.UID.fromString(component.resourceUID()),
+                    ) if error else None,
+                    )
+                )
+        
+        result = MultiStatusResponse(xmlresponses)
+        
+        newctag = (yield self.getSyncToken())
+        result.headers.setRawHeaders("CTag", (newctag,))
+
+        # Setup some useful logging
+        request.submethod = "Simple batch"
+        if not hasattr(request, "extendedLogItems"):
+            request.extendedLogItems = {}
+        request.extendedLogItems["rcount"] = len(xmlresponses)
+
+        returnValue(result)
+        
+    @inlineCallbacks
+    def crudBatchPOST(self, request, xmlroot):
+        
+        # Need to force some kind of overall authentication on the request
+        yield self.authorize(request, (davxml.Read(), davxml.Write(),))
+
+        # If CTag precondition
+        yield self.checkCTagPrecondition(request)
+        
+        # Look for return changed data option
+        return_changed = self.checkReturnChanged(request)
+
+        # Build response
+        xmlresponses = []
+        checkedBindPrivelege = None
+        checkedUnbindPrivelege = None
+        for xmlchild in xmlroot.children:
+            
+            # Determine the multiput operation: create, update, delete
+            href = xmlchild.childOfType(davxml.HRef.qname())
+            set = xmlchild.childOfType(davxml.Set.qname())
+            xmldata = set.childOfType(self.xmlDataElementType().qname()) if set is not None else None
+            if href is None:
+                
+                # Do privilege check on collection once 
+                if checkedBindPrivelege is None:
+                    try:
+                        yield self.authorize(request, (davxml.Bind(),))
+                        checkedBindPrivelege = True
+                    except HTTPError, e:
+                        checkedBindPrivelege = e
+
+                # Create operations
+                yield self.crudCreate(request, xmldata.generateComponent(), xmlresponses, return_changed, checkedBindPrivelege)
+            else:
+                delete = xmlchild.childOfType(customxml.Delete.qname())
+                ifmatch = xmlchild.childOfType(customxml.IfMatch.qname())
+                if ifmatch:
+                    ifmatch = str(ifmatch.children[0]) if len(ifmatch.children) == 1 else None
+                if delete is None:
+                    yield self.crudUpdate(request, str(href), xmldata.generateComponent(), ifmatch, return_changed, xmlresponses)
+                else:
+                    # Do privilege check on collection once 
+                    if checkedUnbindPrivelege is None:
+                        try:
+                            yield self.authorize(request, (davxml.Unbind(),))
+                            checkedUnbindPrivelege = True
+                        except HTTPError, e:
+                            checkedUnbindPrivelege = e
+
+                    yield self.crudDelete(request, str(href), ifmatch, xmlresponses, checkedUnbindPrivelege);
+        
+        result = MultiStatusResponse(xmlresponses)
+        
+        newctag = (yield self.getSyncToken())
+        result.headers.setRawHeaders("CTag", (newctag,))
+
+        # Setup some useful logging
+        request.submethod = "CRUD batch"
+        if not hasattr(request, "extendedLogItems"):
+            request.extendedLogItems = {}
+        request.extendedLogItems["rcount"] = len(xmlresponses)
+
+        returnValue(result)
+
+    @inlineCallbacks
+    def crudCreate(self, request, component, xmlresponses, return_changed, hasPrivilege):
+        
+        code = None
+        error = None
+        try:
+            componentdata = str(component)
+            if isinstance(hasPrivilege, HTTPError):
+                raise hasPrivilege
+
+            # Create a new name if one was not provided
+            name =  md5(str(componentdata) + str(time.time()) + request.path).hexdigest() + self.resourceSuffix()
+        
+            # Get a resource for the new item
+            newchildURL = joinURL(request.path, name)
+            newchild = (yield request.locateResource(newchildURL))
+            yield self.storeResourceData(request, newchild, newchildURL, componentdata)
+
+            # FIXME: figure out return_changed behavior
+
+        except HTTPError, e:
+            # Extract the pre-condition
+            code = e.response.code
+            if isinstance(e.response, ErrorResponse):
+                error = e.response.error
+                error = (error.namespace, error.name,)
+
+        except Exception:
+            code = responsecode.BAD_REQUEST
+        
+        if code is None:
+            xmlresponses.append(
+                davxml.PropertyStatusResponse(
+                    davxml.HRef.fromString(newchildURL),
+                    davxml.PropertyStatus(
+                        davxml.PropertyContainer(
+                            davxml.GETETag.fromString(newchild.etag().generate()),
+                            customxml.UID.fromString(component.resourceUID()),
+                        ),
+                        davxml.Status.fromResponseCode(responsecode.OK),
+                    )
+                )
+            )
+        else:
+            xmlresponses.append(
+                davxml.StatusResponse(
+                    davxml.HRef.fromString(""),
+                    davxml.Status.fromResponseCode(code),
+                    davxml.Error(
+                        WebDAVUnknownElement.fromQname(*error),
+                        customxml.UID.fromString(component.resourceUID()),
+                    ) if error else None,
+                )
+            )
+
+    @inlineCallbacks
+    def crudUpdate(self, request, href, component, ifmatch, return_changed, xmlresponses):
+        code = None
+        error = None
+        try:
+            componentdata = str(component)
+
+            updateResource = (yield request.locateResource(href))
+            if not updateResource.exists():
+                raise HTTPError(responsecode.NOT_FOUND)
+
+            # Check privilege
+            yield updateResource.authorize(request, (davxml.Write(),))
+
+            # Check if match
+            if ifmatch and ifmatch != updateResource.etag().generate():
+                raise HTTPError(responsecode.PRECONDITION_FAILED)
+            
+            yield self.storeResourceData(request, updateResource, href, componentdata)
+
+            # FIXME: figure out return_changed behavior
+
+        except HTTPError, e:
+            # Extract the pre-condition
+            code = e.response.code
+            if isinstance(e.response, ErrorResponse):
+                error = e.response.error
+                error = (error.namespace, error.name,)
+
+        except Exception:
+            code = responsecode.BAD_REQUEST
+        
+        if code is None:
+            xmlresponses.append(
+                davxml.PropertyStatusResponse(
+                    davxml.HRef.fromString(href),
+                    davxml.PropertyStatus(
+                        davxml.PropertyContainer(
+                            davxml.GETETag.fromString(updateResource.etag().generate()),
+                        ),
+                        davxml.Status.fromResponseCode(responsecode.OK),
+                    )
+                )
+            )
+        else:
+            xmlresponses.append(
+                davxml.StatusResponse(
+                    davxml.HRef.fromString(href),
+                    davxml.Status.fromResponseCode(code),
+                    davxml.Error(
+                        WebDAVUnknownElement.fromQname(*error),
+                    ) if error else None,
+                )
+            )
+
+    @inlineCallbacks
+    def crudDelete(self, request, href, ifmatch, xmlresponses, hasPrivilege):
+        code = None
+        error = None
+        try:
+            if isinstance(hasPrivilege, HTTPError):
+                raise hasPrivilege
+
+            deleteResource = (yield request.locateResource(href))
+            if not deleteResource.exists():
+                raise HTTPError(responsecode.NOT_FOUND)
+
+            # Check if match
+            if ifmatch and ifmatch != deleteResource.etag().generate():
+                raise HTTPError(responsecode.PRECONDITION_FAILED)
+
+            yield deleteResource.storeRemove(
+                request, 
+                True,
+                href,
+            )
+
+        except HTTPError, e:
+            # Extract the pre-condition
+            code = e.response.code
+            if isinstance(e.response, ErrorResponse):
+                error = e.response.error
+                error = (error.namespace, error.name,)
+
+        except Exception:
+            code = responsecode.BAD_REQUEST
+        
+        if code is None:
+            xmlresponses.append(
+                davxml.StatusResponse(
+                    davxml.HRef.fromString(href),
+                    davxml.Status.fromResponseCode(responsecode.OK),
+                )
+            )
+        else:
+            xmlresponses.append(
+                davxml.StatusResponse(
+                    davxml.HRef.fromString(href),
+                    davxml.Status.fromResponseCode(code),
+                    davxml.Error(
+                        WebDAVUnknownElement.fromQname(*error),
+                    ) if error else None,
+                )
+            )
+
+
 class CalendarCollectionResource(_CommonHomeChildCollectionMixin, CalDAVResource):
     """
     Wrapper around a L{txdav.caldav.icalendar.ICalendar}.
@@ -482,6 +853,9 @@
         self._initializeWithHomeChild(calendar, home)
         self._name = calendar.name() if calendar else name
 
+        if config.EnableBatchUpload:
+            self._postHandlers[("text", "calendar")] = _CommonHomeChildCollectionMixin.simpleBatchPOST
+            self.xmlDocHanders[customxml.Multiput] = _CommonHomeChildCollectionMixin.crudBatchPOST
 
     def __repr__(self):
         return "<Calendar Collection Resource %r:%r %s>" % (
@@ -560,8 +934,31 @@
 
     createCalendarCollection = _CommonHomeChildCollectionMixin.createCollection
 
+    @classmethod
+    def componentsFromData(cls, data):
+        return VCalendar.allFromString(data)
 
+    @classmethod
+    def resourceSuffix(cls):
+        return ".ics"
+
+    @classmethod
+    def xmlDataElementType(cls):
+        return caldavxml.CalendarData
+
     @inlineCallbacks
+    def storeResourceData(self, request, newchild, newchildURL, data):
+        storer = StoreCalendarObjectResource(
+            request = request,
+            destination = newchild,
+            destination_uri = newchildURL,
+            destinationcal = True,
+            destinationparent = self,
+            calendar = data,
+        )
+        yield storer.run()
+
+    @inlineCallbacks
     def storeRemove(self, request, implicitly, where):
         """
         Delete this calendar collection resource, first deleting each contained
@@ -1358,6 +1755,9 @@
         self._initializeWithHomeChild(addressbook, home)
         self._name = addressbook.name() if addressbook else name
 
+        if config.EnableBatchUpload:
+            self._postHandlers[("text", "vcard")] = _CommonHomeChildCollectionMixin.simpleBatchPOST
+            self.xmlDocHanders[customxml.Multiput] = _CommonHomeChildCollectionMixin.crudBatchPOST
 
     def __repr__(self):
         return "<AddressBook Collection Resource %r:%r %s>" % (
@@ -1380,7 +1780,32 @@
 
     createAddressBookCollection = _CommonHomeChildCollectionMixin.createCollection
 
+    @classmethod
+    def componentsFromData(cls, data):
+        return VCard.allFromString(data)
 
+    @classmethod
+    def resourceSuffix(cls):
+        return ".vcf"
+
+    @classmethod
+    def xmlDataElementType(cls):
+        return carddavxml.AddressData
+
+    @inlineCallbacks
+    def storeResourceData(self, request, newchild, newchildURL, data):
+        storer = StoreAddressObjectResource(
+            request = request,
+            sourceadbk = False,
+            destination = newchild,
+            destination_uri = newchildURL,
+            destinationadbk = True,
+            destinationparent = self,
+            vcard = data,
+        )
+        yield storer.run()
+
+
 class GlobalAddressBookCollectionResource(GlobalAddressBookResource, AddressBookCollectionResource):
     """
     Wrapper around a L{txdav.carddav.iaddressbook.IAddressBook}.

Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/vcard.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/vcard.py	2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/vcard.py	2010-12-20 15:12:50 UTC (rev 6702)
@@ -142,6 +142,37 @@
             raise InvalidVCardDataError(e)
 
     @classmethod
+    def allFromString(clazz, string):
+        """
+        Construct a L{Component} from a string.
+        @param string: a string containing vCard data.
+        @return: a C{list} of L{Component}s representing the components described by
+            C{string}.
+        """
+        if type(string) is unicode:
+            string = string.encode("utf-8")
+        return clazz.allFromStream(StringIO.StringIO(string))
+
+    @classmethod
+    def allFromStream(clazz, stream):
+        """
+        Construct possibly multiple L{Component}s from a stream.
+        @param stream: a C{read()}able stream containing vCard data.
+        @return: a C{list} of L{Component}s representing the components described by
+            C{stream}.
+        """
+        
+        results = []
+        try:
+            for vobject in readComponents(stream):
+                results.append(clazz(None, vobject=vobject))
+            return results
+        except vParseError, e:
+            raise InvalidVCardDataError(e)
+        except StopIteration, e:
+            raise InvalidVCardDataError(e)
+
+    @classmethod
     def fromIStream(clazz, stream):
         """
         Construct a L{Component} from a stream.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20101220/1a504d5f/attachment-0001.html>


More information about the calendarserver-changes mailing list