conduit r1458 - in trunk: . conduit/datatypes conduit/modules/GoogleModule conduit/modules/GoogleModule/atom conduit/modules/GoogleModule/gdata conduit/modules/GoogleModule/gdata/apps conduit/modules/GoogleModule/gdata/base conduit/modules/GoogleModule/gdata/calendar conduit/modules/GoogleModule/gdata/codesearch conduit/modules/GoogleModule/gdata/contacts conduit/modules/GoogleModule/gdata/docs conduit/modules/GoogleModule/gdata/exif conduit/modules/GoogleModule/gdata/geo conduit/modules/GoogleModule/gdata/media conduit/modules/GoogleModule/gdata/photos conduit/modules/GoogleModule/gdata/spreadsheet test/python-tests test/python-tests/data



Author: jstowers
Date: Wed May  7 11:25:32 2008
New Revision: 1458
URL: http://svn.gnome.org/viewvc/conduit?rev=1458&view=rev

Log:
2008-05-07  John Stowers  <john stowers gmail com>

	* conduit/datatypes/Contact.py: Add methods for setting the vcard
	email and name attributes.

	* conduit/modules/GoogleModule/atom/__init__.py:
	* conduit/modules/GoogleModule/atom/service.py:
	* conduit/modules/GoogleModule/contacts_example.py:
	* conduit/modules/GoogleModule/gdata/__init__.py:
	* conduit/modules/GoogleModule/gdata/apps/Makefile.am:
	* conduit/modules/GoogleModule/gdata/apps/__init__.py:
	* conduit/modules/GoogleModule/gdata/apps/service.py:
	* conduit/modules/GoogleModule/gdata/base/Makefile.am:
	* conduit/modules/GoogleModule/gdata/base/__init__.py:
	* conduit/modules/GoogleModule/gdata/base/service.py:
	* conduit/modules/GoogleModule/gdata/calendar/__init__.py:
	* conduit/modules/GoogleModule/gdata/calendar/service.py:
	* conduit/modules/GoogleModule/gdata/codesearch/Makefile.am:
	* conduit/modules/GoogleModule/gdata/codesearch/__init__.py:
	* conduit/modules/GoogleModule/gdata/codesearch/service.py:
	* conduit/modules/GoogleModule/gdata/contacts/Makefile.am:
	* conduit/modules/GoogleModule/gdata/contacts/__init__.py:
	* conduit/modules/GoogleModule/gdata/contacts/service.py:
	* conduit/modules/GoogleModule/gdata/docs/Makefile.am:
	* conduit/modules/GoogleModule/gdata/docs/__init__.py:
	* conduit/modules/GoogleModule/gdata/docs/service.py:
	* conduit/modules/GoogleModule/gdata/exif/__init__.py:
	* conduit/modules/GoogleModule/gdata/geo/__init__.py:
	* conduit/modules/GoogleModule/gdata/media/__init__.py:
	* conduit/modules/GoogleModule/gdata/photos/__init__.py:
	* conduit/modules/GoogleModule/gdata/photos/service.py:
	* conduit/modules/GoogleModule/gdata/service.py:
	* conduit/modules/GoogleModule/gdata/spreadsheet/Makefile.am:
	* conduit/modules/GoogleModule/gdata/spreadsheet/__init__.py:
	* conduit/modules/GoogleModule/gdata/spreadsheet/service.py:
	* conduit/modules/GoogleModule/gdata/spreadsheet/text_db.py:
	* conduit/modules/GoogleModule/gdata/test_data.py:
	* conduit/modules/GoogleModule/gdata/urlfetch.py: Update to version 
	1.0.12.1 of python-gdata. This release adds native support for google 
	contacts
	
	* conduit/modules/GoogleModule/GoogleModule.py:
	* test/python-tests/common.py:
	* test/python-tests/data/1.vcard:	
	* test/python-tests/TestCoreContact.py:
	* test/python-tests/TestDataProviderGoogleContacts.py: Add two way sync
	support for google contacts. This still needs work in converting all
	the gdata attributes, detecting duplicate contacts with the same email
	address, and general testing, but it is a start

	* test/python-tests/TestDataProviderPicasa.py: Fix picasa get() tests




Added:
   trunk/conduit/modules/GoogleModule/contacts_example.py
   trunk/conduit/modules/GoogleModule/gdata/apps/
   trunk/conduit/modules/GoogleModule/gdata/apps/Makefile.am
   trunk/conduit/modules/GoogleModule/gdata/apps/__init__.py
   trunk/conduit/modules/GoogleModule/gdata/apps/service.py
   trunk/conduit/modules/GoogleModule/gdata/base/
   trunk/conduit/modules/GoogleModule/gdata/base/Makefile.am
   trunk/conduit/modules/GoogleModule/gdata/base/__init__.py   (contents, props changed)
   trunk/conduit/modules/GoogleModule/gdata/base/service.py   (contents, props changed)
   trunk/conduit/modules/GoogleModule/gdata/codesearch/
   trunk/conduit/modules/GoogleModule/gdata/codesearch/Makefile.am
   trunk/conduit/modules/GoogleModule/gdata/codesearch/__init__.py
   trunk/conduit/modules/GoogleModule/gdata/codesearch/service.py
   trunk/conduit/modules/GoogleModule/gdata/contacts/
   trunk/conduit/modules/GoogleModule/gdata/contacts/Makefile.am
   trunk/conduit/modules/GoogleModule/gdata/contacts/__init__.py
   trunk/conduit/modules/GoogleModule/gdata/contacts/service.py
   trunk/conduit/modules/GoogleModule/gdata/docs/
   trunk/conduit/modules/GoogleModule/gdata/docs/Makefile.am
   trunk/conduit/modules/GoogleModule/gdata/docs/__init__.py
   trunk/conduit/modules/GoogleModule/gdata/docs/service.py
   trunk/conduit/modules/GoogleModule/gdata/spreadsheet/
   trunk/conduit/modules/GoogleModule/gdata/spreadsheet/Makefile.am
   trunk/conduit/modules/GoogleModule/gdata/spreadsheet/__init__.py   (contents, props changed)
   trunk/conduit/modules/GoogleModule/gdata/spreadsheet/service.py   (contents, props changed)
   trunk/conduit/modules/GoogleModule/gdata/spreadsheet/text_db.py
   trunk/conduit/modules/GoogleModule/gdata/test_data.py   (contents, props changed)
   trunk/conduit/modules/GoogleModule/gdata/urlfetch.py
   trunk/test/python-tests/TestDataProviderGoogleContacts.py
      - copied, changed from r1455, /trunk/test/python-tests/TestDataProviderGoogle.py
Modified:
   trunk/ChangeLog
   trunk/conduit/datatypes/Contact.py
   trunk/conduit/modules/GoogleModule/GoogleModule.py
   trunk/conduit/modules/GoogleModule/atom/__init__.py
   trunk/conduit/modules/GoogleModule/atom/service.py
   trunk/conduit/modules/GoogleModule/gdata/__init__.py
   trunk/conduit/modules/GoogleModule/gdata/calendar/__init__.py
   trunk/conduit/modules/GoogleModule/gdata/calendar/service.py
   trunk/conduit/modules/GoogleModule/gdata/exif/__init__.py
   trunk/conduit/modules/GoogleModule/gdata/geo/__init__.py
   trunk/conduit/modules/GoogleModule/gdata/media/__init__.py
   trunk/conduit/modules/GoogleModule/gdata/photos/__init__.py
   trunk/conduit/modules/GoogleModule/gdata/photos/service.py
   trunk/conduit/modules/GoogleModule/gdata/service.py
   trunk/test/python-tests/TestCoreContact.py
   trunk/test/python-tests/TestDataProviderGoogle.py
   trunk/test/python-tests/TestDataProviderPicasa.py
   trunk/test/python-tests/common.py
   trunk/test/python-tests/data/1.vcard

Modified: trunk/conduit/datatypes/Contact.py
==============================================================================
--- trunk/conduit/datatypes/Contact.py	(original)
+++ trunk/conduit/datatypes/Contact.py	Wed May  7 11:25:32 2008
@@ -20,7 +20,7 @@
     def __init__(self, **kwargs):
         DataType.DataType.__init__(self)
         self.vcard = kwargs.get('vcard',vobject.vCard())
-      #  self.g_data = ''
+        self.set_name(**kwargs)
 
     def set_from_vcard_string(self, string):
         self.vcard = vobject.readOne(string)
@@ -36,23 +36,39 @@
         
     def get_name(self):
         #In order of preference, 1)formatted name, 2)name, 3)""
-        #FIXME:Think about this more
+        #FIXME: Return dict of formattedName, givenName, familyName, etc
         for attr in [self.vcard.fn, self.vcard.n]:
             #because str() on a vobject.vcard.Name pads with whitespace
             name = str(attr.value).strip()
             if len(name) > 0:
                 return name
-        return None
+        return ""
         
     def set_name(self, **kwargs):
-        raise NotImplementedError
+        #vcards must have one, and only one N and FN
+        fn = kwargs.get("formattedName","")
+        try:
+            self.vcard.fn
+        except AttributeError:
+            self.vcard.add('fn')
+        if fn:
+            self.vcard.fn.value = fn
+
+        g = kwargs.get("givenName","")
+        f = kwargs.get("familyName","")
+        try:
+            self.vcard.n
+        except AttributeError:
+            self.vcard.add('n')
+        if f or g:
+            self.vcard.n.value = vobject.vcard.Name(family=f,given=g)
+
+    def set_emails(self, *args):
+        for address in args:
+            email = self.vcard.add('email')
+            email.value = address
+            email.type_param = 'INTERNET'
         
-    def set_email(self, **kwargs):
-        raise NotImplementedError
-        
-    def set_address(self, **kwargs):
-        raise NotImplementedError
-
     def __getstate__(self):
         data = DataType.DataType.__getstate__(self)
         data['vcard'] = self.get_vcard_string()

Modified: trunk/conduit/modules/GoogleModule/GoogleModule.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/GoogleModule.py	(original)
+++ trunk/conduit/modules/GoogleModule/GoogleModule.py	Wed May  7 11:25:32 2008
@@ -29,16 +29,15 @@
     import atom
     import gdata
     import gdata.service
-    import gdata.photos
     import gdata.photos.service    
-    import gdata.calendar
     import gdata.calendar.service
+    import gdata.contacts.service
 
     MODULES = {
         "GoogleCalendarTwoWay" : { "type": "dataprovider" },
         "PicasaTwoWay" :         { "type": "dataprovider" },
         "YouTubeSource" :        { "type": "dataprovider" },    
-        "ContactsSource" :       { "type": "dataprovider" },    
+        "ContactsTwoWay" :       { "type": "dataprovider" },    
     }
     log.info("Module Information: %s" % Utils.get_module_information(gdata, None))
 except (ImportError, AttributeError):
@@ -62,8 +61,10 @@
             try:
                 self._do_login()
                 self.loggedIn = True
-            except:
-                self.loggedIn = False
+            except gdata.service.BadAuthentication:
+                log.info("Error logging in: Incorrect username or password")
+            except Exception, e:
+                log.info("Error logging in: %s" % e)
        
     def _set_username(self, username):
         if self.username != username:
@@ -367,9 +368,7 @@
         self.events = {}
 
     def _do_login(self):
-        self.calService.email = self.username
-        self.calService.password = self.password
-        self.calService.ProgrammaticLogin()
+        self.calService.ClientLogin(self.username, self.password)
 
     def _get_all_events(self):
         self._login()
@@ -622,9 +621,12 @@
     def refresh(self):
         Image.ImageTwoWay.refresh(self)
         self._login()
+        if not self.loggedIn:
+            raise Exceptions.RefreshError("Could not log in")
         self._get_album()
 
     def get_all (self):
+        Image.ImageTwoWay.get_all(self)
         self._get_photos()
         return self.gphoto_dict.keys()
         
@@ -758,92 +760,209 @@
         return True
 
 
-class ContactsSource(GoogleBase, DataProvider.DataSource):
+class ContactsTwoWay(GoogleBase,  DataProvider.TwoWay):
     """
     Contacts GData provider
     """
     _name_ = _("Google Contacts")
     _description_ = _("Sync contacts from Google")
     _category_ = conduit.dataproviders.CATEGORY_OFFICE
-    _module_type_ = "source"
+    _module_type_ = "twoway"
     _out_type_ = "contact"
     _icon_ = "contact-new"
 
-    FEED = "http://www.google.com/m8/feeds/contacts/%s/base?max-results=25000";
-
     def __init__(self, *args):
         GoogleBase.__init__(self)
-        DataProvider.DataSource.__init__(self)
-        self.entries = None
-        self.feed = ""
+        DataProvider.TwoWay.__init__(self)
+        self.service = gdata.contacts.service.ContactsService()
+        
+    def _google_contact_from_conduit_contact(self, contact, gc=None):
+        """
+        Fills the apropriate fields in the google gdata contact type based on
+        those in the conduit contact type
+        """
+        name = contact.get_name()
+        emails = contact.get_emails()
+        #Google contacts must feature at least a name and an email address
+        if not (name and emails):
+            return None
 
-    def _set_username(self, username):
-        GoogleBase._set_username(self, username)
-        self.feed = self.FEED % self.username
+        #can also edit existing contacts
+        if not gc:
+            gc = gdata.contacts.ContactEntry()        
+        gc.title = atom.Title(text=name)
+
+        #Create all emails, make first one primary, if the contact doesnt
+        #already have a primary email address
+        primary = 'false'
+        existing = []
+        for ex in gc.email:
+            if ex.primary and ex.primary == 'true':
+                primary = 'true'
+                existing.append(ex)      
+        
+        for email in emails:
+            if email not in existing:
+                log.debug("Adding new email address %s %s" % (email, existing))
+                gc.email.append(gdata.contacts.Email(
+                                            address=email, 
+                                            primary=primary))#,rel=gdata.contacts.REL_WORK))
+                primary = 'false'
+        #notes = contact.get_notes()
+        #if notes: gc.content = atom.Content(text=notes)
+        
+        return gc
+
+        
+    def _conduit_contact_from_google_contact(self, gc):
+        """
+        Extracts available and interesting fields from the google contact
+        and stored them in the conduit contact type
+        """
+        c = Contact.Contact(formattedName=str(gc.title.text))
+        
+        emails = [str(e.address) for e in gc.email]
+        c.set_emails(*emails)
+        
+        #ee_names = map(operator.attrgetter('tag'),gc.extension_elements)
+        #if len(gc.extension_elements) >0:
+        #    for e in [e for e in ee_names if e == 'phoneNumber']:
+        #        c.vcard.add('tel')
+        #        c.vcard.tel.value = gc.extension_elements[ee_names.index('phoneNumber')].text
+        #        c.vcard.tel.type_param = gc.extension_elements[ee_names.index('phoneNumber')].attributes['rel'].split('#')[1]
+        #    for e in [e for e in ee_names if e == 'postalAddress']:
+        #        c.vcard.add('adr')
+        #        c.vcard.adr.value = vobject.vcard.Address(gc.extension_elements[ee_names.index('postalAddress')].text)
+        #        c.vcard.adr.type_param = gc.extension_elements[ee_names.index('postalAddress')].attributes['rel'].split('#')[1]
+        
+        return c
 
     def _do_login(self):
-        self.service = gdata.service.GDataService(service="cp", server="www.google.com")
         self.service.ClientLogin(self.username, self.password)
         
+    def _create_contact(self, contact):
+        gc = self._google_contact_from_conduit_contact(contact)
+        if not gc:
+            log.info("Could not create google contact from conduit contact")
+            return None
+
+        try:            
+            entry = self.service.CreateContact(gc)
+        except gdata.service.RequestError, e:
+            #If the response dict reson is 'Conflict' then we are trying to
+            #store a contact with the same email as one which already exists
+            if e.message.get("reason","") == "Conflict":
+                log.warn("FIXME: FIND THE OLD CONTACT BY EMAIL, GET IT, AND RAISE A CONFLICT EXCEPTION")
+                raise Exceptions.SynchronizeConflictError("FIXME", "FIXME", "FIXME")
+        except Exception, e:
+            log.warn("Error creating contact: %s" % e)
+            return None
+
+        if entry:
+            log.debug("Created contact: %s" % entry.id.text)
+            return entry.id.text
+        else:
+            log.debug("Create contact error")
+            return None
+
+    def _update_contact(self, LUID, contact):
+        #get the gdata contact from google
+        try:
+            oldgc = self.service.Get(LUID, converter=gdata.contacts.ContactEntryFromString)
+        except gdata.service.RequestError:
+            return None
+            
+        #update the contact
+        gc = self._google_contact_from_conduit_contact(contact, oldgc)
+        self.service.UpdateContact(oldgc.GetEditLink().href, gc)
+        
+        #fixme, we should really just return the RID here, but its safer
+        #to use the same code path as get, because I am not sure if/how google
+        #changes the mtime
+        return LUID
+    
     def _get_contact(self, LUID):
+        if not LUID:
+            return None
+
         #get the gdata contact from google
-        gdc = self.service.Get(LUID)
-        if gdc is None or len(gdc.ToString()) < 1:
-            log.warn("Error getting/parsing gdata contact")
+        try:
+            gc = self.service.Get(LUID, converter=gdata.contacts.ContactEntryFromString)
+        except gdata.service.RequestError:
             return None
             
-        #FIXME: We should not be accessing the contact vcard directly.
-        #once we know what we can get from the gdata information, we should
-        #add the appropriate methods to the contact class to allow us to
-        #set these things
-        c = Contact.Contact()
-
-        c.vcard = vobject.vCard()
-        c.vcard.add('n')
-        c.vcard.n.value = vobject.vcard.Name(given="%s"%gdc.title.text)
-        
-        c.vcard.add('fn')
-        c.vcard.fn.value = "%s"%gdc.title.text
-        #isinstance(gdc, atom.Entry)
-        ee_names = map(operator.attrgetter('tag'),gdc.extension_elements)
-        if len(gdc.extension_elements) >0:
-            for e in [e for e in ee_names if e == 'email']:
-                c.vcard.add('email')
-                c.vcard.email.value = gdc.extension_elements[ee_names.index('email')].attributes['address']
-                c.vcard.email.type_param = 'INTERNET'
-            for e in [e for e in ee_names if e == 'phoneNumber']:
-                c.vcard.add('tel')
-                c.vcard.tel.value = gdc.extension_elements[ee_names.index('phoneNumber')].text
-                c.vcard.tel.type_param = gdc.extension_elements[ee_names.index('phoneNumber')].attributes['rel'].split('#')[1]
-            for e in [e for e in ee_names if e == 'postalAddress']:
-                c.vcard.add('adr')
-                c.vcard.adr.value = vobject.vcard.Address(gdc.extension_elements[ee_names.index('postalAddress')].text)
-               # c.vcard.adr.value =
-                c.vcard.adr.type_param = gdc.extension_elements[ee_names.index('postalAddress')].attributes['rel'].split('#')[1]
-        
-        #c.vcard.add('uid').value = gdc.id.text
+        c = self._conduit_contact_from_google_contact(gc)
         c.set_UID(LUID)
-        c.set_mtime(convert_madness_to_datetime(gdc.updated.text))
-        c.set_open_URI(gdc.link[1].href)
-        return c    
+        c.set_mtime(convert_madness_to_datetime(gc.updated.text))
+        return c
+
+    def _get_all_contacts(self):
+        feed = self.service.GetContactsFeed()
+        if not feed.entry:
+            return []
+        return [str(contact.id.text) for contact in feed.entry]
+        
+    def refresh(self):
+        DataProvider.TwoWay.refresh(self)
+        self._login()
+        if not self.loggedIn:
+            raise Exceptions.RefreshError("Could not log in")
 
     def get_all(self):
-        DataProvider.DataSource.get_all(self)
-        # we can ask the server for everything thats changed after a certain date, including deletions
+        DataProvider.TwoWay.get_all(self)
         self._login()
-        contacts = self.service.GetFeed(self.feed).entry
-        return [str(contact.id.text) for contact in contacts]
+        return self._get_all_contacts()
 
     def get(self, LUID):
-        DataProvider.DataSource.get(self, LUID)
-        return self._get_contact(LUID)
+        DataProvider.TwoWay.get(self, LUID)
+        self._login()
+        c = self._get_contact(LUID)
+        if c == None:
+            log.warn("Error getting/parsing gdata contact")
+        return c
+        
+    def put(self, data, overwrite, LUID=None):
+        #http://www.conduit-project.org/wiki/WritingADataProvider/GeneralPutInstructions
+        DataProvider.TwoWay.put(self, data, overwrite, LUID)
+        if overwrite and LUID:
+            LUID = self._update_contact(LUID, data)
+        else:
+            oldData = self._get_contact(LUID)
+            if LUID and oldData:
+                comp = data.compare(oldData)
+                #Possibility 1: If LUID != None (i.e this is a modification/update of a 
+                #previous sync, and we are newer, the go ahead an put the data
+                if LUID != None and comp == conduit.datatypes.COMPARISON_NEWER:
+                    LUID = self._update_contact(LUID, data)
+                #Possibility 3: We are the same, so return either rid
+                elif comp == conduit.datatypes.COMPARISON_EQUAL:
+                    return oldData.get_rid()
+                #Possibility 2, 4: All that remains are conflicts
+                else:
+                    raise Exceptions.SynchronizeConflictError(comp, data, oldData)
+            else:
+                #Possibility 5:
+                LUID = self._create_contact(data)
+                
+        #now return the rid
+        if not LUID:
+            raise Exceptions.SyncronizeError("Google contacts upload error.")
+        else:
+            return self._get_contact(LUID).get_rid()
+
 
     def delete(self, LUID):
+        DataProvider.TwoWay.delete(self, LUID)
         self._login()
-        self.service.Delete(LUID)
+        #get the gdata contact from google
+        try:
+            gc = self.service.Get(LUID, converter=gdata.contacts.ContactEntryFromString)
+            self.service.DeleteContact(gc.GetEditLink().href)
+        except gdata.service.RequestError, e:
+            log.warn("Error deleting: %s" % e)        
 
     def finish(self, aborted, error, conflict):
-        DataProvider.DataSource.finish(self)
+        DataProvider.TwoWay.finish(self)
 
     def configure(self, window):
         """

Modified: trunk/conduit/modules/GoogleModule/atom/__init__.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/atom/__init__.py	(original)
+++ trunk/conduit/modules/GoogleModule/atom/__init__.py	Wed May  7 11:25:32 2008
@@ -44,14 +44,16 @@
 
 __author__ = 'api.jscudder (Jeffrey Scudder)'
 
-
 try:
   from xml.etree import cElementTree as ElementTree
 except ImportError:
   try:
     import cElementTree as ElementTree
   except ImportError:
-    from elementtree import ElementTree
+    try:
+      from xml.etree import ElementTree
+    except ImportError:
+      from elementtree import ElementTree
 
 
 # XML namespaces which are often used in Atom entities.
@@ -87,13 +89,10 @@
     contents of the XML - or None if the root XML tag and namespace did not
     match those of the target class.
   """
-  if string_encoding:
-    tree = ElementTree.fromstring(xml_string.encode(string_encoding))
-  else:
-    if XML_STRING_ENCODING:
-      tree = ElementTree.fromstring(xml_string.encode(XML_STRING_ENCODING))
-    else:
-      tree = ElementTree.fromstring(xml_string)
+  encoding = string_encoding or XML_STRING_ENCODING
+  if encoding and isinstance(xml_string, unicode):
+    xml_string = xml_string.encode(encoding)
+  tree = ElementTree.fromstring(xml_string)
   return _CreateClassFromElementTree(target_class, tree)
 
 
@@ -168,7 +167,10 @@
       if value:
         # Encode the value in the desired type (default UTF-8).
         tree.attrib[attribute] = value.encode(MEMBER_STRING_ENCODING)
-    tree.text = self.text
+    if self.text and not isinstance(self.text, unicode):
+      tree.text = self.text.decode(MEMBER_STRING_ENCODING)
+    else:
+      tree.text = self.text 
 
   def FindExtensions(self, tag=None, namespace=None):
     """Searches extension elements for child nodes with the desired name.
@@ -250,7 +252,8 @@
         setattr(self, self.__class__._attributes[attribute], 
                 value.encode(MEMBER_STRING_ENCODING))
     else:
-      ExtensionContainer._ConvertElementAttributeToMember(self, attribute, value)
+      ExtensionContainer._ConvertElementAttributeToMember(self, attribute, 
+          value)
 
   # Three methods to create an ElementTree from an object
   def _AddMembersToElementTree(self, tree):
@@ -735,8 +738,8 @@
   _attributes = Text._attributes.copy()
   _attributes['src'] = 'src'
 
-  def __init__(self, content_type=None, src=None, text=None, extension_elements=None,
-      extension_attributes=None):
+  def __init__(self, content_type=None, src=None, text=None, 
+      extension_elements=None, extension_attributes=None):
     """Constructor for Content
     
     Args:
@@ -1061,8 +1064,8 @@
   _children['{%s}updated' % ATOM_NAMESPACE] = ('updated', Updated)
 
   def __init__(self, author=None, category=None, contributor=None, 
-      atom_id=None, link=None, rights=None, title=None, updated=None, text=None, 
-      extension_elements=None, extension_attributes=None):
+      atom_id=None, link=None, rights=None, title=None, updated=None, 
+      text=None, extension_elements=None, extension_attributes=None):
     self.author = author or []
     self.category = category or []
     self.contributor = contributor or []
@@ -1089,7 +1092,8 @@
   _children['{%s}subtitle' % ATOM_NAMESPACE] = ('subtitle', Subtitle)
 
   def __init__(self, author=None, category=None, contributor=None,
-      generator=None, icon=None, atom_id=None, link=None, logo=None, rights=None, subtitle=None, title=None, updated=None, text=None,
+      generator=None, icon=None, atom_id=None, link=None, logo=None, 
+      rights=None, subtitle=None, title=None, updated=None, text=None,
       extension_elements=None, extension_attributes=None):
     """Constructor for Source
 

Modified: trunk/conduit/modules/GoogleModule/atom/service.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/atom/service.py	(original)
+++ trunk/conduit/modules/GoogleModule/atom/service.py	Wed May  7 11:25:32 2008
@@ -1,6 +1,6 @@
 #!/usr/bin/python
 #
-# Copyright (C) 2006 Google Inc.
+# Copyright (C) 2006, 2007, 2008 Google Inc.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -21,6 +21,10 @@
                operations with the Atom Publishing Protocol on which GData is
                based. An instance can perform query, insertion, deletion, and
                update.
+
+  HttpRequest: Function that performs a GET, POST, PUT, or DELETE HTTP request
+       to the specified end point. An AtomService object or a subclass can be
+       used to specify information about the request.
 """
 
 __author__ = 'api.jscudder (Jeffrey Scudder)'
@@ -37,10 +41,15 @@
   try:
     import cElementTree as ElementTree
   except ImportError:
-    from elementtree import ElementTree
+    try:
+      from xml.etree import ElementTree
+    except ImportError:
+      from elementtree import ElementTree
+
 
 URL_REGEX = re.compile('http(s)?\://([\w\.-]*)(\:(\d+))?(/.*)?')
 
+
 class AtomService(object):
   """Performs Atom Publishing Protocol CRUD operations.
   
@@ -72,34 +81,7 @@
   def _ProcessUrl(self, url, for_proxy=False):
     """Processes a passed URL.  If the URL does not begin with https?, then
     the default value for self.server is used"""
-
-    server = self.server
-    if for_proxy:
-      port = 80
-      ssl = False
-    else:
-      port = self.port
-      ssl = self.ssl
-    uri = url
-
-    m = URL_REGEX.match(url)
-
-    if m is None:
-      return (server, port, ssl, uri)
-    else:
-      if m.group(1) is not None:
-        port = 443
-        ssl = True
-      if m.group(3) is None:
-        server = m.group(2)
-      else:
-        server = m.group(2)
-        port = int(m.group(4))
-      if m.group(5) is not None:
-        uri = m.group(5)
-      else:
-        uri = '/'
-      return (server, port, ssl, uri)
+    return ProcessUrl(self, url, for_proxy=for_proxy)
 
   def UseBasicAuth(self, username, password, for_proxy=False):
     """Sets an Authenticaiton: Basic HTTP header containing plaintext.
@@ -112,107 +94,31 @@
       username: str
       password: str
     """
+    UseBasicAuth(self, username, password, for_proxy=for_proxy)
 
-    base_64_string = base64.encodestring('%s:%s' % (username, password))
-    base_64_string = base_64_string.strip()
-    if for_proxy:
-      header_name = 'Proxy-Authorization'
-    else:
-      header_name = 'Authorization'
-    self.additional_headers[header_name] = 'Basic %s' % (base_64_string,)
+  def PrepareConnection(self, full_uri):
+    """Opens a connection to the server based on the full URI.
 
-  def _PrepareConnection(self, full_uri):
-    
-    (server, port, ssl, partial_uri) = self._ProcessUrl(full_uri)
-    if ssl:
-      # destination is https
-      proxy = os.environ.get('https_proxy')
-      if proxy:
-        (p_server, p_port, p_ssl, p_uri) = self._ProcessUrl(proxy, True)
-        proxy_username = os.environ.get('proxy-username')
-        proxy_password = os.environ.get('proxy-password')
-        if proxy_username:
-          user_auth = base64.encodestring('%s:%s' % (proxy_username, 
-                                                     proxy_password))
-          proxy_authorization = ('Proxy-authorization: Basic %s\r\n' % (
-              user_auth.strip()))
-        else:
-          proxy_authorization = ''
-        proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % (server,port)
-        user_agent = 'User-Agent: %s\r\n' % (
-            self.additional_headers['User-Agent'])
-        proxy_pieces = (proxy_connect + proxy_authorization + user_agent 
-                        + '\r\n')
-
-        #now connect, very simple recv and error checking
-        p_sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
-        p_sock.connect((p_server,p_port))
-        p_sock.sendall(proxy_pieces)
-        response = ''
-
-        # Wait for the full response.
-        while response.find("\r\n\r\n") == -1:
-          response += p_sock.recv(8192)
-        
-        p_status=response.split()[1]
-        if p_status!=str(200):
-          raise 'Error status=',str(p_status)
-
-        # Trivial setup for ssl socket.
-        ssl = socket.ssl(p_sock, None, None)
-        fake_sock = httplib.FakeSocket(p_sock, ssl)
-
-        # Initalize httplib and replace with the proxy socket.
-        connection = httplib.HTTPConnection(server)
-        connection.sock=fake_sock
-        full_uri = partial_uri
+    Examines the target URI and the proxy settings, which are set as 
+    environment variables, to open a connection with the server. This 
+    connection is used to make an HTTP request.
 
-      else:
-        connection = httplib.HTTPSConnection(server, port)
-        full_uri = partial_uri
+    Args:
+      full_uri: str Which is the target relative (lacks protocol and host) or
+      absolute URL to be opened. Example:
+      'https://www.google.com/accounts/ClientLogin' or
+      'base/feeds/snippets' where the server is set to www.google.com.
 
-    else:
-      # destination is http
-      proxy = os.environ.get('http_proxy')
-      if proxy:
-        (p_server, p_port, p_ssl, p_uri) = self._ProcessUrl(proxy, True)
-        proxy_username = os.environ.get('proxy-username')
-        proxy_password = os.environ.get('proxy-password')
-        if proxy_username:
-          self.UseBasicAuth(proxy_username, proxy_password, True)
-        connection = httplib.HTTPConnection(p_server, p_port)
-        if not full_uri.startswith("http://";):
-          if full_uri.startswith("/"):
-            full_uri = "http://%s%s"; % (self.server, full_uri)
-          else:
-            full_uri = "http://%s/%s"; % (self.server, full_uri)
-      else:
-        connection = httplib.HTTPConnection(server, port)
-        full_uri = partial_uri
+    Returns:
+      A tuple containing the httplib.HTTPConnection and the full_uri for the
+      request.
+    """
+    return PrepareConnection(self, full_uri)   
 
-    return (connection, full_uri)
+  # Alias the old name for the above method to preserve backwards 
+  # compatibility.
+  _PrepareConnection = PrepareConnection
  
-  def _CreateConnection(self, uri, http_operation, extra_headers=None,
-      url_params=None, escape_params=True):
-      
-    full_uri = BuildUri(uri, url_params, escape_params)
-    (connection, full_uri) = self._PrepareConnection(full_uri)
-    connection.putrequest(http_operation, full_uri)
-
-    if isinstance(self.additional_headers, dict):
-      for header in self.additional_headers:
-        connection.putheader(header, self.additional_headers[header])
-    if isinstance(extra_headers, dict):
-      for header in extra_headers:
-        connection.putheader(header, extra_headers[header])
-    connection.endheaders()
-
-    # Turn on debug mode if the debug member is set
-    if self.debug:
-      connection.debuglevel = 1
-
-    return connection
-
   # CRUD operations
   def Get(self, uri, extra_headers=None, url_params=None, escape_params=True):
     """Query the APP server with the given URI
@@ -247,11 +153,8 @@
     Returns:
       httplib.HTTPResponse The server's response to the GET request.
     """
-
-    query_connection = self._CreateConnection(uri, 'GET', extra_headers,
-        url_params, escape_params)
-
-    return query_connection.getresponse()
+    return HttpRequest(self, 'GET', None, uri, extra_headers=extra_headers, 
+        url_params=url_params, escape_params=escape_params)
 
   def Post(self, data, uri, extra_headers=None, url_params=None, 
            escape_params=True, content_type='application/atom+xml'):
@@ -278,19 +181,9 @@
     Returns:
       httplib.HTTPResponse Server's response to the POST request.
     """
-    if ElementTree.iselement(data):
-      data_str = ElementTree.tostring(data)
-    else:
-      data_str = str(data)
-    
-    extra_headers['Content-Length'] = len(data_str)
-    extra_headers['Content-Type'] = content_type
-    insert_connection = self._CreateConnection(uri, 'POST', extra_headers,
-        url_params, escape_params)
-
-    insert_connection.send(data_str)
-
-    return insert_connection.getresponse()
+    return HttpRequest(self, 'POST', data, uri, extra_headers=extra_headers, 
+        url_params=url_params, escape_params=escape_params, 
+        content_type=content_type)
 
   def Put(self, data, uri, extra_headers=None, url_params=None, 
            escape_params=True, content_type='application/atom+xml'):
@@ -317,19 +210,9 @@
     Returns:
       httplib.HTTPResponse Server's response to the PUT request.
     """
-    if ElementTree.iselement(data):
-      data_str = ElementTree.tostring(data)
-    else:
-      data_str = str(data)
-      
-    extra_headers['Content-Length'] = len(data_str)
-    extra_headers['Content-Type'] = content_type
-    update_connection = self._CreateConnection(uri, 'PUT', extra_headers,
-        url_params, escape_params)
-
-    update_connection.send(data_str)
-
-    return update_connection.getresponse()
+    return HttpRequest(self, 'PUT', data, uri, extra_headers=extra_headers, 
+        url_params=url_params, escape_params=escape_params, 
+        content_type=content_type)
 
   def Delete(self, uri, extra_headers=None, url_params=None, 
              escape_params=True):
@@ -354,10 +237,296 @@
     Returns:
       httplib.HTTPResponse Server's response to the DELETE request.
     """
-    delete_connection = self._CreateConnection(uri, 'DELETE', extra_headers,
-        url_params, escape_params)
+    return HttpRequest(self, 'DELETE', None, uri, extra_headers=extra_headers, 
+        url_params=url_params, escape_params=escape_params)
+
+
+def HttpRequest(service, operation, data, uri, extra_headers=None, 
+    url_params=None, escape_params=True, content_type='application/atom+xml'):
+  """Performs an HTTP call to the server, supports GET, POST, PUT, and DELETE.
+
+  Usage example, perform and HTTP GET on http://www.google.com/:
+    import atom.service
+    client = atom.service.AtomService()
+    http_response = client.Get('http://www.google.com/')
+  or you could set the client.server to 'www.google.com' and use the 
+  following:
+    client.server = 'www.google.com'
+    http_response = client.Get('/')
+
+  Args:
+    service: atom.AtomService object which contains some of the parameters 
+        needed to make the request. The following members are used to 
+        construct the HTTP call: server (str), additional_headers (dict), 
+        port (int), and ssl (bool).
+    operation: str The HTTP operation to be performed. This is usually one of
+        'GET', 'POST', 'PUT', or 'DELETE'
+    data: ElementTree, filestream, list of parts, or other object which can be 
+        converted to a string. 
+        Should be set to None when performing a GET or PUT.
+        If data is a file-like object which can be read, this method will read
+        a chunk of 100K bytes at a time and send them. 
+        If the data is a list of parts to be sent, each part will be evaluated
+        and sent.
+    uri: The beginning of the URL to which the request should be sent. 
+        Examples: '/', '/base/feeds/snippets', 
+        '/m8/feeds/contacts/default/base'
+    extra_headers: dict of strings. HTTP headers which should be sent
+        in the request. These headers are in addition to those stored in 
+        service.additional_headers.
+    url_params: dict of strings. Key value pairs to be added to the URL as
+        URL parameters. For example {'foo':'bar', 'test':'param'} will 
+        become ?foo=bar&test=param.
+    escape_params: bool default True. If true, the keys and values in 
+        url_params will be URL escaped when the form is constructed 
+        (Special characters converted to %XX form.)
+    content_type: str The MIME type for the data being sent. Defaults to
+        'application/atom+xml', this is only used if data is set.
+  """
+  full_uri = BuildUri(uri, url_params, escape_params)
+  (connection, full_uri) = PrepareConnection(service, full_uri)
+
+  if extra_headers is None:
+    extra_headers = {}
+
+  # Turn on debug mode if the debug member is set.
+  if service.debug:
+    connection.debuglevel = 1
+
+  connection.putrequest(operation, full_uri)
+
+  # If the list of headers does not include a Content-Length, attempt to 
+  # calculate it based on the data object.
+  if (data and not service.additional_headers.has_key('Content-Length') and 
+      not extra_headers.has_key('Content-Length')):
+    content_length = __CalculateDataLength(data)
+    if content_length:
+      extra_headers['Content-Length'] = content_length
+
+  if content_type:
+    extra_headers['Content-Type'] = content_type 
+
+  # Send the HTTP headers.
+  if isinstance(service.additional_headers, dict):
+    for header in service.additional_headers:
+      connection.putheader(header, service.additional_headers[header])
+  if isinstance(extra_headers, dict):
+    for header in extra_headers:
+      connection.putheader(header, extra_headers[header])
+  connection.endheaders()
+
+  # If there is data, send it in the request.
+  if data:
+    if isinstance(data, list):
+      for data_part in data:
+        __SendDataPart(data_part, connection)
+    else:
+      __SendDataPart(data, connection)
+
+  # Return the HTTP Response from the server.
+  return connection.getresponse()
+
+
+def __SendDataPart(data, connection):
+  if isinstance(data, str):
+    #TODO add handling for unicode.
+    connection.send(data)
+    return
+  elif ElementTree.iselement(data):
+    connection.send(ElementTree.tostring(data))
+    return
+  # Check to see if data is a file-like object that has a read method.
+  elif hasattr(data, 'read'):
+    # Read the file and send it a chunk at a time.
+    while 1:
+      binarydata = data.read(100000)
+      if binarydata == '': break
+      connection.send(binarydata)
+    return
+  else:
+    # The data object was not a file.
+    # Try to convert to a string and send the data.
+    connection.send(str(data))
+    return
+
+
+def __CalculateDataLength(data):
+  """Attempts to determine the length of the data to send. 
+  
+  This method will respond with a length only if the data is a string or
+  and ElementTree element.
+
+  Args:
+    data: object If this is not a string or ElementTree element this funtion
+        will return None.
+  """
+  if isinstance(data, str):
+    return len(data)
+  elif isinstance(data, list):
+    return None
+  elif ElementTree.iselement(data):
+    return len(ElementTree.tostring(data))
+  elif hasattr(data, 'read'):
+    # If this is a file-like object, don't try to guess the length.
+    return None
+  else:
+    return len(str(data))
+
+
+def PrepareConnection(service, full_uri):
+  """Opens a connection to the server based on the full URI.
+
+  Examines the target URI and the proxy settings, which are set as
+  environment variables, to open a connection with the server. This
+  connection is used to make an HTTP request.
+
+  Args:
+    service: atom.AtomService or a subclass. It must have a server string which
+      represents the server host to which the request should be made. It may also
+      have a dictionary of additional_headers to send in the HTTP request.
+    full_uri: str Which is the target relative (lacks protocol and host) or
+    absolute URL to be opened. Example:
+    'https://www.google.com/accounts/ClientLogin' or
+    'base/feeds/snippets' where the server is set to www.google.com.
+
+  Returns:
+    A tuple containing the httplib.HTTPConnection and the full_uri for the
+    request.
+  """
+   
+  (server, port, ssl, partial_uri) = ProcessUrl(service, full_uri)
+  if ssl:
+    # destination is https
+    proxy = os.environ.get('https_proxy')
+    if proxy:
+      (p_server, p_port, p_ssl, p_uri) = ProcessUrl(service, proxy, True)
+      proxy_username = os.environ.get('proxy-username')
+      if not proxy_username:
+        proxy_username = os.environ.get('proxy_username')
+      proxy_password = os.environ.get('proxy-password')
+      if not proxy_password:
+        proxy_password = os.environ.get('proxy_password')
+      if proxy_username:
+        user_auth = base64.encodestring('%s:%s' % (proxy_username,
+                                                   proxy_password))
+        proxy_authorization = ('Proxy-authorization: Basic %s\r\n' % (
+            user_auth.strip()))
+      else:
+        proxy_authorization = ''
+      proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % (server, port)
+      user_agent = 'User-Agent: %s\r\n' % (
+          service.additional_headers['User-Agent'])
+      proxy_pieces = (proxy_connect + proxy_authorization + user_agent
+                       + '\r\n')
+
+      #now connect, very simple recv and error checking
+      p_sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
+      p_sock.connect((p_server,p_port))
+      p_sock.sendall(proxy_pieces)
+      response = ''
+
+      # Wait for the full response.
+      while response.find("\r\n\r\n") == -1:
+        response += p_sock.recv(8192)
+       
+      p_status=response.split()[1]
+      if p_status!=str(200):
+        raise 'Error status=',str(p_status)
+
+      # Trivial setup for ssl socket.
+      ssl = socket.ssl(p_sock, None, None)
+      fake_sock = httplib.FakeSocket(p_sock, ssl)
+
+      # Initalize httplib and replace with the proxy socket.
+      connection = httplib.HTTPConnection(server)
+      connection.sock=fake_sock
+      full_uri = partial_uri
+
+    else:
+      connection = httplib.HTTPSConnection(server, port)
+      full_uri = partial_uri
+
+  else:
+    # destination is http
+    proxy = os.environ.get('http_proxy')
+    if proxy:
+      (p_server, p_port, p_ssl, p_uri) = ProcessUrl(service.server, proxy, True)
+      proxy_username = os.environ.get('proxy-username')
+      if not proxy_username:
+        proxy_username = os.environ.get('proxy_username')
+      proxy_password = os.environ.get('proxy-password')
+      if not proxy_password:
+        proxy_password = os.environ.get('proxy_password')
+      if proxy_username:
+        UseBasicAuth(service, proxy_username, proxy_password, True)
+      connection = httplib.HTTPConnection(p_server, p_port)
+      if not full_uri.startswith("http://";):
+        if full_uri.startswith("/"):
+          full_uri = "http://%s%s"; % (service.server, full_uri)
+        else:
+          full_uri = "http://%s/%s"; % (service.server, full_uri)
+    else:
+      connection = httplib.HTTPConnection(server, port)
+      full_uri = partial_uri
+
+  return (connection, full_uri)
+
+
+def UseBasicAuth(service, username, password, for_proxy=False):
+  """Sets an Authenticaiton: Basic HTTP header containing plaintext.
+  
+  The username and password are base64 encoded and added to an HTTP header
+  which will be included in each request. Note that your username and 
+  password are sent in plaintext. The auth header is added to the 
+  additional_headers dictionary in the service object.
+
+  Args:
+    service: atom.AtomService or a subclass which has an 
+        additional_headers dict as a member.
+    username: str
+    password: str
+  """
+  base_64_string = base64.encodestring('%s:%s' % (username, password))
+  base_64_string = base_64_string.strip()
+  if for_proxy:
+    header_name = 'Proxy-Authorization'
+  else:
+    header_name = 'Authorization'
+  service.additional_headers[header_name] = 'Basic %s' % (base_64_string,)
+
+
+def ProcessUrl(service, url, for_proxy=False):
+  """Processes a passed URL.  If the URL does not begin with https?, then
+  the default value for server is used"""
+
+  server = service.server
+  if for_proxy:
+    port = 80
+    ssl = False
+  else:
+    port = service.port
+    ssl = service.ssl
+  uri = url
+
+  m = URL_REGEX.match(url)
+
+  if m is None:
+    return (server, port, ssl, uri)
+  else:
+    if m.group(1) is not None:
+      port = 443
+      ssl = True
+    if m.group(3) is None:
+      server = m.group(2)
+    else:
+      server = m.group(2)
+      port = int(m.group(4))
+    if m.group(5) is not None:
+      uri = m.group(5)
+    else:
+      uri = '/'
+    return (server, port, ssl, uri)
 
-    return delete_connection.getresponse()
 
 def DictionaryToParamList(url_parameters, escape_params=True):
   """Convert a dictionary of URL arguments into a URL parameter string.

Added: trunk/conduit/modules/GoogleModule/contacts_example.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/contacts_example.py	Wed May  7 11:25:32 2008
@@ -0,0 +1,222 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2008 Google Inc.
+#
+# 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.
+
+
+__author__ = 'api.jscudder (Jeffrey Scudder)'
+
+
+import sys
+import getopt
+import getpass
+import atom
+import gdata.contacts
+import gdata.contacts.service
+
+
+class ContactsSample(object):
+  """ContactsSample object demonstrates operations with the Contacts feed."""
+
+  def __init__(self, email, password):
+    """Constructor for the ContactsSample object.
+    
+    Takes an email and password corresponding to a gmail account to
+    demonstrate the functionality of the Contacts feed.
+    
+    Args:
+      email: [string] The e-mail address of the account to use for the sample.
+      password: [string] The password corresponding to the account specified by
+          the email parameter.
+    
+    Yields:
+      A ContactsSample object used to run the sample demonstrating the
+      functionality of the Contacts feed.
+    """
+    self.gd_client = gdata.contacts.service.ContactsService()
+    self.gd_client.email = email
+    self.gd_client.password = password
+    self.gd_client.source = 'GoogleInc-ContactsPythonSample-1'
+    self.gd_client.ProgrammaticLogin()
+
+  def PrintFeed(self, feed):
+    """Prints out the contents of a feed to the console.
+   
+    Args:
+      feed: A gdata.contacts.ContactsFeed instance.
+    """
+    print '\n'
+    if not feed.entry:
+      print 'No entries in feed.\n'
+    for i, entry in enumerate(feed.entry):
+      print '\n%s %s (%s)' % (i+1, entry.title.text,type(entry))
+      if entry.content:
+        print '    %s' % (entry.content.text)
+      for email in entry.email:
+        if email.primary and email.primary == 'true':
+          print '    %s' % (email.address)
+
+  def ListAllContacts(self):
+    """Retrieves a list of contacts and displays name and primary email."""
+    feed = self.gd_client.GetContactsFeed()
+    self.PrintFeed(feed)
+
+  def CreateMenu(self):
+    """Prompts that enable a user to create a contact."""
+    name = raw_input('Enter contact\'s name: ')
+    notes = raw_input('Enter notes for contact: ')
+    primary_email = raw_input('Enter primary email address: ')
+
+    new_contact = gdata.contacts.ContactEntry()
+    new_contact.title = atom.Title(text=name)
+    #new_contact.content = atom.Content(text=notes)
+    # Create a work email address for the contact and use as primary. 
+    new_contact.email.append(gdata.contacts.Email(address=primary_email, 
+        primary='false', rel=gdata.contacts.REL_WORK))
+    entry = self.gd_client.CreateContact(new_contact)
+
+    if entry:
+      print 'Creation successful!'
+      print 'ID for the new contact:', entry.id.text
+    else:
+      print 'Upload error.'
+
+  def QueryMenu(self):
+    """Prompts for updated-min query parameters and displays results."""
+    updated_min = raw_input(
+        'Enter updated min (example: 2007-03-16T00:00:00): ')
+    query = gdata.contacts.service.ContactsQuery()
+    query.updated_min = updated_min
+    feed = self.gd_client.GetContactsFeed(query.ToUri())
+    self.PrintFeed(feed)
+   
+  def _SelectContact(self):
+    feed = self.gd_client.GetContactsFeed()
+    self.PrintFeed(feed)
+    selection = 5000
+    while selection > len(feed.entry)+1 or selection < 1:
+      selection = int(raw_input(
+          'Enter the number for the contact you would like to modify: '))
+    return feed.entry[selection-1]
+
+  def UpdateContactMenu(self):
+    selected_entry = self._SelectContact()
+    new_name = raw_input('Enter a new name for the contact: ')
+    #if not selected_entry.title:
+    #  selected_entry.title = atom.Title()
+    selected_entry.title.text = new_name
+    self.gd_client.UpdateContact(selected_entry.GetEditLink().href, selected_entry)
+
+  def DeleteContactMenu(self):
+    selected_entry = self._SelectContact()
+    self.gd_client.DeleteContact(selected_entry.GetEditLink().href)
+
+  def PrintMenu(self):
+    """Displays a menu of options for the user to choose from."""
+    print ('\nDocument List Sample\n'
+           '1) List all of your contacts.\n'
+           '2) Create a contact.\n'
+           '3) Query on updated time.\n'
+           '4) Modify a contact.\n'
+           '5) Delete a contact.\n'
+           '6) Exit.\n')
+
+  def GetMenuChoice(self, max):
+    """Retrieves the menu selection from the user.
+    
+    Args:
+      max: [int] The maximum number of allowed choices (inclusive)
+      
+    Returns:
+      The integer of the menu item chosen by the user.
+    """
+    while True:
+      input = raw_input('> ')
+
+      try:
+        num = int(input)
+      except ValueError:
+        print 'Invalid choice. Please choose a value between 1 and', max
+        continue
+      
+      if num > max or num < 1:
+        print 'Invalid choice. Please choose a value between 1 and', max
+      else:
+        return num
+
+  def Run(self):
+    """Prompts the user to choose funtionality to be demonstrated."""
+    try:
+      while True:
+
+        self.PrintMenu()
+
+        choice = self.GetMenuChoice(6)
+
+        if choice == 1:
+          self.ListAllContacts()
+        elif choice == 2:
+          self.CreateMenu()
+        elif choice == 3:
+          self.QueryMenu()
+        elif choice == 4:
+          self.UpdateContactMenu()
+        elif choice == 5:
+          self.DeleteContactMenu()
+        elif choice == 6:
+          return
+
+    except KeyboardInterrupt:
+      print '\nGoodbye.'
+      return
+
+
+def main():
+  """Demonstrates use of the Contacts extension using the ContactsSample object."""
+  # Parse command line options
+  try:
+    opts, args = getopt.getopt(sys.argv[1:], '', ['user=', 'pw='])
+  except getopt.error, msg:
+    print 'python contacts_example.py --user [username] --pw [password]'
+    sys.exit(2)
+
+  user = ''
+  pw = ''
+  # Process options
+  for option, arg in opts:
+    if option == '--user':
+      user = arg
+    elif option == '--pw':
+      pw = arg
+
+  while not user:
+    print 'NOTE: Please run these tests only with a test account.'
+    user = raw_input('Please enter your username: ')
+  while not pw:
+    pw = getpass.getpass()
+    if not pw:
+      print 'Password cannot be blank.'
+
+
+  try:
+    sample = ContactsSample(user, pw)
+  except gdata.service.BadAuthentication:
+    print 'Invalid user credentials given.'
+    return
+
+  sample.Run()
+
+
+if __name__ == '__main__':
+  main()

Modified: trunk/conduit/modules/GoogleModule/gdata/__init__.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/gdata/__init__.py	(original)
+++ trunk/conduit/modules/GoogleModule/gdata/__init__.py	Wed May  7 11:25:32 2008
@@ -24,13 +24,6 @@
 __author__ = 'api.jscudder (Jeffrey Scudder)'
 
 import os
-try:
-  from xml.etree import cElementTree as ElementTree
-except ImportError:
-  try:
-    import cElementTree as ElementTree
-  except ImportError:
-    from elementtree import ElementTree
 import atom
 
 

Added: trunk/conduit/modules/GoogleModule/gdata/apps/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/apps/Makefile.am	Wed May  7 11:25:32 2008
@@ -0,0 +1,5 @@
+conduit_handlersdir = $(libdir)/conduit/modules/GoogleModule/gdata/apps
+conduit_handlers_PYTHON = __init__.py service.py
+
+clean-local:
+	rm -rf *.pyc *.pyo

Added: trunk/conduit/modules/GoogleModule/gdata/apps/__init__.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/apps/__init__.py	Wed May  7 11:25:32 2008
@@ -0,0 +1,451 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2007 SIOS Technology, Inc.
+#
+# 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.
+
+"""Contains objects used with Google Apps."""
+
+__author__ = 'tmatsuo sios com (Takashi MATSUO)'
+
+
+import atom
+import gdata
+
+
+# XML namespaces which are often used in Google Apps entity.
+APPS_NAMESPACE = 'http://schemas.google.com/apps/2006'
+APPS_TEMPLATE = '{http://schemas.google.com/apps/2006}%s'
+
+
+class EmailList(atom.AtomBase):
+  """The Google Apps EmailList element"""
+  
+  _tag = 'emailList'
+  _namespace = APPS_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['name'] = 'name'
+
+  def __init__(self, name=None, extension_elements=None,
+               extension_attributes=None, text=None):
+    self.name = name
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+def EmailListFromString(xml_string):
+  return atom.CreateClassFromXMLString(EmailList, xml_string)
+  
+
+class Who(atom.AtomBase):
+  """The Google Apps Who element"""
+  
+  _tag = 'who'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['rel'] = 'rel'
+  _attributes['email'] = 'email'
+
+  def __init__(self, rel=None, email=None, extension_elements=None,
+               extension_attributes=None, text=None):
+    self.rel = rel
+    self.email = email
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+def WhoFromString(xml_string):
+  return atom.CreateClassFromXMLString(Who, xml_string)
+  
+
+class Login(atom.AtomBase):
+  """The Google Apps Login element"""
+  
+  _tag = 'login'
+  _namespace = APPS_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['userName'] = 'user_name'
+  _attributes['password'] = 'password'
+  _attributes['suspended'] = 'suspended'
+  _attributes['admin'] = 'admin'
+  _attributes['changePasswordAtNextLogin'] = 'change_password'
+  _attributes['agreedToTerms'] = 'agreed_to_terms'
+  _attributes['ipWhitelisted'] = 'ip_whitelisted'
+  _attributes['hashFunctionName'] = 'hash_function_name'
+
+  def __init__(self, user_name=None, password=None, suspended=None,
+               ip_whitelisted=None, hash_function_name=None, 
+               admin=None, change_password=None, agreed_to_terms=None, 
+               extension_elements=None, extension_attributes=None, 
+               text=None):
+    self.user_name = user_name
+    self.password = password
+    self.suspended = suspended
+    self.admin = admin
+    self.change_password = change_password
+    self.agreed_to_terms = agreed_to_terms
+    self.ip_whitelisted = ip_whitelisted
+    self.hash_function_name = hash_function_name
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+    
+def LoginFromString(xml_string):
+    return atom.CreateClassFromXMLString(Login, xml_string)
+    
+
+class Quota(atom.AtomBase):
+  """The Google Apps Quota element"""
+  
+  _tag = 'quota'
+  _namespace = APPS_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['limit'] = 'limit'
+
+  def __init__(self, limit=None, extension_elements=None,
+               extension_attributes=None, text=None):
+    self.limit = limit
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+    
+def QuotaFromString(xml_string):
+    return atom.CreateClassFromXMLString(Quota, xml_string)
+
+    
+class Name(atom.AtomBase):
+  """The Google Apps Name element"""
+
+  _tag = 'name'
+  _namespace = APPS_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()  
+  _attributes['familyName'] = 'family_name'
+  _attributes['givenName'] = 'given_name'
+  
+  def __init__(self, family_name=None, given_name=None,
+               extension_elements=None, extension_attributes=None, text=None):
+    self.family_name = family_name
+    self.given_name = given_name
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def NameFromString(xml_string):
+    return atom.CreateClassFromXMLString(Name, xml_string)
+
+
+class Nickname(atom.AtomBase):
+  """The Google Apps Nickname element"""
+  
+  _tag = 'nickname'
+  _namespace = APPS_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy() 
+  _attributes['name'] = 'name'
+
+  def __init__(self, name=None,
+               extension_elements=None, extension_attributes=None, text=None):
+    self.name = name
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def NicknameFromString(xml_string):
+    return atom.CreateClassFromXMLString(Nickname, xml_string)
+  
+
+class NicknameEntry(gdata.GDataEntry):
+  """A Google Apps flavor of an Atom Entry for Nickname"""
+  
+  _tag = 'entry'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+  _children['{%s}login' % APPS_NAMESPACE] = ('login', Login)
+  _children['{%s}nickname' % APPS_NAMESPACE] = ('nickname', Nickname)
+
+  def __init__(self, author=None, category=None, content=None,
+               atom_id=None, link=None, published=None, 
+               title=None, updated=None,
+               login=None, nickname=None,
+               extended_property=None, 
+               extension_elements=None, extension_attributes=None, text=None):
+
+    gdata.GDataEntry.__init__(self, author=author, category=category, 
+                              content=content,
+                              atom_id=atom_id, link=link, published=published,
+                              title=title, updated=updated)
+    self.login = login
+    self.nickname = nickname
+    self.extended_property = extended_property or []
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def NicknameEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(NicknameEntry, xml_string)
+
+
+class NicknameFeed(gdata.GDataFeed, gdata.LinkFinder):
+  """A Google Apps Nickname feed flavor of an Atom Feed"""
+  
+  _tag = 'feed'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [NicknameEntry])
+
+  def __init__(self, author=None, category=None, contributor=None,
+               generator=None, icon=None, atom_id=None, link=None, logo=None, 
+               rights=None, subtitle=None, title=None, updated=None,
+               entry=None, total_results=None, start_index=None,
+               items_per_page=None, extension_elements=None,
+               extension_attributes=None, text=None):
+    gdata.GDataFeed.__init__(self, author=author, category=category,
+                             contributor=contributor, generator=generator,
+                             icon=icon,  atom_id=atom_id, link=link,
+                             logo=logo, rights=rights, subtitle=subtitle,
+                             title=title, updated=updated, entry=entry,
+                             total_results=total_results,
+                             start_index=start_index,
+                             items_per_page=items_per_page,
+                             extension_elements=extension_elements,
+                             extension_attributes=extension_attributes,
+                             text=text)
+
+
+def NicknameFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(NicknameFeed, xml_string)
+
+
+class UserEntry(gdata.GDataEntry):
+  """A Google Apps flavor of an Atom Entry"""
+  
+  _tag = 'entry'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+  _children['{%s}login' % APPS_NAMESPACE] = ('login', Login)
+  _children['{%s}name' % APPS_NAMESPACE] = ('name', Name)
+  _children['{%s}quota' % APPS_NAMESPACE] = ('quota', Quota)
+  # This child may already be defined in GDataEntry, confirm before removing.
+  _children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link', 
+                                                       [gdata.FeedLink])
+  _children['{%s}who' % gdata.GDATA_NAMESPACE] = ('who', Who)
+
+  def __init__(self, author=None, category=None, content=None,
+               atom_id=None, link=None, published=None, 
+               title=None, updated=None,
+               login=None, name=None, quota=None, who=None, feed_link=None,
+               extended_property=None, 
+               extension_elements=None, extension_attributes=None, text=None):
+
+    gdata.GDataEntry.__init__(self, author=author, category=category, 
+                              content=content,
+                              atom_id=atom_id, link=link, published=published,
+                              title=title, updated=updated)
+    self.login = login
+    self.name = name
+    self.quota = quota
+    self.who = who
+    self.feed_link = feed_link or []
+    self.extended_property = extended_property or []
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+    
+
+def UserEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(UserEntry, xml_string)
+
+  
+class UserFeed(gdata.GDataFeed, gdata.LinkFinder):
+  """A Google Apps User feed flavor of an Atom Feed"""
+  
+  _tag = 'feed'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [UserEntry])
+
+  def __init__(self, author=None, category=None, contributor=None,
+               generator=None, icon=None, atom_id=None, link=None, logo=None, 
+               rights=None, subtitle=None, title=None, updated=None,
+               entry=None, total_results=None, start_index=None,
+               items_per_page=None, extension_elements=None,
+               extension_attributes=None, text=None):
+    gdata.GDataFeed.__init__(self, author=author, category=category,
+                             contributor=contributor, generator=generator,
+                             icon=icon,  atom_id=atom_id, link=link,
+                             logo=logo, rights=rights, subtitle=subtitle,
+                             title=title, updated=updated, entry=entry,
+                             total_results=total_results,
+                             start_index=start_index,
+                             items_per_page=items_per_page,
+                             extension_elements=extension_elements,
+                             extension_attributes=extension_attributes,
+                             text=text)
+
+
+def UserFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(UserFeed, xml_string)
+
+
+class EmailListEntry(gdata.GDataEntry):
+  """A Google Apps EmailList flavor of an Atom Entry"""
+  
+  _tag = 'entry'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+  _children['{%s}emailList' % APPS_NAMESPACE] = ('email_list', EmailList)
+  # Might be able to remove this _children entry.
+  _children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link', 
+                                                       [gdata.FeedLink])
+
+  def __init__(self, author=None, category=None, content=None,
+               atom_id=None, link=None, published=None, 
+               title=None, updated=None,
+               email_list=None, feed_link=None,
+               extended_property=None, 
+               extension_elements=None, extension_attributes=None, text=None):
+
+    gdata.GDataEntry.__init__(self, author=author, category=category, 
+                              content=content,
+                              atom_id=atom_id, link=link, published=published,
+                              title=title, updated=updated)
+    self.email_list = email_list
+    self.feed_link = feed_link or []
+    self.extended_property = extended_property or []
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def EmailListEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(EmailListEntry, xml_string)
+  
+
+class EmailListFeed(gdata.GDataFeed, gdata.LinkFinder):
+  """A Google Apps EmailList feed flavor of an Atom Feed"""
+  
+  _tag = 'feed'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [EmailListEntry])
+
+  def __init__(self, author=None, category=None, contributor=None,
+               generator=None, icon=None, atom_id=None, link=None, logo=None, 
+               rights=None, subtitle=None, title=None, updated=None,
+               entry=None, total_results=None, start_index=None,
+               items_per_page=None, extension_elements=None,
+               extension_attributes=None, text=None):
+    gdata.GDataFeed.__init__(self, author=author, category=category,
+                             contributor=contributor, generator=generator,
+                             icon=icon,  atom_id=atom_id, link=link,
+                             logo=logo, rights=rights, subtitle=subtitle,
+                             title=title, updated=updated, entry=entry,
+                             total_results=total_results,
+                             start_index=start_index,
+                             items_per_page=items_per_page,
+                             extension_elements=extension_elements,
+                             extension_attributes=extension_attributes,
+                             text=text)
+                             
+
+def EmailListFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(EmailListFeed, xml_string)
+
+
+class EmailListRecipientEntry(gdata.GDataEntry):
+  """A Google Apps EmailListRecipient flavor of an Atom Entry"""
+  
+  _tag = 'entry'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+  _children['{%s}who' % gdata.GDATA_NAMESPACE] = ('who', Who)
+
+  def __init__(self, author=None, category=None, content=None,
+               atom_id=None, link=None, published=None, 
+               title=None, updated=None,
+               who=None,
+               extended_property=None, 
+               extension_elements=None, extension_attributes=None, text=None):
+
+    gdata.GDataEntry.__init__(self, author=author, category=category, 
+                              content=content,
+                              atom_id=atom_id, link=link, published=published,
+                              title=title, updated=updated)
+    self.who = who
+    self.extended_property = extended_property or []
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def EmailListRecipientEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(EmailListRecipientEntry, xml_string)
+
+
+class EmailListRecipientFeed(gdata.GDataFeed, gdata.LinkFinder):
+  """A Google Apps EmailListRecipient feed flavor of an Atom Feed"""
+  
+  _tag = 'feed'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', 
+                                                  [EmailListRecipientEntry])
+
+  def __init__(self, author=None, category=None, contributor=None,
+               generator=None, icon=None, atom_id=None, link=None, logo=None, 
+               rights=None, subtitle=None, title=None, updated=None,
+               entry=None, total_results=None, start_index=None,
+               items_per_page=None, extension_elements=None,
+               extension_attributes=None, text=None):
+    gdata.GDataFeed.__init__(self, author=author, category=category,
+                             contributor=contributor, generator=generator,
+                             icon=icon,  atom_id=atom_id, link=link,
+                             logo=logo, rights=rights, subtitle=subtitle,
+                             title=title, updated=updated, entry=entry,
+                             total_results=total_results,
+                             start_index=start_index,
+                             items_per_page=items_per_page,
+                             extension_elements=extension_elements,
+                             extension_attributes=extension_attributes,
+                             text=text)
+
+
+def EmailListRecipientFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(EmailListRecipientFeed, xml_string)
+
+
+
+
+
+
+
+
+

Added: trunk/conduit/modules/GoogleModule/gdata/apps/service.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/apps/service.py	Wed May  7 11:25:32 2008
@@ -0,0 +1,370 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2007 SIOS Technology, Inc.
+#
+# 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.
+
+__author__ = 'tmatsuo sios com (Takashi MATSUO)'
+
+try:
+  from xml.etree import cElementTree as ElementTree
+except ImportError:
+  try:
+    import cElementTree as ElementTree
+  except ImportError:
+    try:
+      from xml.etree import ElementTree
+    except Import Error:
+      from elementtree import ElementTree
+import urllib
+import gdata
+import atom.service
+import gdata.service
+import gdata.apps
+import atom
+
+API_VER="2.0"
+HTTP_OK=200
+
+UNKOWN_ERROR=1000
+USER_DELETED_RECENTLY=1100
+USER_SUSPENDED=1101
+DOMAIN_USER_LIMIT_EXCEEDED=1200
+DOMAIN_ALIAS_LIMIT_EXCEEDED=1201
+DOMAIN_SUSPENDED=1202
+DOMAIN_FEATURE_UNAVAILABLE=1203
+ENTITY_EXISTS=1300
+ENTITY_DOES_NOT_EXIST=1301
+ENTITY_NAME_IS_RESERVED=1302
+ENTITY_NAME_NOT_VALID=1303
+INVALID_GIVEN_NAME=1400
+INVALID_FAMILY_NAME=1401
+INVALID_PASSWORD=1402
+INVALID_USERNAME=1403
+INVALID_HASH_FUNCTION_NAME=1404
+INVALID_HASH_DIGGEST_LENGTH=1405
+INVALID_EMAIL_ADDRESS=1406
+INVALID_QUERY_PARAMETER_VALUE=1407
+TOO_MANY_RECIPIENTS_ON_EMAIL_LIST=1500
+
+DEFAULT_QUOTA_LIMIT='2048'
+
+class Error(Exception):
+  pass
+
+class AppsForYourDomainException(Error):
+
+  def __init__(self, response):
+
+    self.args = response
+    try:
+      self.element_tree = ElementTree.fromstring(response['body'])
+      self.error_code = int(self.element_tree[0].attrib['errorCode'])
+      self.reason = self.element_tree[0].attrib['reason']
+      self.invalidInput = self.element_tree[0].attrib['invalidInput']
+    except:
+      self.error_code = UNKOWN_ERROR
+
+class AppsService(gdata.service.GDataService):
+  """Client for the Google Apps Provisioning service."""
+
+  def __init__(self, email=None, password=None, domain=None, source=None,
+               server='www.google.com', additional_headers=None):
+    gdata.service.GDataService.__init__(self, email=email, password=password,
+                                        service='apps', source=source,
+                                        server=server,
+                                        additional_headers=additional_headers)
+    self.ssl = True
+    self.port = 443
+    self.domain = domain
+
+  def _baseURL(self):
+    return "/a/feeds/%s" % self.domain 
+
+  def AddAllElementsFromAllPages(self, link_finder, func):
+    """retrieve all pages and add all elements"""
+    next = link_finder.GetNextLink()
+    while next is not None:
+      next_feed = func(str(self.Get(next.href)))
+      for a_entry in next_feed.entry:
+        link_finder.entry.append(a_entry)
+      next = next_feed.GetNextLink()
+    return link_finder
+
+  def RetrievePageOfEmailLists(self, start_email_list_name=None):
+    """Retrieve one page of email list"""
+
+    uri = "%s/emailList/%s" % (self._baseURL(), API_VER)
+    if start_email_list_name is not None:
+      uri += "?startEmailListName=%s" % start_email_list_name
+    try:
+      return gdata.apps.EmailListFeedFromString(str(self.Get(uri)))
+    except gdata.service.RequestError, e:
+      raise AppsForYourDomainException(e.args[0])
+    
+  def RetrieveAllEmailLists(self):
+    """Retrieve all email list of a domain."""
+
+    ret = self.RetrievePageOfEmailLists()
+    # pagination
+    return self.AddAllElementsFromAllPages(
+      ret, gdata.apps.EmailListFeedFromString)
+
+  def RetrieveEmailList(self, list_name):
+    """Retreive a single email list by the list's name."""
+
+    uri = "%s/emailList/%s/%s" % (
+      self._baseURL(), API_VER, list_name)
+    try:
+      return self.Get(uri, converter=gdata.apps.EmailListEntryFromString)
+    except gdata.service.RequestError, e:
+      raise AppsForYourDomainException(e.args[0])
+
+  def RetrieveEmailLists(self, recipient):
+    """Retrieve All Email List Subscriptions for an Email Address."""
+
+    uri = "%s/emailList/%s?recipient=%s" % (
+      self._baseURL(), API_VER, recipient)
+    try:
+      ret = gdata.apps.EmailListFeedFromString(str(self.Get(uri)))
+    except gdata.service.RequestError, e:
+      raise AppsForYourDomainException(e.args[0])
+    
+    # pagination
+    return self.AddAllElementsFromAllPages(
+      ret, gdata.apps.EmailListFeedFromString)
+
+  def RemoveRecipientFromEmailList(self, recipient, list_name):
+    """Remove recipient from email list."""
+    
+    uri = "%s/emailList/%s/%s/recipient/%s" % (
+      self._baseURL(), API_VER, list_name, recipient)
+    try:
+      self.Delete(uri)
+    except gdata.service.RequestError, e:
+      raise AppsForYourDomainException(e.args[0])
+
+  def RetrievePageOfRecipients(self, list_name, start_recipient=None):
+    """Retrieve one page of recipient of an email list. """
+
+    uri = "%s/emailList/%s/%s/recipient" % (
+      self._baseURL(), API_VER, list_name)
+
+    if start_recipient is not None:
+      uri += "?startRecipient=%s" % start_recipient
+    try:
+      return gdata.apps.EmailListRecipientFeedFromString(str(self.Get(uri)))
+    except gdata.service.RequestError, e:
+      raise AppsForYourDomainException(e.args[0])
+
+  def RetrieveAllRecipients(self, list_name):
+    """Retrieve all recipient of an email list."""
+
+    ret = self.RetrievePageOfRecipients(list_name)
+    # pagination
+    return self.AddAllElementsFromAllPages(
+      ret, gdata.apps.EmailListRecipientFeedFromString)
+
+  def AddRecipientToEmailList(self, recipient, list_name):
+    """Add a recipient to a email list."""
+
+    uri = "%s/emailList/%s/%s/recipient" % (
+      self._baseURL(), API_VER, list_name)
+    recipient_entry = gdata.apps.EmailListRecipientEntry()
+    recipient_entry.who = gdata.apps.Who(email=recipient)
+
+    try:
+      return gdata.apps.EmailListRecipientEntryFromString(
+        str(self.Post(recipient_entry, uri)))
+    except gdata.service.RequestError, e:
+      raise AppsForYourDomainException(e.args[0])
+
+  def DeleteEmailList(self, list_name):
+    """Delete a email list"""
+
+    uri = "%s/emailList/%s/%s" % (self._baseURL(), API_VER, list_name)
+    try:
+      self.Delete(uri)
+    except gdata.service.RequestError, e:
+      raise AppsForYourDomainException(e.args[0])
+
+  def CreateEmailList(self, list_name):
+    """Create a email list. """
+
+    uri = "%s/emailList/%s" % (self._baseURL(), API_VER)
+    email_list_entry = gdata.apps.EmailListEntry()
+    email_list_entry.email_list = gdata.apps.EmailList(name=list_name)
+
+    try: 
+      return gdata.apps.EmailListEntryFromString(
+        str(self.Post(email_list_entry, uri)))
+    except gdata.service.RequestError, e:
+      raise AppsForYourDomainException(e.args[0])
+
+  def DeleteNickname(self, nickname):
+    """Delete a nickname"""
+
+    uri = "%s/nickname/%s/%s" % (self._baseURL(), API_VER, nickname)
+    try:
+      self.Delete(uri)
+    except gdata.service.RequestError, e:
+      raise AppsForYourDomainException(e.args[0])
+
+  def RetrievePageOfNicknames(self, start_nickname=None):
+    """Retrieve one page of nicknames in the domain"""
+
+    uri = "%s/nickname/%s" % (self._baseURL(), API_VER)
+    if start_nickname is not None:
+      uri += "?startNickname=%s" % start_nickname
+    try:
+      return gdata.apps.NicknameFeedFromString(str(self.Get(uri)))
+    except gdata.service.RequestError, e:
+      raise AppsForYourDomainException(e.args[0])
+
+  def RetrieveAllNicknames(self):
+    """Retrieve all nicknames in the domain"""
+
+    ret = self.RetrievePageOfNicknames()
+    # pagination
+    return self.AddAllElementsFromAllPages(
+      ret, gdata.apps.NicknameFeedFromString)
+
+  def RetrieveNicknames(self, user_name):
+    """Retrieve nicknames of the user"""
+
+    uri = "%s/nickname/%s?username=%s" % (self._baseURL(), API_VER, user_name)
+    try:
+      ret = gdata.apps.NicknameFeedFromString(str(self.Get(uri)))
+    except gdata.service.RequestError, e:
+      raise AppsForYourDomainException(e.args[0])
+
+    # pagination
+    return self.AddAllElementsFromAllPages(
+      ret, gdata.apps.NicknameFeedFromString)
+
+  def RetrieveNickname(self, nickname):
+    """Retrieve a nickname.
+
+    Args:
+      nickname: string The nickname to retrieve
+
+    Returns:
+      gdata.apps.NicknameEntry
+    """
+
+    uri = "%s/nickname/%s/%s" % (self._baseURL(), API_VER, nickname)
+    try:
+      return gdata.apps.NicknameEntryFromString(str(self.Get(uri)))
+    except gdata.service.RequestError, e:
+      raise AppsForYourDomainException(e.args[0])
+
+  def CreateNickname(self, user_name, nickname):
+    """Create a nickname"""
+
+    uri = "%s/nickname/%s" % (self._baseURL(), API_VER)
+    nickname_entry = gdata.apps.NicknameEntry()
+    nickname_entry.login = gdata.apps.Login(user_name=user_name)
+    nickname_entry.nickname = gdata.apps.Nickname(name=nickname)
+
+    try: 
+      return gdata.apps.NicknameEntryFromString(
+        str(self.Post(nickname_entry, uri)))
+    except gdata.service.RequestError, e:
+      raise AppsForYourDomainException(e.args[0])
+
+  def DeleteUser(self, user_name):
+    """Delete a user account"""
+
+    uri = "%s/user/%s/%s" % (self._baseURL(), API_VER, user_name)
+    try:
+      return self.Delete(uri)
+    except gdata.service.RequestError, e:
+      raise AppsForYourDomainException(e.args[0])
+
+  def UpdateUser(self, user_name, user_entry):
+    """Update a user account."""
+
+    uri = "%s/user/%s/%s" % (self._baseURL(), API_VER, user_name)
+    try: 
+      return gdata.apps.UserEntryFromString(str(self.Put(user_entry, uri)))
+    except gdata.service.RequestError, e:
+      raise AppsForYourDomainException(e.args[0])
+
+  def CreateUser(self, user_name, family_name, given_name, password,
+                 suspended='false', quota_limit=None, 
+                 password_hash_function=None):
+    """Create a user account. """
+
+    uri = "%s/user/%s" % (self._baseURL(), API_VER)
+    user_entry = gdata.apps.UserEntry()
+    user_entry.login = gdata.apps.Login(
+        user_name=user_name, password=password, suspended=suspended,
+        hash_function_name=password_hash_function)
+    user_entry.name = gdata.apps.Name(family_name=family_name,
+                                      given_name=given_name)
+    if quota_limit is not None:
+      user_entry.quota = gdata.apps.Quota(limit=str(quota_limit))
+
+    try: 
+      return gdata.apps.UserEntryFromString(str(self.Post(user_entry, uri)))
+    except gdata.service.RequestError, e:
+      raise AppsForYourDomainException(e.args[0])
+
+  def SuspendUser(self, user_name):
+    user_entry = self.RetrieveUser(user_name)
+    if user_entry.login.suspended != 'true':
+      user_entry.login.suspended = 'true'
+      user_entry = self.UpdateUser(user_name, user_entry)
+    return user_entry
+
+  def RestoreUser(self, user_name):
+    user_entry = self.RetrieveUser(user_name)
+    if user_entry.login.suspended != 'false':
+      user_entry.login.suspended = 'false'
+      user_entry = self.UpdateUser(user_name, user_entry)
+    return user_entry
+
+  def RetrieveUser(self, user_name):
+    """Retrieve an user account.
+
+    Args:
+      user_name: string The user name to retrieve
+
+    Returns:
+      gdata.apps.UserEntry
+    """
+
+    uri = "%s/user/%s/%s" % (self._baseURL(), API_VER, user_name)
+    try:
+      return gdata.apps.UserEntryFromString(str(self.Get(uri)))
+    except gdata.service.RequestError, e:
+      raise AppsForYourDomainException(e.args[0])
+
+  def RetrievePageOfUsers(self, start_username=None):
+    """Retrieve one page of users in this domain."""
+
+    uri = "%s/user/%s" % (self._baseURL(), API_VER)
+    if start_username is not None:
+      uri += "?startUsername=%s" % start_username
+    try:
+      return gdata.apps.UserFeedFromString(str(self.Get(uri)))
+    except gdata.service.RequestError, e:
+      raise AppsForYourDomainException(e.args[0])
+
+  def RetrieveAllUsers(self):
+    """Retrieve all users in this domain."""
+
+    ret = self.RetrievePageOfUsers()
+    # pagination
+    return self.AddAllElementsFromAllPages(
+      ret, gdata.apps.UserFeedFromString)

Added: trunk/conduit/modules/GoogleModule/gdata/base/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/base/Makefile.am	Wed May  7 11:25:32 2008
@@ -0,0 +1,5 @@
+conduit_handlersdir = $(libdir)/conduit/modules/GoogleModule/gdata/base
+conduit_handlers_PYTHON = __init__.py service.py
+
+clean-local:
+	rm -rf *.pyc *.pyo

Added: trunk/conduit/modules/GoogleModule/gdata/base/__init__.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/base/__init__.py	Wed May  7 11:25:32 2008
@@ -0,0 +1,687 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2006 Google Inc.
+#
+# 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.
+
+"""Contains extensions to Atom objects used with Google Base."""
+
+
+__author__ = 'api.jscudder (Jeffrey Scudder)'
+
+
+try:
+  from xml.etree import cElementTree as ElementTree
+except ImportError:
+  try:
+    import cElementTree as ElementTree
+  except ImportError:
+    try:
+      from xml.etree import ElementTree
+    except ImportError:
+      from elementtree import ElementTree
+import atom
+import gdata
+
+
+# XML namespaces which are often used in Google Base entities.
+GBASE_NAMESPACE = 'http://base.google.com/ns/1.0'
+GBASE_TEMPLATE = '{http://base.google.com/ns/1.0}%s'
+GMETA_NAMESPACE = 'http://base.google.com/ns-metadata/1.0'
+GMETA_TEMPLATE = '{http://base.google.com/ns-metadata/1.0}%s'
+
+
+class ItemAttributeContainer(object):
+  """Provides methods for finding Google Base Item attributes.
+  
+  Google Base item attributes are child nodes in the gbase namespace. Google
+  Base allows you to define your own item attributes and this class provides
+  methods to interact with the custom attributes.   
+  """
+
+  def GetItemAttributes(self, name):
+    """Returns a list of all item attributes which have the desired name.
+
+    Args:
+      name: str The tag of the desired base attributes. For example, calling
+          this method with 'rating' would return a list of ItemAttributes
+          represented by a 'g:rating' tag.
+
+    Returns:
+      A list of matching ItemAttribute objects.
+    """
+    result = []
+    for attrib in self.item_attributes:
+      if attrib.name == name:
+        result.append(attrib)
+    return result
+
+  def FindItemAttribute(self, name):
+    """Get the contents of the first Base item attribute which matches name.
+
+    This method is deprecated, please use GetItemAttributes instead.
+    
+    Args: 
+      name: str The tag of the desired base attribute. For example, calling
+          this method with name = 'rating' would search for a tag rating
+          in the GBase namespace in the item attributes. 
+
+    Returns:
+      The text contents of the item attribute, or none if the attribute was
+      not found.
+    """
+  
+    for attrib in self.item_attributes:
+      if attrib.name == name:
+        return attrib.text
+    return None
+
+  def AddItemAttribute(self, name, value, value_type=None, access=None):
+    """Adds a new item attribute tag containing the value.
+    
+    Creates a new extension element in the GBase namespace to represent a
+    Google Base item attribute.
+    
+    Args:
+      name: str The tag name for the new attribute. This must be a valid xml
+        tag name. The tag will be placed in the GBase namespace.
+      value: str Contents for the item attribute
+      value_type: str (optional) The type of data in the vlaue, Examples: text
+          float
+      access: str (optional) Used to hide attributes. The attribute is not 
+          exposed in the snippets feed if access is set to 'private'.
+    """
+
+    new_attribute =  ItemAttribute(name, text=value, 
+        text_type=value_type, access=access)
+    self.item_attributes.append(new_attribute)
+    
+  def SetItemAttribute(self, name, value):
+    """Changes an existing item attribute's value."""
+
+    for attrib in self.item_attributes:
+      if attrib.name == name:
+        attrib.text = value
+        return
+
+  def RemoveItemAttribute(self, name):
+    """Deletes the first extension element which matches name.
+    
+    Deletes the first extension element which matches name. 
+    """
+
+    for i in xrange(len(self.item_attributes)):
+      if self.item_attributes[i].name == name:
+        del self.item_attributes[i]
+        return
+  
+  # We need to overwrite _ConvertElementTreeToMember to add special logic to
+  # convert custom attributes to members
+  def _ConvertElementTreeToMember(self, child_tree):
+    # Find the element's tag in this class's list of child members
+    if self.__class__._children.has_key(child_tree.tag):
+      member_name = self.__class__._children[child_tree.tag][0]
+      member_class = self.__class__._children[child_tree.tag][1]
+      # If the class member is supposed to contain a list, make sure the
+      # matching member is set to a list, then append the new member
+      # instance to the list.
+      if isinstance(member_class, list):
+        if getattr(self, member_name) is None:
+          setattr(self, member_name, [])
+        getattr(self, member_name).append(atom._CreateClassFromElementTree(
+            member_class[0], child_tree))
+      else:
+        setattr(self, member_name, 
+                atom._CreateClassFromElementTree(member_class, child_tree))
+    elif child_tree.tag.find('{%s}' % GBASE_NAMESPACE) == 0:
+      # If this is in the gbase namespace, make it into an extension element.
+      name = child_tree.tag[child_tree.tag.index('}')+1:]
+      value = child_tree.text
+      if child_tree.attrib.has_key('type'):
+        value_type = child_tree.attrib['type']
+      else:
+        value_type = None
+      self.AddItemAttribute(name, value, value_type)
+    else:
+      atom.ExtensionContainer._ConvertElementTreeToMember(self, child_tree)
+  
+  # We need to overwtite _AddMembersToElementTree to add special logic to
+  # convert custom members to XML nodes.
+  def _AddMembersToElementTree(self, tree):
+    # Convert the members of this class which are XML child nodes. 
+    # This uses the class's _children dictionary to find the members which
+    # should become XML child nodes.
+    member_node_names = [values[0] for tag, values in 
+                                       self.__class__._children.iteritems()]
+    for member_name in member_node_names:
+      member = getattr(self, member_name)
+      if member is None:
+        pass
+      elif isinstance(member, list):
+        for instance in member:
+          instance._BecomeChildElement(tree)
+      else:
+        member._BecomeChildElement(tree)
+    # Convert the members of this class which are XML attributes.
+    for xml_attribute, member_name in self.__class__._attributes.iteritems():
+      member = getattr(self, member_name)
+      if member is not None:
+        tree.attrib[xml_attribute] = member
+    # Convert all special custom item attributes to nodes
+    for attribute in self.item_attributes:
+      attribute._BecomeChildElement(tree)
+    # Lastly, call the ExtensionContainers's _AddMembersToElementTree to 
+    # convert any extension attributes.
+    atom.ExtensionContainer._AddMembersToElementTree(self, tree)
+
+
+class ItemAttribute(atom.Text):
+  """An optional or user defined attribute for a GBase item.
+  
+  Google Base allows items to have custom attribute child nodes. These nodes
+  have contents and a type attribute which tells Google Base whether the
+  contents are text, a float value with units, etc. The Atom text class has 
+  the same structure, so this class inherits from Text.
+  """
+  
+  _namespace = GBASE_NAMESPACE
+  _children = atom.Text._children.copy()
+  _attributes = atom.Text._attributes.copy()
+  _attributes['access'] = 'access'
+
+  def __init__(self, name, text_type=None, access=None, text=None, 
+      extension_elements=None, extension_attributes=None):
+    """Constructor for a GBase item attribute
+
+    Args:
+      name: str The name of the attribute. Examples include
+          price, color, make, model, pages, salary, etc.
+      text_type: str (optional) The type associated with the text contents
+      access: str (optional) If the access attribute is set to 'private', the
+          attribute will not be included in the item's description in the 
+          snippets feed
+      text: str (optional) The text data in the this element
+      extension_elements: list (optional) A  list of ExtensionElement 
+          instances
+      extension_attributes: dict (optional) A dictionary of attribute 
+          value string pairs
+    """
+
+    self.name = name
+    self.type = text_type
+    self.access = access
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+    
+  def _BecomeChildElement(self, tree):
+    new_child = ElementTree.Element('')
+    tree.append(new_child)
+    new_child.tag = '{%s}%s' % (self.__class__._namespace, 
+                                self.name)
+    self._AddMembersToElementTree(new_child)
+  
+  def _ToElementTree(self):
+    new_tree = ElementTree.Element('{%s}%s' % (self.__class__._namespace,
+                                               self.name))
+    self._AddMembersToElementTree(new_tree)
+    return new_tree
+    
+
+def ItemAttributeFromString(xml_string):
+  element_tree = ElementTree.fromstring(xml_string)
+  return _ItemAttributeFromElementTree(element_tree)  
+  
+  
+def _ItemAttributeFromElementTree(element_tree):
+  if element_tree.tag.find(GBASE_TEMPLATE % '') == 0:
+    to_return = ItemAttribute('')
+    to_return._HarvestElementTree(element_tree)
+    to_return.name = element_tree.tag[element_tree.tag.index('}')+1:]
+    if to_return.name and to_return.name != '':
+      return to_return
+  return None
+  
+
+class Label(atom.AtomBase):
+  """The Google Base label element"""
+  
+  _tag = 'label'
+  _namespace = GBASE_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  def __init__(self, text=None, extension_elements=None,
+      extension_attributes=None):
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def LabelFromString(xml_string):
+  return atom.CreateClassFromXMLString(Label, xml_string)
+
+
+class Thumbnail(atom.AtomBase):
+  """The Google Base thumbnail element"""
+  
+  _tag = 'thumbnail'
+  _namespace = GMETA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['width'] = 'width'
+  _attributes['height'] = 'height'
+
+  def __init__(self, width=None, height=None, text=None, extension_elements=None,
+      extension_attributes=None):
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+    self.width = width
+    self.height = height
+
+
+def ThumbnailFromString(xml_string):
+  return atom.CreateClassFromXMLString(Thumbnail, xml_string)
+
+
+class ImageLink(atom.Text):
+  """The Google Base image_link element"""
+  
+  _tag = 'image_link'
+  _namespace = GBASE_NAMESPACE
+  _children = atom.Text._children.copy()
+  _attributes = atom.Text._attributes.copy()
+  _children['{%s}thumbnail' % GMETA_NAMESPACE] = ('thumbnail', [Thumbnail])
+
+  def __init__(self, thumbnail=None, text=None, extension_elements=None,
+      text_type=None, extension_attributes=None):
+    self.thumbnail = thumbnail or []
+    self.text = text
+    self.type = text_type
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+    
+
+def ImageLinkFromString(xml_string):
+  return atom.CreateClassFromXMLString(ImageLink, xml_string)
+
+
+class ItemType(atom.Text):
+  """The Google Base item_type element"""
+  
+  _tag = 'item_type'
+  _namespace = GBASE_NAMESPACE
+  _children = atom.Text._children.copy()
+  _attributes = atom.Text._attributes.copy()
+
+  def __init__(self, text=None, extension_elements=None,
+      text_type=None, extension_attributes=None):
+    self.text = text
+    self.type = text_type
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def ItemTypeFromString(xml_string):
+  return atom.CreateClassFromXMLString(ItemType, xml_string)
+
+
+class MetaItemType(ItemType):
+  """The Google Base item_type element"""
+  
+  _tag = 'item_type'
+  _namespace = GMETA_NAMESPACE
+  _children = ItemType._children.copy()
+  _attributes = ItemType._attributes.copy()
+
+  
+def MetaItemTypeFromString(xml_string):
+  return atom.CreateClassFromXMLString(MetaItemType, xml_string)
+
+
+class Value(atom.AtomBase):
+  """Metadata about common values for a given attribute
+  
+  A value is a child of an attribute which comes from the attributes feed.
+  The value's text is a commonly used value paired with an attribute name
+  and the value's count tells how often this value appears for the given
+  attribute in the search results.
+  """
+  
+  _tag = 'value'
+  _namespace = GMETA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['count'] = 'count'
+ 
+  def __init__(self, count=None, text=None, extension_elements=None, 
+      extension_attributes=None):
+    """Constructor for Attribute metadata element
+
+    Args:
+      count: str (optional) The number of times the value in text is given
+          for the parent attribute.
+      text: str (optional) The value which appears in the search results.
+      extension_elements: list (optional) A  list of ExtensionElement
+          instances
+      extension_attributes: dict (optional) A dictionary of attribute value
+          string pairs
+    """
+
+    self.count = count
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def ValueFromString(xml_string):
+  return atom.CreateClassFromXMLString(Value, xml_string)
+
+
+class Attribute(atom.Text):
+  """Metadata about an attribute from the attributes feed
+  
+  An entry from the attributes feed contains a list of attributes. Each 
+  attribute describes the attribute's type and count of the items which
+  use the attribute.
+  """
+  
+  _tag = 'attribute'
+  _namespace = GMETA_NAMESPACE
+  _children = atom.Text._children.copy()
+  _attributes = atom.Text._attributes.copy()
+  _children['{%s}value' % GMETA_NAMESPACE] = ('value', [Value])
+  _attributes['count'] = 'count'
+  _attributes['name'] = 'name'
+
+  def __init__(self, name=None, attribute_type=None, count=None, value=None, 
+      text=None, extension_elements=None, extension_attributes=None):
+    """Constructor for Attribute metadata element
+
+    Args:
+      name: str (optional) The name of the attribute
+      attribute_type: str (optional) The type for the attribute. Examples:
+          test, float, etc.
+      count: str (optional) The number of times this attribute appears in
+          the query results.
+      value: list (optional) The values which are often used for this 
+          attirbute.
+      text: str (optional) The text contents of the XML for this attribute.
+      extension_elements: list (optional) A  list of ExtensionElement 
+          instances
+      extension_attributes: dict (optional) A dictionary of attribute value 
+          string pairs
+    """
+
+    self.name = name
+    self.type = attribute_type
+    self.count = count
+    self.value = value or []
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def AttributeFromString(xml_string):
+  return atom.CreateClassFromXMLString(Attribute, xml_string)
+
+  
+class Attributes(atom.AtomBase):
+  """A collection of Google Base metadata attributes"""
+  
+  _tag = 'attributes'
+  _namespace = GMETA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _children['{%s}attribute' % GMETA_NAMESPACE] = ('attribute', [Attribute])
+  
+  def __init__(self, attribute=None, extension_elements=None, 
+      extension_attributes=None, text=None):
+    self.attribute = attribute or []
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+    self.text = text  
+  
+  
+class GBaseItem(ItemAttributeContainer, gdata.BatchEntry):
+  """An Google Base flavor of an Atom Entry.
+  
+  Google Base items have required attributes, recommended attributes, and user
+  defined attributes. The required attributes are stored in this class as 
+  members, and other attributes are stored as extension elements. You can 
+  access the recommended and user defined attributes by using 
+  AddItemAttribute, SetItemAttribute, FindItemAttribute, and 
+  RemoveItemAttribute.
+  
+  The Base Item
+  """
+  
+  _tag = 'entry'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.BatchEntry._children.copy()
+  _attributes = gdata.BatchEntry._attributes.copy()
+  _children['{%s}label' % GBASE_NAMESPACE] = ('label', [Label])
+  _children['{%s}item_type' % GBASE_NAMESPACE] = ('item_type', ItemType)
+  
+  def __init__(self, author=None, category=None, content=None,
+      contributor=None, atom_id=None, link=None, published=None, rights=None,
+      source=None, summary=None, title=None, updated=None, control=None, 
+      label=None, item_type=None, item_attributes=None,
+      batch_operation=None, batch_id=None, batch_status=None,
+      text=None, extension_elements=None, extension_attributes=None):
+    self.author = author or []
+    self.category = category or []
+    self.content = content
+    self.contributor = contributor or []
+    self.id = atom_id
+    self.link = link or []
+    self.published = published
+    self.rights = rights
+    self.source = source
+    self.summary = summary
+    self.title = title
+    self.updated = updated
+    self.control = control
+    self.label = label or []
+    self.item_type = item_type
+    self.item_attributes = item_attributes or []
+    self.batch_operation = batch_operation
+    self.batch_id = batch_id
+    self.batch_status = batch_status
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def GBaseItemFromString(xml_string):
+  return atom.CreateClassFromXMLString(GBaseItem, xml_string)
+
+
+class GBaseSnippet(GBaseItem):
+  _tag = 'entry'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = GBaseItem._children.copy()
+  _attributes = GBaseItem._attributes.copy()
+  
+  
+def GBaseSnippetFromString(xml_string):
+  return atom.CreateClassFromXMLString(GBaseSnippet, xml_string)
+
+
+class GBaseAttributeEntry(gdata.GDataEntry):
+  """An Atom Entry from the attributes feed"""
+  
+  _tag = 'entry'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+  _children['{%s}attribute' % GMETA_NAMESPACE] = ('attribute', [Attribute])
+
+  def __init__(self, author=None, category=None, content=None,
+      contributor=None, atom_id=None, link=None, published=None, rights=None,
+      source=None, summary=None, title=None, updated=None, label=None,
+      attribute=None, control=None,
+      text=None, extension_elements=None, extension_attributes=None):
+    self.author = author or []
+    self.category = category or []
+    self.content = content
+    self.contributor = contributor or []
+    self.id = atom_id
+    self.link = link or []
+    self.published = published
+    self.rights = rights
+    self.source = source
+    self.summary = summary
+    self.control = control
+    self.title = title
+    self.updated = updated
+    self.label = label or []
+    self.attribute = attribute or []
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {} 
+
+
+def GBaseAttributeEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(GBaseAttributeEntry, xml_string)
+
+
+class GBaseItemTypeEntry(gdata.GDataEntry):
+  """An Atom entry from the item types feed
+  
+  These entries contain a list of attributes which are stored in one
+  XML node called attributes. This class simplifies the data structure
+  by treating attributes as a list of attribute instances. 
+
+  Note that the item_type for an item type entry is in the Google Base meta
+  namespace as opposed to item_types encountered in other feeds.
+  """
+  
+  _tag = 'entry'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+  _children['{%s}attributes' % GMETA_NAMESPACE] = ('attributes', Attributes)
+  _children['{%s}attribute' % GMETA_NAMESPACE] = ('attribute', [Attribute])
+  _children['{%s}item_type' % GMETA_NAMESPACE] = ('item_type', MetaItemType)
+
+  def __init__(self, author=None, category=None, content=None,
+      contributor=None, atom_id=None, link=None, published=None, rights=None,
+      source=None, summary=None, title=None, updated=None, label=None,
+      item_type=None, control=None, attribute=None, attributes=None,
+      text=None, extension_elements=None, extension_attributes=None):
+    self.author = author or []
+    self.category = category or []
+    self.content = content
+    self.contributor = contributor or []
+    self.id = atom_id
+    self.link = link or []
+    self.published = published
+    self.rights = rights
+    self.source = source
+    self.summary = summary
+    self.title = title
+    self.updated = updated
+    self.control = control
+    self.label = label or []
+    self.item_type = item_type
+    self.attributes = attributes
+    self.attribute = attribute  or []
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def GBaseItemTypeEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(GBaseItemTypeEntry, xml_string)
+  
+  
+class GBaseItemFeed(gdata.BatchFeed):
+  """A feed containing Google Base Items"""
+  
+  _tag = 'feed'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.BatchFeed._children.copy()
+  _attributes = gdata.BatchFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [GBaseItem])
+
+
+def GBaseItemFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(GBaseItemFeed, xml_string)
+
+
+class GBaseSnippetFeed(gdata.GDataFeed):
+  """A feed containing Google Base Snippets"""
+  
+  _tag = 'feed'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [GBaseSnippet])
+
+
+def GBaseSnippetFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(GBaseSnippetFeed, xml_string)
+
+
+class GBaseAttributesFeed(gdata.GDataFeed):
+  """A feed containing Google Base Attributes
+ 
+  A query sent to the attributes feed will return a feed of
+  attributes which are present in the items that match the
+  query. 
+  """
+  
+  _tag = 'feed'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', 
+                                                  [GBaseAttributeEntry])
+
+
+def GBaseAttributesFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(GBaseAttributesFeed, xml_string)
+
+
+class GBaseLocalesFeed(gdata.GDataFeed):
+  """The locales feed from Google Base.
+
+  This read-only feed defines the permitted locales for Google Base. The 
+  locale value identifies the language, currency, and date formats used in a
+  feed.
+  """
+  
+  _tag = 'feed'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+
+  
+def GBaseLocalesFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(GBaseLocalesFeed, xml_string)
+ 
+ 
+class GBaseItemTypesFeed(gdata.GDataFeed):
+  """A feed from the Google Base item types feed"""
+  
+  _tag = 'feed'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [GBaseItemTypeEntry])
+
+
+def GBaseItemTypesFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(GBaseItemTypesFeed, xml_string)

Added: trunk/conduit/modules/GoogleModule/gdata/base/service.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/base/service.py	Wed May  7 11:25:32 2008
@@ -0,0 +1,244 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2006 Google Inc.
+#
+# 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.
+
+"""GBaseService extends the GDataService to streamline Google Base operations.
+
+  GBaseService: Provides methods to query feeds and manipulate items. Extends 
+                GDataService.
+
+  DictionaryToParamList: Function which converts a dictionary into a list of 
+                         URL arguments (represented as strings). This is a 
+                         utility function used in CRUD operations.
+"""
+
+__author__ = 'api.jscudder (Jeffrey Scudder)'
+
+import urllib
+import gdata
+import atom.service
+import gdata.service
+import gdata.base
+import atom
+
+
+# URL to which all batch requests are sent.
+BASE_BATCH_URL = 'http://www.google.com/base/feeds/items/batch'
+
+
+class Error(Exception):
+  pass
+
+
+class RequestError(Error):
+  pass
+
+
+class GBaseService(gdata.service.GDataService):
+  """Client for the Google Base service."""
+
+  def __init__(self, email=None, password=None, source=None, 
+               server='base.google.com', api_key=None, 
+               additional_headers=None, handler=None):
+    gdata.service.GDataService.__init__(self, email=email, password=password,
+                                        service='gbase', source=source, 
+                                        server=server, 
+                                        additional_headers=additional_headers,
+                                        handler=handler)
+    self.api_key = api_key
+  
+  def _SetAPIKey(self, api_key):
+    if not isinstance(self.additional_headers, dict):
+      self.additional_headers = {}
+    self.additional_headers['X-Google-Key'] = api_key
+
+  def __SetAPIKey(self, api_key):
+    self._SetAPIKey(api_key)
+
+  def _GetAPIKey(self):
+    if 'X-Google-Key' not in self.additional_headers:
+      return None
+    else:
+      return self.additional_headers['X-Google-Key']
+
+  def __GetAPIKey(self):
+    return self._GetAPIKey()
+
+  api_key = property(__GetAPIKey, __SetAPIKey,
+      doc="""Get or set the API key to be included in all requests.""")
+    
+  def Query(self, uri, converter=None):
+    """Performs a style query and returns a resulting feed or entry.
+
+    Args:
+      uri: string The full URI which be queried. Examples include
+          '/base/feeds/snippets?bq=digital+camera', 
+          'http://www.google.com/base/feeds/snippets?bq=digital+camera'
+          '/base/feeds/items'
+          I recommend creating a URI using a query class.
+      converter: func (optional) A function which will be executed on the
+          server's response. Examples include GBaseItemFromString, etc. 
+
+    Returns:
+      If converter was specified, returns the results of calling converter on
+      the server's response. If converter was not specified, and the result
+      was an Atom Entry, returns a GBaseItem, by default, the method returns
+      the result of calling gdata.service's Get method.
+    """
+ 
+    result = self.Get(uri, converter=converter)
+    if converter:
+      return result
+    elif isinstance(result, atom.Entry):
+      return gdata.base.GBaseItemFromString(result.ToString())
+    return result
+
+  def QuerySnippetsFeed(self, uri):
+    return self.Get(uri, converter=gdata.base.GBaseSnippetFeedFromString)
+
+  def QueryItemsFeed(self, uri):
+    return self.Get(uri, converter=gdata.base.GBaseItemFeedFromString)
+
+  def QueryAttributesFeed(self, uri):
+    return self.Get(uri, converter=gdata.base.GBaseAttributesFeedFromString)
+
+  def QueryItemTypesFeed(self, uri):
+    return self.Get(uri, converter=gdata.base.GBaseItemTypesFeedFromString)
+
+  def QueryLocalesFeed(self, uri):
+    return self.Get(uri, converter=gdata.base.GBaseLocalesFeedFromString)
+
+  def GetItem(self, uri):
+    return self.Get(uri, converter=gdata.base.GBaseItemFromString)
+
+  def GetSnippet(self, uri):
+    return self.Get(uri, converter=gdata.base.GBaseSnippetFromString)
+
+  def GetAttribute(self, uri):
+    return self.Get(uri, converter=gdata.base.GBaseAttributeEntryFromString)
+
+  def GetItemType(self, uri):
+    return self.Get(uri, converter=gdata.base.GBaseItemTypeEntryFromString)
+
+  def GetLocale(self, uri):
+    return self.Get(uri, converter=gdata.base.GDataEntryFromString)
+
+  def InsertItem(self, new_item, url_params=None, escape_params=True, 
+      converter=None):
+    """Adds an item to Google Base.
+
+    Args: 
+      new_item: atom.Entry or subclass A new item which is to be added to 
+                Google Base.
+      url_params: dict (optional) Additional URL parameters to be included
+                  in the insertion request. 
+      escape_params: boolean (optional) If true, the url_parameters will be
+                     escaped before they are included in the request.
+      converter: func (optional) Function which is executed on the server's
+          response before it is returned. Usually this is a function like
+          GBaseItemFromString which will parse the response and turn it into
+          an object.
+
+    Returns:
+      If converter is defined, the results of running converter on the server's
+      response. Otherwise, it will be a GBaseItem.
+    """
+
+    response = self.Post(new_item, '/base/feeds/items', url_params=url_params,
+                         escape_params=escape_params, converter=converter)
+
+    if not converter and isinstance(response, atom.Entry):
+      return gdata.base.GBaseItemFromString(response.ToString())
+    return response
+
+  def DeleteItem(self, item_id, url_params=None, escape_params=True):
+    """Removes an item with the specified ID from Google Base.
+
+    Args:
+      item_id: string The ID of the item to be deleted. Example:
+               'http://www.google.com/base/feeds/items/13185446517496042648'
+      url_params: dict (optional) Additional URL parameters to be included
+                  in the deletion request.
+      escape_params: boolean (optional) If true, the url_parameters will be
+                     escaped before they are included in the request.
+
+    Returns:
+      True if the delete succeeded.
+    """
+    
+    return self.Delete('/%s' % (item_id.lstrip('http://www.google.com/')),
+                       url_params=url_params, escape_params=escape_params)
+                           
+  def UpdateItem(self, item_id, updated_item, url_params=None, 
+                 escape_params=True, 
+                 converter=gdata.base.GBaseItemFromString):
+    """Updates an existing item.
+
+    Args:
+      item_id: string The ID of the item to be updated.  Example:
+               'http://www.google.com/base/feeds/items/13185446517496042648'
+      updated_item: atom.Entry, subclass, or string, containing
+                    the Atom Entry which will replace the base item which is 
+                    stored at the item_id.
+      url_params: dict (optional) Additional URL parameters to be included
+                  in the update request.
+      escape_params: boolean (optional) If true, the url_parameters will be
+                     escaped before they are included in the request.
+      converter: func (optional) Function which is executed on the server's
+          response before it is returned. Usually this is a function like
+          GBaseItemFromString which will parse the response and turn it into
+          an object.
+
+    Returns:
+      If converter is defined, the results of running converter on the server's
+      response. Otherwise, it will be a GBaseItem.
+    """
+    
+    response = self.Put(updated_item, 
+        item_id, url_params=url_params, escape_params=escape_params, 
+        converter=converter)
+    if not converter and isinstance(response, atom.Entry):
+      return gdata.base.GBaseItemFromString(response.ToString())
+    return response
+
+  def ExecuteBatch(self, batch_feed, 
+                   converter=gdata.base.GBaseItemFeedFromString):
+    """Sends a batch request feed to the server.
+    
+    Args: 
+      batch_feed: gdata.BatchFeed A feed containing BatchEntry elements which
+          contain the desired CRUD operation and any necessary entry data.
+      converter: Function (optional) Function to be executed on the server's
+          response. This function should take one string as a parameter. The
+          default value is GBaseItemFeedFromString which will turn the result 
+          into a gdata.base.GBaseItem object.
+
+    Returns:
+      A gdata.BatchFeed containing the results.
+    """
+    
+    return self.Post(batch_feed, BASE_BATCH_URL, converter=converter) 
+
+
+class BaseQuery(gdata.service.Query):
+
+  def _GetBaseQuery(self):
+    return self['bq']
+
+  def _SetBaseQuery(self, base_query):
+    self['bq'] = base_query
+
+  bq = property(_GetBaseQuery, _SetBaseQuery, 
+      doc="""The bq query parameter""")

Modified: trunk/conduit/modules/GoogleModule/gdata/calendar/__init__.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/gdata/calendar/__init__.py	(original)
+++ trunk/conduit/modules/GoogleModule/gdata/calendar/__init__.py	Wed May  7 11:25:32 2008
@@ -21,15 +21,20 @@
 
 """Contains extensions to ElementWrapper objects used with Google Calendar."""
 
+
 __author__ = 'api.vli (Vivian Li), api.rboyd (Ryan Boyd)'
 
+
 try:
   from xml.etree import cElementTree as ElementTree
 except ImportError:
   try:
     import cElementTree as ElementTree
   except ImportError:
-    from elementtree import ElementTree
+    try:
+      from xml.etree import ElementTree
+    except ImportError:
+      from elementtree import ElementTree
 import atom
 import gdata
 

Modified: trunk/conduit/modules/GoogleModule/gdata/calendar/service.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/gdata/calendar/service.py	(original)
+++ trunk/conduit/modules/GoogleModule/gdata/calendar/service.py	Wed May  7 11:25:32 2008
@@ -24,15 +24,10 @@
                          utility function used in CRUD operations.
 """
 
+
 __author__ = 'api.vli (Vivian Li)'
 
-try:
-  from xml.etree import cElementTree as ElementTree
-except ImportError:
-  try:
-    import cElementTree as ElementTree
-  except ImportError:
-    from elementtree import ElementTree
+
 import urllib
 import gdata
 import atom.service
@@ -65,36 +60,36 @@
                                         additional_headers=additional_headers)
 
   def GetCalendarEventFeed(self, uri='/calendar/feeds/default/private/full'):
-    return gdata.calendar.CalendarEventFeedFromString(str(self.Get(uri)))
+    return self.Get(uri, converter=gdata.calendar.CalendarEventFeedFromString)
 
   def GetCalendarEventEntry(self, uri):
-    return gdata.calendar.CalendarEventEntryFromString(str(self.Get(uri)))
+    return self.Get(uri, converter=gdata.calendar.CalendarEventEntryFromString)
 
   def GetCalendarListFeed(self, uri='/calendar/feeds/default/allcalendars/full'):
-    return gdata.calendar.CalendarListFeedFromString(str(self.Get(uri)))
+    return self.Get(uri, converter=gdata.calendar.CalendarListFeedFromString)
 
   def GetAllCalendarsFeed(self, uri='/calendar/feeds/default/allcalendars/full'):
-    return gdata.calendar.CalendarListFeedFromString(str(self.Get(uri)))
+    return self.Get(uri, converter=gdata.calendar.CalendarListFeedFromString)
 
   def GetOwnCalendarsFeed(self, uri='/calendar/feeds/default/owncalendars/full'):
-    return gdata.calendar.CalendarListFeedFromString(str(self.Get(uri)))
+    return self.Get(uri, converter=gdata.calendar.CalendarListFeedFromString)
 
   def GetCalendarListEntry(self, uri):
-    return gdata.calendar.CalendarListEntryFromString(str(self.Get(uri)))
+    return self.Get(uri, converter=gdata.calendar.CalendarListEntryFromString)
 
   def GetCalendarAclFeed(self, uri='/calendar/feeds/default/acl/full'):
-    return gdata.calendar.CalendarAclFeedFromString(str(self.Get(uri)))
+    return self.Get(uri, converter=gdata.calendar.CalendarAclFeedFromString)
 
   def GetCalendarAclEntry(self, uri):
-    return gdata.calendar.CalendarAclEntryFromString(str(self.Get(uri)))
+    return self.Get(uri, converter=gdata.calendar.CalendarAclEntryFromString)
 
   def GetCalendarEventCommentFeed(self, uri):
-    return gdata.calendar.CalendarEventCommentFeedFromString(str(self.Get(uri)))
+    return self.Get(uri, converter=gdata.calendar.CalendarEventCommentFeedFromString)
 
   def GetCalendarEventCommentEntry(self, uri):
-    return gdata.calendar.CalendarEventCommentEntryFromString(str(self.Get(uri)))
+    return self.Get(uri, converter=gdata.calendar.CalendarEventCommentEntryFromString)
  
-  def Query(self, uri):
+  def Query(self, uri, converter=None):
     """Performs a query and returns a resulting feed or entry.
 
     Args:
@@ -109,26 +104,31 @@
          'body': HTTP body of the server's response}
     """
 
-    result = self.Get(uri)
+    if converter:
+      result = self.Get(uri, converter=converter)
+    else:
+      result = self.Get(uri)
     return result
 
   def CalendarQuery(self, query):
-    result = self.Query(query.ToUri())
     if isinstance(query, CalendarEventQuery):
-      return gdata.calendar.CalendarEventFeedFromString(result.ToString())
+      return self.Query(query.ToUri(), 
+          converter=gdata.calendar.CalendarEventFeedFromString)
     elif isinstance(query, CalendarListQuery):
-      return gdata.calendar.CalendarListFeedFromString(result.ToString())
+      return self.Query(query.ToUri(), 
+          converter=gdata.calendar.CalendarListFeedFromString)
     elif isinstance(query, CalendarEventCommentQuery):
-      return gdata.calendar.CalendarEventCommentFeedFromString(result.ToString())
+      return self.Query(query.ToUri(), 
+          converter=gdata.calendar.CalendarEventCommentFeedFromString)
     else:
-      return result
+      return self.Query(query.ToUri())
     
   def InsertEvent(self, new_event, insert_uri, url_params=None, 
                   escape_params=True):
     """Adds an event to Google Calendar.
 
     Args: 
-      new_event: ElementTree._Element A new event which is to be added to 
+      new_event: atom.Entry or subclass A new event which is to be added to 
                 Google Calendar.
       insert_uri: the URL to post new events to the feed
       url_params: dict (optional) Additional URL parameters to be included
@@ -144,13 +144,9 @@
          'body': HTTP body of the server's response}
     """
 
-    response = self.Post(new_event, insert_uri, url_params=url_params,
-                         escape_params=escape_params)
-
-    if isinstance(response, atom.Entry):
-      return gdata.calendar.CalendarEventEntryFromString(response.ToString())
-    else:
-      return response
+    return self.Post(new_event, insert_uri, url_params=url_params,
+                     escape_params=escape_params, 
+                     converter=gdata.calendar.CalendarEventEntryFromString)
 
   def InsertCalendarSubscription(self, calendar, url_params=None, 
                                  escape_params=True):
@@ -172,10 +168,9 @@
     """
     
     insert_uri = '/calendar/feeds/default/allcalendars/full'
-    response = self.Post(calendar, insert_uri, url_params=url_params,
-                         escape_params=escape_params, 
-                         converter=gdata.calendar.CalendarListEntryFromString)
-    return response
+    return self.Post(calendar, insert_uri, url_params=url_params,
+                     escape_params=escape_params, 
+                     converter=gdata.calendar.CalendarListEntryFromString)
 
   def InsertCalendar(self, new_calendar, url_params=None,
                                  escape_params=True):
@@ -232,7 +227,7 @@
     """Adds an ACL entry (rule) to Google Calendar.
 
     Args: 
-      new_entry: ElementTree._Element A new ACL entry which is to be added to 
+      new_entry: atom.Entry or subclass A new ACL entry which is to be added to 
                 Google Calendar.
       insert_uri: the URL to post new entries to the ACL feed
       url_params: dict (optional) Additional URL parameters to be included
@@ -248,20 +243,16 @@
          'body': HTTP body of the server's response}
     """
 
-    response = self.Post(new_entry, insert_uri, url_params=url_params,
-                         escape_params=escape_params)
-
-    if isinstance(response, atom.Entry):
-      return gdata.calendar.CalendarAclEntryFromString(response.ToString())
-    else:
-      return response
+    return self.Post(new_entry, insert_uri, url_params=url_params,
+                         escape_params=escape_params, 
+                         converter=gdata.calendar.CalendarAclEntryFromString)
 
   def InsertEventComment(self, new_entry, insert_uri, url_params=None,
                   escape_params=True):
     """Adds an entry to Google Calendar.
 
     Args:
-      new_entry: ElementTree._Element A new entry which is to be added to
+      new_entry: atom.Entry or subclass A new entry which is to be added to
                 Google Calendar.
       insert_uri: the URL to post new entrys to the feed
       url_params: dict (optional) Additional URL parameters to be included
@@ -277,13 +268,9 @@
          'body': HTTP body of the server's response}
     """
 
-    response = self.Post(new_entry, insert_uri, url_params=url_params,
-                         escape_params=escape_params)
-
-    if isinstance(response, atom.Entry):
-      return gdata.calendar.CalendarEventCommentEntryFromString(response.ToString())
-    else:
-      return response
+    return self.Post(new_entry, insert_uri, url_params=url_params,
+        escape_params=escape_params, 
+        converter=gdata.calendar.CalendarEventCommentEntryFromString)
 
   def DeleteEvent(self, edit_uri, extra_headers=None, 
       url_params=None, escape_params=True):
@@ -368,7 +355,7 @@
 
     Args:
       edit_uri: string The edit link URI for the element being updated
-      updated_event: string, ElementTree._Element, or ElementWrapper containing
+      updated_event: string, atom.Entry, or subclass containing
                     the Atom Entry which will replace the event which is 
                     stored at the edit_url 
       url_params: dict (optional) Additional URL parameters to be included
@@ -387,13 +374,10 @@
     url_prefix = 'http://%s/' % self.server
     if edit_uri.startswith(url_prefix):
       edit_uri = edit_uri[len(url_prefix):]
-    response = self.Put(updated_event, '/%s' % edit_uri,
-                        url_params=url_params, 
-                        escape_params=escape_params)
-    if isinstance(response, atom.Entry):
-      return gdata.calendar.CalendarEventEntryFromString(response.ToString())
-    else:
-      return response
+    return self.Put(updated_event, '/%s' % edit_uri,
+                    url_params=url_params, 
+                    escape_params=escape_params, 
+                    converter=gdata.calendar.CalendarEventEntryFromString)
 
   def UpdateAclEntry(self, edit_uri, updated_rule, url_params=None, 
                      escape_params=True):
@@ -401,7 +385,7 @@
 
     Args:
       edit_uri: string The edit link URI for the element being updated
-      updated_rule: string, ElementTree._Element, or ElementWrapper containing
+      updated_rule: string, atom.Entry, or subclass containing
                     the Atom Entry which will replace the event which is 
                     stored at the edit_url 
       url_params: dict (optional) Additional URL parameters to be included
@@ -420,13 +404,10 @@
     url_prefix = 'http://%s/' % self.server
     if edit_uri.startswith(url_prefix):
       edit_uri = edit_uri[len(url_prefix):]
-    response = self.Put(updated_rule, '/%s' % edit_uri,
-                        url_params=url_params, 
-                        escape_params=escape_params)
-    if isinstance(response, atom.Entry):
-      return gdata.calendar.CalendarAclEntryFromString(response.ToString())
-    else:
-      return response
+    return self.Put(updated_rule, '/%s' % edit_uri,
+                    url_params=url_params, 
+                    escape_params=escape_params,
+                    converter=gdata.calendar.CalendarAclEntryFromString)
 
   def ExecuteBatch(self, batch_feed, url, 
       converter=gdata.calendar.CalendarEventFeedFromString):

Added: trunk/conduit/modules/GoogleModule/gdata/codesearch/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/codesearch/Makefile.am	Wed May  7 11:25:32 2008
@@ -0,0 +1,5 @@
+conduit_handlersdir = $(libdir)/conduit/modules/GoogleModule/gdata/codesearch
+conduit_handlers_PYTHON = __init__.py service.py
+
+clean-local:
+	rm -rf *.pyc *.pyo

Added: trunk/conduit/modules/GoogleModule/gdata/codesearch/__init__.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/codesearch/__init__.py	Wed May  7 11:25:32 2008
@@ -0,0 +1,136 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2007 Benoit Chesneau <benoitc metavers net>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+
+"""Contains extensions to Atom objects used by Google Codesearch"""
+
+__author__ = 'Benoit Chesneau'
+
+
+import atom
+import gdata
+
+
+CODESEARCH_NAMESPACE='http://schemas.google.com/codesearch/2006'
+CODESEARCH_TEMPLATE='{http://shema.google.com/codesearch/2006}%s'
+
+
+class Match(atom.AtomBase):
+    """ The Google Codesearch match element """
+    _tag = 'match'
+    _namespace = CODESEARCH_NAMESPACE
+    _children = atom.AtomBase._children.copy()
+    _attributes = atom.AtomBase._attributes.copy()
+    _attributes['lineNumber'] = 'line_number'
+    _attributes['type'] = 'type'
+
+    def __init__(self, line_number=None, type=None, extension_elements=None,
+            extension_attributes=None, text=None):
+        self.text = text
+        self.type = type
+        self.line_number = line_number 
+        self.extension_elements = extension_elements or []
+        self.extension_attributes = extension_attributes or {}
+
+
+class File(atom.AtomBase):
+    """ The Google Codesearch file element"""
+    _tag = 'file'
+    _namespace = CODESEARCH_NAMESPACE
+    _children = atom.AtomBase._children.copy()
+    _attributes = atom.AtomBase._attributes.copy()
+    _attributes['name'] = 'name'
+
+    def __init__(self, name=None, extension_elements=None,
+            extension_attributes=None, text=None):
+        self.text = text
+        self.name = name
+        self.extension_elements = extension_elements or []
+        self.extension_attributes = extension_attributes or {}
+
+
+class Package(atom.AtomBase):
+    """ The Google Codesearch package element"""
+    _tag = 'package'
+    _namespace = CODESEARCH_NAMESPACE
+    _children = atom.AtomBase._children.copy()
+    _attributes = atom.AtomBase._attributes.copy()
+    _attributes['name'] = 'name'
+    _attributes['uri'] = 'uri'
+
+    def __init__(self, name=None, uri=None, extension_elements=None,
+            extension_attributes=None, text=None):
+        self.text = text
+        self.name = name
+        self.uri = uri
+        self.extension_elements = extension_elements or []
+        self.extension_attributes = extension_attributes or {}
+
+
+class CodesearchEntry(gdata.GDataEntry):
+    """ Google codesearch atom entry"""
+    _tag = gdata.GDataEntry._tag
+    _namespace = gdata.GDataEntry._namespace
+    _children = gdata.GDataEntry._children.copy()
+    _attributes = gdata.GDataEntry._attributes.copy()
+    
+    _children['{%s}file' % CODESEARCH_NAMESPACE] = ('file', File)
+    _children['{%s}package' % CODESEARCH_NAMESPACE] = ('package', Package)
+    _children['{%s}match' % CODESEARCH_NAMESPACE] = ('match', [Match])
+    
+    def __init__(self, author=None, category=None, content=None,
+            atom_id=None, link=None, published=None, 
+            title=None, updated=None, 
+            match=None, 
+            extension_elements=None, extension_attributes=None, text=None):
+        
+        gdata.GDataEntry.__init__(self, author=author, category=category, 
+                content=content, atom_id=atom_id, link=link, 
+                published=published, title=title, 
+                updated=updated, text=None)
+
+        self.match = match or []
+
+
+def CodesearchEntryFromString(xml_string):
+    """Converts an XML string into a CodesearchEntry object.
+
+    Args:
+        xml_string: string The XML describing a Codesearch feed entry.
+
+    Returns:
+        A CodesearchEntry object corresponding to the given XML.
+    """
+    return atom.CreateClassFromXMLString(CodesearchEntry, xml_string)
+
+
+class CodesearchFeed(gdata.GDataFeed):
+    """feed containing list of Google codesearch Items"""
+    _tag = gdata.GDataFeed._tag
+    _namespace = gdata.GDataFeed._namespace
+    _children = gdata.GDataFeed._children.copy()
+    _attributes = gdata.GDataFeed._attributes.copy()
+    _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [CodesearchEntry])
+
+    
+def CodesearchFeedFromString(xml_string):
+    """Converts an XML string into a CodesearchFeed object.
+    Args:
+    xml_string: string The XML describing a Codesearch feed.
+    Returns:
+    A CodeseartchFeed object corresponding to the given XML.
+    """
+    return atom.CreateClassFromXMLString(CodesearchFeed, xml_string)

Added: trunk/conduit/modules/GoogleModule/gdata/codesearch/service.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/codesearch/service.py	Wed May  7 11:25:32 2008
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2007 Benoit Chesneau <benoitc metavers net>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+
+"""CodesearchService extends GDataService to streamline Google Codesearch 
+operations"""
+
+
+__author__ = 'Benoit Chesneau'
+
+
+import atom
+import gdata.service
+import gdata.codesearch
+
+
+class CodesearchService(gdata.service.GDataService): 
+    """Client extension for Google codesearch service"""
+    
+    def __init__(self, email=None, password=None, source=None, 
+            server='www.google.com', additional_headers=None):
+        """Constructor for the CodesearchService.
+
+        Args:
+            email: string (optional) The e-mail address of the account to use for
+                   authentication.
+            password: string (optional) The password of the account to use for
+                      authentication.
+            source: string (optional) The name of the user's application.
+            server: string (optional) The server the feed is hosted on.
+            additional_headers: dict (optional) Any additional HTTP headers to be
+                                transmitted to the service in the form of key-value
+                                pairs.
+        Yields:
+            A CodesearchService object used to communicate with the Google Codesearch
+            service.
+        """
+
+        gdata.service.GDataService.__init__(self,
+                email=email, password=password, service='codesearch',
+                source=source,server=server,
+                additional_headers=additional_headers)
+    
+    def Query(self, uri, converter=gdata.codesearch.CodesearchFeedFromString):
+        """Queries the Codesearch feed and returns the resulting feed of
+           entries.
+
+        Args:
+        uri: string The full URI to be queried. This can contain query
+             parameters, a hostname, or simply the relative path to a Document
+             List feed. The DocumentQuery object is useful when constructing
+             query parameters.
+        converter: func (optional) A function which will be executed on the
+                   retrieved item, generally to render it into a Python object.
+                   By default the CodesearchFeedFromString function is used to
+                   return a CodesearchFeed object. This is because most feed
+                   queries will result in a feed and not a single entry.
+
+        Returns :
+            A CodesearchFeed objects representing the feed returned by the server
+        """
+        return self.Get(uri, converter=converter)
+
+    def GetSnippetsFeed(self, text_query=None):
+        """Retrieve Codesearch feed for a keyword
+
+        Args:
+            text_query : string (optional) The contents of the q query parameter. This
+                         string is URL escaped upon conversion to a URI.
+        Returns:
+            A CodesearchFeed objects representing the feed returned by the server
+        """
+
+        query=gdata.codesearch.service.CodesearchQuery(text_query=text_query)
+        feed = self.Query(query.ToUri())
+        return feed
+
+
+class CodesearchQuery(gdata.service.Query):
+    """Object used to construct the query to the Google Codesearch feed. here only as a shorcut"""
+
+    def __init__(self, feed='/codesearch/feeds/search', text_query=None, 
+            params=None, categories=None):
+        """Constructor for Codesearch Query.
+
+        Args:
+            feed: string (optional) The path for the feed. (e.g. '/codesearch/feeds/search')
+            text_query: string (optional) The contents of the q query parameter. This
+                        string is URL escaped upon conversion to a URI.
+            params: dict (optional) Parameter value string pairs which become URL
+                    params when translated to a URI. These parameters are added to
+                    the query's items.
+            categories: list (optional) List of category strings which should be
+                        included as query categories. See gdata.service.Query for
+                        additional documentation.
+
+        Yelds:
+            A CodesearchQuery object to construct a URI based on Codesearch feed
+        """
+
+        gdata.service.Query.__init__(self, feed, text_query, params, categories)

Added: trunk/conduit/modules/GoogleModule/gdata/contacts/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/contacts/Makefile.am	Wed May  7 11:25:32 2008
@@ -0,0 +1,5 @@
+conduit_handlersdir = $(libdir)/conduit/modules/GoogleModule/gdata/contacts
+conduit_handlers_PYTHON = __init__.py service.py
+
+clean-local:
+	rm -rf *.pyc *.pyo

Added: trunk/conduit/modules/GoogleModule/gdata/contacts/__init__.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/contacts/__init__.py	Wed May  7 11:25:32 2008
@@ -0,0 +1,258 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2008 Google Inc.
+#
+# 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.
+
+"""Contains extensions to ElementWrapper objects used with Google Contacts."""
+
+__author__ = 'dbrattli (Dag Brattli)'
+
+
+import atom
+import gdata
+
+
+## Constants from http://code.google.com/apis/gdata/elements.html ##
+REL_HOME = 'http://schemas.google.com/g/2005#home'
+REL_WORK = 'http://schemas.google.com/g/2005#work'
+REL_OTHER = 'http://schemas.google.com/g/2005#other'
+
+
+IM_AIM = 'http://schemas.google.com/g/2005#AIM' # AOL Instant Messenger protocol
+IM_MSN = 'http://schemas.google.com/g/2005#MSN' # MSN Messenger protocol
+IM_YAHOO = 'http://schemas.google.com/g/2005#YAHOO' # Yahoo Messenger protocol
+IM_SKYPE = 'http://schemas.google.com/g/2005#SKYPE' # Skype protocol
+IM_QQ = 'http://schemas.google.com/g/2005#QQ' # QQ protocol
+IM_GOOGLE_TALK = 'http://schemas.google.com/g/2005#GOOGLE_TALK' # Google Talk protocol
+IM_ICQ = 'http://schemas.google.com/g/2005#ICQ' # ICQ protocol
+IM_JABBER = 'http://schemas.google.com/g/2005#JABBER' # Jabber protocol
+
+
+class OrgName(atom.AtomBase):
+  _tag = 'orgName'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  def __init__(self, text=None, 
+      extension_elements=None, extension_attributes=None):
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+class OrgTitle(atom.AtomBase):
+  _tag = 'orgTitle'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  def __init__(self, text=None, 
+      extension_elements=None, extension_attributes=None):
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+class Organization(atom.AtomBase):
+  _tag = 'organization'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  _attributes['rel'] = 'rel'
+  _attributes['label'] = 'label'
+  _attributes['primary'] = 'primary'
+  
+  _children['{%s}orgName' % gdata.GDATA_NAMESPACE] = ('org_name', OrgName)
+  _children['{%s}orgTitle' % gdata.GDATA_NAMESPACE] = ('org_title', OrgTitle)
+
+  def __init__(self, rel=None, primary='false', org_name=None, org_title=None, 
+      label=None, text=None, extension_elements=None, 
+      extension_attributes=None):
+    self.rel = rel or REL_OTHER
+    self.primary = primary
+    self.org_name = org_name
+    self.org_title = org_title
+    self.label = label
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+class PostalAddress(atom.AtomBase):
+  _tag = 'postalAddress'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  _attributes['primary'] = 'primary'
+  _attributes['rel'] = 'rel'
+
+  def __init__(self, primary=None, rel=None, text=None, 
+      extension_elements=None, extension_attributes=None):
+    self.primary = primary
+    self.rel = rel or REL_OTHER
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+class IM(atom.AtomBase):
+  _tag = 'im'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  _attributes['address'] = 'address'
+  _attributes['primary'] = 'primary'
+  _attributes['protocol'] = 'protocol'
+  _attributes['label'] = 'label'
+  _attributes['rel'] = 'rel'
+
+  def __init__(self, primary=None, rel=None, address=None, protocol=None,
+      label=None, text=None, extension_elements=None, 
+      extension_attributes=None):
+    self.protocol = protocol
+    self.address = address
+    self.primary = primary
+    self.rel = rel or REL_OTHER
+    self.label = label
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+class Email(atom.AtomBase):
+  _tag = 'email'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  _attributes['address'] = 'address'
+  _attributes['primary'] = 'primary'
+  _attributes['rel'] = 'rel'
+  _attributes['label'] = 'label'
+
+  def __init__(self, primary=None, rel=None, address=None, text=None, 
+      label=None, extension_elements=None, extension_attributes=None):
+    self.address = address
+    self.primary = primary
+    self.rel = rel or REL_OTHER
+    self.label = label
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+class PhoneNumber(atom.AtomBase):
+  _tag = 'phoneNumber'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  _attributes['primary'] = 'primary'
+  _attributes['rel'] = 'rel'
+
+  def __init__(self, primary=None, rel=None, text=None, 
+      extension_elements=None, extension_attributes=None):
+    self.primary = primary
+    self.rel = rel or REL_OTHER
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+class Deleted(atom.AtomBase):
+  _tag = 'deleted'
+  _namespace = gdata.GDATA_NAMESPACE
+
+  def __init__(self, text=None, 
+      extension_elements=None, extension_attributes=None):
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+class ContactEntry(gdata.BatchEntry):
+  """A Google Contact flavor of an Atom Entry """
+
+  _tag = gdata.BatchEntry._tag
+  _namespace = gdata.BatchEntry._namespace
+  _children = gdata.BatchEntry._children.copy()
+  _attributes = gdata.BatchEntry._attributes.copy()
+
+  _children['{%s}postalAddress' % gdata.GDATA_NAMESPACE] = ('postal_address', [PostalAddress])
+  _children['{%s}phoneNumber' % gdata.GDATA_NAMESPACE] = ('phone_number', [PhoneNumber])
+  _children['{%s}organization' % gdata.GDATA_NAMESPACE] = ('organization', Organization)
+  _children['{%s}email' % gdata.GDATA_NAMESPACE] = ('email', [Email])
+  _children['{%s}im' % gdata.GDATA_NAMESPACE] = ('im', [IM])
+  _children['{%s}deleted' % gdata.GDATA_NAMESPACE] = ('deleted', Deleted)
+  
+  def __init__(self, author=None, category=None, content=None,
+      atom_id=None, link=None, published=None, 
+      title=None, updated=None, 
+      transparency=None, comments=None, email=None,
+      postal_address=None, deleted=None,
+      organization=None, phone_number=None, im=None,
+      extended_property=None, original_event=None,
+      batch_operation=None, batch_id=None, batch_status=None,
+      extension_elements=None, extension_attributes=None, text=None):
+
+
+    gdata.BatchEntry.__init__(self, author=author, category=category, 
+                        content=content,
+                        atom_id=atom_id, link=link, published=published,
+                        batch_operation=batch_operation, batch_id=batch_id, 
+                        batch_status=batch_status,
+                        title=title, updated=updated)
+    
+    self.transparency = transparency 
+    self.comments = comments
+    self.organization = organization
+    self.deleted = deleted
+    self.phone_number = phone_number or []
+    self.postal_address = postal_address or []
+    self.im = im or []  
+    self.extended_property = extended_property or []
+    self.email = email or []
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+def ContactEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(ContactEntry, xml_string)
+
+class ContactsFeed(gdata.GDataFeed, gdata.LinkFinder):
+  """A Google Contacts feed flavor of an Atom Feed"""
+
+  _tag = gdata.GDataFeed._tag
+  _namespace = gdata.GDataFeed._namespace
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [ContactEntry])
+
+  def __init__(self, author=None, category=None, contributor=None,
+               generator=None, icon=None, atom_id=None, link=None, logo=None, 
+               rights=None, subtitle=None, title=None, updated=None,
+               entry=None, total_results=None, start_index=None,
+               items_per_page=None, extension_elements=None,
+               extension_attributes=None, text=None):
+    gdata.GDataFeed.__init__(self, author=author, category=category,
+                             contributor=contributor, generator=generator,
+                             icon=icon,  atom_id=atom_id, link=link,
+                             logo=logo, rights=rights, subtitle=subtitle,
+                             title=title, updated=updated, entry=entry,
+                             total_results=total_results,
+                             start_index=start_index,
+                             items_per_page=items_per_page,
+                             extension_elements=extension_elements,
+                             extension_attributes=extension_attributes,
+                             text=text)
+                             
+def ContactsFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(ContactsFeed, xml_string)

Added: trunk/conduit/modules/GoogleModule/gdata/contacts/service.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/contacts/service.py	Wed May  7 11:25:32 2008
@@ -0,0 +1,150 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2008 Google Inc.
+#
+# 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.
+
+"""ContactsService extends the GDataService to streamline Google Contacts operations.
+
+  ContactsService: Provides methods to query feeds and manipulate items. Extends 
+                GDataService.
+
+  DictionaryToParamList: Function which converts a dictionary into a list of 
+                         URL arguments (represented as strings). This is a 
+                         utility function used in CRUD operations.
+"""
+
+__author__ = 'dbrattli (Dag Brattli)'
+
+
+import gdata
+import atom.service
+import gdata.service
+import gdata.calendar
+import atom
+
+class Error(Exception):
+  pass
+
+class RequestError(Error):
+  pass
+
+class ContactsService(gdata.service.GDataService):
+  """Client for the Google Contats service."""
+
+  def __init__(self, email=None, password=None, source=None, 
+               server='www.google.com', 
+               additional_headers=None):
+    gdata.service.GDataService.__init__(self, email=email, password=password,
+                                        service='cp', source=source, 
+                                        server=server, 
+                                        additional_headers=additional_headers)
+
+  def GetContactsFeed(self, 
+      uri='http://www.google.com/m8/feeds/contacts/default/base'):
+    return self.Get(uri, converter=gdata.contacts.ContactsFeedFromString)
+
+  def CreateContact(self, new_contact, 
+      insert_uri='/m8/feeds/contacts/default/base', url_params=None, 
+      escape_params=True):
+    """Adds an event to Google Contacts.
+
+    Args: 
+      new_contact: atom.Entry or subclass A new event which is to be added to
+                Google Contacts.
+      insert_uri: the URL to post new contacts to the feed
+      url_params: dict (optional) Additional URL parameters to be included
+                  in the insertion request. 
+      escape_params: boolean (optional) If true, the url_parameters will be
+                     escaped before they are included in the request.
+
+    Returns:
+      On successful insert,  an entry containing the contact created
+      On failure, a RequestError is raised of the form:
+        {'status': HTTP status code from server, 
+         'reason': HTTP reason from the server, 
+         'body': HTTP body of the server's response}
+    """
+    return self.Post(new_contact, insert_uri, url_params=url_params,
+        escape_params=escape_params,
+        converter=gdata.contacts.ContactEntryFromString)
+
+      
+  def UpdateContact(self, edit_uri, updated_contact, url_params=None, 
+                    escape_params=True):
+    """Updates an existing contact.
+
+    Args:
+      edit_uri: string The edit link URI for the element being updated
+      updated_contact: string, atom.Entry or subclass containing
+                    the Atom Entry which will replace the event which is 
+                    stored at the edit_url 
+      url_params: dict (optional) Additional URL parameters to be included
+                  in the update request.
+      escape_params: boolean (optional) If true, the url_parameters will be
+                     escaped before they are included in the request.
+
+    Returns:
+      On successful update,  a httplib.HTTPResponse containing the server's
+        response to the PUT request.
+      On failure, a RequestError is raised of the form:
+        {'status': HTTP status code from server, 
+         'reason': HTTP reason from the server, 
+         'body': HTTP body of the server's response}
+    """
+    url_prefix = 'http://%s/' % self.server
+    if edit_uri.startswith(url_prefix):
+      edit_uri = edit_uri[len(url_prefix):]
+    response = self.Put(updated_contact, '/%s' % edit_uri,
+                        url_params=url_params, 
+                        escape_params=escape_params)
+    if isinstance(response, atom.Entry):
+      return gdata.contacts.ContactEntryFromString(response.ToString())
+    else:
+      return response
+
+  def DeleteContact(self, edit_uri, extra_headers=None, 
+      url_params=None, escape_params=True):
+    """Removes an event with the specified ID from Google Contacts.
+
+    Args:
+      edit_uri: string The edit URL of the entry to be deleted. Example:
+               'http://www.google.com/m8/feeds/contacts/default/base/xxx/yyy'
+      url_params: dict (optional) Additional URL parameters to be included
+                  in the deletion request.
+      escape_params: boolean (optional) If true, the url_parameters will be
+                     escaped before they are included in the request.
+
+    Returns:
+      On successful delete,  a httplib.HTTPResponse containing the server's
+        response to the DELETE request.
+      On failure, a RequestError is raised of the form:
+        {'status': HTTP status code from server, 
+         'reason': HTTP reason from the server, 
+         'body': HTTP body of the server's response}
+    """
+    
+    url_prefix = 'http://%s/' % self.server
+    if edit_uri.startswith(url_prefix):
+      edit_uri = edit_uri[len(url_prefix):]
+    return self.Delete('/%s' % edit_uri,
+                       url_params=url_params, escape_params=escape_params)
+
+
+class ContactsQuery(gdata.service.Query):
+
+  def __init__(self, feed=None, text_query=None, params=None,
+      categories=None):
+    self.feed = feed or '/m8/feeds/contacts/default/base'
+    gdata.service.Query.__init__(self, feed=self.feed, text_query=text_query,
+        params=params, categories=categories)

Added: trunk/conduit/modules/GoogleModule/gdata/docs/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/docs/Makefile.am	Wed May  7 11:25:32 2008
@@ -0,0 +1,5 @@
+conduit_handlersdir = $(libdir)/conduit/modules/GoogleModule/gdata/docs
+conduit_handlers_PYTHON = __init__.py service.py
+
+clean-local:
+	rm -rf *.pyc *.pyo

Added: trunk/conduit/modules/GoogleModule/gdata/docs/__init__.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/docs/__init__.py	Wed May  7 11:25:32 2008
@@ -0,0 +1,66 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2006 Google Inc.
+#
+# 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.
+
+"""Contains extensions to Atom objects used with Google Documents."""
+
+__author__ = 'api.jfisher (Jeff Fisher)'
+
+import atom
+import gdata
+
+
+class DocumentListEntry(gdata.GDataEntry):
+  """The Google Documents version of an Atom Entry"""
+  
+  _tag = 'entry'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+
+
+def DocumentListEntryFromString(xml_string):
+  """Converts an XML string into a DocumentListEntry object.
+
+  Args:
+    xml_string: string The XML describing a Document List feed entry.
+
+  Returns:
+    A DocumentListEntry object corresponding to the given XML.
+  """
+  return atom.CreateClassFromXMLString(DocumentListEntry, xml_string)
+
+
+class DocumentListFeed(gdata.GDataFeed):
+  """A feed containing a list of Google Documents Items"""
+  
+  _tag = 'feed'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', 
+                                                  [DocumentListEntry])
+
+
+def DocumentListFeedFromString(xml_string):
+  """Converts an XML string into a DocumentListFeed object.
+
+  Args:
+    xml_string: string The XML describing a DocumentList feed.
+
+  Returns:
+    A DocumentListFeed object corresponding to the given XML.
+  """
+  return atom.CreateClassFromXMLString(DocumentListFeed, xml_string)

Added: trunk/conduit/modules/GoogleModule/gdata/docs/service.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/docs/service.py	Wed May  7 11:25:32 2008
@@ -0,0 +1,292 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2006 Google Inc.
+#
+# 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.
+
+"""DocsService extends the GDataService to streamline Google Documents
+  operations.
+
+  DocsService: Provides methods to query feeds and manipulate items.
+                    Extends GDataService.
+
+  DocumentQuery: Queries a Google Document list feed.
+"""
+
+
+__author__ = 'api.jfisher (Jeff Fisher)'
+
+
+import urllib
+import atom
+import gdata.service
+import gdata.docs
+
+
+# XML Namespaces used in Google Documents entities.
+DATA_KIND_SCHEME = 'http://schemas.google.com/g/2005#kind'
+DOCUMENT_KIND_TERM = 'http://schemas.google.com/docs/2007#document'
+SPREADSHEET_KIND_TERM = 'http://schemas.google.com/docs/2007#spreadsheet'
+PRESENTATION_KIND_TERM = 'http://schemas.google.com/docs/2007#presentation'
+# File extensions of documents that are permitted to be uploaded.
+SUPPORTED_FILETYPES = {
+  'CSV': 'text/csv',
+  'TSV': 'text/tab-separated-values',
+  'TAB': 'text/tab-separated-values',
+  'DOC': 'application/msword',
+  'ODS': 'application/x-vnd.oasis.opendocument.spreadsheet',
+  'ODT': 'application/vnd.oasis.opendocument.text',
+  'RTF': 'application/rtf',
+  'SXW': 'application/vnd.sun.xml.writer',
+  'TXT': 'text/plain',
+  'XLS': 'application/vnd.ms-excel',
+  'PPT': 'application/vnd.ms-powerpoint',
+  'PPS': 'application/vnd.ms-powerpoint',
+  'HTM': 'text/html',
+  'HTML' : 'text/html'}
+
+
+class DocsService(gdata.service.GDataService):
+
+  """Client extension for the Google Documents service Document List feed."""
+
+  def __init__(self, email=None, password=None, source=None,
+      server='docs.google.com', additional_headers=None):
+    """Constructor for the DocsService.
+
+    Args:
+      email: string (optional) The e-mail address of the account to use for
+             authentication.
+      password: string (optional) The password of the account to use for
+                authentication.
+      source: string (optional) The name of the user's application.
+      server: string (optional) The server the feed is hosted on.
+      additional_headers: dict (optional) Any additional HTTP headers to be
+                          transmitted to the service in the form of key-value
+                          pairs.
+
+    Yields:
+      A DocsService object used to communicate with the Google Documents
+      service.
+    """
+    gdata.service.GDataService.__init__(self, email=email, password=password,
+                                        service='writely', source=source,
+                                        server=server,
+                                        additional_headers=additional_headers)
+
+  def Query(self, uri, converter=gdata.docs.DocumentListFeedFromString):
+    """Queries the Document List feed and returns the resulting feed of
+       entries.
+
+    Args:
+      uri: string The full URI to be queried. This can contain query
+           parameters, a hostname, or simply the relative path to a Document
+           List feed. The DocumentQuery object is useful when constructing
+           query parameters.
+      converter: func (optional) A function which will be executed on the
+                 retrieved item, generally to render it into a Python object.
+                 By default the DocumentListFeedFromString function is used to
+                 return a DocumentListFeed object. This is because most feed
+                 queries will result in a feed and not a single entry.
+    """
+    return self.Get(uri, converter=converter)
+
+  def QueryDocumentListFeed(self, uri):
+    """Retrieves a DocumentListFeed by retrieving a URI based off the Document
+       List feed, including any query parameters. A DocumentQuery object can
+       be used to construct these parameters.
+
+    Args:
+      uri: string The URI of the feed being retrieved possibly with query
+           parameters.
+
+    Returns:
+      A DocumentListFeed object representing the feed returned by the server.
+    """
+    return self.Get(uri, converter=gdata.docs.DocumentListFeedFromString)
+
+  def GetDocumentListEntry(self, uri):
+    """Retrieves a particular DocumentListEntry by its unique URI.
+
+    Args:
+      uri: string The unique URI of an entry in a Document List feed.
+
+    Returns:
+      A DocumentListEntry object representing the retrieved entry.
+      """
+    return self.Get(uri, converter=gdata.docs.DocumentListEntryFromString)
+
+  def GetDocumentListFeed(self):
+    """Retrieves a feed containing all of a user's documents."""
+    q = gdata.docs.service.DocumentQuery();
+    return self.QueryDocumentListFeed(q.ToUri())
+
+  def UploadPresentation(self, media_source, title):
+    """Uploads a presentation inside of a MediaSource object to the Document
+       List feed with the given title.
+
+    Args:
+      media_source: MediaSource The MediaSource object containing a
+          presentation file to be uploaded.
+      title: string The title of the presentation on the server after being
+          uploaded.
+
+    Returns:
+      A GDataEntry containing information about the presentation created on the
+      Google Documents service.
+    """
+    category = atom.Category(scheme=DATA_KIND_SCHEME,
+        term=PRESENTATION_KIND_TERM)
+    return self._UploadFile(media_source, title, category)
+
+  def UploadSpreadsheet(self, media_source, title):
+    """Uploads a spreadsheet inside of a MediaSource object to the Document
+       List feed with the given title.
+
+    Args:
+      media_source: MediaSource The MediaSource object containing a spreadsheet
+                    file to be uploaded.
+      title: string The title of the spreadsheet on the server after being
+             uploaded.
+
+    Returns:
+      A GDataEntry containing information about the spreadsheet created on the
+      Google Documents service.
+    """
+    category = atom.Category(scheme=DATA_KIND_SCHEME,
+        term=SPREADSHEET_KIND_TERM)
+    return self._UploadFile(media_source, title, category)
+
+  def UploadDocument(self, media_source, title):
+    """Uploads a document inside of a MediaSource object to the Document List
+       feed with the given title.
+
+    Args:
+      media_source: MediaSource The gdata.MediaSource object containing a
+                    document file to be uploaded.
+      title: string The title of the document on the server after being
+             uploaded.
+
+    Returns:
+      A GDataEntry containing information about the document created on the
+      Google Documents service.
+    """
+    category = atom.Category(scheme=DATA_KIND_SCHEME,
+        term=DOCUMENT_KIND_TERM)
+    return self._UploadFile(media_source, title, category)
+
+  def _UploadFile(self, media_source, title, category):
+    """Uploads a file to the Document List feed.
+    
+    Args:
+      media_source: A gdata.MediaSource object containing the file to be
+                    uploaded.
+      title: string The title of the document on the server after being
+             uploaded.
+      category: An atom.Category object specifying the appropriate document
+                type
+    Returns:
+      A GDataEntry containing information about the document created on
+      the Google Documents service.
+     """
+    media_entry = gdata.GDataEntry()
+    media_entry.title = atom.Title(text=title)
+    media_entry.category.append(category)
+    media_entry = self.Post(media_entry, '/feeds/documents/private/full',
+        media_source = media_source,
+        extra_headers = {'Slug' : media_source.file_name })
+
+    return media_entry
+
+
+class DocumentQuery(gdata.service.Query):
+ 
+  """Object used to construct a URI to query the Google Document List feed"""
+
+  def __init__(self, feed='/feeds/documents', visibility='private',
+      projection='full', text_query=None, params=None,
+      categories=None):
+    """Constructor for Document List Query
+
+    Args:
+      feed: string (optional) The path for the feed. (e.g. '/feeds/documents')
+      visibility: string (optional) The visibility chosen for the current feed.
+      projection: string (optional) The projection chosen for the current feed.
+      text_query: string (optional) The contents of the q query parameter. This
+                  string is URL escaped upon conversion to a URI.
+      params: dict (optional) Parameter value string pairs which become URL
+              params when translated to a URI. These parameters are added to
+              the query's items.
+      categories: list (optional) List of category strings which should be
+              included as query categories. See gdata.service.Query for
+              additional documentation.
+
+    Yields:
+      A DocumentQuery object used to construct a URI based on the Document
+      List feed.
+    """
+    self.visibility = visibility
+    self.projection = projection
+    gdata.service.Query.__init__(self, feed, text_query, params, categories)
+
+  def ToUri(self):
+    """Generates a URI from the query parameters set in the object.
+
+    Returns:
+      A string containing the URI used to retrieve entries from the Document
+      List feed.
+    """
+    old_feed = self.feed
+    self.feed = '/'.join([old_feed, self.visibility, self.projection])
+    new_feed = gdata.service.Query.ToUri(self)
+    self.feed = old_feed
+    return new_feed
+
+  def AddNamedFolder(self, email, folder_name):
+    """Adds a named folder category, qualified by a schema.
+
+    This function lets you query for documents that are contained inside a
+    named folder without fear of collision with other categories.
+
+    Args:
+      email: string The email of the user who owns the folder.
+      folder_name: string The name of the folder.
+
+      Returns:
+        The string of the category that was added to the object.
+    """
+
+    category = '{http://schemas.google.com/docs/2007/folders/'
+    category += email + '}' + folder_name
+
+    self.categories.append(category)
+
+    return category
+
+  def RemoveNamedFolder(self, email, folder_name):
+    """Removes a named folder category, qualified by a schema.
+
+    Args:
+      email: string The email of the user who owns the folder.
+      folder_name: string The name of the folder.
+
+      Returns:
+        The string of the category that was removed to the object.
+    """
+
+    category = '{http://schemas.google.com/docs/2007/folders/'
+    category += email + '}' + folder_name
+
+    self.categories.remove(category)
+
+    return category

Modified: trunk/conduit/modules/GoogleModule/gdata/exif/__init__.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/gdata/exif/__init__.py	(original)
+++ trunk/conduit/modules/GoogleModule/gdata/exif/__init__.py	Wed May  7 11:25:32 2008
@@ -47,13 +47,7 @@
 __author__ = u'havard gulldahl no'# (HÃvard Gulldahl)' #BUG: pydoc chokes on non-ascii chars in __author__
 __license__ = 'Apache License v2'
 
-try:
-  from xml.etree import cElementTree as ElementTree
-except ImportError:
-  try:
-    import cElementTree as ElementTree
-  except ImportError:
-    from elementtree import ElementTree
+
 import atom
 import gdata
 

Modified: trunk/conduit/modules/GoogleModule/gdata/geo/__init__.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/gdata/geo/__init__.py	(original)
+++ trunk/conduit/modules/GoogleModule/gdata/geo/__init__.py	Wed May  7 11:25:32 2008
@@ -42,13 +42,7 @@
 __author__ = u'havard gulldahl no'# (HÃvard Gulldahl)' #BUG: api chokes on non-ascii chars in __author__
 __license__ = 'Apache License v2'
 
-try:
-  from xml.etree import cElementTree as ElementTree
-except ImportError:
-  try:
-    import cElementTree as ElementTree
-  except ImportError:
-    from elementtree import ElementTree
+
 import atom
 import gdata
 
@@ -129,7 +123,7 @@
   def location(self):
     "(float, float) Return Where.Point.pos.text as a (lat,lon) tuple"
     try:
-      return tuple(float(z) for z in self.Point.pos.text.split(' '))
+      return tuple([float(z) for z in self.Point.pos.text.split(' ')])
     except AttributeError:
       return tuple()
   def set_location(self, latlon):

Modified: trunk/conduit/modules/GoogleModule/gdata/media/__init__.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/gdata/media/__init__.py	(original)
+++ trunk/conduit/modules/GoogleModule/gdata/media/__init__.py	Wed May  7 11:25:32 2008
@@ -48,13 +48,7 @@
 __author__ = u'havard gulldahl no'# (HÃvard Gulldahl)' #BUG: api chokes on non-ascii chars in __author__
 __license__ = 'Apache License v2'
 
-try:
-  from xml.etree import cElementTree as ElementTree
-except ImportError:
-  try:
-    import cElementTree as ElementTree
-  except ImportError:
-    from elementtree import ElementTree
+
 import atom
 import gdata
 

Modified: trunk/conduit/modules/GoogleModule/gdata/photos/__init__.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/gdata/photos/__init__.py	(original)
+++ trunk/conduit/modules/GoogleModule/gdata/photos/__init__.py	Wed May  7 11:25:32 2008
@@ -38,14 +38,16 @@
 __version__ = '$Revision: 164 $'[11:-2]
 
 import re
-
 try:
   from xml.etree import cElementTree as ElementTree
 except ImportError:
   try:
     import cElementTree as ElementTree
   except ImportError:
-    from elementtree import ElementTree
+    try:
+      from xml.etree import ElementTree
+    except ImportError:
+      from elementtree import ElementTree
 import atom
 import gdata
 
@@ -96,7 +98,7 @@
   _attributes = gdata.GDataFeed._attributes.copy()
   _children = gdata.GDataFeed._children.copy()
   # We deal with Entry elements ourselves
-  _children.pop('{%s}entry' % atom.ATOM_NAMESPACE) 
+  del _children['{%s}entry' % atom.ATOM_NAMESPACE]
     
   def __init__(self, author=None, category=None, contributor=None,
                generator=None, icon=None, atom_id=None, link=None, logo=None,
@@ -352,6 +354,48 @@
 def SizeFromString(xml_string):
   return atom.CreateClassFromXMLString(Size, xml_string)
 
+class Snippet(PhotosBaseElement):
+  """The Google Photo `snippet' element.
+
+  When searching, the snippet element will contain a 
+  string with the word you're looking for, highlighted in html markup
+  E.g. when your query is `hafjell', this element may contain:
+  `... here at <b>Hafjell</b>.'
+
+  You'll find this element in searches -- that is, feeds that combine the 
+  `kind=photo' and `q=yoursearch' parameters in the request.
+
+  See also gphoto:truncated and gphoto:snippettype.
+  
+  """
+  
+  _tag = 'snippet'
+def SnippetFromString(xml_string):
+  return atom.CreateClassFromXMLString(Snippet, xml_string)
+
+class Snippettype(PhotosBaseElement):
+  """The Google Photo `Snippettype' element
+
+  When searching, this element will tell you the type of element that matches.
+
+  You'll find this element in searches -- that is, feeds that combine the 
+  `kind=photo' and `q=yoursearch' parameters in the request.
+
+  See also gphoto:snippet and gphoto:truncated.
+  
+  Possible values and their interpretation: 
+  o ALBUM_TITLE       - The album title matches 
+  o PHOTO_TAGS        - The match is a tag/keyword
+  o PHOTO_DESCRIPTION - The match is in the photo's description
+
+  If you discover a value not listed here, please submit a patch to update this docstring.
+  
+  """
+  
+  _tag = 'snippettype'
+def SnippettypeFromString(xml_string):
+  return atom.CreateClassFromXMLString(Snippettype, xml_string)
+
 class Thumbnail(PhotosBaseElement):
   """The Google Photo `Thumbnail' element
 
@@ -398,6 +442,22 @@
 def TimestampFromString(xml_string):
   return atom.CreateClassFromXMLString(Timestamp, xml_string)
 
+class Truncated(PhotosBaseElement):
+  """The Google Photo `Truncated' element
+
+  You'll find this element in searches -- that is, feeds that combine the 
+  `kind=photo' and `q=yoursearch' parameters in the request.
+
+  See also gphoto:snippet and gphoto:snippettype.
+  
+  Possible values and their interpretation:
+  0 -- unknown 
+  """
+  
+  _tag = 'Truncated'
+def TruncatedFromString(xml_string):
+  return atom.CreateClassFromXMLString(Truncated, xml_string)
+
 class User(PhotosBaseElement):
   "The Google Photo `User' element"
   
@@ -615,6 +675,10 @@
   _children['{%s}tags' % EXIF_NAMESPACE] = ('exif', Exif.Tags)
   _children['{%s}where' % GEORSS_NAMESPACE] = ('geo', Geo.Where)
   _children['{%s}group' % MEDIA_NAMESPACE] = ('media', Media.Group)
+  # These elements show up in search feeds 
+  _children['{%s}snippet' % PHOTOS_NAMESPACE] = ('snippet', Snippet)
+  _children['{%s}snippettype' % PHOTOS_NAMESPACE] = ('snippettype', Snippettype)
+  _children['{%s}truncated' % PHOTOS_NAMESPACE] = ('truncated', Truncated)
   gphoto_id = None
   albumid = None
   checksum = None
@@ -628,6 +692,9 @@
   width = None
   commentingEnabled = None
   commentCount = None
+  snippet=None
+  snippettype=None
+  truncated=None
   media = Media.Group()
   geo = Geo.Where()
   tags = Exif.Tags()

Modified: trunk/conduit/modules/GoogleModule/gdata/photos/service.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/gdata/photos/service.py	(original)
+++ trunk/conduit/modules/GoogleModule/gdata/photos/service.py	Wed May  7 11:25:32 2008
@@ -82,14 +82,6 @@
 
 import sys, os.path, StringIO
 import time
-try:
-  from xml.etree import cElementTree as ElementTree
-except ImportError:
-  try:
-    import cElementTree as ElementTree
-  except ImportError:
-    from elementtree import ElementTree
-
 import gdata.service
 import gdata
 import atom.service
@@ -128,13 +120,6 @@
         self.error_code = code
         break
     self.args = [self.error_code, self.reason, self.body]
-    #try:
-      #self.element_tree = ElementTree.fromstring(response['body'])
-      #self.error_code = int(self.element_tree[0].attrib['errorCode'])
-      #self.reason = self.element_tree[0].attrib['reason']
-      #self.invalidInput = self.element_tree[0].attrib['invalidInput']
-    #except:
-      #self.error_code = UNKOWN_ERROR
 
 class PhotosService(gdata.service.GDataService):
   userUri = '/data/feed/api/user/%s'
@@ -484,7 +469,7 @@
     if keywords is not None:
       if isinstance(keywords, list):
         keywords = ','.join(keywords)
-      metadata.media.keywords = gdata.photos.media.Keywords(text=keywords)
+      metadata.media.keywords = gdata.media.Keywords(text=keywords)
     return self.InsertPhoto(album_or_uri, metadata, filename_or_handle,
       content_type)
 
@@ -593,7 +578,7 @@
       entry_uri = photo_or_uri.GetEditMediaLink().href
     try:
       return self.Put(photoblob, entry_uri,
-      converter=gdata.photos.PhotoEntryFromString)
+          converter=gdata.photos.PhotoEntryFromString)
     except gdata.service.RequestError, e:
       raise GooglePhotosException(e.args[0])
 
@@ -681,8 +666,8 @@
 
 def GetSmallestThumbnail(media_thumbnail_list):
   """Helper function to get the smallest thumbnail of a list of
-    gdata.photos.media.Thumbnail.
-  Returns gdata.photos.media.Thumbnail """
+    gdata.media.Thumbnail.
+  Returns gdata.media.Thumbnail """
   r = {}
   for thumb in media_thumbnail_list:
       r[int(thumb.width)*int(thumb.height)] = thumb

Modified: trunk/conduit/modules/GoogleModule/gdata/service.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/gdata/service.py	(original)
+++ trunk/conduit/modules/GoogleModule/gdata/service.py	Wed May  7 11:25:32 2008
@@ -53,6 +53,7 @@
          called on it.
 """
 
+
 __author__ = 'api.jscudder (Jeffrey Scudder)'
 
 
@@ -65,7 +66,10 @@
   try:
     import cElementTree as ElementTree
   except ImportError:
-    from elementtree import ElementTree
+    try:
+      from xml.etree import ElementTree
+    except ImportError:
+      from elementtree import ElementTree
 import atom.service
 import gdata
 import atom
@@ -74,7 +78,13 @@
 
 PROGRAMMATIC_AUTH_LABEL = 'GoogleLogin auth'
 AUTHSUB_AUTH_LABEL = 'AuthSub token'
-AUTH_SERVER_HOST = 'https://www.google.com/'
+AUTH_SERVER_HOST = 'https://www.google.com'
+
+
+# Module level variable specifies which module should be used by GDataService
+# objects to make HttpRequests. This setting can be overridden on each 
+# instance of GDataService.
+http_request_handler = atom.service
 
 
 class Error(Exception):
@@ -114,25 +124,28 @@
 
   def __init__(self, email=None, password=None, account_type='HOSTED_OR_GOOGLE',
                service=None, source=None, server=None, 
-               additional_headers=None):
+               additional_headers=None, handler=None):
     """Creates an object of type GDataService.
 
     Args:
       email: string (optional) The user's email address, used for
-             authentication.
+          authentication.
       password: string (optional) The user's password.
       account_type: string (optional) The type of account to use. Use
-              'GOOGLE' for regular Google accounts or 'HOSTED' for Google
-              Apps accounts, or 'HOSTED_OR_GOOGLE' to try finding a HOSTED
-              account first and, if it doesn't exist, try finding a regular
-              GOOGLE account. Default value: 'HOSTED_OR_GOOGLE'.
+          'GOOGLE' for regular Google accounts or 'HOSTED' for Google
+          Apps accounts, or 'HOSTED_OR_GOOGLE' to try finding a HOSTED
+          account first and, if it doesn't exist, try finding a regular
+          GOOGLE account. Default value: 'HOSTED_OR_GOOGLE'.
       service: string (optional) The desired service for which credentials
-               will be obtained.
+          will be obtained.
       source: string (optional) The name of the user's application.
       server: string (optional) The name of the server to which a connection
-              will be opened. Default value: 'base.google.com'.
+          will be opened. Default value: 'base.google.com'.
       additional_headers: dictionary (optional) Any additional headers which 
-                          should be included with CRUD operations.
+          should be included with CRUD operations.
+      handler: module (optional) The module whose HttpRequest function 
+          should be used when making requests to the server. The default 
+          value is atom.service.
     """
 
     self.email = email
@@ -141,6 +154,7 @@
     self.service = service
     self.server = server
     self.additional_headers = additional_headers or {}
+    self.handler = handler or http_request_handler
     self.__SetSource(source)
     self.__auth_token = None
     self.__captcha_token = None
@@ -220,7 +234,9 @@
 
   def GetAuthSubToken(self):
     if self.__auth_token.startswith(AUTHSUB_AUTH_LABEL):
-      return self.__auth_token.lstrip(AUTHSUB_AUTH_LABEL + '=')
+      # Strip off the leading 'AUTHSUB_AUTH_LABEL=' and just return the
+      # token value.
+      return self.__auth_token[len(AUTHSUB_AUTH_LABEL)+1:]
     else:
       return None
 
@@ -229,7 +245,9 @@
 
   def GetClientLoginToken(self):
     if self.__auth_token.startswith(PROGRAMMATIC_AUTH_LABEL):
-      return self.__auth_token.lstrip(PROGRAMMATIC_AUTH_LABEL + '=')
+      # Strip off the leading 'PROGRAMMATIC_AUTH_LABEL=' and just return the
+      # token value.
+      return self.__auth_token[len(PROGRAMMATIC_AUTH_LABEL)+1:]
     else:
       return None
 
@@ -243,7 +261,7 @@
   def __SetSource(self, new_source):
     self.__source = new_source
     # Update the UserAgent header to include the new application name.
-    self.additional_headers['User-Agent'] = '%s GData-Python/1.0.9' % self.__source
+    self.additional_headers['User-Agent'] = '%s GData-Python/1.0.12.1' % self.__source
 
   source = property(__GetSource, __SetSource, 
       doc="""The source is the name of the application making the request. 
@@ -277,33 +295,19 @@
         self.password, self.service, self.source, self.account_type, 
         captcha_token, captcha_response)
 
-    # Open a connection to the authentication server.
-    (auth_connection, uri) = self._PrepareConnection(AUTH_SERVER_HOST)
-
-    # Begin the POST request to the client login service.
-    auth_connection.putrequest('POST', '/accounts/ClientLogin')
-    # Set the required headers for an Account Authentication request.
-    auth_connection.putheader('Content-type',
-                              'application/x-www-form-urlencoded')
-    auth_connection.putheader('Content-Length',str(len(request_body)))
-    auth_connection.endheaders()
-
-    auth_connection.send(request_body)
-
-    # Process the response and throw exceptions if the login request did not
-    # succeed.
-    auth_response = auth_connection.getresponse()
+    auth_response = self.handler.HttpRequest(self, 'POST', request_body, 
+        AUTH_SERVER_HOST + '/accounts/ClientLogin', 
+        extra_headers={'Content-Length':str(len(request_body))},
+        content_type='application/x-www-form-urlencoded')
     response_body = auth_response.read()
 
     if auth_response.status == 200:
-      
       self.__auth_token = gdata.auth.GenerateClientLoginAuthToken(
            response_body)
       self.__captcha_token = None
       self.__captcha_url = None
 
     elif auth_response.status == 403:
-
       # Examine each line to find the error type and the captcha token and
       # captch URL if they are present.
       captcha_parameters = gdata.auth.GetCaptchChallenge(response_body, 
@@ -377,7 +381,7 @@
 
     request_params = urllib.urlencode({'next': next, 'scope': scope,
                                     'secure': secure, 'session': session})
-    return '%saccounts/AuthSubRequest?%s' % (AUTH_SERVER_HOST, request_params)
+    return '%s/accounts/AuthSubRequest?%s' % (AUTH_SERVER_HOST, request_params)
 
   def UpgradeToSessionToken(self):
     """Upgrades a single use AuthSub token to a session token.
@@ -389,22 +393,16 @@
     if not self.__auth_token.startswith(AUTHSUB_AUTH_LABEL):
       raise NonAuthSubToken
 
-    (upgrade_connection, uri) = self._PrepareConnection(
-        AUTH_SERVER_HOST)
-    upgrade_connection.putrequest('GET', '/accounts/AuthSubSessionToken')
-    
-    upgrade_connection.putheader('Content-Type',
-                                 'application/x-www-form-urlencoded')
-    upgrade_connection.putheader('Authorization', self.__auth_token)
-    upgrade_connection.endheaders()
-
-    response = upgrade_connection.getresponse()
+    response = self.handler.HttpRequest(self, 'GET', None, 
+        AUTH_SERVER_HOST + '/accounts/AuthSubSessionToken', 
+        extra_headers={'Authorization':self.__auth_token}, 
+        content_type='application/x-www-form-urlencoded')
 
     response_body = response.read()
     if response.status == 200:
       for response_line in response_body.splitlines():
         if response_line.startswith('Token='):
-          self.__auth_token = response_line.lstrip('Token=')
+          self.SetAuthSubToken(response_line.lstrip('Token='))
 
   def RevokeAuthSubToken(self):
     """Revokes an existing AuthSub token.
@@ -416,21 +414,16 @@
     if not self.__auth_token.startswith(AUTHSUB_AUTH_LABEL):
       raise NonAuthSubToken
     
-    (revoke_connection, uri) = self._PrepareConnection(
-        AUTH_SERVER_HOST)
-    revoke_connection.putrequest('GET', '/accounts/AuthSubRevokeToken')
-    
-    revoke_connection.putheader('Content-Type', 
-                                'application/x-www-form-urlencoded')
-    revoke_connection.putheader('Authorization', self.__auth_token)
-    revoke_connection.endheaders()
-
-    response = revoke_connection.getresponse()
+    response = self.handler.HttpRequest(self, 'GET', None, 
+        AUTH_SERVER_HOST + '/accounts/AuthSubRevokeToken', 
+        extra_headers={'Authorization':self.__auth_token}, 
+        content_type='application/x-www-form-urlencoded')
     if response.status == 200:
       self.__auth_token = None
 
   # CRUD operations
-  def Get(self, uri, extra_headers=None, redirects_remaining=4, encoding='UTF-8', converter=None):
+  def Get(self, uri, extra_headers=None, redirects_remaining=4, 
+      encoding='UTF-8', converter=None):
     """Query the GData API with the given URI
 
     The uri is the portion of the URI after the server value 
@@ -482,7 +475,8 @@
         else:
           uri += '?gsessionid=%s' % (self.__gsessionid,)
 
-    server_response = atom.service.AtomService.Get(self, uri, extra_headers)
+    server_response = self.handler.HttpRequest(self, 'GET', None, uri, 
+        extra_headers=extra_headers)
     result_body = server_response.read()
 
     if server_response.status == 200:
@@ -526,9 +520,8 @@
     """Returns a MediaSource containing media and its metadata from the given
     URI string.
     """
-    connection = atom.service.AtomService._CreateConnection(self, uri, 'GET',
-        extra_headers)
-    response_handle = connection.getresponse()
+    response_handle = self.handler.HttpRequest(self, 'GET', None, uri, 
+        extra_headers=extra_headers)
     return gdata.MediaSource(response_handle, response_handle.getheader('Content-Type'),
         response_handle.getheader('Content-Length'))
 
@@ -609,12 +602,52 @@
     else:
       return None
 
-  def Post(self, data, uri, extra_headers=None, url_params=None, 
+  def Post(self, data, uri, extra_headers=None, url_params=None,
+           escape_params=True, redirects_remaining=4, media_source=None,
+           converter=None):
+    """Insert or update  data into a GData service at the given URI.
+
+    Args:
+      data: string, ElementTree._Element, atom.Entry, or gdata.GDataEntry The
+            XML to be sent to the uri.
+      uri: string The location (feed) to which the data should be inserted.
+           Example: '/base/feeds/items'.
+      extra_headers: dict (optional) HTTP headers which are to be included.
+                     The client automatically sets the Content-Type,
+                     Authorization, and Content-Length headers.
+      url_params: dict (optional) Additional URL parameters to be included
+                  in the URI. These are translated into query arguments
+                  in the form '&dict_key=value&...'.
+                  Example: {'max-results': '250'} becomes &max-results=250
+      escape_params: boolean (optional) If false, the calling code has already
+                     ensured that the query will form a valid URL (all
+                     reserved characters have been escaped). If true, this
+                     method will escape the query and any URL parameters
+                     provided.
+      media_source: MediaSource (optional) Container for the media to be sent
+          along with the entry, if provided.
+      converter: func (optional) A function which will be executed on the
+          server's response. Often this is a function like
+          GDataEntryFromString which will parse the body of the server's
+          response and return a GDataEntry.
+
+    Returns:
+      If the post succeeded, this method will return a GDataFeed, GDataEntry,
+      or the results of running converter on the server's result body (if
+      converter was specified).
+    """
+    return self.PostOrPut('POST', data, uri, extra_headers=extra_headers, 
+        url_params=url_params, escape_params=escape_params, 
+        redirects_remaining=redirects_remaining, 
+        media_source=media_source, converter=converter)
+
+  def PostOrPut(self, verb, data, uri, extra_headers=None, url_params=None, 
            escape_params=True, redirects_remaining=4, media_source=None, 
            converter=None):
     """Insert data into a GData service at the given URI.
 
     Args:
+      verb: string, either 'POST' or 'PUT'
       data: string, ElementTree._Element, atom.Entry, or gdata.GDataEntry The
             XML to be sent to the uri. 
       uri: string The location (feed) to which the data should be inserted. 
@@ -670,52 +703,38 @@
           media_source.content_type+'\r\n\r\n')
       multipart.append('\r\n--END_OF_PART--\r\n')
         
-      extra_headers['Content-Type'] = 'multipart/related; boundary=END_OF_PART'
       extra_headers['MIME-version'] = '1.0'
       extra_headers['Content-Length'] = str(len(multipart[0]) +
           len(multipart[1]) + len(multipart[2]) +
           len(data_str) + media_source.content_length)
-          
-      insert_connection = atom.service.AtomService._CreateConnection(self,
-          uri, 'POST', extra_headers, url_params, escape_params)
-
-      insert_connection.send(multipart[0])
-      insert_connection.send(data_str)
-      insert_connection.send(multipart[1])
-
-      while 1:
-        binarydata = media_source.file_handle.read(100000)
-        if (binarydata == ""): break
-        insert_connection.send(binarydata)
-        
-      insert_connection.send(multipart[2])
-      
-      server_response = insert_connection.getresponse()
+
+      server_response = self.handler.HttpRequest(self, verb, 
+          [multipart[0], data_str, multipart[1], media_source.file_handle,
+              multipart[2]], uri,
+          extra_headers=extra_headers, url_params=url_params, 
+          escape_params=escape_params, 
+          content_type='multipart/related; boundary=END_OF_PART')
       result_body = server_response.read()
       
-    elif media_source:
-      extra_headers['Content-Type'] = media_source.content_type
+    elif media_source or isinstance(data, gdata.MediaSource):
+      if isinstance(data, gdata.MediaSource):
+        media_source = data
       extra_headers['Content-Length'] = media_source.content_length
-      insert_connection = atom.service.AtomService._CreateConnection(self, uri,
-          'POST', extra_headers, url_params, escape_params)
-
-      while 1:
-        binarydata = media_source.file_handle.read(100000)
-        if (binarydata == ""): break
-        insert_connection.send(binarydata)
-      
-      server_response = insert_connection.getresponse()
+      server_response = self.handler.HttpRequest(self, verb, 
+          media_source.file_handle, uri, extra_headers=extra_headers, 
+          url_params=url_params, escape_params=escape_params, 
+          content_type=media_source.content_type)
       result_body = server_response.read()
 
     else:
       http_data = data
       content_type = 'application/atom+xml'
-
-      server_response = atom.service.AtomService.Post(self, http_data, uri,
-          extra_headers, url_params, escape_params, content_type)
+      server_response = self.handler.HttpRequest(self, verb, 
+          http_data, uri, extra_headers=extra_headers, 
+          url_params=url_params, escape_params=escape_params, 
+          content_type=content_type)
       result_body = server_response.read()
 
-
     # Server returns 201 for most post requests, but when performing a batch
     # request the server responds with a 200 on success.
     if server_response.status == 201 or server_response.status == 200:
@@ -782,108 +801,10 @@
       or the results of running converter on the server's result body (if
       converter was specified).
     """
-    if extra_headers is None:
-      extra_headers = {}
-
-    # Add the authentication header to the Get request
-    if self.__auth_token:
-      extra_headers['Authorization'] = self.__auth_token
-
-    if self.__gsessionid is not None:
-      if uri.find('gsessionid=') < 0:
-        if uri.find('?') > -1:
-          uri += '&gsessionid=%s' % (self.__gsessionid,)
-        else:
-          uri += '?gsessionid=%s' % (self.__gsessionid,)
-
-    if media_source and data:
-      if ElementTree.iselement(data):
-        data_str = ElementTree.tostring(data)
-      else:
-        data_str = str(data)
-
-      multipart = []
-      multipart.append('Media multipart posting\r\n--END_OF_PART\r\n' + \
-          'Content-Type: application/atom+xml\r\n\r\n')
-      multipart.append('\r\n--END_OF_PART\r\nContent-Type: ' + \
-          media_source.content_type+'\r\n\r\n')
-      multipart.append('\r\n--END_OF_PART--\r\n')
-        
-      extra_headers['Content-Type'] = 'multipart/related; boundary=END_OF_PART'
-      extra_headers['MIME-version'] = '1.0'
-      extra_headers['Content-Length'] = str(len(multipart[0]) +
-          len(multipart[1]) + len(multipart[2]) +
-          len(data_str) + media_source.content_length)
-          
-      insert_connection = atom.service.AtomService._CreateConnection(self, uri,
-          'PUT', extra_headers, url_params, escape_params)
-
-      insert_connection.send(multipart[0])
-      insert_connection.send(data_str)
-      insert_connection.send(multipart[1])
-      
-      while 1:
-        binarydata = media_source.file_handle.read(100000)
-        if (binarydata == ""): break
-        insert_connection.send(binarydata)
-        
-      insert_connection.send(multipart[2])
-      
-      server_response = insert_connection.getresponse()
-      result_body = server_response.read()
-
-    elif media_source:
-      extra_headers['Content-Type'] = media_source.content_type
-      extra_headers['Content-Length'] = media_source.content_length
-      insert_connection = atom.service.AtomService._CreateConnection(self, uri,
-          'PUT', extra_headers, url_params, escape_params)
-
-      while 1:
-        binarydata = media_source.file_handle.read(100000)
-        if (binarydata == ""): break
-        insert_connection.send(binarydata)
-      
-      server_response = insert_connection.getresponse()
-      result_body = server_response.read()
-    else:
-      http_data = data
-      content_type = 'application/atom+xml'
-
-      server_response = atom.service.AtomService.Put(self, http_data, uri,
-          extra_headers, url_params, escape_params, content_type)
-      result_body = server_response.read()
-
-    if server_response.status == 200:
-      if converter:
-        return converter(result_body)
-      feed = gdata.GDataFeedFromString(result_body)
-      if not feed:
-        entry = gdata.GDataEntryFromString(result_body)
-        if not entry:
-          return result_body
-        return entry
-      return feed
-    elif server_response.status == 302:
-      if redirects_remaining > 0:
-        location = server_response.getheader('Location')
-        if location is not None:
-          m = re.compile('[\?\&]gsessionid=(\w*)').search(location)
-          if m is not None:
-            self.__gsessionid = m.group(1) 
-          return self.Put(data, location, extra_headers, url_params,
-              escape_params, redirects_remaining - 1, 
-              media_source=media_source, converter=converter)
-        else:
-          raise RequestError, {'status': server_response.status,
-              'reason': '302 received without Location header',
-              'body': result_body}
-      else:
-        raise RequestError, {'status': server_response.status,
-            'reason': 'Redirect received, but redirects_remaining <= 0',
-            'body': result_body}
-    else:
-      raise RequestError, {'status': server_response.status,
-          'reason': server_response.reason, 'body': result_body}
+    return self.PostOrPut('PUT', data, uri, extra_headers=extra_headers, 
+        url_params=url_params, escape_params=escape_params, 
+        redirects_remaining=redirects_remaining, 
+        media_source=media_source, converter=converter)
 
   def Delete(self, uri, extra_headers=None, url_params=None, 
              escape_params=True, redirects_remaining=4):
@@ -921,9 +842,10 @@
           uri += '&gsessionid=%s' % (self.__gsessionid,)
         else:
           uri += '?gsessionid=%s' % (self.__gsessionid,)
-                                                  
-    server_response = atom.service.AtomService.Delete(self, uri,
-        extra_headers, url_params, escape_params)
+  
+    server_response = self.handler.HttpRequest(self, 'DELETE', None, uri,
+        extra_headers=extra_headers, url_params=url_params, 
+        escape_params=escape_params)
     result_body = server_response.read()
 
     if server_response.status == 200:
@@ -986,7 +908,7 @@
     """
     
     self.feed = feed
-    self.categories = []
+    self.categories = categories or []
     if text_query:
       self.text_query = text_query
     if isinstance(params, dict):
@@ -1108,6 +1030,17 @@
   max_results = property(_GetMaxResults, _SetMaxResults,
       doc="""The feed query's max-results parameter""")
 
+  def _GetOrderBy(self):
+    if 'orderby' in self.keys():
+      return self['orderby']
+    else:
+      return None
+ 
+  def _SetOrderBy(self, query):
+    self['orderby'] = query
+  
+  orderby = property(_GetOrderBy, _SetOrderBy, 
+      doc="""The feed query's orderby parameter""")
 
   def ToUri(self):
     q_feed = self.feed or ''
@@ -1118,4 +1051,5 @@
       q_feed = q_feed + '/-/' + category_string
     return atom.service.BuildUri(q_feed, self)
 
-  
+  def __str__(self):
+    return self.ToUri()

Added: trunk/conduit/modules/GoogleModule/gdata/spreadsheet/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/spreadsheet/Makefile.am	Wed May  7 11:25:32 2008
@@ -0,0 +1,5 @@
+conduit_handlersdir = $(libdir)/conduit/modules/GoogleModule/gdata/spreadsheet
+conduit_handlers_PYTHON = __init__.py service.py text_db.py
+
+clean-local:
+	rm -rf *.pyc *.pyo

Added: trunk/conduit/modules/GoogleModule/gdata/spreadsheet/__init__.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/spreadsheet/__init__.py	Wed May  7 11:25:32 2008
@@ -0,0 +1,474 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2007 Google Inc.
+#
+# 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.
+
+"""Contains extensions to Atom objects used with Google Spreadsheets.
+"""
+
+__author__ = 'api laurabeth gmail com (Laura Beth Lincoln)'
+
+
+try:
+  from xml.etree import cElementTree as ElementTree
+except ImportError:
+  try:
+    import cElementTree as ElementTree
+  except ImportError:
+    try:
+      from xml.etree import ElementTree
+    except ImportError:
+      from elementtree import ElementTree
+import atom
+import gdata
+import re
+import string
+
+
+# XML namespaces which are often used in Google Spreadsheets entities.
+GSPREADSHEETS_NAMESPACE = 'http://schemas.google.com/spreadsheets/2006'
+GSPREADSHEETS_TEMPLATE = '{http://schemas.google.com/spreadsheets/2006}%s'
+
+GSPREADSHEETS_EXTENDED_NAMESPACE = ('http://schemas.google.com/spreadsheets'
+                                    '/2006/extended')
+GSPREADSHEETS_EXTENDED_TEMPLATE = ('{http://schemas.google.com/spreadsheets'
+                                   '/2006/extended}%s')
+
+
+class ColCount(atom.AtomBase):
+  """The Google Spreadsheets colCount element """
+  
+  _tag = 'colCount'
+  _namespace = GSPREADSHEETS_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  def __init__(self, text=None, extension_elements=None,
+      extension_attributes=None):
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+    
+def ColCountFromString(xml_string):
+  return atom.CreateClassFromXMLString(ColCount, xml_string)
+
+
+class RowCount(atom.AtomBase):
+  """The Google Spreadsheets rowCount element """
+  
+  _tag = 'rowCount'
+  _namespace = GSPREADSHEETS_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  def __init__(self, text=None, extension_elements=None,
+      extension_attributes=None):
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+def RowCountFromString(xml_string):
+  return atom.CreateClassFromXMLString(RowCount, xml_string)
+      
+
+class Cell(atom.AtomBase):
+  """The Google Spreadsheets cell element """
+  
+  _tag = 'cell'
+  _namespace = GSPREADSHEETS_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['row'] = 'row'
+  _attributes['col'] = 'col'
+  _attributes['inputValue'] = 'inputValue'
+  _attributes['numericValue'] = 'numericValue'
+  
+  def __init__(self, text=None, row=None, col=None, inputValue=None, 
+      numericValue=None, extension_elements=None, extension_attributes=None):
+    self.text = text
+    self.row = row
+    self.col = col
+    self.inputValue = inputValue
+    self.numericValue = numericValue
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def CellFromString(xml_string):
+  return atom.CreateClassFromXMLString(Cell, xml_string)
+
+
+class Custom(atom.AtomBase):
+  """The Google Spreadsheets custom element"""
+  
+  _namespace = GSPREADSHEETS_EXTENDED_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  def __init__(self, column=None, text=None, extension_elements=None,
+      extension_attributes=None):
+    self.column = column   # The name of the column
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+    
+  def _BecomeChildElement(self, tree):
+    new_child = ElementTree.Element('')
+    tree.append(new_child)
+    new_child.tag = '{%s}%s' % (self.__class__._namespace, 
+                                self.column)
+    self._AddMembersToElementTree(new_child)
+  
+  def _ToElementTree(self):
+    new_tree = ElementTree.Element('{%s}%s' % (self.__class__._namespace,
+                                               self.column))
+    self._AddMembersToElementTree(new_tree)
+    return new_tree
+    
+  def _HarvestElementTree(self, tree):
+    namespace_uri, local_tag = string.split(tree.tag[1:], "}", 1)
+    self.column = local_tag
+    # Fill in the instance members from the contents of the XML tree.
+    for child in tree:
+      self._ConvertElementTreeToMember(child)
+    for attribute, value in tree.attrib.iteritems():
+      self._ConvertElementAttributeToMember(attribute, value)
+    self.text = tree.text
+
+
+def CustomFromString(xml_string):
+  element_tree = ElementTree.fromstring(xml_string)
+  return _CustomFromElementTree(element_tree)
+
+  
+def _CustomFromElementTree(element_tree):
+  namespace_uri, local_tag = string.split(element_tree.tag[1:], "}", 1)
+  if namespace_uri == GSPREADSHEETS_EXTENDED_NAMESPACE:
+    new_custom = Custom()
+    new_custom._HarvestElementTree(element_tree)
+    new_custom.column = local_tag
+    return new_custom
+  return None
+
+  
+
+
+                                                  
+class SpreadsheetsSpreadsheet(gdata.GDataEntry):
+  """A Google Spreadsheets flavor of a Spreadsheet Atom Entry """
+  
+  _tag = 'entry'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+  
+  def __init__(self, author=None, category=None, content=None,
+      contributor=None, atom_id=None, link=None, published=None, rights=None,
+      source=None, summary=None, title=None, control=None, updated=None,
+      text=None, extension_elements=None, extension_attributes=None):
+    self.author = author or []
+    self.category = category or []
+    self.content = content
+    self.contributor = contributor or []
+    self.id = atom_id
+    self.link = link or []
+    self.published = published
+    self.rights = rights
+    self.source = source
+    self.summary = summary
+    self.control = control
+    self.title = title
+    self.updated = updated
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+    
+    
+def SpreadsheetsSpreadsheetFromString(xml_string):
+  return atom.CreateClassFromXMLString(SpreadsheetsSpreadsheet, 
+                                       xml_string)
+
+
+class SpreadsheetsWorksheet(gdata.GDataEntry):
+  """A Google Spreadsheets flavor of a Worksheet Atom Entry """
+  
+  _tag = 'entry'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+  _children['{%s}rowCount' % GSPREADSHEETS_NAMESPACE] = ('row_count', 
+                                                         RowCount)
+  _children['{%s}colCount' % GSPREADSHEETS_NAMESPACE] = ('col_count', 
+                                                         ColCount)
+  
+  def __init__(self, author=None, category=None, content=None,
+      contributor=None, atom_id=None, link=None, published=None, rights=None,
+      source=None, summary=None, title=None, control=None, updated=None, 
+      row_count=None, col_count=None, text=None, extension_elements=None, 
+      extension_attributes=None):
+    self.author = author or []
+    self.category = category or []
+    self.content = content
+    self.contributor = contributor or []
+    self.id = atom_id
+    self.link = link or []
+    self.published = published
+    self.rights = rights
+    self.source = source
+    self.summary = summary
+    self.control = control
+    self.title = title
+    self.updated = updated
+    self.row_count = row_count
+    self.col_count = col_count
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+    
+def SpreadsheetsWorksheetFromString(xml_string):
+  return atom.CreateClassFromXMLString(SpreadsheetsWorksheet, 
+                                       xml_string)
+
+
+class SpreadsheetsCell(gdata.BatchEntry):
+  """A Google Spreadsheets flavor of a Cell Atom Entry """
+  
+  _tag = 'entry'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.BatchEntry._children.copy()
+  _attributes = gdata.BatchEntry._attributes.copy()
+  _children['{%s}cell' % GSPREADSHEETS_NAMESPACE] = ('cell', Cell)
+  
+  def __init__(self, author=None, category=None, content=None,
+      contributor=None, atom_id=None, link=None, published=None, rights=None,
+      source=None, summary=None, title=None, control=None, updated=None, 
+      cell=None, batch_operation=None, batch_id=None, batch_status=None,
+      text=None, extension_elements=None, extension_attributes=None):
+    self.author = author or []
+    self.category = category or []
+    self.content = content
+    self.contributor = contributor or []
+    self.id = atom_id
+    self.link = link or []
+    self.published = published
+    self.rights = rights
+    self.source = source
+    self.summary = summary
+    self.control = control
+    self.title = title
+    self.batch_operation = batch_operation
+    self.batch_id = batch_id
+    self.batch_status = batch_status
+    self.updated = updated
+    self.cell = cell
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def SpreadsheetsCellFromString(xml_string):
+  return atom.CreateClassFromXMLString(SpreadsheetsCell, 
+                                       xml_string)
+
+                                       
+class SpreadsheetsList(gdata.GDataEntry):
+  """A Google Spreadsheets flavor of a List Atom Entry """
+  
+  _tag = 'entry'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+  
+  def __init__(self, author=None, category=None, content=None,
+      contributor=None, atom_id=None, link=None, published=None, rights=None,
+      source=None, summary=None, title=None, control=None, updated=None, 
+      custom=None, 
+      text=None, extension_elements=None, extension_attributes=None):
+    self.author = author or []
+    self.category = category or []
+    self.content = content
+    self.contributor = contributor or []
+    self.id = atom_id
+    self.link = link or []
+    self.published = published
+    self.rights = rights
+    self.source = source
+    self.summary = summary
+    self.control = control
+    self.title = title
+    self.updated = updated
+    self.custom = custom or {}
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+    
+  # We need to overwrite _ConvertElementTreeToMember to add special logic to
+  # convert custom attributes to members
+  def _ConvertElementTreeToMember(self, child_tree):
+    # Find the element's tag in this class's list of child members
+    if self.__class__._children.has_key(child_tree.tag):
+      member_name = self.__class__._children[child_tree.tag][0]
+      member_class = self.__class__._children[child_tree.tag][1]
+      # If the class member is supposed to contain a list, make sure the
+      # matching member is set to a list, then append the new member
+      # instance to the list.
+      if isinstance(member_class, list):
+        if getattr(self, member_name) is None:
+          setattr(self, member_name, [])
+        getattr(self, member_name).append(atom._CreateClassFromElementTree(
+            member_class[0], child_tree))
+      else:
+        setattr(self, member_name, 
+                atom._CreateClassFromElementTree(member_class, child_tree))
+    elif child_tree.tag.find('{%s}' % GSPREADSHEETS_EXTENDED_NAMESPACE) == 0:
+      # If this is in the custom namespace, make add it to the custom dict.
+      name = child_tree.tag[child_tree.tag.index('}')+1:]
+      custom = _CustomFromElementTree(child_tree)
+      if custom:
+        self.custom[name] = custom
+    else:
+      ExtensionContainer._ConvertElementTreeToMember(self, child_tree)
+  
+  # We need to overwtite _AddMembersToElementTree to add special logic to
+  # convert custom members to XML nodes.
+  def _AddMembersToElementTree(self, tree):
+    # Convert the members of this class which are XML child nodes. 
+    # This uses the class's _children dictionary to find the members which
+    # should become XML child nodes.
+    member_node_names = [values[0] for tag, values in 
+                                       self.__class__._children.iteritems()]
+    for member_name in member_node_names:
+      member = getattr(self, member_name)
+      if member is None:
+        pass
+      elif isinstance(member, list):
+        for instance in member:
+          instance._BecomeChildElement(tree)
+      else:
+        member._BecomeChildElement(tree)
+    # Convert the members of this class which are XML attributes.
+    for xml_attribute, member_name in self.__class__._attributes.iteritems():
+      member = getattr(self, member_name)
+      if member is not None:
+        tree.attrib[xml_attribute] = member
+    # Convert all special custom item attributes to nodes
+    for name, custom in self.custom.iteritems():
+      custom._BecomeChildElement(tree)
+    # Lastly, call the ExtensionContainers's _AddMembersToElementTree to 
+    # convert any extension attributes.
+    atom.ExtensionContainer._AddMembersToElementTree(self, tree)
+
+  
+def SpreadsheetsListFromString(xml_string):
+  return atom.CreateClassFromXMLString(SpreadsheetsList, 
+                                       xml_string)
+  element_tree = ElementTree.fromstring(xml_string)
+  return _SpreadsheetsListFromElementTree(element_tree)
+
+
+class SpreadsheetsSpreadsheetsFeed(gdata.GDataFeed):
+  """A feed containing Google Spreadsheets Spreadsheets"""
+  
+  _tag = 'feed'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', 
+                                                  [SpreadsheetsSpreadsheet])
+
+
+def SpreadsheetsSpreadsheetsFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(SpreadsheetsSpreadsheetsFeed, 
+                                       xml_string)
+                                       
+      
+class SpreadsheetsWorksheetsFeed(gdata.GDataFeed):
+  """A feed containing Google Spreadsheets Spreadsheets"""
+  
+  _tag = 'feed'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', 
+                                                  [SpreadsheetsWorksheet])
+
+
+def SpreadsheetsWorksheetsFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(SpreadsheetsWorksheetsFeed, 
+                                       xml_string)
+
+
+class SpreadsheetsCellsFeed(gdata.BatchFeed):
+  """A feed containing Google Spreadsheets Cells"""
+  
+  _tag = 'feed'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.BatchFeed._children.copy()
+  _attributes = gdata.BatchFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', 
+                                                  [SpreadsheetsCell])
+  _children['{%s}rowCount' % GSPREADSHEETS_NAMESPACE] = ('row_count', 
+                                                         RowCount)
+  _children['{%s}colCount' % GSPREADSHEETS_NAMESPACE] = ('col_count', 
+                                                         ColCount)
+                                                  
+  def __init__(self, author=None, category=None, contributor=None,
+               generator=None, icon=None, atom_id=None, link=None, logo=None, 
+               rights=None, subtitle=None, title=None, updated=None,
+               entry=None, total_results=None, start_index=None,
+               items_per_page=None, extension_elements=None,
+               extension_attributes=None, text=None, row_count=None,
+               col_count=None, interrupted=None):
+    gdata.BatchFeed.__init__(self, author=author, category=category,
+                             contributor=contributor, generator=generator,
+                             icon=icon,  atom_id=atom_id, link=link,
+                             logo=logo, rights=rights, subtitle=subtitle,
+                             title=title, updated=updated, entry=entry,
+                             total_results=total_results,
+                             start_index=start_index,
+                             items_per_page=items_per_page,
+                             extension_elements=extension_elements,
+                             extension_attributes=extension_attributes,
+                             text=text, interrupted=interrupted)
+    self.row_count = row_count
+    self.col_count = col_count
+
+  def GetBatchLink(self):
+    for link in self.link:
+      if link.rel == 'http://schemas.google.com/g/2005#batch':
+        return link
+    return None
+
+
+def SpreadsheetsCellsFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(SpreadsheetsCellsFeed, 
+                                       xml_string)
+
+      
+class SpreadsheetsListFeed(gdata.GDataFeed):
+  """A feed containing Google Spreadsheets Spreadsheets"""
+  
+  _tag = 'feed'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', 
+                                                  [SpreadsheetsList])
+
+
+def SpreadsheetsListFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(SpreadsheetsListFeed, 
+                                       xml_string)

Added: trunk/conduit/modules/GoogleModule/gdata/spreadsheet/service.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/spreadsheet/service.py	Wed May  7 11:25:32 2008
@@ -0,0 +1,467 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2007 Google Inc.
+#
+# 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.
+
+"""SpreadsheetsService extends the GDataService to streamline Google
+Spreadsheets operations.
+
+  GBaseService: Provides methods to query feeds and manipulate items. Extends
+                GDataService.
+
+  DictionaryToParamList: Function which converts a dictionary into a list of
+                         URL arguments (represented as strings). This is a
+                         utility function used in CRUD operations.
+"""
+
+__author__ = 'api laurabeth gmail com (Laura Beth Lincoln)'
+
+
+import gdata
+import atom.service
+import gdata.service
+import gdata.spreadsheet
+import atom
+
+
+class Error(Exception):
+  """Base class for exceptions in this module."""
+  pass
+
+
+class RequestError(Error):
+  pass
+
+
+class SpreadsheetsService(gdata.service.GDataService):
+  """Client for the Google Spreadsheets service."""
+
+  def __init__(self, email=None, password=None, source=None,
+               server='spreadsheets.google.com',
+               additional_headers=None):
+    gdata.service.GDataService.__init__(self, email=email, password=password,
+                                        service='wise', source=source,
+                                        server=server,
+                                        additional_headers=additional_headers)
+                                        
+  def GetSpreadsheetsFeed(self, key=None, query=None, visibility='private', 
+      projection='full'):
+    """Gets a spreadsheets feed or a specific entry if a key is defined
+    Args:
+      key: string (optional) The spreadsheet key defined in /ccc?key=
+      query: DocumentQuery (optional) Query parameters
+      
+    Returns:
+      If there is no key, then a SpreadsheetsSpreadsheetsFeed.
+      If there is a key, then a SpreadsheetsSpreadsheet.
+    """
+    
+    uri = ('http://%s/feeds/spreadsheets/%s/%s' 
+           % (self.server, visibility, projection))
+    
+    if key is not None:
+      uri = '%s/%s' % (uri, key)
+      
+    if query != None:
+      query.feed = uri
+      uri = query.ToUri()
+
+    if key:
+      return self.Get(uri, 
+          converter=gdata.spreadsheet.SpreadsheetsSpreadsheetFromString)
+    else:
+      return self.Get(uri,
+          converter=gdata.spreadsheet.SpreadsheetsSpreadsheetsFeedFromString)
+  
+  def GetWorksheetsFeed(self, key, wksht_id=None, query=None, 
+      visibility='private', projection='full'):
+    """Gets a worksheets feed or a specific entry if a wksht is defined
+    Args:
+      key: string The spreadsheet key defined in /ccc?key=
+      wksht_id: string (optional) The id for a specific worksheet entry
+      query: DocumentQuery (optional) Query parameters
+      
+    Returns:
+      If there is no wksht_id, then a SpreadsheetsWorksheetsFeed.
+      If there is a wksht_id, then a SpreadsheetsWorksheet.
+    """
+    
+    uri = ('http://%s/feeds/worksheets/%s/%s/%s' 
+           % (self.server, key, visibility, projection))
+    
+    if wksht_id != None:
+      uri = '%s/%s' % (uri, wksht_id)
+      
+    if query != None:
+      query.feed = uri
+      uri = query.ToUri()
+
+    if wksht_id:
+      return self.Get(uri, 
+          converter=gdata.spreadsheet.SpreadsheetsWorksheetFromString)
+    else:
+      return self.Get(uri,
+          converter=gdata.spreadsheet.SpreadsheetsWorksheetsFeedFromString)
+
+  def AddWorksheet(self, title, row_count, col_count, key):
+    """Creates a new worksheet in the desired spreadsheet.
+
+    The new worksheet is appended to the end of the list of worksheets. The
+    new worksheet will only have the available number of columns and cells 
+    specified.
+
+    Args:
+      title: str The title which will be displayed in the list of worksheets.
+      row_count: int or str The number of rows in the new worksheet.
+      col_count: int or str The number of columns in the new worksheet.
+      key: str The spreadsheet key to the spreadsheet to which the new 
+          worksheet should be added. 
+
+    Returns:
+      A SpreadsheetsWorksheet if the new worksheet was created succesfully.  
+    """
+    new_worksheet = gdata.spreadsheet.SpreadsheetsWorksheet(
+        title=atom.Title(text=title), 
+        row_count=gdata.spreadsheet.RowCount(text=str(row_count)), 
+        col_count=gdata.spreadsheet.ColCount(text=str(col_count)))
+    return self.Post(new_worksheet, 
+        'http://%s/feeds/worksheets/%s/private/full' % (self.server, key),
+        converter=gdata.spreadsheet.SpreadsheetsWorksheetFromString)
+
+  def UpdateWorksheet(self, worksheet_entry, url=None):
+    """Changes the size and/or title of the desired worksheet.
+    
+    Args:
+      worksheet_entry: SpreadsheetWorksheet The new contents of the 
+          worksheet. 
+      url: str (optional) The URL to which the edited worksheet entry should
+          be sent. If the url is None, the edit URL from the worksheet will
+          be used.
+
+    Returns: 
+      A SpreadsheetsWorksheet with the new information about the worksheet.
+    """
+    target_url = url or worksheet_entry.GetEditLink().href
+    return self.Put(worksheet_entry, target_url, 
+        converter=gdata.spreadsheet.SpreadsheetsWorksheetFromString)
+    
+  def DeleteWorksheet(self, worksheet_entry=None, url=None):
+    """Removes the desired worksheet from the spreadsheet
+    
+    Args:
+      worksheet_entry: SpreadsheetWorksheet (optional) The worksheet to
+          be deleted. If this is none, then the DELETE reqest is sent to 
+          the url specified in the url parameter.
+      url: str (optaional) The URL to which the DELETE request should be
+          sent. If left as None, the worksheet's edit URL is used.
+
+    Returns:
+      True if the worksheet was deleted successfully. 
+    """
+    if url:
+      target_url = url
+    else:
+      target_url = worksheet_entry.GetEditLink().href
+    return self.Delete(target_url)
+  
+  def GetCellsFeed(self, key, wksht_id='default', cell=None, query=None, 
+      visibility='private', projection='full'):
+    """Gets a cells feed or a specific entry if a cell is defined
+    Args:
+      key: string The spreadsheet key defined in /ccc?key=
+      wksht_id: string The id for a specific worksheet entry
+      cell: string (optional) The R1C1 address of the cell
+      query: DocumentQuery (optional) Query parameters
+      
+    Returns:
+      If there is no cell, then a SpreadsheetsCellsFeed.
+      If there is a cell, then a SpreadsheetsCell.
+    """
+    
+    uri = ('http://%s/feeds/cells/%s/%s/%s/%s' 
+           % (self.server, key, wksht_id, visibility, projection))
+    
+    if cell != None:
+      uri = '%s/%s' % (uri, cell)
+      
+    if query != None:
+      query.feed = uri
+      uri = query.ToUri()
+
+    if cell:
+      return self.Get(uri, 
+          converter=gdata.spreadsheet.SpreadsheetsCellFromString)
+    else:
+      return self.Get(uri, 
+          converter=gdata.spreadsheet.SpreadsheetsCellsFeedFromString)
+  
+  def GetListFeed(self, key, wksht_id='default', row_id=None, query=None,
+      visibility='private', projection='full'):
+    """Gets a list feed or a specific entry if a row_id is defined
+    Args:
+      key: string The spreadsheet key defined in /ccc?key=
+      wksht_id: string The id for a specific worksheet entry
+      row_id: string (optional) The row_id of a row in the list
+      query: DocumentQuery (optional) Query parameters
+      
+    Returns:
+      If there is no row_id, then a SpreadsheetsListFeed.
+      If there is a row_id, then a SpreadsheetsList.
+    """
+    
+    uri = ('http://%s/feeds/list/%s/%s/%s/%s' 
+           % (self.server, key, wksht_id, visibility, projection))
+
+    if row_id is not None:
+      uri = '%s/%s' % (uri, row_id)
+      
+    if query is not None:
+      query.feed = uri
+      uri = query.ToUri()
+
+    if row_id:
+      return self.Get(uri, 
+          converter=gdata.spreadsheet.SpreadsheetsListFromString)
+    else:
+      return self.Get(uri, 
+          converter=gdata.spreadsheet.SpreadsheetsListFeedFromString)
+    
+  def UpdateCell(self, row, col, inputValue, key, wksht_id='default'):
+    """Updates an existing cell.
+    
+    Args:
+      row: int The row the cell to be editted is in
+      col: int The column the cell to be editted is in
+      inputValue: str the new value of the cell
+      key: str The key of the spreadsheet in which this cell resides.
+      wksht_id: str The ID of the worksheet which holds this cell.
+      
+    Returns:
+      The updated cell entry
+    """
+    row = str(row)
+    col = str(col)
+    # make the new cell
+    new_cell = gdata.spreadsheet.Cell(row=row, col=col, inputValue=inputValue)
+    # get the edit uri and PUT
+    cell = 'R%sC%s' % (row, col)
+    entry = self.GetCellsFeed(key, wksht_id, cell)
+    for a_link in entry.link:
+      if a_link.rel == 'edit':
+        entry.cell = new_cell
+        return self.Put(entry, a_link.href, 
+            converter=gdata.spreadsheet.SpreadsheetsCellFromString)
+
+  def _GenerateCellsBatchUrl(self, spreadsheet_key, worksheet_id):
+    return ('http://spreadsheets.google.com/feeds/cells/%s/%s/'
+            'private/full/batch' % (spreadsheet_key, worksheet_id))
+
+  def ExecuteBatch(self, batch_feed, url=None, spreadsheet_key=None, 
+      worksheet_id=None,
+      converter=gdata.spreadsheet.SpreadsheetsCellsFeedFromString):
+    """Sends a batch request feed to the server.
+
+    The batch request needs to be sent to the batch URL for a particular 
+    worksheet. You can specify the worksheet by providing the spreadsheet_key
+    and worksheet_id, or by sending the URL from the cells feed's batch link.
+
+    Args:
+      batch_feed: gdata.spreadsheet.SpreadsheetsCellFeed A feed containing 
+          BatchEntry elements which contain the desired CRUD operation and 
+          any necessary data to modify a cell.
+      url: str (optional) The batch URL for the cells feed to which these 
+          changes should be applied. This can be found by calling 
+          cells_feed.GetBatchLink().href.
+      spreadsheet_key: str (optional) Used to generate the batch request URL
+          if the url argument is None. If using the spreadsheet key to 
+          generate the URL, the worksheet id is also required.
+      worksheet_id: str (optional) Used if the url is not provided, it is 
+          oart of the batch feed target URL. This is used with the spreadsheet
+          key.
+      converter: Function (optional) Function to be executed on the server's
+          response. This function should take one string as a parameter. The
+          default value is SpreadsheetsCellsFeedFromString which will turn the result
+          into a gdata.base.GBaseItem object.
+
+    Returns:
+      A gdata.BatchFeed containing the results.
+    """
+
+    if url is None:
+      url = self._GenerateCellsBatchUrl(spreadsheet_key, worksheet_id)
+    return self.Post(batch_feed, url, converter=converter)
+    
+  def InsertRow(self, row_data, key, wksht_id='default'):
+    """Inserts a new row with the provided data
+    
+    Args:
+      uri: string The post uri of the list feed
+      row_data: dict A dictionary of column header to row data
+    
+    Returns:
+      The inserted row
+    """
+    new_entry = gdata.spreadsheet.SpreadsheetsList()
+    for k, v in row_data.iteritems():
+      new_custom = gdata.spreadsheet.Custom()
+      new_custom.column = k
+      new_custom.text = v
+      new_entry.custom[new_custom.column] = new_custom
+    # Generate the post URL for the worksheet which will receive the new entry.
+    post_url = 'http://spreadsheets.google.com/feeds/list/%s/%s/private/full'%(
+        key, wksht_id) 
+    return self.Post(new_entry, post_url, 
+        converter=gdata.spreadsheet.SpreadsheetsListFromString)
+    
+  def UpdateRow(self, entry, new_row_data):
+    """Updates a row with the provided data
+    
+    Args:
+      entry: gdata.spreadsheet.SpreadsheetsList The entry to be updated
+      new_row_data: dict A dictionary of column header to row data
+      
+    Returns:
+      The updated row
+    """
+    entry.custom = {}
+    for k, v in new_row_data.iteritems():
+      new_custom = gdata.spreadsheet.Custom()
+      new_custom.column = k
+      new_custom.text = v
+      entry.custom[k] = new_custom
+    for a_link in entry.link:
+      if a_link.rel == 'edit':
+        return self.Put(entry, a_link.href, 
+            converter=gdata.spreadsheet.SpreadsheetsListFromString)
+        
+  def DeleteRow(self, entry):
+    """Deletes a row, the provided entry
+    
+    Args:
+      entry: gdata.spreadsheet.SpreadsheetsList The row to be deleted
+    
+    Returns:
+      The delete response
+    """
+    for a_link in entry.link:
+      if a_link.rel == 'edit':
+        return self.Delete(a_link.href)
+
+
+class DocumentQuery(gdata.service.Query):
+
+  def _GetTitleQuery(self):
+    return self['title']
+
+  def _SetTitleQuery(self, document_query):
+    self['title'] = document_query
+    
+  title = property(_GetTitleQuery, _SetTitleQuery,
+      doc="""The title query parameter""")
+
+  def _GetTitleExactQuery(self):
+    return self['title-exact']
+
+  def _SetTitleExactQuery(self, document_query):
+    self['title-exact'] = document_query
+    
+  title_exact = property(_GetTitleExactQuery, _SetTitleExactQuery,
+      doc="""The title-exact query parameter""")
+ 
+ 
+class CellQuery(gdata.service.Query):
+
+  def _GetMinRowQuery(self):
+    return self['min-row']
+
+  def _SetMinRowQuery(self, cell_query):
+    self['min-row'] = cell_query
+    
+  min_row = property(_GetMinRowQuery, _SetMinRowQuery,
+      doc="""The min-row query parameter""")
+
+  def _GetMaxRowQuery(self):
+    return self['max-row']
+
+  def _SetMaxRowQuery(self, cell_query):
+    self['max-row'] = cell_query
+    
+  max_row = property(_GetMaxRowQuery, _SetMaxRowQuery,
+      doc="""The max-row query parameter""")
+      
+  def _GetMinColQuery(self):
+    return self['min-col']
+
+  def _SetMinColQuery(self, cell_query):
+    self['min-col'] = cell_query
+    
+  min_col = property(_GetMinColQuery, _SetMinColQuery,
+      doc="""The min-col query parameter""")
+      
+  def _GetMaxColQuery(self):
+    return self['max-col']
+
+  def _SetMaxColQuery(self, cell_query):
+    self['max-col'] = cell_query
+    
+  max_col = property(_GetMaxColQuery, _SetMaxColQuery,
+      doc="""The max-col query parameter""")
+      
+  def _GetRangeQuery(self):
+    return self['range']
+
+  def _SetRangeQuery(self, cell_query):
+    self['range'] = cell_query
+    
+  range = property(_GetRangeQuery, _SetRangeQuery,
+      doc="""The range query parameter""")
+      
+  def _GetReturnEmptyQuery(self):
+    return self['return-empty']
+
+  def _SetReturnEmptyQuery(self, cell_query):
+    self['return-empty'] = cell_query
+    
+  return_empty = property(_GetReturnEmptyQuery, _SetReturnEmptyQuery,
+      doc="""The return-empty query parameter""")
+ 
+ 
+class ListQuery(gdata.service.Query):
+
+  def _GetSpreadsheetQuery(self):
+    return self['sq']
+
+  def _SetSpreadsheetQuery(self, list_query):
+    self['sq'] = list_query
+    
+  sq = property(_GetSpreadsheetQuery, _SetSpreadsheetQuery,
+      doc="""The sq query parameter""")
+      
+  def _GetOrderByQuery(self):
+    return self['orderby']
+
+  def _SetOrderByQuery(self, list_query):
+    self['orderby'] = list_query
+    
+  orderby = property(_GetOrderByQuery, _SetOrderByQuery,
+      doc="""The orderby query parameter""")
+      
+  def _GetReverseQuery(self):
+    return self['reverse']
+
+  def _SetReverseQuery(self, list_query):
+    self['reverse'] = list_query
+    
+  reverse = property(_GetReverseQuery, _SetReverseQuery,
+      doc="""The reverse query parameter""")

Added: trunk/conduit/modules/GoogleModule/gdata/spreadsheet/text_db.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/spreadsheet/text_db.py	Wed May  7 11:25:32 2008
@@ -0,0 +1,556 @@
+#!/usr/bin/python
+#
+# Copyright Google 2007-2008, 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 StringIO
+import gdata
+import gdata.service
+import gdata.spreadsheet
+import gdata.spreadsheet.service
+import gdata.docs
+import gdata.docs.service
+
+
+"""Make the Google Documents API feel more like using a database.
+
+This module contains a client and other classes which make working with the 
+Google Documents List Data API and the Google Spreadsheets Data API look a
+bit more like working with a heirarchical database. Using the DatabaseClient,
+you can create or find spreadsheets and use them like a database, with 
+worksheets representing tables and rows representing records.
+
+Example Usage:
+# Create a new database, a new table, and add records.
+client = gdata.spreadsheet.text_db.DatabaseClient(username='jo example com', 
+    password='12345')
+database = client.CreateDatabase('My Text Database')
+table = database.CreateTable('addresses', ['name','email',
+    'phonenumber', 'mailingaddress'])
+record = table.AddRecord({'name':'Bob', 'email':'bob example com', 
+    'phonenumber':'555-555-1234', 'mailingaddress':'900 Imaginary St.'})
+
+# Edit a record
+record.content['email'] = 'bob2 example com'
+record.Push()
+
+# Delete a table
+table.Delete
+
+Warnings: 
+Care should be exercised when using this module on spreadsheets
+which contain formulas. This module treats all rows as containing text and
+updating a row will overwrite any formula with the output of the formula. 
+The intended use case is to allow easy storage of text data in a spreadsheet.
+
+  Error: Domain specific extension of Exception.
+  BadCredentials: Error raised is username or password was incorrect.
+  CaptchaRequired: Raised if a login attempt failed and a CAPTCHA challenge 
+      was issued.
+  DatabaseClient: Communicates with Google Docs APIs servers.
+  Database: Represents a spreadsheet and interacts with tables.
+  Table: Represents a worksheet and interacts with records.
+  RecordResultSet: A list of records in a table.
+  Record: Represents a row in a worksheet allows manipulation of text data.
+"""
+
+
+__author__ = 'api.jscudder (Jeffrey Scudder)'
+
+
+class Error(Exception):
+  pass
+
+
+class BadCredentials(Error):
+  pass
+
+
+class CaptchaRequired(Error):
+  pass
+
+
+class DatabaseClient(object):
+  """Allows creation and finding of Google Spreadsheets databases.
+
+  The DatabaseClient simplifies the process of creating and finding Google 
+  Spreadsheets and will talk to both the Google Spreadsheets API and the 
+  Google Documents List API. 
+  """
+
+  def __init__(self, username=None, password=None):
+    """Constructor for a Database Client. 
+  
+    If the username and password are present, the constructor  will contact
+    the Google servers to authenticate.
+
+    Args:
+      username: str (optional) Example: jo example com
+      password: str (optional)
+    """
+    self.__docs_client = gdata.docs.service.DocsService()
+    self.__spreadsheets_client = (
+        gdata.spreadsheet.service.SpreadsheetsService())
+    self.SetCredentials(username, password)
+
+  def SetCredentials(self, username, password):
+    """Attempts to log in to Google APIs using the provided credentials.
+
+    If the username or password are None, the client will not request auth 
+    tokens.
+
+    Args:
+      username: str (optional) Example: jo example com
+      password: str (optional)
+    """
+    self.__docs_client.email = username
+    self.__docs_client.password = password
+    self.__spreadsheets_client.email = username
+    self.__spreadsheets_client.password = password
+    if username and password:
+      try:
+        self.__docs_client.ProgrammaticLogin()
+        self.__spreadsheets_client.ProgrammaticLogin()
+      except gdata.service.CaptchaRequired:
+        raise CaptchaRequired('Please visit https://www.google.com/accounts/'
+                            'DisplayUnlockCaptcha to unlock your account.')
+      except gdata.service.BadAuthentication:
+        raise BadCredentials('Username or password incorrect.')
+    
+  def CreateDatabase(self, name):
+    """Creates a new Google Spreadsheet with the desired name.
+
+    Args:
+      name: str The title for the spreadsheet.
+
+    Returns:
+      A Database instance representing the new spreadsheet. 
+    """
+    # Create a Google Spreadsheet to form the foundation of this database.
+    # Spreadsheet is created by uploading a file to the Google Documents
+    # List API.
+    virtual_csv_file = StringIO.StringIO(',,,')
+    virtual_media_source = gdata.MediaSource(file_handle=virtual_csv_file, content_type='text/csv', content_length=3)
+    db_entry = self.__docs_client.UploadSpreadsheet(virtual_media_source, name)
+    return Database(spreadsheet_entry=db_entry, database_client=self)
+
+  def GetDatabases(self, spreadsheet_key=None, name=None):
+    """Finds spreadsheets which have the unique key or title.
+
+    If querying on the spreadsheet_key there will be at most one result, but
+    searching by name could yield multiple results.
+
+    Args:
+      spreadsheet_key: str The unique key for the spreadsheet, this 
+          usually in the the form 'pk23...We' or 'o23...423.12,,,3'.
+      name: str The title of the spreadsheets.
+
+    Returns:
+      A list of Database objects representing the desired spreadsheets.
+    """
+    if spreadsheet_key:
+      db_entry = self.__docs_client.GetDocumentListEntry(
+          r'/feeds/documents/private/full/spreadsheet%3A' + spreadsheet_key)
+      return [Database(spreadsheet_entry=db_entry, database_client=self)]
+    else:
+      title_query = gdata.docs.service.DocumentQuery()
+      title_query['title'] = name
+      db_feed = self.__docs_client.QueryDocumentListFeed(title_query.ToUri())
+      matching_databases = []
+      for entry in db_feed.entry:
+        matching_databases.append(Database(spreadsheet_entry=entry, 
+                                           database_client=self))
+      return matching_databases
+    
+  def _GetDocsClient(self):
+    return self.__docs_client
+
+  def _GetSpreadsheetsClient(self):
+    return self.__spreadsheets_client
+
+
+class Database(object):
+  """Provides interface to find and create tables.
+
+  The database represents a Google Spreadsheet.
+  """
+
+  def __init__(self, spreadsheet_entry=None, database_client=None):
+    """Constructor for a database object.
+
+    Args:
+      spreadsheet_entry: gdata.docs.DocumentListEntry The 
+          Atom entry which represents the Google Spreadsheet. The
+          spreadsheet's key is extracted from the entry and stored as a 
+          member.
+      database_client: DatabaseClient A client which can talk to the
+          Google Spreadsheets servers to perform operations on worksheets
+          within this spreadsheet.
+    """
+    self.entry = spreadsheet_entry
+    if self.entry:
+      id_parts = spreadsheet_entry.id.text.split('/')
+      self.spreadsheet_key = id_parts[-1].replace('spreadsheet%3A', '')
+    self.client = database_client
+
+  def CreateTable(self, name, fields=None):
+    """Add a new worksheet to this spreadsheet and fill in column names.
+
+    Args:
+      name: str The title of the new worksheet.
+      fields: list of strings The column names which are placed in the
+          first row of this worksheet. These names are converted into XML
+          tags by the server. To avoid changes during the translation
+          process I recommend using all lowercase alphabetic names. For
+          example ['somelongname', 'theothername']
+
+    Returns:
+      Table representing the newly created worksheet.
+    """
+    worksheet = self.client._GetSpreadsheetsClient().AddWorksheet(title=name,
+        row_count=1, col_count=len(fields), key=self.spreadsheet_key)
+    return Table(name=name, worksheet_entry=worksheet, 
+        database_client=self.client, 
+        spreadsheet_key=self.spreadsheet_key, fields=fields)
+
+  def GetTables(self, worksheet_id=None, name=None):
+    """Searches for a worksheet with the specified ID or name.
+
+    The list of results should have one table at most, or no results
+    if the id or name were not found.
+
+    Args:
+      worksheet_id: str The ID of the worksheet, example: 'od6'
+      name: str The title of the worksheet.
+
+    Returns:
+      A list of length 0 or 1 containing the desired Table. A list is returned
+      to make this method feel like GetDatabases and GetRecords.
+    """
+    if worksheet_id:
+      worksheet_entry = self.client._GetSpreadsheetsClient().GetWorksheetsFeed(
+          self.spreadsheet_key, wksht_id=worksheet_id)
+      return [Table(name=worksheet_entry.title.text, 
+          worksheet_entry=worksheet_entry, database_client=self.client, 
+          spreadsheet_key=self.spreadsheet_key)]
+    else:
+      matching_tables = []
+      title_query = gdata.spreadsheet.service.DocumentQuery()
+      title_query.title = name
+      worksheet_feed = self.client._GetSpreadsheetsClient().GetWorksheetsFeed(
+          self.spreadsheet_key, query=title_query)
+      for entry in worksheet_feed.entry:
+        matching_tables.append(Table(name=entry.title.text, 
+            worksheet_entry=entry, database_client=self.client, 
+            spreadsheet_key=self.spreadsheet_key))
+      return matching_tables
+
+  def Delete(self):
+    """Deletes the entire database spreadsheet from Google Spreadsheets."""
+    entry = self.client._GetDocsClient().Get(
+        r'http://docs.google.com/feeds/documents/private/full/spreadsheet%3A' +
+        self.spreadsheet_key)
+    self.client._GetDocsClient().Delete(entry.GetEditLink().href)
+
+
+class Table(object):
+
+  def __init__(self, name=None, worksheet_entry=None, database_client=None, 
+      spreadsheet_key=None, fields=None):
+    self.name = name
+    self.entry = worksheet_entry
+    id_parts = worksheet_entry.id.text.split('/')
+    self.worksheet_id = id_parts[-1]
+    self.spreadsheet_key = spreadsheet_key
+    self.client = database_client
+    self.fields = fields or []
+    if fields:
+      self.SetFields(fields)
+
+  def LookupFields(self):
+    """Queries to find the column names in the first row of the worksheet.
+    
+    Useful when you have retrieved the table from the server and you don't 
+    know the column names.
+    """
+    if self.entry:
+      first_row_contents = []
+      query = gdata.spreadsheet.service.CellQuery()
+      query.max_row = '1'
+      query.min_row = '1'
+      feed = self.client._GetSpreadsheetsClient().GetCellsFeed(
+          self.spreadsheet_key, wksht_id=self.worksheet_id, query=query)
+      for entry in feed.entry:
+        first_row_contents.append(entry.content.text)
+      # Get the next set of cells if needed.
+      next_link = feed.GetNextLink()
+      while next_link:
+        feed = self.client._GetSpreadsheetsClient().Get(next_link.href, 
+            converter=gdata.spreadsheet.SpreadsheetsCellsFeedFromString)
+        for entry in feed.entry:
+          first_row_contents.append(entry.content.text)
+        next_link = feed.GetNextLink()
+      # Convert the contents of the cells to valid headers.
+      self.fields = ConvertStringsToColumnHeaders(first_row_contents)
+    
+  def SetFields(self, fields):
+    """Changes the contents of the cells in the first row of this worksheet.
+
+    Args:
+      fields: list of strings The names in the list comprise the
+          first row of the worksheet. These names are converted into XML
+          tags by the server. To avoid changes during the translation
+          process I recommend using all lowercase alphabetic names. For
+          example ['somelongname', 'theothername']
+    """
+    # TODO: If the table already had fields, we might want to clear out the,
+    # current column headers.
+    self.fields = fields
+    i = 0
+    for column_name in fields:
+      i = i + 1
+      # TODO: speed this up by using a batch request to update cells.
+      self.client._GetSpreadsheetsClient().UpdateCell(1, i, column_name, 
+          self.spreadsheet_key, self.worksheet_id)
+
+  def Delete(self):
+    """Deletes this worksheet from the spreadsheet."""
+    worksheet = self.client._GetSpreadsheetsClient().GetWorksheetsFeed(
+        self.spreadsheet_key, wksht_id=self.worksheet_id)
+    self.client._GetSpreadsheetsClient().DeleteWorksheet(
+        worksheet_entry=worksheet)
+
+  def AddRecord(self, data):
+    """Adds a new row to this worksheet.
+
+    Args:
+      data: dict of strings Mapping of string values to column names. 
+
+    Returns:
+      Record which represents this row of the spreadsheet.
+    """
+    new_row = self.client._GetSpreadsheetsClient().InsertRow(data, 
+        self.spreadsheet_key, wksht_id=self.worksheet_id)
+    return Record(content=data, row_entry=new_row, 
+        spreadsheet_key=self.spreadsheet_key, worksheet_id=self.worksheet_id,
+        database_client=self.client)
+
+  def GetRecord(self, row_id=None, row_number=None):
+    """Gets a single record from the worksheet based on row ID or number.
+    
+    Args:
+      row_id: The ID for the individual row.
+      row_number: str or int The position of the desired row. Numbering 
+          begins at 1, which refers to the second row in the worksheet since
+          the first row is used for column names.
+
+    Returns:
+      Record for the desired row.
+    """
+    if row_id:
+      row_entry = self.client._GetSpreadsheetsClient().GetListFeed(
+          self.spreadsheet_key, wksht_id=self.worksheet_id, row_id=row_id)
+      return Record(content=None, row_entry=row_entry, 
+           spreadsheet_key=self.spreadsheet_key, 
+           worksheet_id=self.worksheet_id, database_client=self.client)
+    else:
+      row_query = gdata.spreadsheet.service.ListQuery()
+      row_query.start_index = str(row_number)
+      row_query.max_results = '1'
+      row_feed = self.client._GetSpreadsheetsClient().GetListFeed(
+          self.spreadsheet_key, wksht_id=self.worksheet_id, query=row_query)
+      if len(row_feed.entry) >= 1:
+        return Record(content=None, row_entry=row_feed.entry[0],
+            spreadsheet_key=self.spreadsheet_key,
+            worksheet_id=self.worksheet_id, database_client=self.client)
+      else:
+        return None
+
+  def GetRecords(self, start_row, end_row):
+    """Gets all rows between the start and end row numbers inclusive.
+
+    Args:
+      start_row: str or int
+      end_row: str or int
+
+    Returns:
+      RecordResultSet for the desired rows.
+    """
+    start_row = int(start_row)
+    end_row = int(end_row)
+    max_rows = end_row - start_row + 1
+    row_query = gdata.spreadsheet.service.ListQuery()
+    row_query.start_index = str(start_row)
+    row_query.max_results = str(max_rows)
+    rows_feed = self.client._GetSpreadsheetsClient().GetListFeed(
+        self.spreadsheet_key, wksht_id=self.worksheet_id, query=row_query)
+    return RecordResultSet(rows_feed, self.client, self.spreadsheet_key,
+        self.worksheet_id)
+
+  def FindRecords(self, query_string):
+    """Performs a query against the worksheet to find rows which match.
+
+    For details on query string syntax see the section on sq under
+    http://code.google.com/apis/spreadsheets/reference.html#list_Parameters
+
+    Args:
+      query_string: str Examples: 'name == john' to find all rows with john
+          in the name column, '(cost < 19.50 and name != toy) or cost > 500'
+
+    Returns:
+      RecordResultSet with the first group of matches.
+    """
+    row_query = gdata.spreadsheet.service.ListQuery()
+    row_query.sq = query_string
+    matching_feed = self.client._GetSpreadsheetsClient().GetListFeed(
+        self.spreadsheet_key, wksht_id=self.worksheet_id, query=row_query)
+    return RecordResultSet(matching_feed, self.client, 
+        self.spreadsheet_key, self.worksheet_id)
+
+
+class RecordResultSet(list):
+  """A collection of rows which allows fetching of the next set of results.
+
+  The server may not send all rows in the requested range because there are
+  too many. Using this result set you can access the first set of results
+  as if it is a list, then get the next batch (if there are more results) by
+  calling GetNext().
+  """
+
+  def __init__(self, feed, client, spreadsheet_key, worksheet_id):
+    self.client = client
+    self.spreadsheet_key = spreadsheet_key
+    self.worksheet_id = worksheet_id
+    self.feed = feed
+    list(self)
+    for entry in self.feed.entry:
+      self.append(Record(content=None, row_entry=entry, 
+          spreadsheet_key=spreadsheet_key, worksheet_id=worksheet_id,
+          database_client=client))
+
+  def GetNext(self):
+    """Fetches the next batch of rows in the result set.
+
+    Returns:
+      A new RecordResultSet.
+    """
+    next_link = self.feed.GetNextLink()
+    if next_link and next_link.href:
+      new_feed = self.client._GetSpreadsheetsClient().Get(next_link.href, 
+          converter=gdata.spreadsheet.SpreadsheetsListFeedFromString)
+      return RecordResultSet(new_feed, self.client, self.spreadsheet_key,
+          self.worksheet_id)
+
+
+class Record(object):
+  """Represents one row in a worksheet and provides a dictionary of values.
+
+  Attributes:
+    custom: dict Represents the contents of the row with cell values mapped
+        to column headers.
+  """
+
+  def __init__(self, content=None, row_entry=None, spreadsheet_key=None, 
+       worksheet_id=None, database_client=None):
+    """Constructor for a record.
+    
+    Args:
+      content: dict of strings Mapping of string values to column names.
+      row_entry: gdata.spreadsheet.SpreadsheetsList The Atom entry 
+          representing this row in the worksheet.
+      spreadsheet_key: str The ID of the spreadsheet in which this row 
+          belongs.
+      worksheet_id: str The ID of the worksheet in which this row belongs.
+      database_client: DatabaseClient The client which can be used to talk
+          the Google Spreadsheets server to edit this row.
+    """
+    self.entry = row_entry
+    self.spreadsheet_key = spreadsheet_key
+    self.worksheet_id = worksheet_id
+    if row_entry:
+      self.row_id = row_entry.id.text.split('/')[-1]
+    else:
+      self.row_id = None
+    self.client = database_client
+    self.content = content or {}
+    if not content:
+      self.ExtractContentFromEntry(row_entry)
+
+  def ExtractContentFromEntry(self, entry):
+    """Populates the content and row_id based on content of the entry.
+
+    This method is used in the Record's contructor.
+
+    Args:
+      entry: gdata.spreadsheet.SpreadsheetsList The Atom entry 
+          representing this row in the worksheet.
+    """
+    self.content = {}
+    if entry:
+      self.row_id = entry.id.text.split('/')[-1]
+      for label, custom in entry.custom.iteritems():
+        self.content[label] = custom.text
+
+  def Push(self):
+    """Send the content of the record to spreadsheets to edit the row.
+
+    All items in the content dictionary will be sent. Items which have been
+    removed from the content may remain in the row. The content member
+    of the record will not be modified so additional fields in the row
+    might be absent from this local copy.
+    """
+    self.entry = self.client._GetSpreadsheetsClient().UpdateRow(self.entry, self.content)
+
+  def Pull(self):
+    """Query Google Spreadsheets to get the latest data from the server.
+
+    Fetches the entry for this row and repopulates the content dictionary 
+    with the data found in the row.
+    """
+    if self.row_id:
+      self.entry = self.client._GetSpreadsheetsClient().GetListFeed(
+          self.spreadsheet_key, wksht_id=self.worksheet_id, row_id=self.row_id)
+    self.ExtractContentFromEntry(self.entry)
+
+  def Delete(self):
+    self.client._GetSpreadsheetsClient().DeleteRow(self.entry)
+
+
+def ConvertStringsToColumnHeaders(proposed_headers):
+  """Converts a list of strings to column names which spreadsheets accepts.
+
+  When setting values in a record, the keys which represent column names must
+  fit certain rules. They are all lower case, contain no spaces or special
+  characters. If two columns have the same name after being sanitized, the 
+  columns further to the right have _2, _3 _4, etc. appended to them.
+
+  If there are column names which consist of all special characters, or if
+  the column header is blank, an obfuscated value will be used for a column
+  name. This method does not handle blank column names or column names with
+  only special characters.
+  """
+  headers = []
+  for input_string in proposed_headers:
+    # TODO: probably a more efficient way to do this. Perhaps regex.
+    sanitized = input_string.lower().replace('_', '').replace(
+        ':', '').replace(' ', '')
+    # When the same sanitized header appears multiple times in the first row
+    # of a spreadsheet, _n is appended to the name to make it unique.
+    header_count = headers.count(sanitized)
+    if header_count > 0:
+      headers.append('%s_%i' % (sanitized, header_count+1))
+    else:
+      headers.append(sanitized)
+  return headers

Added: trunk/conduit/modules/GoogleModule/gdata/test_data.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/test_data.py	Wed May  7 11:25:32 2008
@@ -0,0 +1,2426 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2006 Google Inc.
+#
+# 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.
+
+
+
+XML_ENTRY_1 = """<?xml version='1.0'?>
+<entry xmlns='http://www.w3.org/2005/Atom'
+       xmlns:g='http://base.google.com/ns/1.0'>
+  <category scheme="http://base.google.com/categories/itemtypes";
+            term="products"/>
+  <id>    http://www.google.com/test/id/url   </id>
+  <title type='text'>Testing 2000 series laptop</title>
+  <content type='xhtml'>
+    <div xmlns='http://www.w3.org/1999/xhtml'>A Testing Laptop</div>
+  </content>
+  <link rel='alternate' type='text/html'
+        href='http://www.provider-host.com/123456789'/>
+  <link rel='license' 
+        href='http://creativecommons.org/licenses/by-nc/2.5/rdf'/>
+  <g:label>Computer</g:label>
+  <g:label>Laptop</g:label>
+  <g:label>testing laptop</g:label>
+  <g:item_type>products</g:item_type>
+</entry>"""
+
+
+TEST_BASE_ENTRY = """<?xml version='1.0'?>
+<entry xmlns='http://www.w3.org/2005/Atom'
+       xmlns:g='http://base.google.com/ns/1.0'>
+  <category scheme="http://base.google.com/categories/itemtypes";
+            term="products"/>
+  <title type='text'>Testing 2000 series laptop</title>
+  <content type='xhtml'>
+    <div xmlns='http://www.w3.org/1999/xhtml'>A Testing Laptop</div>
+  </content>
+  <app:control xmlns:app='http://purl.org/atom/app#'>
+    <app:draft>yes</app:draft>
+    <gm:disapproved xmlns:gm='http://base.google.com/ns-metadata/1.0'/>   
+  </app:control>
+  <link rel='alternate' type='text/html'
+        href='http://www.provider-host.com/123456789'/>
+  <g:label>Computer</g:label>
+  <g:label>Laptop</g:label>
+  <g:label>testing laptop</g:label>
+  <g:item_type>products</g:item_type>
+</entry>"""
+
+
+BIG_FEED = """<?xml version="1.0" encoding="utf-8"?>
+   <feed xmlns="http://www.w3.org/2005/Atom";>
+     <title type="text">dive into mark</title>
+     <subtitle type="html">
+       A &lt;em&gt;lot&lt;/em&gt; of effort
+       went into making this effortless
+     </subtitle>
+     <updated>2005-07-31T12:29:29Z</updated>
+     <id>tag:example.org,2003:3</id>
+     <link rel="alternate" type="text/html"
+      hreflang="en" href="http://example.org/"/>
+     <link rel="self" type="application/atom+xml"
+      href="http://example.org/feed.atom"/>
+     <rights>Copyright (c) 2003, Mark Pilgrim</rights>
+     <generator uri="http://www.example.com/"; version="1.0">
+       Example Toolkit
+     </generator>
+     <entry>
+       <title>Atom draft-07 snapshot</title>
+       <link rel="alternate" type="text/html"
+        href="http://example.org/2005/04/02/atom"/>
+       <link rel="enclosure" type="audio/mpeg" length="1337"
+        href="http://example.org/audio/ph34r_my_podcast.mp3"/>
+       <id>tag:example.org,2003:3.2397</id>
+       <updated>2005-07-31T12:29:29Z</updated>
+       <published>2003-12-13T08:29:29-04:00</published>
+       <author>
+         <name>Mark Pilgrim</name>
+         <uri>http://example.org/</uri>
+         <email>f8dy example com</email>
+       </author>
+       <contributor>
+         <name>Sam Ruby</name>
+       </contributor>
+       <contributor>
+         <name>Joe Gregorio</name>
+       </contributor>
+       <content type="xhtml" xml:lang="en"
+        xml:base="http://diveintomark.org/";>
+         <div xmlns="http://www.w3.org/1999/xhtml";>
+           <p><i>[Update: The Atom draft is finished.]</i></p>
+         </div>
+       </content>
+     </entry>
+   </feed>
+"""
+
+SMALL_FEED = """<?xml version="1.0" encoding="utf-8"?>
+   <feed xmlns="http://www.w3.org/2005/Atom";>
+     <title>Example Feed</title>
+     <link href="http://example.org/"/>
+     <updated>2003-12-13T18:30:02Z</updated>
+     <author>
+       <name>John Doe</name>
+     </author>
+     <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
+     <entry>
+       <title>Atom-Powered Robots Run Amok</title>
+       <link href="http://example.org/2003/12/13/atom03"/>
+       <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+       <updated>2003-12-13T18:30:02Z</updated>
+       <summary>Some text.</summary>
+     </entry>
+   </feed>
+"""
+
+GBASE_FEED = """<?xml version='1.0' encoding='UTF-8'?>
+<feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:g='http://base.google.com/ns/1.0' xmlns:batch='http://schemas.google.com/gdata/batch'>
+<id>http://www.google.com/base/feeds/snippets</id>
+<updated>2007-02-08T23:18:21.935Z</updated>
+<title type='text'>Items matching query: digital camera</title>
+<link rel='alternate' type='text/html' href='http://base.google.com'>
+</link>
+<link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://www.google.com/base/feeds/snippets'>
+</link>
+<link rel='self' type='application/atom+xml' href='http://www.google.com/base/feeds/snippets?start-index=1&amp;max-results=25&amp;bq=digital+camera'>
+</link>
+<link rel='next' type='application/atom+xml' href='http://www.google.com/base/feeds/snippets?start-index=26&amp;max-results=25&amp;bq=digital+camera'>
+</link>
+<generator version='1.0' uri='http://base.google.com'>GoogleBase              </generator>
+<openSearch:totalResults>2171885</openSearch:totalResults>
+<openSearch:startIndex>1</openSearch:startIndex>
+<openSearch:itemsPerPage>25</openSearch:itemsPerPage>
+<entry>
+<id>http://www.google.com/base/feeds/snippets/13246453826751927533</id>
+<published>2007-02-08T13:23:27.000Z</published>
+<updated>2007-02-08T16:40:57.000Z</updated>
+<category scheme='http://base.google.com/categories/itemtypes' term='Products'>
+</category>
+<title type='text'>Digital Camera Battery Notebook Computer 12v DC Power Cable - 5.5mm x 2.5mm (Center +) Camera Connecting Cables</title>
+<content type='html'>Notebook Computer 12v DC Power Cable - 5.5mm x 2.1mm (Center +) This connection cable will allow any Digital Pursuits battery pack to power portable computers that operate with 12v power and have a 2.1mm power connector (center +) Digital  ...</content>
+<link rel='alternate' type='text/html' href='http://www.bhphotovideo.com/bnh/controller/home?O=productlist&amp;A=details&amp;Q=&amp;sku=305668&amp;is=REG&amp;kw=DIDCB5092&amp;BI=583'>
+</link>
+<link rel='self' type='application/atom+xml' href='http://www.google.com/base/feeds/snippets/13246453826751927533'>
+</link>
+<author>
+<name>B&amp;H Photo-Video</name>
+<email>anon-szot0wdsq0at base google com</email>
+</author>
+<g:payment_notes type='text'>PayPal &amp; Bill Me Later credit available online only.</g:payment_notes>
+<g:condition type='text'>new</g:condition>
+<g:location type='location'>420 9th Ave. 10001</g:location>
+<g:id type='text'>305668-REG</g:id>
+<g:item_type type='text'>Products</g:item_type>
+<g:brand type='text'>Digital Camera Battery</g:brand>
+<g:expiration_date type='dateTime'>2007-03-10T13:23:27.000Z</g:expiration_date>
+<g:customer_id type='int'>1172711</g:customer_id>
+<g:price type='floatUnit'>34.95 usd</g:price>
+<g:product_type type='text'>Digital Photography&gt;Camera Connecting Cables</g:product_type>
+<g:item_language type='text'>EN</g:item_language>
+<g:manufacturer_id type='text'>DCB5092</g:manufacturer_id>
+<g:target_country type='text'>US</g:target_country>
+<g:weight type='float'>1.0</g:weight>
+<g:image_link type='url'>http://base.google.com/base_image?q=http%3A%2F%2Fwww.bhphotovideo.com%2Fimages%2Fitems%2F305668.jpg&amp;dhm=ffffffff84c9a95e&amp;size=6</g:image_link>
+</entry>
+<entry>
+<id>http://www.google.com/base/feeds/snippets/10145771037331858608</id>
+<published>2007-02-08T13:23:27.000Z</published>
+<updated>2007-02-08T16:40:57.000Z</updated>
+<category scheme='http://base.google.com/categories/itemtypes' term='Products'>
+</category>
+<title type='text'>Digital Camera Battery Electronic Device 5v DC Power Cable - 5.5mm x 2.5mm (Center +) Camera Connecting Cables</title>
+<content type='html'>Electronic Device 5v DC Power Cable - 5.5mm x 2.5mm (Center +) This connection cable will allow any Digital Pursuits battery pack to power any electronic device that operates with 5v power and has a 2.5mm power connector (center +) Digital  ...</content>
+<link rel='alternate' type='text/html' href='http://www.bhphotovideo.com/bnh/controller/home?O=productlist&amp;A=details&amp;Q=&amp;sku=305656&amp;is=REG&amp;kw=DIDCB5108&amp;BI=583'>
+</link>
+<link rel='self' type='application/atom+xml' href='http://www.google.com/base/feeds/snippets/10145771037331858608'>
+</link>
+<author>
+<name>B&amp;H Photo-Video</name>
+<email>anon-szot0wdsq0at base google com</email>
+</author>
+<g:location type='location'>420 9th Ave. 10001</g:location>
+<g:condition type='text'>new</g:condition>
+<g:weight type='float'>0.18</g:weight>
+<g:target_country type='text'>US</g:target_country>
+<g:product_type type='text'>Digital Photography&gt;Camera Connecting Cables</g:product_type>
+<g:payment_notes type='text'>PayPal &amp; Bill Me Later credit available online only.</g:payment_notes>
+<g:id type='text'>305656-REG</g:id>
+<g:image_link type='url'>http://base.google.com/base_image?q=http%3A%2F%2Fwww.bhphotovideo.com%2Fimages%2Fitems%2F305656.jpg&amp;dhm=7315bdc8&amp;size=6</g:image_link>
+<g:manufacturer_id type='text'>DCB5108</g:manufacturer_id>
+<g:upc type='text'>838098005108</g:upc>
+<g:price type='floatUnit'>34.95 usd</g:price>
+<g:item_language type='text'>EN</g:item_language>
+<g:brand type='text'>Digital Camera Battery</g:brand>
+<g:customer_id type='int'>1172711</g:customer_id>
+<g:item_type type='text'>Products</g:item_type>
+<g:expiration_date type='dateTime'>2007-03-10T13:23:27.000Z</g:expiration_date>
+</entry>
+<entry>
+<id>http://www.google.com/base/feeds/snippets/3128608193804768644</id>
+<published>2007-02-08T02:21:27.000Z</published>
+<updated>2007-02-08T15:40:13.000Z</updated>
+<category scheme='http://base.google.com/categories/itemtypes' term='Products'>
+</category>
+<title type='text'>Digital Camera Battery Power Cable for Kodak 645 Pro-Back ProBack &amp; DCS-300 Series Camera Connecting Cables</title>
+<content type='html'>Camera Connection Cable - to Power Kodak 645 Pro-Back DCS-300 Series Digital Cameras This connection cable will allow any Digital Pursuits battery pack to power the following digital cameras: Kodak DCS Pro Back 645 DCS-300 series Digital Photography ...</content>
+<link rel='alternate' type='text/html' href='http://www.bhphotovideo.com/bnh/controller/home?O=productlist&amp;A=details&amp;Q=&amp;sku=305685&amp;is=REG&amp;kw=DIDCB6006&amp;BI=583'>
+</link>
+<link rel='self' type='application/atom+xml' href='http://www.google.com/base/feeds/snippets/3128608193804768644'>
+</link>
+<author>
+<name>B&amp;H Photo-Video</name>
+<email>anon-szot0wdsq0at base google com</email>
+</author>
+<g:weight type='float'>0.3</g:weight>
+<g:manufacturer_id type='text'>DCB6006</g:manufacturer_id>
+<g:image_link type='url'>http://base.google.com/base_image?q=http%3A%2F%2Fwww.bhphotovideo.com%2Fimages%2Fitems%2F305685.jpg&amp;dhm=72f0ca0a&amp;size=6</g:image_link>
+<g:location type='location'>420 9th Ave. 10001</g:location>
+<g:payment_notes type='text'>PayPal &amp; Bill Me Later credit available online only.</g:payment_notes>
+<g:item_type type='text'>Products</g:item_type>
+<g:target_country type='text'>US</g:target_country>
+<g:accessory_for type='text'>digital kodak camera</g:accessory_for>
+<g:brand type='text'>Digital Camera Battery</g:brand>
+<g:expiration_date type='dateTime'>2007-03-10T02:21:27.000Z</g:expiration_date>
+<g:item_language type='text'>EN</g:item_language>
+<g:condition type='text'>new</g:condition>
+<g:price type='floatUnit'>34.95 usd</g:price>
+<g:customer_id type='int'>1172711</g:customer_id>
+<g:product_type type='text'>Digital Photography&gt;Camera Connecting Cables</g:product_type>
+<g:id type='text'>305685-REG</g:id>
+</entry>
+</feed>"""
+
+EXTENSION_TREE = """<?xml version="1.0" encoding="utf-8"?>
+   <feed xmlns="http://www.w3.org/2005/Atom";>
+     <g:author xmlns:g="http://www.google.com";>
+       <g:name>John Doe
+         <g:foo yes="no" up="down">Bar</g:foo>
+       </g:name>
+     </g:author>
+   </feed>
+"""
+
+TEST_AUTHOR = """<?xml version="1.0" encoding="utf-8"?>
+   <author xmlns="http://www.w3.org/2005/Atom";>
+       <name xmlns="http://www.w3.org/2005/Atom";>John Doe</name>
+       <email xmlns="http://www.w3.org/2005/Atom";>johndoes someemailadress com</email>
+       <uri xmlns="http://www.w3.org/2005/Atom";>http://www.google.com</uri>
+   </author>
+"""
+
+TEST_LINK = """<?xml version="1.0" encoding="utf-8"?>
+   <link xmlns="http://www.w3.org/2005/Atom"; href="http://www.google.com"; 
+       rel="test rel" foo1="bar" foo2="rab"/>
+"""
+
+TEST_GBASE_ATTRIBUTE = """<?xml version="1.0" encoding="utf-8"?>
+   <g:brand type='text' xmlns:g="http://base.google.com/ns/1.0";>Digital Camera Battery</g:brand>
+"""
+   
+
+CALENDAR_FEED = """<?xml version='1.0' encoding='utf-8'?>
+<feed xmlns='http://www.w3.org/2005/Atom'
+xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'
+xmlns:gd='http://schemas.google.com/g/2005'
+xmlns:gCal='http://schemas.google.com/gCal/2005'>
+  <id>http://www.google.com/calendar/feeds/default</id>
+  <updated>2007-03-20T22:48:57.833Z</updated>
+  <title type='text'>GData Ops Demo's Calendar List</title>
+  <link rel='http://schemas.google.com/g/2005#feed'
+  type='application/atom+xml'
+  href='http://www.google.com/calendar/feeds/default'></link>
+  <link rel='http://schemas.google.com/g/2005#post'
+  type='application/atom+xml'
+  href='http://www.google.com/calendar/feeds/default'></link>
+  <link rel='self' type='application/atom+xml'
+  href='http://www.google.com/calendar/feeds/default'></link>
+  <author>
+    <name>GData Ops Demo</name>
+    <email>gdata ops demo gmail com</email>
+  </author>
+  <generator version='1.0' uri='http://www.google.com/calendar'>
+  Google Calendar</generator>
+  <openSearch:startIndex>1</openSearch:startIndex>
+  <entry>
+    <id>
+    http://www.google.com/calendar/feeds/default/gdata.ops.demo%40gmail.com</id>
+    <published>2007-03-20T22:48:57.837Z</published>
+    <updated>2007-03-20T22:48:52.000Z</updated>
+    <title type='text'>GData Ops Demo</title>
+    <link rel='alternate' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/gdata.ops.demo%40gmail.com/private/full'>
+    </link>
+    <link rel='self' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/gdata.ops.demo%40gmail.com'>
+    </link>
+    <author>
+      <name>GData Ops Demo</name>
+      <email>gdata ops demo gmail com</email>
+    </author>
+    <gCal:color value='#2952A3'></gCal:color>
+    <gCal:accesslevel value='owner'></gCal:accesslevel>
+    <gCal:hidden value='false'></gCal:hidden>
+    <gCal:timezone value='America/Los_Angeles'></gCal:timezone>
+  </entry>
+  <entry>
+    <id>
+    http://www.google.com/calendar/feeds/default/jnh21ovnjgfph21h32gvms2758%40group.calendar.google.com</id>
+    <published>2007-03-20T22:48:57.837Z</published>
+    <updated>2007-03-20T22:48:53.000Z</updated>
+    <title type='text'>GData Ops Demo Secondary Calendar</title>
+    <summary type='text'></summary>
+    <link rel='alternate' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/jnh21ovnjgfph21h32gvms2758%40group.calendar.google.com/private/full'>
+    </link>
+    <link rel='self' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/jnh21ovnjgfph21h32gvms2758%40group.calendar.google.com'>
+    </link>
+    <author>
+      <name>GData Ops Demo Secondary Calendar</name>
+    </author>
+    <gCal:color value='#528800'></gCal:color>
+    <gCal:accesslevel value='owner'></gCal:accesslevel>
+    <gCal:hidden value='false'></gCal:hidden>
+    <gCal:timezone value='America/Los_Angeles'></gCal:timezone>
+    <gd:where valueString=''></gd:where>
+  </entry>
+</feed>
+"""
+
+CALENDAR_FULL_EVENT_FEED = """<?xml version='1.0' encoding='utf-8'?>
+<feed xmlns='http://www.w3.org/2005/Atom'
+xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'
+xmlns:gd='http://schemas.google.com/g/2005'
+xmlns:gCal='http://schemas.google.com/gCal/2005'>
+  <id>
+  http://www.google.com/calendar/feeds/default/private/full</id>
+  <updated>2007-03-20T21:29:57.000Z</updated>
+  <category scheme='http://schemas.google.com/g/2005#kind'
+  term='http://schemas.google.com/g/2005#event'></category>
+  <title type='text'>GData Ops Demo</title>
+  <subtitle type='text'>GData Ops Demo</subtitle>
+  <link rel='http://schemas.google.com/g/2005#feed'
+  type='application/atom+xml'
+  href='http://www.google.com/calendar/feeds/default/private/full'>
+  </link>
+  <link rel='http://schemas.google.com/g/2005#post'
+  type='application/atom+xml'
+  href='http://www.google.com/calendar/feeds/default/private/full'>
+  </link>
+  <link rel='self' type='application/atom+xml'
+  href='http://www.google.com/calendar/feeds/default/private/full?updated-min=2001-01-01&amp;max-results=25'>
+  </link>
+  <author>
+    <name>GData Ops Demo</name>
+    <email>gdata ops demo gmail com</email>
+  </author>
+  <generator version='1.0' uri='http://www.google.com/calendar'>
+  Google Calendar</generator>
+  <openSearch:totalResults>10</openSearch:totalResults>
+  <openSearch:startIndex>1</openSearch:startIndex>
+  <openSearch:itemsPerPage>25</openSearch:itemsPerPage>
+  <gCal:timezone value='America/Los_Angeles'></gCal:timezone>
+  <entry>
+    <id>
+    http://www.google.com/calendar/feeds/default/private/full/o99flmgmkfkfrr8u745ghr3100</id>
+    <published>2007-03-20T21:29:52.000Z</published>
+    <updated>2007-03-20T21:29:57.000Z</updated>
+    <category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://schemas.google.com/g/2005#event'></category>
+    <title type='text'>test deleted</title>
+    <content type='text'></content>
+    <link rel='alternate' type='text/html'
+    href='http://www.google.com/calendar/event?eid=bzk5ZmxtZ21rZmtmcnI4dTc0NWdocjMxMDAgZ2RhdGEub3BzLmRlbW9AbQ'
+    title='alternate'></link>
+    <link rel='self' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/o99flmgmkfkfrr8u745ghr3100'>
+    </link>
+    <link rel='edit' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/o99flmgmkfkfrr8u745ghr3100/63310109397'>
+    </link>
+    <author>
+      <name>GData Ops Demo</name>
+      <email>gdata ops demo gmail com</email>
+    </author>
+    <gCal:sendEventNotifications value='false'>
+    </gCal:sendEventNotifications>
+    <gd:eventStatus value='http://schemas.google.com/g/2005#event.canceled'>
+    </gd:eventStatus>
+    <gd:comments>
+      <gd:feedLink href='http://www.google.com/calendar/feeds/default/private/full/o99flmgmkfkfrr8u745ghr3100/comments'>
+      </gd:feedLink>
+    </gd:comments>
+    <gd:visibility value='http://schemas.google.com/g/2005#event.default'>
+    </gd:visibility>
+    <gd:transparency value='http://schemas.google.com/g/2005#event.opaque'>
+    </gd:transparency>
+    <gd:when startTime='2007-03-23T12:00:00.000-07:00'
+    endTime='2007-03-23T13:00:00.000-07:00'>
+      <gd:reminder minutes='10'></gd:reminder>
+    </gd:when>
+    <gd:where></gd:where>
+  </entry>
+  <entry>
+    <id>
+    http://www.google.com/calendar/feeds/default/private/full/2qt3ao5hbaq7m9igr5ak9esjo0</id>
+    <published>2007-03-20T21:26:04.000Z</published>
+    <updated>2007-03-20T21:28:46.000Z</updated>
+    <category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://schemas.google.com/g/2005#event'></category>
+    <title type='text'>Afternoon at Dolores Park with Kim</title>
+    <content type='text'></content>
+    <link rel='alternate' type='text/html'
+    href='http://www.google.com/calendar/event?eid=MnF0M2FvNWhiYXE3bTlpZ3I1YWs5ZXNqbzAgZ2RhdGEub3BzLmRlbW9AbQ'
+    title='alternate'></link>
+    <link rel='self' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/2qt3ao5hbaq7m9igr5ak9esjo0'>
+    </link>
+    <link rel='edit' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/2qt3ao5hbaq7m9igr5ak9esjo0/63310109326'>
+    </link>
+    <author>
+      <name>GData Ops Demo</name>
+      <email>gdata ops demo gmail com</email>
+    </author>
+    <gCal:sendEventNotifications value='false'>
+    </gCal:sendEventNotifications>
+    <gd:eventStatus value='http://schemas.google.com/g/2005#event.confirmed'>
+    </gd:eventStatus>
+    <gd:comments>
+      <gd:feedLink href='http://www.google.com/calendar/feeds/default/private/full/2qt3ao5hbaq7m9igr5ak9esjo0/comments'>
+      </gd:feedLink>
+    </gd:comments>
+    <gd:visibility value='http://schemas.google.com/g/2005#event.private'>
+    </gd:visibility>
+    <gd:transparency value='http://schemas.google.com/g/2005#event.opaque'>
+    </gd:transparency>
+    <gd:who rel='http://schemas.google.com/g/2005#event.organizer'
+    valueString='GData Ops Demo' email='gdata ops demo gmail com'>
+      <gd:attendeeStatus value='http://schemas.google.com/g/2005#event.accepted'>
+      </gd:attendeeStatus>
+    </gd:who>
+    <gd:who rel='http://schemas.google.com/g/2005#event.attendee'
+    valueString='Ryan Boyd (API)' email='api rboyd gmail com'>
+      <gd:attendeeStatus value='http://schemas.google.com/g/2005#event.invited'>
+      </gd:attendeeStatus>
+    </gd:who>
+    <gd:when startTime='2007-03-24T12:00:00.000-07:00'
+    endTime='2007-03-24T15:00:00.000-07:00'>
+      <gd:reminder minutes='20'></gd:reminder>
+    </gd:when>
+    <gd:where valueString='Dolores Park with Kim'></gd:where>
+  </entry>
+  <entry>
+    <id>
+    http://www.google.com/calendar/feeds/default/private/full/uvsqhg7klnae40v50vihr1pvos</id>
+    <published>2007-03-20T21:28:37.000Z</published>
+    <updated>2007-03-20T21:28:37.000Z</updated>
+    <category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://schemas.google.com/g/2005#event'></category>
+    <title type='text'>Team meeting</title>
+    <content type='text'></content>
+    <link rel='alternate' type='text/html'
+    href='http://www.google.com/calendar/event?eid=dXZzcWhnN2tsbmFlNDB2NTB2aWhyMXB2b3NfMjAwNzAzMjNUMTYwMDAwWiBnZGF0YS5vcHMuZGVtb0Bt'
+    title='alternate'></link>
+    <link rel='self' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/uvsqhg7klnae40v50vihr1pvos'>
+    </link>
+    <link rel='edit' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/uvsqhg7klnae40v50vihr1pvos/63310109317'>
+    </link>
+    <author>
+      <name>GData Ops Demo</name>
+      <email>gdata ops demo gmail com</email>
+    </author>
+    <gd:recurrence>DTSTART;TZID=America/Los_Angeles:20070323T090000
+    DTEND;TZID=America/Los_Angeles:20070323T100000
+    RRULE:FREQ=WEEKLY;BYDAY=FR;UNTIL=20070817T160000Z;WKST=SU
+    BEGIN:VTIMEZONE TZID:America/Los_Angeles
+    X-LIC-LOCATION:America/Los_Angeles BEGIN:STANDARD
+    TZOFFSETFROM:-0700 TZOFFSETTO:-0800 TZNAME:PST
+    DTSTART:19701025T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+    END:STANDARD BEGIN:DAYLIGHT TZOFFSETFROM:-0800 TZOFFSETTO:-0700
+    TZNAME:PDT DTSTART:19700405T020000
+    RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU END:DAYLIGHT
+    END:VTIMEZONE</gd:recurrence>
+    <gCal:sendEventNotifications value='true'>
+    </gCal:sendEventNotifications>
+    <gd:eventStatus value='http://schemas.google.com/g/2005#event.confirmed'>
+    </gd:eventStatus>
+    <gd:visibility value='http://schemas.google.com/g/2005#event.public'>
+    </gd:visibility>
+    <gd:transparency value='http://schemas.google.com/g/2005#event.opaque'>
+    </gd:transparency>
+    <gd:reminder minutes='10'></gd:reminder>
+    <gd:where valueString=''></gd:where>
+  </entry>
+  <entry>
+    <id>
+    http://www.google.com/calendar/feeds/default/private/full/st4vk9kiffs6rasrl32e4a7alo</id>
+    <published>2007-03-20T21:25:46.000Z</published>
+    <updated>2007-03-20T21:25:46.000Z</updated>
+    <category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://schemas.google.com/g/2005#event'></category>
+    <title type='text'>Movie with Kim and danah</title>
+    <content type='text'></content>
+    <link rel='alternate' type='text/html'
+    href='http://www.google.com/calendar/event?eid=c3Q0dms5a2lmZnM2cmFzcmwzMmU0YTdhbG8gZ2RhdGEub3BzLmRlbW9AbQ'
+    title='alternate'></link>
+    <link rel='self' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/st4vk9kiffs6rasrl32e4a7alo'>
+    </link>
+    <link rel='edit' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/st4vk9kiffs6rasrl32e4a7alo/63310109146'>
+    </link>
+    <author>
+      <name>GData Ops Demo</name>
+      <email>gdata ops demo gmail com</email>
+    </author>
+    <gCal:sendEventNotifications value='false'>
+    </gCal:sendEventNotifications>
+    <gd:eventStatus value='http://schemas.google.com/g/2005#event.confirmed'>
+    </gd:eventStatus>
+    <gd:comments>
+      <gd:feedLink href='http://www.google.com/calendar/feeds/default/private/full/st4vk9kiffs6rasrl32e4a7alo/comments'>
+      </gd:feedLink>
+    </gd:comments>
+    <gd:visibility value='http://schemas.google.com/g/2005#event.default'>
+    </gd:visibility>
+    <gd:transparency value='http://schemas.google.com/g/2005#event.opaque'>
+    </gd:transparency>
+    <gd:when startTime='2007-03-24T20:00:00.000-07:00'
+    endTime='2007-03-24T21:00:00.000-07:00'>
+      <gd:reminder minutes='10'></gd:reminder>
+    </gd:when>
+    <gd:where></gd:where>
+  </entry>
+  <entry>
+    <id>
+    http://www.google.com/calendar/feeds/default/private/full/ofl1e45ubtsoh6gtu127cls2oo</id>
+    <published>2007-03-20T21:24:43.000Z</published>
+    <updated>2007-03-20T21:25:08.000Z</updated>
+    <category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://schemas.google.com/g/2005#event'></category>
+    <title type='text'>Dinner with Kim and Sarah</title>
+    <content type='text'></content>
+    <link rel='alternate' type='text/html'
+    href='http://www.google.com/calendar/event?eid=b2ZsMWU0NXVidHNvaDZndHUxMjdjbHMyb28gZ2RhdGEub3BzLmRlbW9AbQ'
+    title='alternate'></link>
+    <link rel='self' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/ofl1e45ubtsoh6gtu127cls2oo'>
+    </link>
+    <link rel='edit' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/ofl1e45ubtsoh6gtu127cls2oo/63310109108'>
+    </link>
+    <author>
+      <name>GData Ops Demo</name>
+      <email>gdata ops demo gmail com</email>
+    </author>
+    <gCal:sendEventNotifications value='false'>
+    </gCal:sendEventNotifications>
+    <gd:eventStatus value='http://schemas.google.com/g/2005#event.confirmed'>
+    </gd:eventStatus>
+    <gd:comments>
+      <gd:feedLink href='http://www.google.com/calendar/feeds/default/private/full/ofl1e45ubtsoh6gtu127cls2oo/comments'>
+      </gd:feedLink>
+    </gd:comments>
+    <gd:visibility value='http://schemas.google.com/g/2005#event.default'>
+    </gd:visibility>
+    <gd:transparency value='http://schemas.google.com/g/2005#event.opaque'>
+    </gd:transparency>
+    <gd:when startTime='2007-03-20T19:00:00.000-07:00'
+    endTime='2007-03-20T21:30:00.000-07:00'>
+      <gd:reminder minutes='10'></gd:reminder>
+    </gd:when>
+    <gd:where></gd:where>
+  </entry>
+  <entry>
+    <id>
+    http://www.google.com/calendar/feeds/default/private/full/b69s2avfi2joigsclecvjlc91g</id>
+    <published>2007-03-20T21:24:19.000Z</published>
+    <updated>2007-03-20T21:25:05.000Z</updated>
+    <category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://schemas.google.com/g/2005#event'></category>
+    <title type='text'>Dinner with Jane and John</title>
+    <content type='text'></content>
+    <link rel='alternate' type='text/html'
+    href='http://www.google.com/calendar/event?eid=YjY5czJhdmZpMmpvaWdzY2xlY3ZqbGM5MWcgZ2RhdGEub3BzLmRlbW9AbQ'
+    title='alternate'></link>
+    <link rel='self' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/b69s2avfi2joigsclecvjlc91g'>
+    </link>
+    <link rel='edit' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/b69s2avfi2joigsclecvjlc91g/63310109105'>
+    </link>
+    <author>
+      <name>GData Ops Demo</name>
+      <email>gdata ops demo gmail com</email>
+    </author>
+    <gCal:sendEventNotifications value='false'>
+    </gCal:sendEventNotifications>
+    <gd:eventStatus value='http://schemas.google.com/g/2005#event.confirmed'>
+    </gd:eventStatus>
+    <gd:comments>
+      <gd:feedLink href='http://www.google.com/calendar/feeds/default/private/full/b69s2avfi2joigsclecvjlc91g/comments'>
+      </gd:feedLink>
+    </gd:comments>
+    <gd:visibility value='http://schemas.google.com/g/2005#event.default'>
+    </gd:visibility>
+    <gd:transparency value='http://schemas.google.com/g/2005#event.opaque'>
+    </gd:transparency>
+    <gd:when startTime='2007-03-22T17:00:00.000-07:00'
+    endTime='2007-03-22T19:30:00.000-07:00'>
+      <gd:reminder minutes='10'></gd:reminder>
+    </gd:when>
+    <gd:where></gd:where>
+  </entry>
+  <entry>
+    <id>
+    http://www.google.com/calendar/feeds/default/private/full/u9p66kkiotn8bqh9k7j4rcnjjc</id>
+    <published>2007-03-20T21:24:33.000Z</published>
+    <updated>2007-03-20T21:24:33.000Z</updated>
+    <category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://schemas.google.com/g/2005#event'></category>
+    <title type='text'>Tennis with Elizabeth</title>
+    <content type='text'></content>
+    <link rel='alternate' type='text/html'
+    href='http://www.google.com/calendar/event?eid=dTlwNjZra2lvdG44YnFoOWs3ajRyY25qamMgZ2RhdGEub3BzLmRlbW9AbQ'
+    title='alternate'></link>
+    <link rel='self' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/u9p66kkiotn8bqh9k7j4rcnjjc'>
+    </link>
+    <link rel='edit' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/u9p66kkiotn8bqh9k7j4rcnjjc/63310109073'>
+    </link>
+    <author>
+      <name>GData Ops Demo</name>
+      <email>gdata ops demo gmail com</email>
+    </author>
+    <gCal:sendEventNotifications value='false'>
+    </gCal:sendEventNotifications>
+    <gd:eventStatus value='http://schemas.google.com/g/2005#event.confirmed'>
+    </gd:eventStatus>
+    <gd:comments>
+      <gd:feedLink href='http://www.google.com/calendar/feeds/default/private/full/u9p66kkiotn8bqh9k7j4rcnjjc/comments'>
+      </gd:feedLink>
+    </gd:comments>
+    <gd:visibility value='http://schemas.google.com/g/2005#event.default'>
+    </gd:visibility>
+    <gd:transparency value='http://schemas.google.com/g/2005#event.opaque'>
+    </gd:transparency>
+    <gd:when startTime='2007-03-24T10:00:00.000-07:00'
+    endTime='2007-03-24T11:00:00.000-07:00'>
+      <gd:reminder minutes='10'></gd:reminder>
+    </gd:when>
+    <gd:where></gd:where>
+  </entry>
+  <entry>
+    <id>
+    http://www.google.com/calendar/feeds/default/private/full/76oj2kceidob3s708tvfnuaq3c</id>
+    <published>2007-03-20T21:24:00.000Z</published>
+    <updated>2007-03-20T21:24:00.000Z</updated>
+    <category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://schemas.google.com/g/2005#event'></category>
+    <title type='text'>Lunch with Jenn</title>
+    <content type='text'></content>
+    <link rel='alternate' type='text/html'
+    href='http://www.google.com/calendar/event?eid=NzZvajJrY2VpZG9iM3M3MDh0dmZudWFxM2MgZ2RhdGEub3BzLmRlbW9AbQ'
+    title='alternate'></link>
+    <link rel='self' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/76oj2kceidob3s708tvfnuaq3c'>
+    </link>
+    <link rel='edit' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/76oj2kceidob3s708tvfnuaq3c/63310109040'>
+    </link>
+    <author>
+      <name>GData Ops Demo</name>
+      <email>gdata ops demo gmail com</email>
+    </author>
+    <gCal:sendEventNotifications value='false'>
+    </gCal:sendEventNotifications>
+    <gd:eventStatus value='http://schemas.google.com/g/2005#event.confirmed'>
+    </gd:eventStatus>
+    <gd:comments>
+      <gd:feedLink href='http://www.google.com/calendar/feeds/default/private/full/76oj2kceidob3s708tvfnuaq3c/comments'>
+      </gd:feedLink>
+    </gd:comments>
+    <gd:visibility value='http://schemas.google.com/g/2005#event.default'>
+    </gd:visibility>
+    <gd:transparency value='http://schemas.google.com/g/2005#event.opaque'>
+    </gd:transparency>
+    <gd:when startTime='2007-03-20T11:30:00.000-07:00'
+    endTime='2007-03-20T12:30:00.000-07:00'>
+      <gd:reminder minutes='10'></gd:reminder>
+    </gd:when>
+    <gd:where></gd:where>
+  </entry>
+  <entry>
+    <id>
+    http://www.google.com/calendar/feeds/default/private/full/5np9ec8m7uoauk1vedh5mhodco</id>
+    <published>2007-03-20T07:50:02.000Z</published>
+    <updated>2007-03-20T20:39:26.000Z</updated>
+    <category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://schemas.google.com/g/2005#event'></category>
+    <title type='text'>test entry</title>
+    <content type='text'>test desc</content>
+    <link rel='alternate' type='text/html'
+    href='http://www.google.com/calendar/event?eid=NW5wOWVjOG03dW9hdWsxdmVkaDVtaG9kY28gZ2RhdGEub3BzLmRlbW9AbQ'
+    title='alternate'></link>
+    <link rel='self' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/5np9ec8m7uoauk1vedh5mhodco'>
+    </link>
+    <link rel='edit' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/5np9ec8m7uoauk1vedh5mhodco/63310106366'>
+    </link>
+    <author>
+      <name>GData Ops Demo</name>
+      <email>gdata ops demo gmail com</email>
+    </author>
+    <gCal:sendEventNotifications value='false'>
+    </gCal:sendEventNotifications>
+    <gd:eventStatus value='http://schemas.google.com/g/2005#event.confirmed'>
+    </gd:eventStatus>
+    <gd:comments>
+      <gd:feedLink href='http://www.google.com/calendar/feeds/default/private/full/5np9ec8m7uoauk1vedh5mhodco/comments'>
+      </gd:feedLink>
+    </gd:comments>
+    <gd:visibility value='http://schemas.google.com/g/2005#event.private'>
+    </gd:visibility>
+    <gd:transparency value='http://schemas.google.com/g/2005#event.opaque'>
+    </gd:transparency>
+    <gd:who rel='http://schemas.google.com/g/2005#event.attendee'
+    valueString='Vivian Li' email='vli google com'>
+      <gd:attendeeStatus value='http://schemas.google.com/g/2005#event.declined'>
+      </gd:attendeeStatus>
+    </gd:who>
+    <gd:who rel='http://schemas.google.com/g/2005#event.organizer'
+    valueString='GData Ops Demo' email='gdata ops demo gmail com'>
+      <gd:attendeeStatus value='http://schemas.google.com/g/2005#event.accepted'>
+      </gd:attendeeStatus>
+    </gd:who>
+    <gd:when startTime='2007-03-21T08:00:00.000-07:00'
+    endTime='2007-03-21T09:00:00.000-07:00'>
+      <gd:reminder minutes='10'></gd:reminder>
+    </gd:when>
+    <gd:where valueString='anywhere'></gd:where>
+  </entry>
+  <entry>
+    <id>
+    http://www.google.com/calendar/feeds/default/private/full/fu6sl0rqakf3o0a13oo1i1a1mg</id>
+    <published>2007-02-14T23:23:37.000Z</published>
+    <updated>2007-02-14T23:25:30.000Z</updated>
+    <category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://schemas.google.com/g/2005#event'></category>
+    <title type='text'>test</title>
+    <content type='text'></content>
+    <link rel='alternate' type='text/html'
+    href='http://www.google.com/calendar/event?eid=ZnU2c2wwcnFha2YzbzBhMTNvbzFpMWExbWcgZ2RhdGEub3BzLmRlbW9AbQ'
+    title='alternate'></link>
+    <link rel='self' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/fu6sl0rqakf3o0a13oo1i1a1mg'>
+    </link>
+    <link rel='edit' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/fu6sl0rqakf3o0a13oo1i1a1mg/63307178730'>
+    </link>
+    <link rel="http://schemas.google.com/gCal/2005/webContent"; title="World Cup" href="http://www.google.com/calendar/images/google-holiday.gif"; type="image/gif">
+      <gCal:webContent width="276" height="120" url="http://www.google.com/logos/worldcup06.gif"; />
+    </link>
+    <author>
+      <name>GData Ops Demo</name>
+      <email>gdata ops demo gmail com</email>
+    </author>
+    <gCal:sendEventNotifications value='false'>
+    </gCal:sendEventNotifications>
+    <gd:eventStatus value='http://schemas.google.com/g/2005#event.confirmed'>
+    </gd:eventStatus>
+    <gd:comments>
+      <gd:feedLink href='http://www.google.com/calendar/feeds/default/private/full/fu6sl0rqakf3o0a13oo1i1a1mg/comments'>
+      </gd:feedLink>
+    </gd:comments>
+    <gd:visibility value='http://schemas.google.com/g/2005#event.default'>
+    </gd:visibility>
+    <gd:transparency value='http://schemas.google.com/g/2005#event.opaque'>
+    </gd:transparency>
+    <gd:when startTime='2007-02-15T08:30:00.000-08:00'
+    endTime='2007-02-15T09:30:00.000-08:00'>
+      <gd:reminder minutes='10'></gd:reminder>
+    </gd:when>
+    <gd:where></gd:where>
+  </entry>
+  <entry>
+    <id>
+    http://www.google.com/calendar/feeds/default/private/full/h7a0haa4da8sil3rr19ia6luvc</id>
+    <published>2007-07-16T22:13:28.000Z</published>
+    <updated>2007-07-16T22:13:29.000Z</updated>
+    <category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://schemas.google.com/g/2005#event' />
+    <title type='text'></title>
+    <content type='text' />
+    <link rel='alternate' type='text/html'
+    href='http://www.google.com/calendar/event?eid=aDdhMGhhYTRkYThzaWwzcnIxOWlhNmx1dmMgZ2RhdGEub3BzLmRlbW9AbQ'
+    title='alternate' />
+    <link rel='http://schemas.google.com/gCal/2005/webContent'
+    type='application/x-google-gadgets+xml'
+    href='http://gdata.ops.demo.googlepages.com/birthdayicon.gif'
+    title='Date and Time Gadget'>
+      <gCal:webContent width='300' height='136'
+      url='http://google.com/ig/modules/datetime.xml'>
+        <gCal:webContentGadgetPref name='color' value='green' />
+      </gCal:webContent>
+    </link>
+    <link rel='self' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/h7a0haa4da8sil3rr19ia6luvc' />
+    <link rel='edit' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/full/h7a0haa4da8sil3rr19ia6luvc/63320307209' />
+    <author>
+      <name>GData Ops Demo</name>
+      <email>gdata ops demo gmail com</email>
+    </author>
+    <gd:comments>
+      <gd:feedLink href='http://www.google.com/calendar/feeds/default/private/full/h7a0haa4da8sil3rr19ia6luvc/comments' />
+    </gd:comments>
+    <gCal:sendEventNotifications value='false'>
+    </gCal:sendEventNotifications>
+    <gd:eventStatus value='http://schemas.google.com/g/2005#event.confirmed' />
+    <gd:visibility value='http://schemas.google.com/g/2005#event.default' />
+    <gd:transparency value='http://schemas.google.com/g/2005#event.opaque' />
+    <gd:when startTime='2007-03-14' endTime='2007-03-15' />
+    <gd:where />
+  </entry>
+</feed>
+"""
+
+CALENDAR_BATCH_REQUEST = """<?xml version='1.0' encoding='utf-8'?>
+<feed xmlns='http://www.w3.org/2005/Atom' 
+      xmlns:batch='http://schemas.google.com/gdata/batch'
+      xmlns:gCal='http://schemas.google.com/gCal/2005'>
+  <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/g/2005#event' />
+  <entry>
+    <batch:id>1</batch:id>
+    <batch:operation type='insert' />
+    <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/g/2005#event' />
+    <title type='text'>Event inserted via batch</title>
+  </entry>
+  <entry>
+    <batch:id>2</batch:id>
+    <batch:operation type='query' />
+    <id>http://www.google.com/calendar/feeds/default/private/full/glcs0kv2qqa0gf52qi1jo018gc</id>
+    <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/g/2005#event' />
+    <title type='text'>Event queried via batch</title>
+  </entry>
+  <entry>
+    <batch:id>3</batch:id>
+    <batch:operation type='update' />
+    <id>http://www.google.com/calendar/feeds/default/private/full/ujm0go5dtngdkr6u91dcqvj0qs</id>
+    <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/g/2005#event' />
+    <title type='text'>Event updated via batch</title>
+    <link rel='alternate' type='text/html' 
+        href='http://www.google.com/calendar/event?eid=dWptMGdvNWR0bmdka3I2dTkxZGNxdmowcXMgaGFyaXNodi50ZXN0QG0' title='alternate' />
+    <link rel='self' type='application/atom+xml' 
+        href='http://www.google.com/calendar/feeds/default/private/full/ujm0go5dtngdkr6u91dcqvj0qs' />
+    <link rel='edit' type='application/atom+xml' 
+        href='http://www.google.com/calendar/feeds/default/private/full/ujm0go5dtngdkr6u91dcqvj0qs/63326098791' />
+  </entry>
+  <entry>
+    <batch:id>4</batch:id>
+    <batch:operation type='delete' />
+    <id>http://www.google.com/calendar/feeds/default/private/full/d8qbg9egk1n6lhsgq1sjbqffqc</id>
+    <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/g/2005#event' />
+    <title type='text'>Event deleted via batch</title>
+    <link rel='alternate' type='text/html' 
+        href='http://www.google.com/calendar/event?eid=ZDhxYmc5ZWdrMW42bGhzZ3Exc2picWZmcWMgaGFyaXNodi50ZXN0QG0' title='alternate' />
+    <link rel='self' type='application/atom+xml' 
+        href='http://www.google.com/calendar/feeds/default/private/full/d8qbg9egk1n6lhsgq1sjbqffqc' />
+    <link rel='edit' type='application/atom+xml' 
+        href='http://www.google.com/calendar/feeds/default/private/full/d8qbg9egk1n6lhsgq1sjbqffqc/63326018324' />
+  </entry>
+</feed>
+"""
+
+CALENDAR_BATCH_RESPONSE = """<?xml version='1.0' encoding='UTF-8'?>
+<feed xmlns='http://www.w3.org/2005/Atom' 
+      xmlns:batch='http://schemas.google.com/gdata/batch'
+      xmlns:gCal='http://schemas.google.com/gCal/2005'>
+  <id>http://www.google.com/calendar/feeds/default/private/full</id>
+  <updated>2007-09-21T23:01:00.380Z</updated>
+  <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/g/2005#event'></category>
+  <title type='text'>Batch Feed</title>
+  <link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' 
+      href='http://www.google.com/calendar/feeds/default/private/full' />
+  <link rel='http://schemas.google.com/g/2005#post' type='application/atom+xml' 
+      href='http://www.google.com/calendar/feeds/default/private/full' />
+  <link rel='http://schemas.google.com/g/2005#batch' type='application/atom+xml' 
+      href='http://www.google.com/calendar/feeds/default/private/full/batch' />
+  <entry>
+    <batch:id>1</batch:id>
+    <batch:status code='201' reason='Created' />
+    <batch:operation type='insert' />
+    <id>http://www.google.com/calendar/feeds/default/private/full/n9ug78gd9tv53ppn4hdjvk68ek</id>
+    <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/g/2005#event' />
+    <title type='text'>Event inserted via batch</title>
+    <link rel='alternate' type='text/html' 
+        href='http://www.google.com/calendar/event?eid=bjl1Zzc4Z2Q5dHY1M3BwbjRoZGp2azY4ZWsgaGFyaXNodi50ZXN0QG0' title='alternate' />
+    <link rel='self' type='application/atom+xml' 
+        href='http://www.google.com/calendar/feeds/default/private/full/n9ug78gd9tv53ppn4hdjvk68ek' />
+    <link rel='edit' type='application/atom+xml' 
+      href='http://www.google.com/calendar/feeds/default/private/full/n9ug78gd9tv53ppn4hdjvk68ek/63326098860' />
+  </entry>
+  <entry>
+    <batch:id>2</batch:id>
+    <batch:status code='200' reason='Success' />
+    <batch:operation type='query' />
+    <id>http://www.google.com/calendar/feeds/default/private/full/glsc0kv2aqa0ff52qi1jo018gc</id>
+    <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/g/2005#event' />
+    <title type='text'>Event queried via batch</title>
+    <link rel='alternate' type='text/html' 
+        href='http://www.google.com/calendar/event?eid=Z2xzYzBrdjJhcWEwZmY1MnFpMWpvMDE4Z2MgaGFyaXNodi50ZXN0QG0' title='alternate' />
+    <link rel='self' type='application/atom+xml' 
+        href='http://www.google.com/calendar/feeds/default/private/full/glsc0kv2aqa0ff52qi1jo018gc' />
+    <link rel='edit' type='application/atom+xml' 
+        href='http://www.google.com/calendar/feeds/default/private/full/glsc0kv2aqa0ff52qi1jo018gc/63326098791' />
+  </entry>
+  <entry xmlns:gCal='http://schemas.google.com/gCal/2005'>
+    <batch:id>3</batch:id>
+    <batch:status code='200' reason='Success' />
+    <batch:operation type='update' />
+    <id>http://www.google.com/calendar/feeds/default/private/full/ujm0go5dtngdkr6u91dcqvj0qs</id>
+    <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/g/2005#event' />
+    <title type='text'>Event updated via batch</title>
+    <link rel='alternate' type='text/html' 
+        href='http://www.google.com/calendar/event?eid=dWptMGdvNWR0bmdka3I2dTkxZGNxdmowcXMgaGFyaXNodi50ZXN0QG0' title='alternate' />
+    <link rel='self' type='application/atom+xml' 
+        href='http://www.google.com/calendar/feeds/default/private/full/ujm0go5dtngdkr6u91dcqvj0qs' />
+    <link rel='edit' type='application/atom+xml' 
+        href='http://www.google.com/calendar/feeds/default/private/full/ujm0go5dtngdkr6u91dcqvj0qs/63326098860' />
+    <batch:id>3</batch:id>
+    <batch:status code='200' reason='Success' />
+    <batch:operation type='update' />
+  </entry>
+  <entry>
+    <batch:id>4</batch:id>
+    <batch:status code='200' reason='Success' />
+    <batch:operation type='delete' />
+    <id>http://www.google.com/calendar/feeds/default/private/full/d8qbg9egk1n6lhsgq1sjbqffqc</id>
+    <category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/g/2005#event' />
+    <title type='text'>Event deleted via batch</title>
+    <content type='text'>Deleted</content>
+  </entry>
+</feed>
+"""
+
+GBASE_ATTRIBUTE_FEED = """<?xml version='1.0' encoding='UTF-8'?>
+    <feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:gm='http://base.google.com/ns-metadata/1.0'>
+      <id>http://www.google.com/base/feeds/attributes</id>
+      <updated>2006-11-01T20:35:59.578Z</updated>
+      <category scheme='http://base.google.com/categories/itemtypes' term='online jobs'></category>
+      <category scheme='http://base.google.com/categories/itemtypes' term='jobs'></category>
+      <title type='text'>Attribute histogram for query: [item type:jobs]</title>
+      <link rel='alternate' type='text/html' href='http://base.google.com'></link>
+      <link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://www.google.com/base/feeds
+/attributes'></link>
+      <link rel='self' type='application/atom+xml' href='http://www.google.com/base/feeds/attributes/-/jobs'></link>
+      <generator version='1.0' uri='http://base.google.com'>GoogleBase</generator>
+      <openSearch:totalResults>16</openSearch:totalResults>
+      <openSearch:startIndex>1</openSearch:startIndex>
+      <openSearch:itemsPerPage>16</openSearch:itemsPerPage>
+      <entry>
+        <id>http://www.google.com/base/feeds/attributes/job+industry%28text%29N%5Bitem+type%3Ajobs%5D</id>
+        <updated>2006-11-01T20:36:00.100Z</updated>
+        <title type='text'>job industry(text)</title>
+        <content type='text'>Attribute"job industry" of type text.
+        </content>
+        <link rel='self' type='application/atom+xml' href='http://www.google.com/base/feeds/attributes/job+industry%28text
+%29N%5Bitem+type%3Ajobs%5D'></link>
+        <gm:attribute name='job industry' type='text' count='4416629'>
+          <gm:value count='380772'>it internet</gm:value>
+          <gm:value count='261565'>healthcare</gm:value>
+          <gm:value count='142018'>information technology</gm:value>
+          <gm:value count='124622'>accounting</gm:value>
+          <gm:value count='111311'>clerical and administrative</gm:value>
+          <gm:value count='82928'>other</gm:value>
+          <gm:value count='77620'>sales and sales management</gm:value>
+          <gm:value count='68764'>information systems</gm:value>
+          <gm:value count='65859'>engineering and architecture</gm:value>
+          <gm:value count='64757'>sales</gm:value>
+        </gm:attribute>
+      </entry>
+    </feed>
+"""
+
+
+GBASE_ATTRIBUTE_ENTRY = """<?xml version='1.0' encoding='UTF-8'?>
+ <entry xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:gm='http://base.google.com/ns-metadata/1.0'>
+        <id>http://www.google.com/base/feeds/attributes/job+industry%28text%29N%5Bitem+type%3Ajobs%5D</id>
+        <updated>2006-11-01T20:36:00.100Z</updated>
+        <title type='text'>job industry(text)</title>
+        <content type='text'>Attribute"job industry" of type text.
+        </content>
+        <link rel='self' type='application/atom+xml' href='http://www.google.com/base/feeds/attributes/job+industry%28text%29N%5Bitem+type%3Ajobs%5D'></link>
+        <gm:attribute name='job industry' type='text' count='4416629'>
+          <gm:value count='380772'>it internet</gm:value>
+          <gm:value count='261565'>healthcare</gm:value>
+          <gm:value count='142018'>information technology</gm:value>
+          <gm:value count='124622'>accounting</gm:value>
+          <gm:value count='111311'>clerical and administrative</gm:value>
+          <gm:value count='82928'>other</gm:value>
+          <gm:value count='77620'>sales and sales management</gm:value>
+          <gm:value count='68764'>information systems</gm:value>
+          <gm:value count='65859'>engineering and architecture</gm:value>
+          <gm:value count='64757'>sales</gm:value>
+        </gm:attribute>
+      </entry>
+""" 
+
+GBASE_LOCALES_FEED = """<?xml version='1.0' encoding='UTF-8'?>
+<feed xmlns='http://www.w3.org/2005/Atom'
+      xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'
+      xmlns:gm='http://base.google.com/ns-metadata/1.0'>
+         <id> http://www.google.com/base/feeds/locales/</id>
+  <updated>2006-06-13T18:11:40.120Z</updated>
+  <title type="text">Locales</title> 
+  <link rel="alternate" type="text/html" href="http://base.google.com"/>
+  <link rel="http://schemas.google.com/g/2005#feed"; type="application/atom+xml"
+
+       href="http://www.google.com/base/feeds/locales/"/>
+  <link rel="self" type="application/atom+xml" href="http://www.google.com/base/feeds/locales/"/>
+         <author>
+    <name>Google Inc.</name>
+    <email>base google com</email>
+  </author>
+  <generator version="1.0" uri="http://base.google.com";>GoogleBase</generator>
+  <openSearch:totalResults>3</openSearch:totalResults>
+  <openSearch:itemsPerPage>25</openSearch:itemsPerPage>
+
+<entry>
+  <id>http://www.google.com/base/feeds/locales/en_US</id>
+  <updated>2006-03-27T22:27:36.658Z</updated>
+  <category scheme="http://base.google.com/categories/locales"; term="en_US"/>
+
+  <title type="text">en_US</title>
+  <content type="text">en_US</content>
+  <link rel="self" type="application/atom+xml" 
+     href="http://www.google.com/base/feeds/locales/en_US";></link>
+
+  <link rel="related" type="application/atom+xml" 
+     href="http://www.google.com/base/feeds/itemtypes/en_US"; title="Item types in en_US"/>
+</entry>
+<entry>
+         <id>http://www.google.com/base/feeds/locales/en_GB</id>
+  <updated>2006-06-13T18:14:18.601Z</updated>
+  <category scheme="http://base.google.com/categories/locales"; term="en_GB"/>
+  <title type="text">en_GB</title>
+  <content type="text">en_GB</content>
+  <link rel="related" type="application/atom+xml" 
+     href="http://www.google.com/base/feeds/itemtypes/en_GB"; title="Item types in en_GB"/>
+  <link rel="self" type="application/atom+xml" 
+     href="http://www.google.com/base/feeds/locales/en_GB"/>
+</entry>
+<entry>
+  <id>http://www.google.com/base/feeds/locales/de_DE</id>
+  <updated>2006-06-13T18:14:18.601Z</updated>
+  <category scheme="http://base.google.com/categories/locales"; term="de_DE"/>
+  <title type="text">de_DE</title>
+  <content type="text">de_DE</content>
+  <link rel="related" type="application/atom+xml" 
+     href="http://www.google.com/base/feeds/itemtypes/de_DE"; title="Item types in de_DE"/>
+  <link rel="self" type="application/atom+xml" 
+     href="http://www.google.com/base/feeds/locales/de_DE"/>
+</entry>
+</feed>"""
+
+GBASE_STRING_ENCODING_ENTRY = """<?xml version='1.0' encoding='UTF-8'?>
+<entry xmlns='http://www.w3.org/2005/Atom' xmlns:gm='http://base.google.com/ns-metadata/1.0' 
+       xmlns:g='http://base.google.com/ns/1.0' xmlns:batch='http://schemas.google.com/gdata/batch'>
+  <id>http://www.google.com/base/feeds/snippets/17495780256183230088</id>
+  <published>2007-12-09T03:13:07.000Z</published>
+  <updated>2008-01-07T03:26:46.000Z</updated>
+  <category scheme='http://base.google.com/categories/itemtypes' term='Products'/>
+  <title type='text'>Digital Camera Cord Fits SONY Cybershot DSC-R1 S40</title>
+  <content type='html'>SONY \xC2\xB7 Cybershot Digital Camera Usb Cable DESCRIPTION 
+      This is a 2.5 USB 2.0 A to Mini B (5 Pin) high quality digital camera 
+      cable used for connecting your Sony Digital Cameras and Camcoders. Backward 
+      Compatible with USB 2.0, 1.0 and 1.1. Fully  ...</content>
+  <link rel='alternate' type='text/html' 
+        href='http://adfarm.mediaplex.com/ad/ck/711-5256-8196-2?loc=http%3A%2F%2Fcgi.ebay.com%2FDigital-Camera-Cord-Fits-SONY-Cybershot-DSC-R1-S40_W0QQitemZ270195049057QQcmdZViewItem'/>
+  <link rel='self' type='application/atom+xml' 
+        href='http://www.google.com/base/feeds/snippets/17495780256183230088'/>
+  <author>
+    <name>eBay</name>
+  </author>
+  <g:item_type type='text'>Products</g:item_type>
+  <g:item_language type='text'>EN</g:item_language>
+  <g:target_country type='text'>US</g:target_country>
+  <g:price type='floatUnit'>0.99 usd</g:price>
+  <g:image_link type='url'>http://thumbs.ebaystatic.com/pict/270195049057_1.jpg</g:image_link>
+  <g:category type='text'>Cameras &amp; Photo&gt;Digital Camera Accessories&gt;Cables</g:category>
+  <g:category type='text'>Cords &amp; Connectors&gt;USB Cables&gt;For Other Brands</g:category>
+  <g:customer_id type='int'>11729</g:customer_id>
+  <g:id type='text'>270195049057</g:id>
+  <g:expiration_date type='dateTime'>2008-02-06T03:26:46Z</g:expiration_date>
+</entry>"""
+
+
+RECURRENCE_EXCEPTION_ENTRY = """<entry xmlns='http://www.w3.org/2005/Atom'
+xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'
+xmlns:gd='http://schemas.google.com/g/2005'
+xmlns:gCal='http://schemas.google.com/gCal/2005'>
+    <id>
+    http://www.google.com/calendar/feeds/default/private/composite/i7lgfj69mjqjgnodklif3vbm7g</id>
+    <published>2007-04-05T21:51:49.000Z</published>
+    <updated>2007-04-05T21:51:49.000Z</updated>
+    <category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://schemas.google.com/g/2005#event'></category>
+    <title type='text'>testDavid</title>
+    <content type='text'></content>
+    <link rel='alternate' type='text/html'
+    href='http://www.google.com/calendar/event?eid=aTdsZ2ZqNjltanFqZ25vZGtsaWYzdmJtN2dfMjAwNzA0MDNUMTgwMDAwWiBnZGF0YS5vcHMudGVzdEBt'
+    title='alternate'></link>
+    <link rel='self' type='application/atom+xml'
+    href='http://www.google.com/calendar/feeds/default/private/composite/i7lgfj69mjqjgnodklif3vbm7g'>
+    </link>
+    <author>
+      <name>gdata ops</name>
+      <email>gdata ops test gmail com</email>
+    </author>
+    <gd:visibility value='http://schemas.google.com/g/2005#event.default'>
+    </gd:visibility>
+    <gCal:sendEventNotifications value='true'>
+    </gCal:sendEventNotifications>
+    <gd:transparency value='http://schemas.google.com/g/2005#event.opaque'>
+    </gd:transparency>
+    <gd:eventStatus value='http://schemas.google.com/g/2005#event.confirmed'>
+    </gd:eventStatus>
+    <gd:recurrence>DTSTART;TZID=America/Anchorage:20070403T100000
+    DTEND;TZID=America/Anchorage:20070403T110000
+    RRULE:FREQ=DAILY;UNTIL=20070408T180000Z;WKST=SU
+    EXDATE;TZID=America/Anchorage:20070407T100000
+    EXDATE;TZID=America/Anchorage:20070405T100000
+    EXDATE;TZID=America/Anchorage:20070404T100000 BEGIN:VTIMEZONE
+    TZID:America/Anchorage X-LIC-LOCATION:America/Anchorage
+    BEGIN:STANDARD TZOFFSETFROM:-0800 TZOFFSETTO:-0900 TZNAME:AKST
+    DTSTART:19701025T020000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+    END:STANDARD BEGIN:DAYLIGHT TZOFFSETFROM:-0900 TZOFFSETTO:-0800
+    TZNAME:AKDT DTSTART:19700405T020000
+    RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU END:DAYLIGHT
+    END:VTIMEZONE</gd:recurrence>
+    <gd:where valueString=''></gd:where>
+    <gd:reminder minutes='10'></gd:reminder>
+    <gd:recurrenceException specialized='true'>
+      <gd:entryLink>
+        <entry>
+          <id>i7lgfj69mjqjgnodklif3vbm7g_20070407T180000Z</id>
+          <published>2007-04-05T21:51:49.000Z</published>
+          <updated>2007-04-05T21:52:58.000Z</updated>
+          <category scheme='http://schemas.google.com/g/2005#kind'
+          term='http://schemas.google.com/g/2005#event'></category>
+          <title type='text'>testDavid</title>
+          <content type='text'></content>
+          <link rel='alternate' type='text/html'
+          href='http://www.google.com/calendar/event?eid=aTdsZ2ZqNjltanFqZ25vZGtsaWYzdmJtN2dfMjAwNzA0MDdUMTgwMDAwWiBnZGF0YS5vcHMudGVzdEBt'
+          title='alternate'></link>
+          <author>
+            <name>gdata ops</name>
+            <email>gdata ops test gmail com</email>
+          </author>
+          <gd:visibility value='http://schemas.google.com/g/2005#event.default'>
+          </gd:visibility>
+          <gd:originalEvent id='i7lgfj69mjqjgnodklif3vbm7g'
+          href='http://www.google.com/calendar/feeds/default/private/composite/i7lgfj69mjqjgnodklif3vbm7g'>
+
+            <gd:when startTime='2007-04-07T13:00:00.000-05:00'>
+            </gd:when>
+          </gd:originalEvent>
+          <gCal:sendEventNotifications value='false'>
+          </gCal:sendEventNotifications>
+          <gd:transparency value='http://schemas.google.com/g/2005#event.opaque'>
+          </gd:transparency>
+          <gd:eventStatus value='http://schemas.google.com/g/2005#event.canceled'>
+          </gd:eventStatus>
+          <gd:comments>
+            <gd:feedLink href='http://www.google.com/calendar/feeds/default/private/full/i7lgfj69mjqjgnodklif3vbm7g_20070407T180000Z/comments'>
+
+              <feed>
+                <updated>2007-04-05T21:54:09.285Z</updated>
+                <category scheme='http://schemas.google.com/g/2005#kind'
+                term='http://schemas.google.com/g/2005#message'>
+                </category>
+                <title type='text'>Comments for: testDavid</title>
+                <link rel='alternate' type='text/html'
+                href='http://www.google.com/calendar/feeds/default/private/full/i7lgfj69mjqjgnodklif3vbm7g_20070407T180000Z/comments'
+                title='alternate'></link>
+              </feed>
+            </gd:feedLink>
+          </gd:comments>
+          <gd:when startTime='2007-04-07T13:00:00.000-05:00'
+          endTime='2007-04-07T14:00:00.000-05:00'>
+            <gd:reminder minutes='10'></gd:reminder>
+          </gd:when>
+          <gd:where valueString=''></gd:where>
+        </entry>
+      </gd:entryLink>
+    </gd:recurrenceException>
+  </entry>"""
+
+NICK_ENTRY = """<?xml version="1.0" encoding="UTF-8"?>
+<atom:entry xmlns:atom="http://www.w3.org/2005/Atom";
+  xmlns:apps="http://schemas.google.com/apps/2006";
+  xmlns:gd="http://schemas.google.com/g/2005";>
+  <atom:id>https://www.google.com/a/feeds/example.com/nickname/2.0/Foo</atom:id>
+  <atom:updated>1970-01-01T00:00:00.000Z</atom:updated>
+  <atom:category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://schemas.google.com/apps/2006#nickname'/>
+  <atom:title type="text">Foo</atom:title>
+  <atom:link rel="self" type="application/atom+xml"
+    href="https://www.google.com/a/feeds/example.com/nickname/2.0/Foo"/>
+  <atom:link rel="edit" type="application/atom+xml"
+    href="https://www.google.com/a/feeds/example.com/nickname/2.0/Foo"/>
+  <apps:nickname name="Foo"/>
+  <apps:login userName="TestUser"/>
+</atom:entry>"""
+
+NICK_FEED = """<?xml version="1.0" encoding="UTF-8"?>
+<atom:feed xmlns:atom="http://www.w3.org/2005/Atom";
+  xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/";
+  xmlns:apps="http://schemas.google.com/apps/2006";>
+  <atom:id>
+    http://www.google.com/a/feeds/example.com/nickname/2.0
+  </atom:id>
+  <atom:updated>1970-01-01T00:00:00.000Z</atom:updated>
+  <atom:category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://schemas.google.com/apps/2006#nickname'/>
+  <atom:title type="text">Nicknames for user SusanJones</atom:title>
+  <atom:link rel='http://schemas.google.com/g/2005#feed'
+    type="application/atom+xml"
+    href="http://www.google.com/a/feeds/example.com/nickname/2.0"/>
+  <atom:link rel='http://schemas.google.com/g/2005#post'
+    type="application/atom+xml"
+    href="http://www.google.com/a/feeds/example.com/nickname/2.0"/>
+  <atom:link rel="self" type="application/atom+xml"
+    href="http://www.google.com/a/feeds/example.com/nickname/2.0?username=TestUser"/>
+  <openSearch:startIndex>1</openSearch:startIndex>
+  <openSearch:itemsPerPage>2</openSearch:itemsPerPage>
+  <atom:entry>
+    <atom:id>
+      http://www.google.com/a/feeds/example.com/nickname/2.0/Foo
+    </atom:id>
+    <atom:category scheme='http://schemas.google.com/g/2005#kind'
+      term='http://schemas.google.com/apps/2006#nickname'/>
+    <atom:title type="text">Foo</atom:title>
+    <atom:link rel="self" type="application/atom+xml"
+      href="http://www.google.com/a/feeds/example.com/nickname/2.0/Foo"/>
+    <atom:link rel="edit" type="application/atom+xml"
+      href="http://www.google.com/a/feeds/example.com/nickname/2.0/Foo"/>
+    <apps:nickname name="Foo"/>
+    <apps:login userName="TestUser"/>
+  </atom:entry>
+  <atom:entry>
+    <atom:id>
+      http://www.google.com/a/feeds/example.com/nickname/2.0/suse
+    </atom:id>
+    <atom:category scheme='http://schemas.google.com/g/2005#kind'
+      term='http://schemas.google.com/apps/2006#nickname'/>
+    <atom:title type="text">suse</atom:title>
+    <atom:link rel="self" type="application/atom+xml"
+      href="http://www.google.com/a/feeds/example.com/nickname/2.0/Bar"/>
+    <atom:link rel="edit" type="application/atom+xml"
+      href="http://www.google.com/a/feeds/example.com/nickname/2.0/Bar"/>
+    <apps:nickname name="Bar"/>
+    <apps:login userName="TestUser"/>
+  </atom:entry>
+</atom:feed>"""
+
+USER_ENTRY = """<?xml version="1.0" encoding="UTF-8"?>
+<atom:entry xmlns:atom="http://www.w3.org/2005/Atom";
+            xmlns:apps="http://schemas.google.com/apps/2006";
+            xmlns:gd="http://schemas.google.com/g/2005";>
+  <atom:id>https://www.google.com/a/feeds/example.com/user/2.0/TestUser</atom:id>
+  <atom:updated>1970-01-01T00:00:00.000Z</atom:updated>
+  <atom:category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://schemas.google.com/apps/2006#user'/>
+  <atom:title type="text">TestUser</atom:title>
+  <atom:link rel="self" type="application/atom+xml"
+    href="https://www.google.com/a/feeds/example.com/user/2.0/TestUser"/>
+  <atom:link rel="edit" type="application/atom+xml"
+    href="https://www.google.com/a/feeds/example.com/user/2.0/TestUser"/>
+  <apps:login userName="TestUser" password="password" suspended="false"
+    ipWhitelisted='false' hashFunctionName="SHA-1"/>
+  <apps:name familyName="Test" givenName="User"/>
+  <apps:quota limit="1024"/>
+  <gd:feedLink rel='http://schemas.google.com/apps/2006#user.nicknames'
+    href="https://www.google.com/a/feeds/example.com/nickname/2.0?username=Test-3121"/>
+  <gd:feedLink rel='http://schemas.google.com/apps/2006#user.emailLists'
+    href="https://www.google.com/a/feeds/example.com/emailList/2 0?recipient=testlist example com"/>
+</atom:entry>"""
+
+USER_FEED = """<?xml version="1.0" encoding="UTF-8"?>
+<atom:feed xmlns:atom="http://www.w3.org/2005/Atom"; 
+  xmlns:apps="http://schemas.google.com/apps/2006";
+  xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/";
+  xmlns:gd="http://schemas.google.com/g/2005";>
+    <atom:id>
+        http://www.google.com/a/feeds/example.com/user/2.0
+    </atom:id>
+    <atom:updated>1970-01-01T00:00:00.000Z</atom:updated>
+    <atom:category scheme='http://schemas.google.com/g/2005#kind' 
+        term='http://schemas.google.com/apps/2006#user'/>
+    <atom:title type="text">Users</atom:title>
+    <atom:link rel="next" type="application/atom+xml" 
+        href="http://www.google.com/a/feeds/example.com/user/2.0?startUsername=john"/>
+    <atom:link rel='http://schemas.google.com/g/2005#feed' 
+        type="application/atom+xml" 
+        href="http://www.google.com/a/feeds/example.com/user/2.0"/>
+    <atom:link rel='http://schemas.google.com/g/2005#post'
+        type="application/atom+xml"
+        href="http://www.google.com/a/feeds/example.com/user/2.0"/>
+    <atom:link rel="self" type="application/atom+xml" 
+        href="http://www.google.com/a/feeds/example.com/user/2.0"/>
+    <openSearch:startIndex>1</openSearch:startIndex>
+    <atom:entry>
+        <atom:id>
+            http://www.google.com/a/feeds/example.com/user/2.0/TestUser
+        </atom:id>
+        <atom:category scheme='http://schemas.google.com/g/2005#kind'
+            term='http://schemas.google.com/apps/2006#user'/>
+        <atom:title type="text">TestUser</atom:title>
+        <atom:link rel="self" type="application/atom+xml" 
+            href="http://www.google.com/a/feeds/example.com/user/2.0/TestUser"/>
+        <atom:link rel="edit" type="application/atom+xml"
+            href="http://www.google.com/a/feeds/example.com/user/2.0/TestUser"/>
+        <gd:who rel='http://schemas.google.com/apps/2006#user.recipient' 
+            email="TestUser example com"/>
+        <apps:login userName="TestUser" suspended="false"/>
+        <apps:quota limit="2048"/>
+        <apps:name familyName="Test" givenName="User"/>
+        <gd:feedLink rel='http://schemas.google.com/apps/2006#user.nicknames'
+            href="http://www.google.com/a/feeds/example.com/nickname/2.0?username=TestUser"/>
+        <gd:feedLink rel='http://schemas.google.com/apps/2006#user.emailLists'
+            href="http://www.google.com/a/feeds/example.com/emailList/2 0?recipient=TestUser example com"/>
+    </atom:entry>
+    <atom:entry>
+        <atom:id>
+            http://www.google.com/a/feeds/example.com/user/2.0/JohnSmith
+        </atom:id>
+        <atom:category scheme='http://schemas.google.com/g/2005#kind'
+            term='http://schemas.google.com/apps/2006#user'/>
+        <atom:title type="text">JohnSmith</atom:title>
+        <atom:link rel="self" type="application/atom+xml" 
+            href="http://www.google.com/a/feeds/example.com/user/2.0/JohnSmith"/>
+        <atom:link rel="edit" type="application/atom+xml"
+            href="http://www.google.com/a/feeds/example.com/user/2.0/JohnSmith"/>
+        <gd:who rel='http://schemas.google.com/apps/2006#user.recipient'
+            email="JohnSmith example com"/>
+        <apps:login userName="JohnSmith" suspended="false"/>
+        <apps:quota limit="2048"/>
+        <apps:name familyName="Smith" givenName="John"/>
+        <gd:feedLink rel='http://schemas.google.com/apps/2006#user.nicknames'
+            href="http://www.google.com/a/feeds/example.com/nickname/2.0?username=JohnSmith"/>
+        <gd:feedLink rel='http://schemas.google.com/apps/2006#user.emailLists'
+            href="http://www.google.com/a/feeds/example.com/emailList/2 0?recipient=JohnSmith example com"/>
+    </atom:entry>
+</atom:feed>"""
+
+EMAIL_LIST_ENTRY = """<?xml version="1.0" encoding="UTF-8"?>
+<atom:entry xmlns:atom="http://www.w3.org/2005/Atom";
+  xmlns:apps="http://schemas.google.com/apps/2006";
+  xmlns:gd="http://schemas.google.com/g/2005";>
+    <atom:id>
+      https://www.google.com/a/feeds/example.com/emailList/2.0/testlist
+    </atom:id>
+    <atom:updated>1970-01-01T00:00:00.000Z</atom:updated>
+    <atom:category scheme='http://schemas.google.com/g/2005#kind'
+      term='http://schemas.google.com/apps/2006#emailList'/>
+    <atom:title type="text">testlist</atom:title>
+    <atom:link rel="self" type="application/atom+xml" 
+      href="https://www.google.com/a/feeds/example.com/emailList/2.0/testlist"/>
+    <atom:link rel="edit" type="application/atom+xml" 
+      href="https://www.google.com/a/feeds/example.com/emailList/2.0/testlist"/>
+    <apps:emailList name="testlist"/>
+    <gd:feedLink rel='http://schemas.google.com/apps/2006#emailList.recipients'
+        href="http://www.google.com/a/feeds/example.com/emailList/2.0/testlist/recipient/"/>
+</atom:entry>"""
+
+EMAIL_LIST_FEED = """<?xml version="1.0" encoding="UTF-8"?>
+<atom:feed xmlns:atom="http://www.w3.org/2005/Atom"; 
+  xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/";
+  xmlns:apps="http://schemas.google.com/apps/2006";
+  xmlns:gd="http://schemas.google.com/g/2005";>
+    <atom:id>
+        http://www.google.com/a/feeds/example.com/emailList/2.0
+    </atom:id>
+    <atom:updated>1970-01-01T00:00:00.000Z</atom:updated>
+    <atom:category scheme='http://schemas.google.com/g/2005#kind'
+        term='http://schemas.google.com/apps/2006#emailList'/>
+    <atom:title type="text">EmailLists</atom:title>
+    <atom:link rel="next" type="application/atom+xml" 
+        href="http://www.google.com/a/feeds/example.com/emailList/2.0?startEmailListName=john"/>
+    <atom:link rel='http://schemas.google.com/g/2005#feed'
+        type="application/atom+xml" 
+        href="http://www.google.com/a/feeds/example.com/emailList/2.0"/>
+    <atom:link rel='http://schemas.google.com/g/2005#post' 
+        type="application/atom+xml"
+        href="http://www.google.com/a/feeds/example.com/emailList/2.0"/>
+    <atom:link rel="self" type="application/atom+xml" 
+        href="http://www.google.com/a/feeds/example.com/emailList/2.0"/>
+    <openSearch:startIndex>1</openSearch:startIndex>
+    <atom:entry>
+        <atom:id>
+            http://www.google.com/a/feeds/example.com/emailList/2.0/us-sales
+        </atom:id>
+        <atom:updated>1970-01-01T00:00:00.000Z</atom:updated>
+        <atom:category scheme='http://schemas.google.com/g/2005#kind'
+            term='http://schemas.google.com/apps/2006#emailList'/>
+        <atom:title type="text">us-sales</atom:title>
+        <atom:link rel="self" type="application/atom+xml" 
+            href="http://www.google.com/a/feeds/example.com/emailList/2.0/us-sales"/>
+        <atom:link rel="edit" type="application/atom+xml"
+            href="http://www.google.com/a/feeds/example.com/emailList/2.0/us-sales"/>
+        <apps:emailList name="us-sales"/>
+        <gd:feedLink rel='http://schemas.google.com/apps/2006#emailList.recipients'
+            href="http://www.google.com/a/feeds/example.com/emailList/2.0/us-sales/recipient/"/>
+    </atom:entry>
+    <atom:entry>
+        <atom:id>
+            http://www.google.com/a/feeds/example.com/emailList/2.0/us-eng
+        </atom:id>
+        <atom:updated>1970-01-01T00:00:00.000Z</atom:updated>
+        <atom:category scheme='http://schemas.google.com/g/2005#kind'
+            term='http://schemas.google.com/apps/2006#emailList'/>
+        <atom:title type="text">us-eng</atom:title>
+        <atom:link rel="self" type="application/atom+xml" 
+            href="http://www.google.com/a/feeds/example.com/emailList/2.0/us-eng"/>
+        <atom:link rel="edit" type="application/atom+xml"
+            href="http://www.google.com/a/feeds/example.com/emailList/2.0/us-eng"/>
+        <apps:emailList name="us-eng"/>
+        <gd:feedLink rel='http://schemas.google.com/apps/2006#emailList.recipients'
+            href="http://www.google.com/a/feeds/example.com/emailList/2.0/us-eng/recipient/"/>
+    </atom:entry>
+</atom:feed>"""
+
+EMAIL_LIST_RECIPIENT_ENTRY = """<?xml version="1.0" encoding="UTF-8"?>
+<atom:entry xmlns:atom="http://www.w3.org/2005/Atom";
+  xmlns:apps="http://schemas.google.com/apps/2006";
+  xmlns:gd="http://schemas.google.com/g/2005";>
+    <atom:id>https://www.google.com/a/feeds/example.com/emailList/2.0/us-sales/recipient/TestUser%40example.com</atom:id>
+    <atom:updated>1970-01-01T00:00:00.000Z</atom:updated>
+    <atom:category scheme='http://schemas.google.com/g/2005#kind'
+        term='http://schemas.google.com/apps/2006#emailList.recipient'/>
+    <atom:title type="text">TestUser</atom:title>
+    <atom:link rel="self" type="application/atom+xml" 
+        href="https://www.google.com/a/feeds/example.com/emailList/2.0/us-sales/recipient/TestUser%40example.com"/>
+    <atom:link rel="edit" type="application/atom+xml" 
+        href="https://www.google.com/a/feeds/example.com/emailList/2.0/us-sales/recipient/TestUser%40example.com"/>
+    <gd:who email="TestUser example com"/>
+</atom:entry>"""
+
+EMAIL_LIST_RECIPIENT_FEED = """<?xml version="1.0" encoding="UTF-8"?>
+<atom:feed xmlns:atom="http://www.w3.org/2005/Atom"; 
+  xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/";
+  xmlns:gd="http://schemas.google.com/g/2005";>
+    <atom:id>
+        http://www.google.com/a/feeds/example.com/emailList/2.0/us-sales/recipient
+    </atom:id>
+    <atom:updated>1970-01-01T00:00:00.000Z</atom:updated>
+    <atom:category scheme='http://schemas.google.com/g/2005#kind'
+        term='http://schemas.google.com/apps/2006#emailList.recipient'/>
+    <atom:title type="text">Recipients for email list us-sales</atom:title>
+    <atom:link rel="next" type="application/atom+xml" 
+        href="http://www.google.com/a/feeds/example.com/emailList/2.0/us-sales/recipient/?startRecipient=terry example com"/>
+    <atom:link rel='http://schemas.google.com/g/2005#feed'
+        type="application/atom+xml" 
+        href="http://www.google.com/a/feeds/example.com/emailList/2.0/us-sales/recipient"/>
+    <atom:link rel='http://schemas.google.com/g/2005#post'
+        type="application/atom+xml"
+        href="http://www.google.com/a/feeds/example.com/emailList/2.0/us-sales/recipient"/>
+    <atom:link rel="self" type="application/atom+xml" 
+        href="http://www.google.com/a/feeds/example.com/emailList/2.0/us-sales/recipient"/>
+    <openSearch:startIndex>1</openSearch:startIndex>
+    <atom:entry>
+        <atom:id>
+            http://www.google.com/a/feeds/example.com/emailList/2.0/us-sales/recipient/joe%40example.com
+        </atom:id>
+        <atom:updated>1970-01-01T00:00:00.000Z</atom:updated>
+        <atom:category scheme='http://schemas.google.com/g/2005#kind'
+            term='http://schemas.google.com/apps/2006#emailList.recipient'/>
+        <atom:title type="text">joe example com</atom:title>
+        <atom:link rel="self" type="application/atom+xml" 
+            href="http://www.google.com/a/feeds/example.com/emailList/2.0/us-sales/recipient/joe%40example.com"/>
+        <atom:link rel="edit" type="application/atom+xml"
+            href="http://www.google.com/a/feeds/example.com/emailList/2.0/us-sales/recipient/joe%40example.com"/>
+        <gd:who email="joe example com"/>
+    </atom:entry>
+    <atom:entry>
+        <atom:id>
+            http://www.google.com/a/feeds/example.com/emailList/2.0/us-sales/recipient/susan%40example.com
+        </atom:id>
+        <atom:updated>1970-01-01T00:00:00.000Z</atom:updated>
+        <atom:category scheme='http://schemas.google.com/g/2005#kind'
+            term='http://schemas.google.com/apps/2006#emailList.recipient'/>
+        <atom:title type="text">susan example com</atom:title>
+        <atom:link rel="self" type="application/atom+xml" 
+            href="http://www.google.com/a/feeds/example.com/emailList/2.0/us-sales/recipient/susan%40example.com"/>
+        <atom:link rel="edit" type="application/atom+xml"
+            href="http://www.google.com/a/feeds/example.com/emailList/2.0/us-sales/recipient/susan%40example.com"/>
+        <gd:who email="susan example com"/>
+    </atom:entry>
+</atom:feed>"""
+
+ACL_FEED = """<?xml version='1.0' encoding='UTF-8'?>
+  <feed xmlns='http://www.w3.org/2005/Atom'
+      xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'
+      xmlns:gAcl='http://schemas.google.com/acl/2007'>
+    <id>http://www.google.com/calendar/feeds/liz%40gmail.com/acl/full</id>
+    <updated>2007-04-21T00:52:04.000Z</updated>
+    <title type='text'>Elizabeth Bennet's access control list</title>
+    <link rel='http://schemas.google.com/acl/2007#controlledObject'
+      type='application/atom+xml'
+      href='http://www.google.com/calendar/feeds/liz%40gmail.com/private/full'>
+    </link>
+    <link rel='http://schemas.google.com/g/2005#feed'
+      type='application/atom+xml'
+      href='http://www.google.com/calendar/feeds/liz%40gmail.com/acl/full'>
+    </link>
+    <link rel='http://schemas.google.com/g/2005#post'
+      type='application/atom+xml'
+      href='http://www.google.com/calendar/feeds/liz%40gmail.com/acl/full'>
+    </link>
+    <link rel='self' type='application/atom+xml'
+      href='http://www.google.com/calendar/feeds/liz%40gmail.com/acl/full'>
+    </link>
+    <generator version='1.0'
+      uri='http://www.google.com/calendar'>Google Calendar</generator>
+    <openSearch:totalResults>2</openSearch:totalResults>
+    <openSearch:startIndex>1</openSearch:startIndex>
+    <entry>
+      <id>http://www.google.com/calendar/feeds/liz%40gmail.com/acl/full/user%3Aliz%40gmail.com</id>
+      <updated>2007-04-21T00:52:04.000Z</updated>
+      <category scheme='http://schemas.google.com/g/2005#kind'
+        term='http://schemas.google.com/acl/2007#accessRule'>
+      </category>
+      <title type='text'>owner</title>
+      <content type='text'></content>
+      <link rel='self' type='application/atom+xml'
+        href='http://www.google.com/calendar/feeds/liz%40gmail.com/acl/full/user%3Aliz%40gmail.com'>
+      </link>
+      <link rel='edit' type='application/atom+xml'
+        href='http://www.google.com/calendar/feeds/liz%40gmail.com/acl/full/user%3Aliz%40gmail.com'>
+      </link>
+      <author>
+        <name>Elizabeth Bennet</name>
+        <email>liz gmail com</email>
+      </author>
+      <gAcl:scope type='user' value='liz gmail com'></gAcl:scope>
+      <gAcl:role value='http://schemas.google.com/gCal/2005#owner'>
+      </gAcl:role>
+    </entry>
+    <entry>
+      <id>http://www.google.com/calendar/feeds/liz%40gmail.com/acl/full/default</id>
+      <updated>2007-04-21T00:52:04.000Z</updated>
+      <category scheme='http://schemas.google.com/g/2005#kind'
+        term='http://schemas.google.com/acl/2007#accessRule'>
+      </category>
+      <title type='text'>read</title>
+      <content type='text'></content>
+      <link rel='self' type='application/atom+xml'
+        href='http://www.google.com/calendar/feeds/liz%40gmail.com/acl/full/default'>
+      </link>
+      <link rel='edit' type='application/atom+xml'
+        href='http://www.google.com/calendar/feeds/liz%40gmail.com/acl/full/default'>
+      </link>
+      <author>
+        <name>Elizabeth Bennet</name>
+        <email>liz gmail com</email>
+      </author>
+      <gAcl:scope type='default'></gAcl:scope>
+      <gAcl:role value='http://schemas.google.com/gCal/2005#read'>
+      </gAcl:role>
+    </entry>
+  </feed>"""
+
+ACL_ENTRY = """<?xml version='1.0' encoding='UTF-8'?>
+  <entry xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:gd='http://schemas.google.com/g/2005' xmlns:gCal='http://schemas.google.com/gCal/2005' xmlns:gAcl='http://schemas.google.com/acl/2007'>
+    <id>http://www.google.com/calendar/feeds/liz%40gmail.com/acl/full/user%3Aliz%40gmail.com</id>
+    <updated>2007-04-21T00:52:04.000Z</updated>
+    <category scheme='http://schemas.google.com/g/2005#kind'
+      term='http://schemas.google.com/acl/2007#accessRule'>
+    </category>
+    <title type='text'>owner</title>
+    <content type='text'></content>
+    <link rel='self' type='application/atom+xml'
+      href='http://www.google.com/calendar/feeds/liz%40gmail.com/acl/full/user%3Aliz%40gmail.com'>
+    </link>
+    <link rel='edit' type='application/atom+xml'
+      href='http://www.google.com/calendar/feeds/liz%40gmail.com/acl/full/user%3Aliz%40gmail.com'>
+    </link>
+    <author>
+      <name>Elizabeth Bennet</name>
+      <email>liz gmail com</email>
+    </author>
+    <gAcl:scope type='user' value='liz gmail com'></gAcl:scope>
+    <gAcl:role value='http://schemas.google.com/gCal/2005#owner'>
+    </gAcl:role>
+  </entry>"""
+
+DOCUMENT_LIST_FEED = """<?xml version='1.0' encoding='UTF-8'?>
+<ns0:feed xmlns:ns0="http://www.w3.org/2005/Atom";><ns1:totalResults
+xmlns:ns1="http://a9.com/-/spec/opensearchrss/1.0/";>2</ns1:totalResults><ns1:startIndex
+xmlns:ns1="http://a9.com/-/spec/opensearchrss/1.0/";>1</ns1:startIndex><ns0:entry><ns0:content
+src="http://foo.com/fm?fmcmd=102&amp;key=supercalifragilisticexpeadocious";
+type="text/html"
+/><ns0:author><ns0:name>test.user</ns0:name><ns0:email>test user gmail com</ns0:email></ns0:author><ns0:category
+label="spreadsheet" scheme="http://schemas.google.com/g/2005#kind";
+term="http://schemas.google.com/docs/2007#spreadsheet";
+/><ns0:id>http://docs.google.com/feeds/documents/private/full/spreadsheet%3Asupercalifragilisticexpeadocious</ns0:id><ns0:link
+href="http://foo.com/ccc?key=supercalifragilisticexpeadocious"; rel="alternate"
+type="text/html" /><ns0:link
+href="http://foo.com/feeds/worksheets/supercalifragilisticexpeadocious/private/full";
+rel="http://schemas.google.com/spreadsheets/2006#worksheetsfeed";
+type="application/atom+xml" /><ns0:link
+href="http://docs.google.com/feeds/documents/private/full/spreadsheet%3Asupercalifragilisticexpeadocious";
+rel="self" type="application/atom+xml" /><ns0:title type="text">Test Spreadsheet</ns0:title><ns0:updated>2007-07-03T18:03:32.045Z</ns0:updated></ns0:entry><ns0:entry><ns0:content
+src="http://docs.google.com/RawDocContents?action=fetch&amp;docID=gr00vy";
+type="text/html"
+/><ns0:author><ns0:name>test.user</ns0:name><ns0:email>test user gmail com</ns0:email></ns0:author><ns0:category
+label="document" scheme="http://schemas.google.com/g/2005#kind";
+term="http://schemas.google.com/docs/2007#document";
+/><ns0:id>http://docs.google.com/feeds/documents/private/full/document%3Agr00vy</ns0:id><ns0:link
+href="http://foobar.com/Doc?id=gr00vy"; rel="alternate" type="text/html"
+/><ns0:link
+href="http://docs.google.com/feeds/documents/private/full/document%3Agr00vy";
+rel="self" type="application/atom+xml" /><ns0:title type="text">Test Document</ns0:title><ns0:updated>2007-07-03T18:02:50.338Z</ns0:updated></ns0:entry><ns0:id>http://docs.google.com/feeds/documents/private/full</ns0:id><ns0:link
+href="http://docs.google.com"; rel="alternate" type="text/html" /><ns0:link
+href="http://docs.google.com/feeds/documents/private/full";
+rel="http://schemas.google.com/g/2005#feed"; type="application/atom+xml"
+/><ns0:link href="http://docs.google.com/feeds/documents/private/full";
+rel="http://schemas.google.com/g/2005#post"; type="application/atom+xml"
+/><ns0:link href="http://docs.google.com/feeds/documents/private/full";
+rel="self" type="application/atom+xml" /><ns0:title type="text">Available
+Documents -
+test user gmail com</ns0:title><ns0:updated>2007-07-09T23:07:21.898Z</ns0:updated></ns0:feed>
+"""
+
+DOCUMENT_LIST_ENTRY = """<?xml version='1.0' encoding='UTF-8'?>
+<ns0:entry xmlns:ns0="http://www.w3.org/2005/Atom";><ns0:content
+src="http://foo.com/fm?fmcmd=102&amp;key=supercalifragilisticexpealidocious";
+type="text/html"
+/><ns0:author><ns0:name>test.user</ns0:name><ns0:email>test user gmail com</ns0:email></ns0:author><ns0:category
+label="spreadsheet" scheme="http://schemas.google.com/g/2005#kind";
+term="http://schemas.google.com/docs/2007#spreadsheet";
+/><ns0:id>http://docs.google.com/feeds/documents/private/full/spreadsheet%3Asupercalifragilisticexpealidocious</ns0:id><ns0:link
+href="http://foo.com/ccc?key=supercalifragilisticexpealidocious";
+rel="alternate" type="text/html" /><ns0:link
+href="http://foo.com/feeds/worksheets/supercalifragilisticexpealidocious/private/full";
+rel="http://schemas.google.com/spreadsheets/2006#worksheetsfeed";
+type="application/atom+xml" /><ns0:link
+href="http://docs.google.com/feeds/documents/private/full/spreadsheet%3Asupercalifragilisticexpealidocious";
+rel="self" type="application/atom+xml" /><ns0:title type="text">Test Spreadsheet</ns0:title><ns0:updated>2007-07-03T18:03:32.045Z</ns0:updated></ns0:entry>
+"""
+
+BATCH_ENTRY = """<?xml version='1.0' encoding='UTF-8'?>
+<entry xmlns="http://www.w3.org/2005/Atom";
+       xmlns:batch="http://schemas.google.com/gdata/batch"; 
+       xmlns:g="http://base.google.com/ns/1.0";>
+  <id>http://www.google.com/base/feeds/items/2173859253842813008</id>
+  <published>2006-07-11T14:51:43.560Z</published>
+  <updated>2006-07-11T14:51: 43.560Z</updated>
+  <title type="text">title</title>
+  <content type="html">content</content>
+  <link rel="self" 
+    type="application/atom+xml" 
+    href="http://www.google.com/base/feeds/items/2173859253842813008"/>
+  <link rel="edit" 
+    type="application/atom+xml" 
+    href="http://www.google.com/base/feeds/items/2173859253842813008"/>
+  <g:item_type>recipes</g:item_type>
+  <batch:operation type="insert"/>
+  <batch:id>itemB</batch:id>
+  <batch:status code="201" reason="Created"/>
+</entry>"""
+
+BATCH_FEED_REQUEST = """<?xml version="1.0" encoding="UTF-8"?>
+<feed
+  xmlns="http://www.w3.org/2005/Atom";
+  xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/";
+  xmlns:g="http://base.google.com/ns/1.0";
+  xmlns:batch="http://schemas.google.com/gdata/batch";>
+  <title type="text">My Batch Feed</title>
+  <entry>
+    <id>http://www.google.com/base/feeds/items/13308004346459454600</id>
+    <batch:operation type="delete"/>
+  </entry>
+  <entry>
+    <id>http://www.google.com/base/feeds/items/17437536661927313949</id>
+    <batch:operation type="delete"/>
+  </entry>
+  <entry>
+    <title type="text">...</title>
+    <content type="html">...</content>
+    <batch:id>itemA</batch:id>
+    <batch:operation type="insert"/>
+    <g:item_type>recipes</g:item_type>
+  </entry>
+  <entry>
+    <title type="text">...</title>
+    <content type="html">...</content>
+    <batch:id>itemB</batch:id>
+    <batch:operation type="insert"/>
+    <g:item_type>recipes</g:item_type>
+  </entry>
+</feed>"""
+
+BATCH_FEED_RESULT = """<?xml version="1.0" encoding="UTF-8"?>
+<feed
+  xmlns="http://www.w3.org/2005/Atom";
+  xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/";
+  xmlns:g="http://base.google.com/ns/1.0";
+  xmlns:batch="http://schemas.google.com/gdata/batch";>
+  <id>http://www.google.com/base/feeds/items</id>
+  <updated>2006-07-11T14:51:42.894Z</updated>
+  <title type="text">My Batch</title>
+  <link rel="http://schemas.google.com/g/2005#feed";
+    type="application/atom+xml"
+    href="http://www.google.com/base/feeds/items"/>
+  <link rel="http://schemas.google.com/g/2005#post";
+    type="application/atom+xml"
+    href="http://www.google.com/base/feeds/items"/>
+  <link rel=" http://schemas.google.com/g/2005#batch";
+    type="application/atom+xml"
+    href="http://www.google.com/base/feeds/items/batch"/>
+  <entry>
+    <id>http://www.google.com/base/feeds/items/2173859253842813008</id>
+    <published>2006-07-11T14:51:43.560Z</published>
+    <updated>2006-07-11T14:51: 43.560Z</updated>
+    <title type="text">...</title>
+    <content type="html">...</content>
+    <link rel="self"
+      type="application/atom+xml"
+      href="http://www.google.com/base/feeds/items/2173859253842813008"/>
+    <link rel="edit"
+      type="application/atom+xml"
+      href="http://www.google.com/base/feeds/items/2173859253842813008"/>
+    <g:item_type>recipes</g:item_type>
+    <batch:operation type="insert"/>
+    <batch:id>itemB</batch:id>
+    <batch:status code="201" reason="Created"/>
+  </entry>
+  <entry>
+    <id>http://www.google.com/base/feeds/items/11974645606383737963</id>
+    <published>2006-07-11T14:51:43.247Z</published>
+    <updated>2006-07-11T14:51: 43.247Z</updated>
+    <title type="text">...</title>
+    <content type="html">...</content>
+    <link rel="self"
+      type="application/atom+xml"
+      href="http://www.google.com/base/feeds/items/11974645606383737963"/>
+    <link rel="edit"
+      type="application/atom+xml"
+      href="http://www.google.com/base/feeds/items/11974645606383737963"/>
+    <g:item_type>recipes</g:item_type>
+    <batch:operation type="insert"/>
+    <batch:id>itemA</batch:id>
+    <batch:status code="201" reason="Created"/>
+  </entry>
+  <entry>
+    <id>http://www.google.com/base/feeds/items/13308004346459454600</id>
+    <updated>2006-07-11T14:51:42.894Z</updated>
+    <title type="text">Error</title>
+    <content type="text">Bad request</content>
+    <batch:status code="404"
+      reason="Bad request"
+      content-type="application/xml">
+      <errors>
+        <error type="request" reason="Cannot find item"/>
+      </errors>
+    </batch:status>
+  </entry>
+  <entry>
+    <id>http://www.google.com/base/feeds/items/17437536661927313949</id>
+    <updated>2006-07-11T14:51:43.246Z</updated>
+    <content type="text">Deleted</content>
+    <batch:operation type="delete"/>
+    <batch:status code="200" reason="Success"/>
+  </entry>
+</feed>"""
+
+ALBUM_FEED = """<?xml version="1.0" encoding="UTF-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom"; xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/"; xmlns:exif="http://schemas.google.com/photos/exif/2007"; xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"; xmlns:gml="http://www.opengis.net/gml"; xmlns:georss="http://www.georss.org/georss"; xmlns:photo="http://www.pheed.com/pheed/"; xmlns:media="http://search.yahoo.com/mrss/"; xmlns:batch="http://schemas.google.com/gdata/batch"; xmlns:gphoto="http://schemas.google.com/photos/2007";>
+  <id>http://picasaweb.google.com/data/feed/api/user/sample.user/albumid/1</id>
+  <updated>2007-09-21T18:23:05.000Z</updated>
+  <category scheme="http://schemas.google.com/g/2005#kind"; term="http://schemas.google.com/photos/2007#album"/>
+  <title type="text">Test</title>
+  <subtitle type="text"/>
+  <rights type="text">public</rights>
+  <icon>http://lh6.google.com/sample.user/Rt8WNoDZEJE/AAAAAAAAABk/HQGlDhpIgWo/s160-c/Test.jpg</icon>
+  <link rel="http://schemas.google.com/g/2005#feed"; type="application/atom+xml" href="http://picasaweb.google.com/data/feed/api/user/sample.user/albumid/1"/>
+  <link rel="alternate" type="text/html" href="http://picasaweb.google.com/sample.user/Test"/>
+  <link rel="http://schemas.google.com/photos/2007#slideshow"; type="application/x-shockwave-flash" href="http://picasaweb.google.com/s/c/bin/slideshow.swf?host=picasaweb.google.com&amp;RGB=0x000000&amp;feed=http%3A%2F%2Fpicasaweb.google.com%2Fdata%2Ffeed%2Fapi%2Fuser%2Fsample.user%2Falbumid%2F1%3Falt%3Drss"/>
+  <link rel="self" type="application/atom+xml" href="http://picasaweb.google.com/data/feed/api/user/sample.user/albumid/1?start-index=1&amp;max-results=500&amp;kind=photo%2Ctag"/>
+  <author>
+    <name>sample</name>
+    <uri>http://picasaweb.google.com/sample.user</uri>
+  </author>
+  <generator version="1.00" uri="http://picasaweb.google.com/";>Picasaweb</generator>                                                                                                                                     <openSearch:totalResults>4</openSearch:totalResults>
+  <openSearch:startIndex>1</openSearch:startIndex>
+  <openSearch:itemsPerPage>500</openSearch:itemsPerPage>
+  <gphoto:id>1</gphoto:id>
+  <gphoto:name>Test</gphoto:name>
+  <gphoto:location/>
+  <gphoto:access>public</gphoto:access>                                                                                                                                                                                  <gphoto:timestamp>1188975600000</gphoto:timestamp>
+  <gphoto:numphotos>2</gphoto:numphotos>
+  <gphoto:user>sample.user</gphoto:user>
+  <gphoto:nickname>sample</gphoto:nickname>
+  <gphoto:commentingEnabled>true</gphoto:commentingEnabled>
+  <gphoto:commentCount>0</gphoto:commentCount>
+  <entry>                                                                                                                                                                                                                  <id>http://picasaweb.google.com/data/entry/api/user/sample.user/albumid/1/photoid/2</id>
+    <published>2007-09-05T20:49:23.000Z</published>
+    <updated>2007-09-21T18:23:05.000Z</updated>
+    <category scheme="http://schemas.google.com/g/2005#kind"; term="http://schemas.google.com/photos/2007#photo"/>
+    <title type="text">Aqua Blue.jpg</title>
+    <summary type="text">Blue</summary>
+    <content type="image/jpeg" src="http://lh4.google.com/sample.user/Rt8WU4DZEKI/AAAAAAAAABY/IVgLqmnzJII/Aqua%20Blue.jpg"/>                                                                                               <link rel="http://schemas.google.com/g/2005#feed"; type="application/atom+xml" href="http://picasaweb.google.com/data/feed/api/user/sample.user/albumid/1/photoid/2"/>
+    <link rel="alternate" type="text/html" href="http://picasaweb.google.com/sample.user/Test/photo#2"/>
+    <link rel="self" type="application/atom+xml" href="http://picasaweb.google.com/data/entry/api/user/sample.user/albumid/1/photoid/2"/>
+    <gphoto:id>2</gphoto:id>
+    <gphoto:version>1190398985145172</gphoto:version>
+    <gphoto:position>0.0</gphoto:position>
+    <gphoto:albumid>1</gphoto:albumid>                                                                                                                                                                                     <gphoto:width>2560</gphoto:width>
+    <gphoto:height>1600</gphoto:height>
+    <gphoto:size>883405</gphoto:size>
+    <gphoto:client/>
+    <gphoto:checksum/>
+    <gphoto:timestamp>1189025362000</gphoto:timestamp>
+    <exif:tags>                                                                                                                                                                                                              <exif:flash>true</exif:flash>
+      <exif:imageUniqueID>c041ce17aaa637eb656c81d9cf526c24</exif:imageUniqueID>
+    </exif:tags>
+    <gphoto:commentingEnabled>true</gphoto:commentingEnabled>
+    <gphoto:commentCount>1</gphoto:commentCount>
+    <media:group>
+      <media:title type="plain">Aqua Blue.jpg</media:title>                                                                                                                                                                  <media:description type="plain">Blue</media:description>
+      <media:keywords>tag, test</media:keywords>
+      <media:content url="http://lh4.google.com/sample.user/Rt8WU4DZEKI/AAAAAAAAABY/IVgLqmnzJII/Aqua%20Blue.jpg"; height="1600" width="2560" type="image/jpeg" medium="image"/>
+      <media:thumbnail url="http://lh4.google.com/sample.user/Rt8WU4DZEKI/AAAAAAAAABY/IVgLqmnzJII/s72/Aqua%20Blue.jpg"; height="45" width="72"/>
+      <media:thumbnail url="http://lh4.google.com/sample.user/Rt8WU4DZEKI/AAAAAAAAABY/IVgLqmnzJII/s144/Aqua%20Blue.jpg"; height="90" width="144"/>
+      <media:thumbnail url="http://lh4.google.com/sample.user/Rt8WU4DZEKI/AAAAAAAAABY/IVgLqmnzJII/s288/Aqua%20Blue.jpg"; height="180" width="288"/>
+      <media:credit>sample</media:credit>
+    </media:group>
+  </entry>
+  <entry>
+    <id>http://picasaweb.google.com/data/entry/api/user/sample.user/albumid/1/photoid/3</id>
+    <published>2007-09-05T20:49:24.000Z</published>
+    <updated>2007-09-21T18:19:38.000Z</updated>
+    <category scheme="http://schemas.google.com/g/2005#kind"; term="http://schemas.google.com/photos/2007#photo"/>
+    <title type="text">Aqua Graphite.jpg</title>
+    <summary type="text">Gray</summary>
+    <content type="image/jpeg" src="http://lh5.google.com/sample.user/Rt8WVIDZELI/AAAAAAAAABg/d7e0i7gvhNU/Aqua%20Graphite.jpg"/>
+    <link rel="http://schemas.google.com/g/2005#feed"; type="application/atom+xml" href="http://picasaweb.google.com/data/feed/api/user/sample.user/albumid/1/photoid/3"/>
+    <link rel="alternate" type="text/html" href="http://picasaweb.google.com/sample.user/Test/photo#3"/>
+    <link rel="self" type="application/atom+xml" href="http://picasaweb.google.com/data/entry/api/user/sample.user/albumid/1/photoid/3"/>
+    <gphoto:id>3</gphoto:id>
+    <gphoto:version>1190398778006402</gphoto:version>
+    <gphoto:position>1.0</gphoto:position>
+    <gphoto:albumid>1</gphoto:albumid>
+    <gphoto:width>2560</gphoto:width>
+    <gphoto:height>1600</gphoto:height>
+    <gphoto:size>798334</gphoto:size>
+    <gphoto:client/>
+    <gphoto:checksum/>
+    <gphoto:timestamp>1189025363000</gphoto:timestamp>
+    <exif:tags>
+      <exif:flash>true</exif:flash>
+      <exif:imageUniqueID>a5ce2e36b9df7d3cb081511c72e73926</exif:imageUniqueID>
+    </exif:tags>
+    <gphoto:commentingEnabled>true</gphoto:commentingEnabled>
+    <gphoto:commentCount>0</gphoto:commentCount>
+    <media:group>
+      <media:title type="plain">Aqua Graphite.jpg</media:title>
+      <media:description type="plain">Gray</media:description>
+      <media:keywords/>
+      <media:content url="http://lh5.google.com/sample.user/Rt8WVIDZELI/AAAAAAAAABg/d7e0i7gvhNU/Aqua%20Graphite.jpg"; height="1600" width="2560" type="image/jpeg" medium="image"/>
+      <media:thumbnail url="http://lh5.google.com/sample.user/Rt8WVIDZELI/AAAAAAAAABg/d7e0i7gvhNU/s72/Aqua%20Graphite.jpg"; height="45" width="72"/>
+      <media:thumbnail url="http://lh5.google.com/sample.user/Rt8WVIDZELI/AAAAAAAAABg/d7e0i7gvhNU/s144/Aqua%20Graphite.jpg"; height="90" width="144"/>
+      <media:thumbnail url="http://lh5.google.com/sample.user/Rt8WVIDZELI/AAAAAAAAABg/d7e0i7gvhNU/s288/Aqua%20Graphite.jpg"; height="180" width="288"/>
+      <media:credit>sample</media:credit>
+    </media:group>
+  </entry>
+  <entry>
+    <id>http://picasaweb.google.com/data/entry/api/user/sample.user/albumid/1/tag/tag</id>
+    <updated>2007-09-05T20:49:24.000Z</updated>
+    <category scheme="http://schemas.google.com/g/2005#kind"; term="http://schemas.google.com/photos/2007#tag"/>
+    <title type="text">tag</title>
+    <summary type="text">tag</summary>
+    <link rel="alternate" type="text/html" href="http://picasaweb.google.com/lh/searchbrowse?q=tag&amp;psc=G&amp;uname=sample.user&amp;filter=0"/>
+    <link rel="self" type="application/atom+xml" href="http://picasaweb.google.com/data/entry/api/user/sample.user/albumid/1/tag/tag"/>
+    <author>
+      <name>sample</name>
+      <uri>http://picasaweb.google.com/sample.user</uri>
+    </author>
+  </entry>
+  <entry>
+    <id>http://picasaweb.google.com/data/entry/api/user/sample.user/albumid/1/tag/test</id>
+    <updated>2007-09-05T20:49:24.000Z</updated>
+    <category scheme="http://schemas.google.com/g/2005#kind"; term="http://schemas.google.com/photos/2007#tag"/>
+    <title type="text">test</title>
+    <summary type="text">test</summary>
+    <link rel="alternate" type="text/html" href="http://picasaweb.google.com/lh/searchbrowse?q=test&amp;psc=G&amp;uname=sample.user&amp;filter=0"/>
+    <link rel="self" type="application/atom+xml" href="http://picasaweb.google.com/data/entry/api/user/sample.user/albumid/1/tag/test"/>
+    <author>
+      <name>sample</name>
+      <uri>http://picasaweb.google.com/sample.user</uri>
+    </author>
+  </entry>
+</feed>"""
+
+CODE_SEARCH_FEED = """<?xml version="1.0" encoding="UTF-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom"; xmlns:opensearch="http://a9.com/-/spec/opensearchrss/1.0/"; xmlns:gcs="http://schemas.google.com/codesearch/2006"; xml:base="http://www.google.com";>
+<id>http://www.google.com/codesearch/feeds/search?q=malloc</id>
+<updated>2007-12-19T16:08:04Z</updated> 
+<title type="text">Google Code Search</title>
+<generator version="1.0" uri="http://www.google.com/codesearch";>Google Code Search</generator>
+<opensearch:totalResults>2530000</opensearch:totalResults>
+<opensearch:startIndex>1</opensearch:startIndex>
+<author>
+<name>Google Code Search</name>
+
+<uri>http://www.google.com/codesearch</uri>
+</author>
+<link rel="http://schemas.google.com/g/2006#feed"; type="application/atom+xml" href="http://schemas.google.com/codesearch/2006"/>
+<link rel="self" type="application/atom+xml" href="http://www.google.com/codesearch/feeds/search?q=malloc"/>
+<link rel="next" type="application/atom+xml"  href="http://www.google.com/codesearch/feeds/search?q=malloc&amp;start-index=11"/>
+<link rel="alternate" type="text/html" href="http://www.google.com/codesearch?q=malloc"/>
+<entry><id>http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:LDjwp-Iqc7U:84hEYaYsZk8:xDGReDhvNi0&amp;sa=N&amp;ct=rx&amp;cd=1&amp;cs_p=http://www.gnu.org&amp;cs_f=software/autoconf/manual/autoconf-2.60/autoconf.html-002&amp;cs_p=http://www.gnu.org&amp;cs_f=software/autoconf/manual/autoconf-2.60/autoconf.html-002#first</id><updated>2007-12-19T16:08:04Z</updated><author><name>Code owned by external author.</name></author><title type="text">software/autoconf/manual/autoconf-2.60/autoconf.html</title><link rel="alternate" type="text/html" href="http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:LDjwp-Iqc7U:84hEYaYsZk8:xDGReDhvNi0&amp;sa=N&amp;ct=rx&amp;cd=1&amp;cs_p=http://www.gnu.org&amp;cs_f=software/autoconf/manual/autoconf-2.60/autoconf.html-002&amp;cs_p=http://www.gnu.org&amp;cs_f=software/autoconf/manual/autoconf-2.60/autoconf.html-002#first"/><gcs:package name="http://www.gnu.org"; uri="http://www.gnu.org";></gcs:package><gcs:file name="software/autoconf/manua
 l/autoconf-2.60/autoconf.html-002"></gcs:file><content type="text/html">&lt;pre&gt;     8:      void *&lt;b&gt;malloc&lt;/b&gt; ();
+        
+
+&lt;/pre&gt;</content><gcs:match lineNumber="4" type="text/html">&lt;pre&gt;     #undef &lt;b&gt;malloc&lt;/b&gt;
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="8" type="text/html">&lt;pre&gt;     void *&lt;b&gt;malloc&lt;/b&gt; ();
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="14" type="text/html">&lt;pre&gt;     rpl_&lt;b&gt;malloc&lt;/b&gt; (size_t n)
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="18" type="text/html">&lt;pre&gt;       return &lt;b&gt;malloc&lt;/b&gt; (n);
+
+&lt;/pre&gt;</gcs:match></entry>
+<entry><id>http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:h4hfh-fV-jI:niBq_bwWZNs:H0OhClf0HWQ&amp;sa=N&amp;ct=rx&amp;cd=2&amp;cs_p=ftp://ftp.gnu.org/gnu/guile/guile-1.6.8.tar.gz&amp;cs_f=guile-1.6.8/libguile/mallocs.c&amp;cs_p=ftp://ftp.gnu.org/gnu/guile/guile-1.6.8.tar.gz&amp;cs_f=guile-1.6.8/libguile/mallocs.c#first</id><updated>2007-12-19T16:08:04Z</updated><author><name>Code owned by external author.</name></author><title type="text">guile-1.6.8/libguile/mallocs.c</title><link rel="alternate" type="text/html" href="http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:h4hfh-fV-jI:niBq_bwWZNs:H0OhClf0HWQ&amp;sa=N&amp;ct=rx&amp;cd=2&amp;cs_p=ftp://ftp.gnu.org/gnu/guile/guile-1.6.8.tar.gz&amp;cs_f=guile-1.6.8/libguile/mallocs.c&amp;cs_p=ftp://ftp.gnu.org/gnu/guile/guile-1.6.8.tar.gz&amp;cs_f=guile-1.6.8/libguile/mallocs.c#first"/><gcs:package name="ftp://ftp.gnu.org/gnu/guile/guile-1.6.8.tar.gz"; uri="ftp://ftp.gnu.org/gnu/guile/guile-1.6.8.tar.gz";></gcs:packa
 ge><gcs:file name="guile-1.6.8/libguile/mallocs.c"></gcs:file><content type="text/html">&lt;pre&gt;    86: {
+          scm_t_bits mem = n ? (scm_t_bits) &lt;b&gt;malloc&lt;/b&gt; (n) : 0;
+          if (n &amp;amp;&amp;amp; !mem)
+
+&lt;/pre&gt;</content><gcs:match lineNumber="54" type="text/html">&lt;pre&gt;#include &amp;lt;&lt;b&gt;malloc&lt;/b&gt;.h&amp;gt;
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="62" type="text/html">&lt;pre&gt;scm_t_bits scm_tc16_&lt;b&gt;malloc&lt;/b&gt;;
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="66" type="text/html">&lt;pre&gt;&lt;b&gt;malloc&lt;/b&gt;_free (SCM ptr)
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="75" type="text/html">&lt;pre&gt;&lt;b&gt;malloc&lt;/b&gt;_print (SCM exp, SCM port, scm_print_state *pstate SCM_UNUSED)
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="77" type="text/html">&lt;pre&gt;  scm_puts(&amp;quot;#&amp;lt;&lt;b&gt;malloc&lt;/b&gt; &amp;quot;, port);
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="87" type="text/html">&lt;pre&gt;  scm_t_bits mem = n ? (scm_t_bits) &lt;b&gt;malloc&lt;/b&gt; (n) : 0;
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="90" type="text/html">&lt;pre&gt;  SCM_RETURN_NEWSMOB (scm_tc16_&lt;b&gt;malloc&lt;/b&gt;, mem);
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="98" type="text/html">&lt;pre&gt;  scm_tc16_&lt;b&gt;malloc&lt;/b&gt; = scm_make_smob_type (&amp;quot;&lt;b&gt;malloc&lt;/b&gt;&amp;quot;, 0);
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="99" type="text/html">&lt;pre&gt;  scm_set_smob_free (scm_tc16_&lt;b&gt;malloc&lt;/b&gt;, &lt;b&gt;malloc&lt;/b&gt;_free);
+&lt;/pre&gt;</gcs:match><rights>GPL</rights></entry>
+
+<entry><id>http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:9wyZUG-N_30:7_dFxoC1ZrY:C0_iYbFj90M&amp;sa=N&amp;ct=rx&amp;cd=3&amp;cs_p=http://ftp.gnu.org/gnu/bash/bash-3.0.tar.gz&amp;cs_f=bash-3.0/lib/malloc/alloca.c&amp;cs_p=http://ftp.gnu.org/gnu/bash/bash-3.0.tar.gz&amp;cs_f=bash-3.0/lib/malloc/alloca.c#first</id><updated>2007-12-19T16:08:04Z</updated><author><name>Code owned by external author.</name></author><title type="text">bash-3.0/lib/malloc/alloca.c</title><link rel="alternate" type="text/html" href="http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:9wyZUG-N_30:7_dFxoC1ZrY:C0_iYbFj90M&amp;sa=N&amp;ct=rx&amp;cd=3&amp;cs_p=http://ftp.gnu.org/gnu/bash/bash-3.0.tar.gz&amp;cs_f=bash-3.0/lib/malloc/alloca.c&amp;cs_p=http://ftp.gnu.org/gnu/bash/bash-3.0.tar.gz&amp;cs_f=bash-3.0/lib/malloc/alloca.c#first"/><gcs:package name="http://ftp.gnu.org/gnu/bash/bash-3.0.tar.gz"; uri="http://ftp.gnu.org/gnu/bash/bash-3.0.tar.gz";></gcs:package><gcs:file name="bash-3.0/
 lib/malloc/alloca.c"></gcs:file><content type="text/html">&lt;pre&gt;    78: #ifndef emacs
+        #define &lt;b&gt;malloc&lt;/b&gt; x&lt;b&gt;malloc&lt;/b&gt;
+        extern pointer x&lt;b&gt;malloc&lt;/b&gt; ();
+
+&lt;/pre&gt;</content><gcs:match lineNumber="69" type="text/html">&lt;pre&gt;   &lt;b&gt;malloc&lt;/b&gt;.  The Emacs executable needs alloca to call x&lt;b&gt;malloc&lt;/b&gt;, because
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="70" type="text/html">&lt;pre&gt;   ordinary &lt;b&gt;malloc&lt;/b&gt; isn&amp;#39;t protected from input signals.  On the other
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="71" type="text/html">&lt;pre&gt;   hand, the utilities in lib-src need alloca to call &lt;b&gt;malloc&lt;/b&gt;; some of
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="72" type="text/html">&lt;pre&gt;   them are very simple, and don&amp;#39;t have an x&lt;b&gt;malloc&lt;/b&gt; routine.
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="76" type="text/html">&lt;pre&gt;   Callers below should use &lt;b&gt;malloc&lt;/b&gt;.  */
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="79" type="text/html">&lt;pre&gt;#define &lt;b&gt;malloc&lt;/b&gt; x&lt;b&gt;malloc&lt;/b&gt;
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="80" type="text/html">&lt;pre&gt;extern pointer x&lt;b&gt;malloc&lt;/b&gt; ();
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="132" type="text/html">&lt;pre&gt;   It is very important that sizeof(header) agree with &lt;b&gt;malloc&lt;/b&gt;
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="198" type="text/html">&lt;pre&gt;    register pointer new = &lt;b&gt;malloc&lt;/b&gt; (sizeof (header) + size);
+&lt;/pre&gt;</gcs:match><rights>GPL</rights></entry>
+<entry><id>http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:uhVCKyPcT6k:8juMxxzmUJw:H7_IDsTB2L4&amp;sa=N&amp;ct=rx&amp;cd=4&amp;cs_p=http://ftp.mozilla.org/pub/mozilla.org/mozilla/releases/mozilla1.7b/src/mozilla-source-1.7b-source.tar.bz2&amp;cs_f=mozilla/xpcom/build/malloc.c&amp;cs_p=http://ftp.mozilla.org/pub/mozilla.org/mozilla/releases/mozilla1.7b/src/mozilla-source-1.7b-source.tar.bz2&amp;cs_f=mozilla/xpcom/build/malloc.c#first</id><updated>2007-12-19T16:08:04Z</updated><author><name>Code owned by external author.</name></author><title type="text">mozilla/xpcom/build/malloc.c</title><link rel="alternate" type="text/html" href="http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:uhVCKyPcT6k:8juMxxzmUJw:H7_IDsTB2L4&amp;sa=N&amp;ct=rx&amp;cd=4&amp;cs_p=http://ftp.mozilla.org/pub/mozilla.org/mozilla/releases/mozilla1.7b/src/mozilla-source-1.7b-source.tar.bz2&amp;cs_f=mozilla/xpcom/build/malloc.c&amp;cs_p=http://ftp.mozilla.org/pub/mozilla.org/mozilla/release
 s/mozilla1.7b/src/mozilla-source-1.7b-source.tar.bz2&amp;cs_f=mozilla/xpcom/build/malloc.c#first"/><gcs:package name="http://ftp.mozilla.org/pub/mozilla.org/mozilla/releases/mozilla1.7b/src/mozilla-source-1.7b-source.tar.bz2"; uri="http://ftp.mozilla.org/pub/mozilla.org/mozilla/releases/mozilla1.7b/src/mozilla-source-1.7b-source.tar.bz2";></gcs:package><gcs:file name="mozilla/xpcom/build/malloc.c"></gcs:file><content type="text/html">&lt;pre&gt;    54:      http://gee.cs.oswego.edu/dl/html/&lt;b&gt;malloc&lt;/b&gt;.html
+        
+          You may already by default be using a c library containing a &lt;b&gt;malloc&lt;/b&gt;
+
+&lt;/pre&gt;</content><gcs:match lineNumber="4" type="text/html">&lt;pre&gt;/* ---------- To make a &lt;b&gt;malloc&lt;/b&gt;.h, start cutting here ------------ */
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="22" type="text/html">&lt;pre&gt;   Note: There may be an updated version of this &lt;b&gt;malloc&lt;/b&gt; obtainable at
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="23" type="text/html">&lt;pre&gt;           ftp://gee.cs.oswego.edu/pub/misc/&lt;b&gt;malloc&lt;/b&gt;.c
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="34" type="text/html">&lt;pre&gt;* Why use this &lt;b&gt;malloc&lt;/b&gt;?
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="37" type="text/html">&lt;pre&gt;  most tunable &lt;b&gt;malloc&lt;/b&gt; ever written. However it is among the fastest
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="40" type="text/html">&lt;pre&gt;  allocator for &lt;b&gt;malloc&lt;/b&gt;-intensive programs.
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="54" type="text/html">&lt;pre&gt;     http://gee.cs.oswego.edu/dl/html/&lt;b&gt;malloc&lt;/b&gt;.html
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="56" type="text/html">&lt;pre&gt;  You may already by default be using a c library containing a &lt;b&gt;malloc&lt;/b&gt;
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="57" type="text/html">&lt;pre&gt;  that is somehow based on some version of this &lt;b&gt;malloc&lt;/b&gt; (for example in
+&lt;/pre&gt;</gcs:match><rights>Mozilla</rights></entry>
+<entry><id>http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:4n1P2HVOISs:Ybbpph0wR2M:OhIN_sDrG0U&amp;sa=N&amp;ct=rx&amp;cd=5&amp;cs_p=http://regexps.srparish.net/src/hackerlab/hackerlab-1.0pre2.tar.gz&amp;cs_f=hackerlab-1.0pre2/src/hackerlab/tests/mem-tests/unit-must-malloc.sh&amp;cs_p=http://regexps.srparish.net/src/hackerlab/hackerlab-1.0pre2.tar.gz&amp;cs_f=hackerlab-1.0pre2/src/hackerlab/tests/mem-tests/unit-must-malloc.sh#first</id><updated>2007-12-19T16:08:04Z</updated><author><name>Code owned by external author.</name></author><title type="text">hackerlab-1.0pre2/src/hackerlab/tests/mem-tests/unit-must-malloc.sh</title><link rel="alternate" type="text/html" href="http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:4n1P2HVOISs:Ybbpph0wR2M:OhIN_sDrG0U&amp;sa=N&amp;ct=rx&amp;cd=5&amp;cs_p=http://regexps.srparish.net/src/hackerlab/hackerlab-1.0pre2.tar.gz&amp;cs_f=hackerlab-1.0pre2/src/hackerlab/tests/mem-tests/unit-must-malloc.sh&amp;cs_p=http://regexps.srp
 arish.net/src/hackerlab/hackerlab-1.0pre2.tar.gz&amp;cs_f=hackerlab-1.0pre2/src/hackerlab/tests/mem-tests/unit-must-malloc.sh#first"/><gcs:package name="http://regexps.srparish.net/src/hackerlab/hackerlab-1.0pre2.tar.gz"; uri="http://regexps.srparish.net/src/hackerlab/hackerlab-1.0pre2.tar.gz";></gcs:package><gcs:file name="hackerlab-1.0pre2/src/hackerlab/tests/mem-tests/unit-must-malloc.sh"></gcs:file><content type="text/html">&lt;pre&gt;    11: echo ================ unit-must-&lt;b&gt;malloc&lt;/b&gt; tests ================
+        ./unit-must-&lt;b&gt;malloc&lt;/b&gt;
+        echo ...passed
+
+&lt;/pre&gt;</content><gcs:match lineNumber="2" type="text/html">&lt;pre&gt;# tag: Tom Lord Tue Dec  4 14:54:29 2001 (mem-tests/unit-must-&lt;b&gt;malloc&lt;/b&gt;.sh)
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="11" type="text/html">&lt;pre&gt;echo ================ unit-must-&lt;b&gt;malloc&lt;/b&gt; tests ================
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="12" type="text/html">&lt;pre&gt;./unit-must-&lt;b&gt;malloc&lt;/b&gt;
+&lt;/pre&gt;</gcs:match><rights>GPL</rights></entry>
+<entry><id>http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:GzkwiWG266M:ykuz3bG00ws:2sTvVSif08g&amp;sa=N&amp;ct=rx&amp;cd=6&amp;cs_p=http://ftp.gnu.org/gnu/tar/tar-1.14.tar.bz2&amp;cs_f=tar-1.14/lib/malloc.c&amp;cs_p=http://ftp.gnu.org/gnu/tar/tar-1.14.tar.bz2&amp;cs_f=tar-1.14/lib/malloc.c#first</id><updated>2007-12-19T16:08:04Z</updated><author><name>Code owned by external author.</name></author><title type="text">tar-1.14/lib/malloc.c</title><link rel="alternate" type="text/html" href="http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:GzkwiWG266M:ykuz3bG00ws:2sTvVSif08g&amp;sa=N&amp;ct=rx&amp;cd=6&amp;cs_p=http://ftp.gnu.org/gnu/tar/tar-1.14.tar.bz2&amp;cs_f=tar-1.14/lib/malloc.c&amp;cs_p=http://ftp.gnu.org/gnu/tar/tar-1.14.tar.bz2&amp;cs_f=tar-1.14/lib/malloc.c#first"/><gcs:package name="http://ftp.gnu.org/gnu/tar/tar-1.14.tar.bz2"; uri="http://ftp.gnu.org/gnu/tar/tar-1.14.tar.bz2";></gcs:package><gcs:file name="tar-1.14/lib/malloc.c"></gcs:file><content t
 ype="text/html">&lt;pre&gt;    22: #endif
+        #undef &lt;b&gt;malloc&lt;/b&gt;
+        
+
+&lt;/pre&gt;</content><gcs:match lineNumber="1" type="text/html">&lt;pre&gt;/* Work around bug on some systems where &lt;b&gt;malloc&lt;/b&gt; (0) fails.
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="23" type="text/html">&lt;pre&gt;#undef &lt;b&gt;malloc&lt;/b&gt;
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="31" type="text/html">&lt;pre&gt;rpl_&lt;b&gt;malloc&lt;/b&gt; (size_t n)
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="35" type="text/html">&lt;pre&gt;  return &lt;b&gt;malloc&lt;/b&gt; (n);
+
+&lt;/pre&gt;</gcs:match><rights>GPL</rights></entry>
+<entry><id>http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:o_TFIeBY6dY:ktI_dt8wPao:AI03BD1Dz0Y&amp;sa=N&amp;ct=rx&amp;cd=7&amp;cs_p=http://ftp.gnu.org/gnu/tar/tar-1.16.1.tar.gz&amp;cs_f=tar-1.16.1/lib/malloc.c&amp;cs_p=http://ftp.gnu.org/gnu/tar/tar-1.16.1.tar.gz&amp;cs_f=tar-1.16.1/lib/malloc.c#first</id><updated>2007-12-19T16:08:04Z</updated><author><name>Code owned by external author.</name></author><title type="text">tar-1.16.1/lib/malloc.c</title><link rel="alternate" type="text/html" href="http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:o_TFIeBY6dY:ktI_dt8wPao:AI03BD1Dz0Y&amp;sa=N&amp;ct=rx&amp;cd=7&amp;cs_p=http://ftp.gnu.org/gnu/tar/tar-1.16.1.tar.gz&amp;cs_f=tar-1.16.1/lib/malloc.c&amp;cs_p=http://ftp.gnu.org/gnu/tar/tar-1.16.1.tar.gz&amp;cs_f=tar-1.16.1/lib/malloc.c#first"/><gcs:package name="http://ftp.gnu.org/gnu/tar/tar-1.16.1.tar.gz"; uri="http://ftp.gnu.org/gnu/tar/tar-1.16.1.tar.gz";></gcs:package><gcs:file name="tar-1.16.1/lib/malloc.c"></g
 cs:file><content type="text/html">&lt;pre&gt;    21: #include &amp;lt;config.h&amp;gt;
+        #undef &lt;b&gt;malloc&lt;/b&gt;
+        
+
+&lt;/pre&gt;</content><gcs:match lineNumber="1" type="text/html">&lt;pre&gt;/* &lt;b&gt;malloc&lt;/b&gt;() function that is glibc compatible.
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="22" type="text/html">&lt;pre&gt;#undef &lt;b&gt;malloc&lt;/b&gt;
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="30" type="text/html">&lt;pre&gt;rpl_&lt;b&gt;malloc&lt;/b&gt; (size_t n)
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="34" type="text/html">&lt;pre&gt;  return &lt;b&gt;malloc&lt;/b&gt; (n);
+
+&lt;/pre&gt;</gcs:match><rights>GPL</rights></entry>
+<entry><id>http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:_ibw-VLkMoI:jBOtIJSmFd4:-0NUEVeCwfY&amp;sa=N&amp;ct=rx&amp;cd=8&amp;cs_p=http://freshmeat.net/redir/uclibc/20616/url_bz2/uClibc-0.9.28.1.tar.bz2&amp;cs_f=uClibc-0.9.29/include/malloc.h&amp;cs_p=http://freshmeat.net/redir/uclibc/20616/url_bz2/uClibc-0.9.28.1.tar.bz2&amp;cs_f=uClibc-0.9.29/include/malloc.h#first</id><updated>2007-12-19T16:08:04Z</updated><author><name>Code owned by external author.</name></author><title type="text">uClibc-0.9.29/include/malloc.h</title><link rel="alternate" type="text/html" href="http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:_ibw-VLkMoI:jBOtIJSmFd4:-0NUEVeCwfY&amp;sa=N&amp;ct=rx&amp;cd=8&amp;cs_p=http://freshmeat.net/redir/uclibc/20616/url_bz2/uClibc-0.9.28.1.tar.bz2&amp;cs_f=uClibc-0.9.29/include/malloc.h&amp;cs_p=http://freshmeat.net/redir/uclibc/20616/url_bz2/uClibc-0.9.28.1.tar.bz2&amp;cs_f=uClibc-0.9.29/include/malloc.h#first"/><gcs:package name="http://fresh
 meat.net/redir/uclibc/20616/url_bz2/uClibc-0.9.28.1.tar.bz2" uri="http://freshmeat.net/redir/uclibc/20616/url_bz2/uClibc-0.9.28.1.tar.bz2";></gcs:package><gcs:file name="uClibc-0.9.29/include/malloc.h"></gcs:file><content type="text/html">&lt;pre&gt;     1: /* Prototypes and definition for &lt;b&gt;malloc&lt;/b&gt; implementation.
+           Copyright (C) 1996, 1997, 1999, 2000 Free Software Foundation, Inc.
+
+&lt;/pre&gt;</content><gcs:match lineNumber="1" type="text/html">&lt;pre&gt;/* Prototypes and definition for &lt;b&gt;malloc&lt;/b&gt; implementation.
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="26" type="text/html">&lt;pre&gt;  `pt&lt;b&gt;malloc&lt;/b&gt;&amp;#39;, a &lt;b&gt;malloc&lt;/b&gt; implementation for multiple threads without
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="28" type="text/html">&lt;pre&gt;  See the files `pt&lt;b&gt;malloc&lt;/b&gt;.c&amp;#39; or `COPYRIGHT&amp;#39; for copying conditions.
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="32" type="text/html">&lt;pre&gt;  This work is mainly derived from &lt;b&gt;malloc&lt;/b&gt;-2.6.4 by Doug Lea
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="35" type="text/html">&lt;pre&gt;                 ftp://g.oswego.edu/pub/misc/&lt;b&gt;malloc&lt;/b&gt;.c
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="40" type="text/html">&lt;pre&gt;  `pt&lt;b&gt;malloc&lt;/b&gt;.c&amp;#39;.
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="45" type="text/html">&lt;pre&gt;# define __&lt;b&gt;malloc&lt;/b&gt;_ptr_t  void *
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="51" type="text/html">&lt;pre&gt;# define __&lt;b&gt;malloc&lt;/b&gt;_ptr_t  char *
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="56" type="text/html">&lt;pre&gt;# define __&lt;b&gt;malloc&lt;/b&gt;_size_t size_t
+&lt;/pre&gt;</gcs:match><rights>LGPL</rights></entry>
+<entry><id>http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:F6qHcZ9vefo:bTX7o9gKfks:hECF4r_eKC0&amp;sa=N&amp;ct=rx&amp;cd=9&amp;cs_p=http://ftp.gnu.org/gnu/glibc/glibc-2.0.1.tar.gz&amp;cs_f=glibc-2.0.1/hurd/hurdmalloc.h&amp;cs_p=http://ftp.gnu.org/gnu/glibc/glibc-2.0.1.tar.gz&amp;cs_f=glibc-2.0.1/hurd/hurdmalloc.h#first</id><updated>2007-12-19T16:08:04Z</updated><author><name>Code owned by external author.</name></author><title type="text">glibc-2.0.1/hurd/hurdmalloc.h</title><link rel="alternate" type="text/html" href="http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:F6qHcZ9vefo:bTX7o9gKfks:hECF4r_eKC0&amp;sa=N&amp;ct=rx&amp;cd=9&amp;cs_p=http://ftp.gnu.org/gnu/glibc/glibc-2.0.1.tar.gz&amp;cs_f=glibc-2.0.1/hurd/hurdmalloc.h&amp;cs_p=http://ftp.gnu.org/gnu/glibc/glibc-2.0.1.tar.gz&amp;cs_f=glibc-2.0.1/hurd/hurdmalloc.h#first"/><gcs:package name="http://ftp.gnu.org/gnu/glibc/glibc-2.0.1.tar.gz"; uri="http://ftp.gnu.org/gnu/glibc/glibc-2.0.1.tar.gz";></gcs:pack
 age><gcs:file name="glibc-2.0.1/hurd/hurdmalloc.h"></gcs:file><content type="text/html">&lt;pre&gt;    15: #define &lt;b&gt;malloc&lt;/b&gt;   _hurd_&lt;b&gt;malloc&lt;/b&gt;
+        #define realloc _hurd_realloc
+
+&lt;/pre&gt;</content><gcs:match lineNumber="3" type="text/html">&lt;pre&gt;   All hurd-internal code which uses &lt;b&gt;malloc&lt;/b&gt; et al includes this file so it
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="4" type="text/html">&lt;pre&gt;   will use the internal &lt;b&gt;malloc&lt;/b&gt; routines _hurd_{&lt;b&gt;malloc&lt;/b&gt;,realloc,free}
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="7" type="text/html">&lt;pre&gt;   of &lt;b&gt;malloc&lt;/b&gt; et al is the unixoid one using sbrk.
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="11" type="text/html">&lt;pre&gt;extern void *_hurd_&lt;b&gt;malloc&lt;/b&gt; (size_t);
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="15" type="text/html">&lt;pre&gt;#define &lt;b&gt;malloc&lt;/b&gt;        _hurd_&lt;b&gt;malloc&lt;/b&gt;
+&lt;/pre&gt;</gcs:match><rights>GPL</rights></entry>
+
+<entry><id>http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:CHUvHYzyLc8:pdcAfzDA6lY:wjofHuNLTHg&amp;sa=N&amp;ct=rx&amp;cd=10&amp;cs_p=ftp://apache.mirrors.pair.com/httpd/httpd-2.2.4.tar.bz2&amp;cs_f=httpd-2.2.4/srclib/apr/include/arch/netware/apr_private.h&amp;cs_p=ftp://apache.mirrors.pair.com/httpd/httpd-2.2.4.tar.bz2&amp;cs_f=httpd-2.2.4/srclib/apr/include/arch/netware/apr_private.h#first</id><updated>2007-12-19T16:08:04Z</updated><author><name>Code owned by external author.</name></author><title type="text">httpd-2.2.4/srclib/apr/include/arch/netware/apr_private.h</title><link rel="alternate" type="text/html" href="http://www.google.com/codesearch?hl=en&amp;q=+malloc+show:CHUvHYzyLc8:pdcAfzDA6lY:wjofHuNLTHg&amp;sa=N&amp;ct=rx&amp;cd=10&amp;cs_p=ftp://apache.mirrors.pair.com/httpd/httpd-2.2.4.tar.bz2&amp;cs_f=httpd-2.2.4/srclib/apr/include/arch/netware/apr_private.h&amp;cs_p=ftp://apache.mirrors.pair.com/httpd/httpd-2.2.4.tar.bz2&amp;cs_f=httpd-2.2.4/srclib/apr/i
 nclude/arch/netware/apr_private.h#first"/><gcs:package name="ftp://apache.mirrors.pair.com/httpd/httpd-2.2.4.tar.bz2"; uri="ftp://apache.mirrors.pair.com/httpd/httpd-2.2.4.tar.bz2";></gcs:package><gcs:file name="httpd-2.2.4/srclib/apr/include/arch/netware/apr_private.h"></gcs:file><content type="text/html">&lt;pre&gt;   173: #undef &lt;b&gt;malloc&lt;/b&gt;
+        #define &lt;b&gt;malloc&lt;/b&gt;(x) library_&lt;b&gt;malloc&lt;/b&gt;(gLibHandle,x)
+
+&lt;/pre&gt;</content><gcs:match lineNumber="170" type="text/html">&lt;pre&gt;/* Redefine &lt;b&gt;malloc&lt;/b&gt; to use the library &lt;b&gt;malloc&lt;/b&gt; call so
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="173" type="text/html">&lt;pre&gt;#undef &lt;b&gt;malloc&lt;/b&gt;
+
+&lt;/pre&gt;</gcs:match><gcs:match lineNumber="174" type="text/html">&lt;pre&gt;#define &lt;b&gt;malloc&lt;/b&gt;(x) library_&lt;b&gt;malloc&lt;/b&gt;(gLibHandle,x)
+&lt;/pre&gt;</gcs:match><rights>Apache</rights></entry>
+
+</feed>"""
+
+YOU_TUBE_VIDEO_FEED = """<?xml version='1.0' encoding='UTF-8'?>
+<feed xmlns='http://www.w3.org/2005/Atom'
+         xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'
+         xmlns:gml='http://www.opengis.net/gml'
+         xmlns:georss='http://www.georss.org/georss'
+         xmlns:media='http://search.yahoo.com/mrss/'
+         xmlns:yt='http://gdata.youtube.com/schemas/2007'
+         xmlns:gd='http://schemas.google.com/g/2005'>
+  <id>http://gdata.youtube.com/feeds/api/standardfeeds/top_rated</id>
+  <updated>2008-02-21T18:57:10.801Z</updated>
+  <category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://gdata.youtube.com/schemas/2007#video'/>
+  <title type='text'>Top Rated</title>
+  <logo>http://www.youtube.com/img/pic_youtubelogo_123x63.gif</logo>
+  <link rel='alternate' type='text/html'
+    href='http://www.youtube.com/browser?s=tr'/>
+  <link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml'
+    href='http://gdata.youtube.com/feeds/api/standardfeeds/top_rated'/>
+  <link rel='self' type='application/atom+xml'
+    href='http://gdata.youtube.com/feeds/api/standardfeeds/top_rated?start_index=1&amp;max-results=25'/>
+  <link rel='self' type='application/atom+xml'
+    href='http://gdata.youtube.com/feeds/api/standardfeeds/top_rated?start_index=26&amp;max-results=25'/>
+  <author>
+    <name>YouTube</name>
+    <uri>http://www.youtube.com/</uri>
+  </author>
+  <generator version='beta'
+    uri='http://gdata.youtube.com/'>YouTube data API</generator>
+  <openSearch:totalResults>99</openSearch:totalResults>
+  <openSearch:startIndex>1</openSearch:startIndex>
+  <openSearch:itemsPerPage>25</openSearch:itemsPerPage>
+  <entry>
+    <id>http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b</id>
+    <published>2007-02-16T20:22:57.000Z</published>
+    <updated>2007-02-16T20:22:57.000Z</updated>
+    <category scheme="http://schemas.google.com/g/2005#kind";
+      term="http://gdata.youtube.com/schemas/2007#video"/>
+    <category scheme="http://gdata.youtube.com/schemas/2007/keywords.cat";
+      term="Steventon"/>
+    <category scheme="http://gdata.youtube.com/schemas/2007/keywords.cat";
+      term="walk"/>
+    <category scheme="http://gdata.youtube.com/schemas/2007/keywords.cat";
+      term="Darcy"/>
+    <category scheme="http://gdata.youtube.com/schemas/2007/categories.cat";
+      term="Entertainment" label="Entertainment"/>
+    <title type="text">My walk with Mr. Darcy</title>
+    <content type="html"><div ... html content trimmed ...></content>
+    <link rel="self" type="application/atom+xml"
+      href="http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b"/>
+    <link rel="alternate" type="text/html"
+      href="http://www.youtube.com/watch?v=ZTUVgYoeN_b"/>
+    <link rel="http://gdata.youtube.com/schemas/2007#video.responses";
+      type="application/atom+xml"
+      href="http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/responses"/>
+    <link rel="http://gdata.youtube.com/schemas/2007#video.ratings";
+      type="application/atom+xml"
+      href="http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/ratings"/>
+    <link rel="http://gdata.youtube.com/schemas/2007#video.complaints";
+      type="application/atom+xml"
+      href="http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/complaints"/>
+    <link rel="http://gdata.youtube.com/schemas/2007#video.related";
+      type="application/atom+xml"
+      href="http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/related"/>
+    <author>
+      <name>Andy Samplo</name>
+      <uri>http://gdata.youtube.com/feeds/api/users/andyland74</uri>
+    </author>
+  <media:group>
+    <media:title type="plain">Shopping for Coats</media:title>
+    <media:description type="plain">
+      What could make for more exciting video?
+    </media:description>
+    <media:keywords>Shopping, parkas</media:keywords>
+    <yt:duration seconds="79"/>
+    <media:category label="People"
+      scheme="http://gdata.youtube.com/schemas/2007/categories.cat";>People
+    </media:category>
+    <media:content
+      url='http://www.youtube.com/v/ZTUVgYoeN_b'
+      type='application/x-shockwave-flash' medium='video' 
+      isDefault='true' expression="full" duration='215' yt:format="5"/>
+    <media:content
+      url='rtsp://rtsp2.youtube.com/ChoLENy73bIAEQ1k30OPEgGDA==/0/0/0/video.3gp'
+      type='video/3gpp' medium='video' 
+      expression="full" duration='215' yt:format="1"/>
+    <media:content
+      url='rtsp://rtsp2.youtube.com/ChoLENy73bIAEQ1k30OPEgGDA==/0/0/0/video.3gp'
+      type='video/3gpp' medium='video' 
+      expression="full" duration='215' yt:format="6"/>
+    <media:player url="http://www.youtube.com/watch?v=ZTUVgYoeN_b"/>
+    <media:thumbnail url="http://img.youtube.com/vi/ZTUVgYoeN_b/2.jpg";
+      height="97" width="130" time="00:00:03.500"/>
+    <media:thumbnail url="http://img.youtube.com/vi/ZTUVgYoeN_b/1.jpg";
+      height="97" width="130" time="00:00:01.750"/>
+    <media:thumbnail url="http://img.youtube.com/vi/ZTUVgYoeN_b/3.jpg";
+      height="97" width="130" time="00:00:05.250"/>
+    <media:thumbnail url="http://img.youtube.com/vi/ZTUVgYoeN_b/0.jpg";
+      height="240" width="320" time="00:00:03.500"/>
+  </media:group>
+  <yt:statistics viewCount="93"/>
+  <gd:rating min='1' max='5' numRaters='435' average='4.94'/>
+  <gd:comments>
+    <gd:feedLink
+      href="http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/comments";
+      countHint='2197'/>
+  </gd:comments>
+</entry>
+</feed>"""
+
+YOU_TUBE_COMMENT_FEED = """<?xml version='1.0' encoding='UTF-8'?>
+<feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'>
+  <id>http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/comments?start-index=1&amp;max-results=25</id>
+  <updated>2008-02-25T23:14:03.148Z</updated>
+  <category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#comment'/>
+  <title type='text'>Comments on 'My walk with Mr. Darcy'</title>
+  <logo>http://www.youtube.com/img/pic_youtubelogo_123x63.gif</logo>
+  <link rel='related' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b'/>
+  <link rel='alternate' type='text/html' href='http://www.youtube.com/watch?v=ZTUVgYoeN_b'/>
+  <link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/comments'/>
+  <link rel='self' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/comments?start-index=1&amp;max-results=25'/>
+  <link rel='next' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/comments?start-index=26&amp;max-results=25'/>
+  <author>                                                    
+    <name>YouTube</name>                                 
+    <uri>http://www.youtube.com/</uri>
+  </author>                                                           
+  <generator version='beta' uri='http://gdata.youtube.com/'>YouTube data API</generator>
+  <openSearch:totalResults>100</openSearch:totalResults>
+  <openSearch:startIndex>1</openSearch:startIndex>
+  <openSearch:itemsPerPage>25</openSearch:itemsPerPage>
+  <entry>
+    <id>http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/comments/7F2BAAD03653A691</id>
+    <published>2007-05-23T00:21:59.000-07:00</published>
+    <updated>2007-05-23T00:21:59.000-07:00</updated>
+    <category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#comment'/>
+    <title type='text'>Walking is fun.</title>
+    <content type='text'>Walking is fun.</content>
+    <link rel='related' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b'/>
+    <link rel='alternate' type='text/html' href='http://www.youtube.com/watch?v=ZTUVgYoeN_b'/>
+    <link rel='self' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/comments/7F2BAAD03653A691'/>
+    <author>
+      <name>andyland744</name>
+      <uri>http://gdata.youtube.com/feeds/api/users/andyland744</uri> 
+    </author>                                                                                    
+  </entry>                
+</feed>"""
+
+YOU_TUBE_PLAYLIST_FEED = """<?xml version='1.0' encoding='UTF-8'?>
+<feed xmlns='http://www.w3.org/2005/Atom'
+    xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' 
+    xmlns:media='http://search.yahoo.com/mrss/' 
+    xmlns:yt='http://gdata.youtube.com/schemas/2007' 
+    xmlns:gd='http://schemas.google.com/g/2005'>
+  <id>http://gdata.youtube.com/feeds/users/andyland74/playlists?start-index=1&amp;max-results=25</id>
+  <updated>2008-02-26T00:26:15.635Z</updated>
+  <category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#playlistLink'/>
+  <title type='text'>andyland74's Playlists</title>
+  <logo>http://www.youtube.com/img/pic_youtubelogo_123x63.gif</logo>
+  <link rel='related' type='application/atom+xml' href='http://gdata.youtube.com/feeds/users/andyland74'/>
+  <link rel='alternate' type='text/html' href='http://www.youtube.com/profile_play_list?user=andyland74'/>
+  <link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://gdata.youtube.com/feeds/users/andyland74/playlists'/>
+  <link rel='self' type='application/atom+xml' href='http://gdata.youtube.com/feeds/users/andyland74/playlists?start-index=1&amp;max-results=25'/>
+  <author>
+    <name>andyland74</name>
+    <uri>http://gdata.youtube.com/feeds/users/andyland74</uri>
+  </author>
+  <generator version='beta' uri='http://gdata.youtube.com/'>YouTube data API</generator>
+  <openSearch:totalResults>1</openSearch:totalResults>
+  <openSearch:startIndex>1</openSearch:startIndex>
+  <openSearch:itemsPerPage>25</openSearch:itemsPerPage>
+  <entry>
+    <id>http://gdata.youtube.com/feeds/users/andyland74/playlists/8BCDD04DE8F771B2</id>
+    <published>2007-11-04T17:30:27.000-08:00</published>
+    <updated>2008-02-22T09:55:14.000-08:00</updated>
+    <category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#playlistLink'/>
+    <title type='text'>My New Playlist Title</title>
+    <content type='text'>My new playlist Description</content>
+    <link rel='related' type='application/atom+xml' href='http://gdata.youtube.com/feeds/users/andyland74'/>
+    <link rel='alternate' type='text/html' href='http://www.youtube.com/view_play_list?p=8BCDD04DE8F771B2'/>
+    <link rel='self' type='application/atom+xml' href='http://gdata.youtube.com/feeds/users/andyland74/playlists/8BCDD04DE8F771B2'/>
+    <author>
+      <name>andyland74</name>                              
+      <uri>http://gdata.youtube.com/feeds/users/andyland74</uri>
+    </author>
+    <yt:description>My new playlist Description</yt:description>
+    <gd:feedLink rel='http://gdata.youtube.com/schemas/2007#playlist' href='http://gdata.youtube.com/feeds/playlists/8BCDD04DE8F771B2'/>
+  </entry>              
+</feed>"""
+
+YOU_TUBE_SUBSCRIPTIONS_FEED = """<?xml version='1.0' encoding='UTF-8'?>
+<feed xmlns='http://www.w3.org/2005/Atom'
+    xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'
+    xmlns:media='http://search.yahoo.com/mrss/'
+    xmlns:yt='http://gdata.youtube.com/schemas/2007'
+    xmlns:gd='http://schemas.google.com/g/2005'>
+  <id>http://gdata.youtube.com/feeds/users/andyland74/subscriptions?start-index=1&amp;max-results=25</id>
+  <updated>2008-02-26T00:26:15.635Z</updated>
+  <category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://gdata.youtube.com/schemas/2007#subscription'/>
+  <title type='text'>andyland74's Subscriptions</title>
+  <logo>http://www.youtube.com/img/pic_youtubelogo_123x63.gif</logo>
+  <link rel='related' type='application/atom+xml'
+    href='http://gdata.youtube.com/feeds/users/andyland74'/>
+  <link rel='alternate' type='text/html'
+    href='http://www.youtube.com/profile_subscriptions?user=andyland74'/>
+  <link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml'
+    href='http://gdata.youtube.com/feeds/users/andyland74/subscriptions'/>
+  <link rel='self' type='application/atom+xml'
+    href='http://gdata.youtube.com/feeds/users/andyland74/subscriptions?start-index=1&amp;max-results=25'/>
+  <author>
+    <name>andyland74</name>
+    <uri>http://gdata.youtube.com/feeds/users/andyland74</uri>
+  </author>
+  <generator version='beta' uri='http://gdata.youtube.com/'>YouTube data API</generator>
+  <openSearch:totalResults>1</openSearch:totalResults>
+  <openSearch:startIndex>1</openSearch:startIndex>
+  <openSearch:itemsPerPage>25</openSearch:itemsPerPage>
+  <entry>
+    <id>http://gdata.youtube.com/feeds/users/andyland74/subscriptions/d411759045e2ad8c</id>
+    <published>2007-11-04T17:30:27.000-08:00</published>
+    <updated>2008-02-22T09:55:14.000-08:00</updated>
+    <category scheme='http://gdata.youtube.com/schemas/2007/subscriptiontypes.cat'
+      term='channel'/>
+    <category scheme='http://schemas.google.com/g/2005#kind'
+      term='http://gdata.youtube.com/schemas/2007#subscription'/>
+    <title type='text'>Videos published by : NBC</title>
+    <link rel='related' type='application/atom+xml'
+      href='http://gdata.youtube.com/feeds/users/andyland74'/>
+    <link rel='alternate' type='text/html'
+      href='http://www.youtube.com/profile_videos?user=NBC'/>
+    <link rel='self' type='application/atom+xml'
+      href='http://gdata.youtube.com/feeds/users/andyland74/subscriptions/d411759045e2ad8c'/>
+    <author>
+      <name>andyland74</name>
+      <uri>http://gdata.youtube.com/feeds/users/andyland74</uri>
+    </author>
+    <yt:username>NBC</yt:username>
+    <gd:feedLink rel='http://gdata.youtube.com/schemas/2007#user.uploads'
+      href='http://gdata.youtube.com/feeds/api/users/nbc/uploads'/>
+  </entry>
+</feed>"""
+
+YOU_TUBE_PROFILE = """<?xml version='1.0' encoding='UTF-8'?>
+<entry xmlns='http://www.w3.org/2005/Atom'
+    xmlns:media='http://search.yahoo.com/mrss/'
+    xmlns:yt='http://gdata.youtube.com/schemas/2007'
+    xmlns:gd='http://schemas.google.com/g/2005'>
+  <id>http://gdata.youtube.com/feeds/users/andyland74</id>
+  <published>2006-10-16T00:09:45.000-07:00</published>
+  <updated>2008-02-26T11:48:21.000-08:00</updated>
+  <category scheme='http://gdata.youtube.com/schemas/2007/channeltypes.cat'
+    term='Standard'/>
+  <category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://gdata.youtube.com/schemas/2007#userProfile'/>
+  <title type='text'>andyland74 Channel</title>
+  <link rel='alternate' type='text/html'
+    href='http://www.youtube.com/profile?user=andyland74'/>
+  <link rel='self' type='application/atom+xml'
+    href='http://gdata.youtube.com/feeds/users/andyland74'/>
+  <author>
+    <name>andyland74</name>
+    <uri>http://gdata.youtube.com/feeds/users/andyland74</uri>
+  </author>
+  <yt:age>33</yt:age>
+  <yt:username>andyland74</yt:username>
+  <yt:books>Catch-22</yt:books>
+  <yt:gender>m</yt:gender>
+  <yt:company>Google</yt:company>
+  <yt:hobbies>Testing YouTube APIs</yt:hobbies>
+  <yt:location>US</yt:location>
+  <yt:movies>Aqua Teen Hungerforce</yt:movies>
+  <yt:music>Elliott Smith</yt:music>
+  <yt:occupation>Technical Writer</yt:occupation>
+  <yt:school>University of North Carolina</yt:school>
+  <media:thumbnail url='http://i.ytimg.com/vi/YFbSxcdOL-w/default.jpg'/>
+  <yt:statistics viewCount='9' videoWatchCount='21' subscriberCount='1'
+    lastWebAccess='2008-02-25T16:03:38.000-08:00'/>
+  <gd:feedLink rel='http://gdata.youtube.com/schemas/2007#user.favorites'
+    href='http://gdata.youtube.com/feeds/users/andyland74/favorites' countHint='4'/>
+  <gd:feedLink rel='http://gdata.youtube.com/schemas/2007#user.contacts'
+    href='http://gdata.youtube.com/feeds/users/andyland74/contacts' countHint='1'/>
+  <gd:feedLink rel='http://gdata.youtube.com/schemas/2007#user.inbox'
+    href='http://gdata.youtube.com/feeds/users/andyland74/inbox' countHint='0'/>
+  <gd:feedLink rel='http://gdata.youtube.com/schemas/2007#user.playlists'
+    href='http://gdata.youtube.com/feeds/users/andyland74/playlists'/>
+  <gd:feedLink rel='http://gdata.youtube.com/schemas/2007#user.subscriptions'
+    href='http://gdata.youtube.com/feeds/users/andyland74/subscriptions' countHint='4'/>
+  <gd:feedLink rel='http://gdata.youtube.com/schemas/2007#user.uploads'
+    href='http://gdata.youtube.com/feeds/users/andyland74/uploads' countHint='1'/>
+</entry>"""
+
+NEW_CONTACT = """<?xml version='1.0' encoding='UTF-8'?>
+<atom:entry xmlns:atom='http://www.w3.org/2005/Atom'
+    xmlns:gd='http://schemas.google.com/g/2005'>
+  <atom:category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://schemas.google.com/contact/2008#contact' />
+  <atom:title type='text'>Elizabeth Bennet</atom:title>
+  <atom:content type='text'>Notes</atom:content>
+  <gd:email rel='http://schemas.google.com/g/2005#work'
+    address='liz gmail com' />
+  <gd:email rel='http://schemas.google.com/g/2005#home'
+    address='liz example org' />
+  <gd:phoneNumber rel='http://schemas.google.com/g/2005#work'
+    primary='true'>(206)555-1212</gd:phoneNumber>
+  <gd:phoneNumber rel='http://schemas.google.com/g/2005#home'>(206)555-1213</gd:phoneNumber>
+  <gd:im address='liz gmail com'
+    protocol='http://schemas.google.com/g/2005#GOOGLE_TALK'
+    rel='http://schemas.google.com/g/2005#home' />
+  <gd:postalAddress rel='http://schemas.google.com/g/2005#work'
+    primary='true'>1600 Amphitheatre Pkwy Mountain View</gd:postalAddress>
+</atom:entry>"""
+
+CONTACTS_FEED = """<?xml version='1.0' encoding='UTF-8'?>
+<feed xmlns='http://www.w3.org/2005/Atom'
+    xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'
+    xmlns:gd='http://schemas.google.com/g/2005'>
+  <id>http://www.google.com/m8/feeds/contacts/liz%40gmail.com/base</id>
+  <updated>2008-03-05T12:36:38.836Z</updated>
+  <category scheme='http://schemas.google.com/g/2005#kind'
+    term='http://schemas.google.com/contact/2008#contact' />
+  <title type='text'>Contacts</title>
+  <link rel='http://schemas.google.com/g/2005#feed'
+    type='application/atom+xml'
+    href='http://www.google.com/m8/feeds/contacts/liz%40gmail.com/base' />
+  <link rel='http://schemas.google.com/g/2005#post'
+    type='application/atom+xml'
+    href='http://www.google.com/m8/feeds/contacts/liz%40gmail.com/base' />
+  <link rel='self' type='application/atom+xml'
+    href='http://www.google.com/m8/feeds/contacts/liz%40gmail.com/base?max-results=25' />
+  <author>
+    <name>Elizabeth Bennet</name>
+    <email>liz gmail com</email>
+  </author>
+  <generator version='1.0' uri='http://www.google.com/m8/feeds/contacts'>Contacts</generator>
+  <openSearch:totalResults>1</openSearch:totalResults>
+  <openSearch:startIndex>1</openSearch:startIndex>
+  <openSearch:itemsPerPage>25</openSearch:itemsPerPage>
+  <entry>
+    <id>http://www.google.com/m8/feeds/contacts/liz%40gmail.com/base/c9012de</id>
+    <updated>2008-03-05T12:36:38.835Z</updated>
+    <category scheme='http://schemas.google.com/g/2005#kind'
+      term='http://schemas.google.com/contact/2008#contact' />
+    <title type='text'>Fitzgerald</title>
+    <link rel='self' type='application/atom+xml'
+      href='http://www.google.com/m8/feeds/contacts/liz%40gmail.com/base/c9012de' />
+    <link rel='edit' type='application/atom+xml'
+      href='http://www.google.com/m8/feeds/contacts/liz%40gmail.com/base/c9012de/1204720598835000' />
+    <gd:phoneNumber rel='http://schemas.google.com/g/2005#home'
+      primary='true'>456</gd:phoneNumber>
+  </entry>
+</feed>"""

Added: trunk/conduit/modules/GoogleModule/gdata/urlfetch.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/urlfetch.py	Wed May  7 11:25:32 2008
@@ -0,0 +1,154 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2008 Google Inc.
+#
+# 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.
+
+
+"""Provides HttpRequest function for gdata.service to use on Google App Engine
+
+HttpRequest: Function that wraps google.appengine.api.urlfetch.Fetch in a 
+    common interface which is used by gdata.service.GDataService. In other 
+    words, this module can be used as the gdata service request handler so 
+    that all HTTP requests will be performed by the hosting Google App Engine
+    server. 
+"""
+
+
+__author__ = 'api.jscudder (Jeffrey Scudder)'
+
+
+import StringIO
+import atom.service
+from google.appengine.api import urlfetch
+
+
+def HttpRequest(service, operation, data, uri, extra_headers=None,
+    url_params=None, escape_params=True, content_type='application/atom+xml'):
+  """Performs an HTTP call to the server, supports GET, POST, PUT, and DELETE.
+
+  To use this module with gdata.service, you can set this module to be the
+  http_request_handler so that HTTP requests use Google App Engine's urlfetch.
+  import gdata.service
+  import gdata.urlfetch
+  gdata.service.http_request_handler = gdata.urlfetch
+
+  Args:
+    service: atom.AtomService object which contains some of the parameters
+        needed to make the request. The following members are used to
+        construct the HTTP call: server (str), additional_headers (dict),
+        port (int), and ssl (bool).
+    operation: str The HTTP operation to be performed. This is usually one of
+        'GET', 'POST', 'PUT', or 'DELETE'
+    data: ElementTree, filestream, list of parts, or other object which can be
+        converted to a string.
+        Should be set to None when performing a GET or PUT.
+        If data is a file-like object which can be read, this method will read
+        a chunk of 100K bytes at a time and send them.
+        If the data is a list of parts to be sent, each part will be evaluated
+        and sent.
+    uri: The beginning of the URL to which the request should be sent.
+        Examples: '/', '/base/feeds/snippets',
+        '/m8/feeds/contacts/default/base'
+    extra_headers: dict of strings. HTTP headers which should be sent
+        in the request. These headers are in addition to those stored in
+        service.additional_headers.
+    url_params: dict of strings. Key value pairs to be added to the URL as
+        URL parameters. For example {'foo':'bar', 'test':'param'} will
+        become ?foo=bar&test=param.
+    escape_params: bool default True. If true, the keys and values in
+        url_params will be URL escaped when the form is constructed
+        (Special characters converted to %XX form.)
+    content_type: str The MIME type for the data being sent. Defaults to
+        'application/atom+xml', this is only used if data is set.
+  """
+  full_uri = atom.service.BuildUri(uri, url_params, escape_params)
+  (server, port, ssl, partial_uri) = atom.service.ProcessUrl(service, full_uri)
+  # Construct the full URL for the request.
+  if ssl:
+    full_url = 'https://%s%s' % (server, partial_uri)
+  else:
+    full_url = 'http://%s%s' % (server, partial_uri)
+
+  # Construct the full payload. 
+  # Assume that data is None or a string.
+  data_str = data
+  if data:
+    if isinstance(data, list):
+      # If data is a list of different objects, convert them all to strings
+      # and join them together.
+      converted_parts = [__ConvertDataPart(x) for x in data]
+      data_str = ''.join(converted_parts)
+    else:
+      data_str = __ConvertDataPart(data)
+
+  # Construct the dictionary of HTTP headers.
+  headers = {}
+  if isinstance(service.additional_headers, dict):
+    headers = service.additional_headers.copy()
+  if isinstance(extra_headers, dict):
+    for header, value in extra_headers.iteritems():
+      headers[header] = value
+  # Add the content type header (we don't need to calculate content length,
+  # since urlfetch.Fetch will calculate for us).
+  if content_type:
+    headers['Content-Type'] = content_type
+
+  # Lookup the urlfetch operation which corresponds to the desired HTTP verb.
+  if operation == 'GET':
+    method = urlfetch.GET
+  elif operation == 'POST':
+    method = urlfetch.POST
+  elif operation == 'PUT':
+    method = urlfetch.PUT
+  elif operation == 'DELETE':
+    method = urlfetch.DELETE
+  else:
+    method = None
+  return HttpResponse(urlfetch.Fetch(url=full_url, payload=data_str, 
+      method=method, headers=headers))
+
+
+def __ConvertDataPart(data):
+  if not data or isinstance(data, str):
+    return data
+  elif hasattr(data, 'read'):
+    # data is a file like object, so read it completely.
+    return data.read()
+  # The data object was not a file.
+  # Try to convert to a string and send the data.
+  return str(data)
+
+
+class HttpResponse(object):
+  """Translates a urlfetch resoinse to look like an hhtplib resoinse.
+  
+  Used to allow the resoinse from HttpRequest to be usable by gdata.service
+  methods.
+  """
+
+  def __init__(self, urlfetch_response):
+    self.body = StringIO.StringIO(urlfetch_response.content)
+    self.headers = urlfetch_response.headers
+    self.status = urlfetch_response.status_code
+    self.reason = ''
+
+  def read(self, length=None):
+    if not length:
+      return self.body.read()
+    else:
+      return self.body.read(length)
+
+  def getheader(self, name):
+    return self.headers[name]
+    

Modified: trunk/test/python-tests/TestCoreContact.py
==============================================================================
--- trunk/test/python-tests/TestCoreContact.py	(original)
+++ trunk/test/python-tests/TestCoreContact.py	Wed May  7 11:25:32 2008
@@ -22,6 +22,12 @@
 EMAIL;TYPE=INTERNET:cantiuni branch statravel co nz
 END:VCARD"""
 
+c = Contact.Contact()
+ok("Created blank contact", len(c.get_vcard_string()) > 0)
+
+c = Contact.Contact(formattedName="Im Cool", givenName="Steve", familyName="Cool")
+ok("Created contact", len(c.get_vcard_string()) > 0)
+
 contacts = Contact.parse_vcf(vcfData)
 ok("Parsed vcf file (got %s vcards)" % len(contacts), len(contacts) == vcfData.count("BEGIN:VCARD"))
 
@@ -30,4 +36,11 @@
 ok("Got email addresses", len(c.get_emails()) > 0)
 ok("Got name", c.get_name() != None)
 
+#now add email addresses
+emails = ("foo bar com","baz f")
+numb4 = len(c.get_emails())
+c.set_emails(*emails)
+numAfta = len(c.get_emails())
+ok("Added email addresses", numAfta == (numb4 | len(emails)))
+
 finished()

Modified: trunk/test/python-tests/TestDataProviderGoogle.py
==============================================================================
--- trunk/test/python-tests/TestDataProviderGoogle.py	(original)
+++ trunk/test/python-tests/TestDataProviderGoogle.py	Wed May  7 11:25:32 2008
@@ -13,6 +13,9 @@
 if not is_online():
     skip()
 
+#-------------------------------------------------------------------------------
+# Calendar
+#-------------------------------------------------------------------------------
 #setup the test
 test = SimpleTest(sinkName="GoogleCalendarTwoWay")
 config = {
@@ -57,24 +60,10 @@
         data=event,
         name="event"
         )
-        
-#setup the test
-test = SimpleTest(sourceName="ContactsSource")
-config = {
-    "username":     os.environ.get("TEST_USERNAME","conduitproject gmail com"),
-    "password":     os.environ["TEST_PASSWORD"],
-}
-test.configure(source=config)
-google = test.get_source().module
-
-#check we can get a contacts list
-contacts = google.get_all()
-num = len(contacts)
-ok("Got %s contacts" % num, num > 0)
-
-c = google.get(contacts[0])
-ok("Got contact", c != None)
 
+#-------------------------------------------------------------------------------
+# Youtube
+#-------------------------------------------------------------------------------
 #Now a very simple youtube test...
 test = SimpleTest(sourceName="YouTubeSource")
 config = {

Copied: trunk/test/python-tests/TestDataProviderGoogleContacts.py (from r1455, /trunk/test/python-tests/TestDataProviderGoogle.py)
==============================================================================
--- /trunk/test/python-tests/TestDataProviderGoogle.py	(original)
+++ trunk/test/python-tests/TestDataProviderGoogleContacts.py	Wed May  7 11:25:32 2008
@@ -1,96 +1,55 @@
 #common sets up the conduit environment
 from common import *
-
-import conduit.datatypes.Event as Event
+import conduit.datatypes.Contact as Contact
 import conduit.utils as Utils
-
-import random
-
-SAFE_CALENDAR_NAME="Conduit Project"
-SAFE_EVENT_UID="2bh7mbagsc880g64qaps06tbp4 google com"
-MAX_YOUTUBE_VIDEOS=5
+import conduit.Exceptions as Exceptions
 
 if not is_online():
     skip()
+    
+SAFE_CONTACT_ID="http://www.google.com/m8/feeds/contacts/conduitproject%40gmail.com/base/89c42ac889d80b8";
+
+vcfData="""
+BEGIN:VCARD
+VERSION:3.0
+FN:Random Person
+N:Person;A;Random;;
+EMAIL;TYPE=INTERNET:%s email com
+END:VCARD"""
 
 #setup the test
-test = SimpleTest(sinkName="GoogleCalendarTwoWay")
+test = SimpleTest(sinkName="ContactsTwoWay")
 config = {
     "username":     os.environ.get("TEST_USERNAME","conduitproject gmail com"),
     "password":     os.environ["TEST_PASSWORD"],
-    "selectedCalendarURI"   :   "conduitproject%40gmail.com",
-    "selectedCalendarName"  :   SAFE_CALENDAR_NAME,
 }
 test.configure(sink=config)
 google = test.get_sink().module
 
-#check we can get the calendar
-found = False
-for cal in google._get_all_calendars():
-    if cal.get_name() == SAFE_CALENDAR_NAME:
-        found = True
-        break
-        
-ok("Found calendar: '%s'" % SAFE_CALENDAR_NAME, found)    
-
-#make a simple event
-hour=random.randint(12,23)
-ics="""BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//PYVOBJECT//NONSGML Version 1//EN
-BEGIN:VEVENT
-DTSTART:2008%(month)02d%(day)02dT%(hour)02d0000Z
-DTEND:2008%(month)02d%(day)02dT%(end)02d0000Z
-SUMMARY:Test Event
-END:VEVENT
-END:VCALENDAR""" % {    "month" :   random.randint(1,12),
-                        "day"   :   random.randint(1,28),
-                        "hour"  :   hour,
-                        "end"   :   hour+1}
+#Log in
+try:
+    google.refresh()
+    ok("Logged in", google.loggedIn == True)
+except Exception, err:
+    ok("Logged in (%s)" % err, False) 
 
-event = Event.Event()
-event.set_from_ical_string(ics)
+#make a new contact with a random email address (so it doesnt conflict)
+contact = Contact.parse_vcf(vcfData % Utils.random_string())[0]
 test.do_dataprovider_tests(
         supportsGet=True,
-        supportsDelete=False,
-        safeLUID=SAFE_EVENT_UID,
-        data=event,
-        name="event"
+        supportsDelete=True,
+        safeLUID=SAFE_CONTACT_ID,
+        data=contact,
+        name="contact"
         )
-        
-#setup the test
-test = SimpleTest(sourceName="ContactsSource")
-config = {
-    "username":     os.environ.get("TEST_USERNAME","conduitproject gmail com"),
-    "password":     os.environ["TEST_PASSWORD"],
-}
-test.configure(source=config)
-google = test.get_source().module
-
-#check we can get a contacts list
-contacts = google.get_all()
-num = len(contacts)
-ok("Got %s contacts" % num, num > 0)
-
-c = google.get(contacts[0])
-ok("Got contact", c != None)
-
-#Now a very simple youtube test...
-test = SimpleTest(sourceName="YouTubeSource")
-config = {
-    "max_downloads" :   MAX_YOUTUBE_VIDEOS
-}
-test.configure(source=config)
-youtube = test.get_source().module
 
+#check we get a conflict if we put a contact with a known existing email address
+#FIXME: We should actually automatically resolve this conflict...
+contact = new_contact(None)
 try:
-    youtube.refresh()
-    ok("Refresh youtube", True)
-except Exception, err:
-    ok("Refresh youtube (%s)" % err, False) 
-
-videos = youtube.get_all()
-num = len(videos)
-ok("Got %s videos" % num, num == MAX_YOUTUBE_VIDEOS)
+    google.put(contact, False)
+    ok("Detected duplicate email", False)
+except Exceptions.SynchronizeConflictError:
+    ok("Detected duplicate email", True)
 
 finished()

Modified: trunk/test/python-tests/TestDataProviderPicasa.py
==============================================================================
--- trunk/test/python-tests/TestDataProviderPicasa.py	(original)
+++ trunk/test/python-tests/TestDataProviderPicasa.py	Wed May  7 11:25:32 2008
@@ -51,6 +51,10 @@
 else:
     ok("Album has an unexpected id: %s instead of %s" % (picasa.galbum.id, SAFE_ALBUM_ID), False)
 
+#Picasa dp gets all the images and stores them in an internal dict. Therefor before
+#the image dataprovider tests below, we must fill that dict
+picasa._get_photos()
+
 #Perform image tests
 test.do_image_dataprovider_tests(
         supportsGet=True,

Modified: trunk/test/python-tests/common.py
==============================================================================
--- trunk/test/python-tests/common.py	(original)
+++ trunk/test/python-tests/common.py	Wed May  7 11:25:32 2008
@@ -387,13 +387,14 @@
         the data to get
         """
         #Test put()
+        uid = None
         if data:
             try:
                 rid = self.sink.module.put(data, True)
                 uid = rid.get_UID()
                 ok("Put a %s (%s) " % (name,rid), True)
             except Exception, err:
-                traceback.print_exc()        
+                traceback.print_exc()
                 ok("Put a %s (%s)" % (name,err), False)
             
         #Test get()
@@ -423,8 +424,8 @@
                 traceback.print_exc()
                 ok("Update %s (%s)" % (name,err), False)
 
-        #Test delete()
-        if supportsDelete:
+        #Test delete() (but only delete data we have put, not the safe data)
+        if supportsDelete and uid:
             try:
                 self.sink.module.refresh()
                 self.sink.module.delete(uid)

Modified: trunk/test/python-tests/data/1.vcard
==============================================================================
--- trunk/test/python-tests/data/1.vcard	(original)
+++ trunk/test/python-tests/data/1.vcard	Wed May  7 11:25:32 2008
@@ -2,6 +2,7 @@
 VERSION:3.0
 FN:Daffy Duck Knudson (with Bugs Bunny and Mr. Pluto)
 N:Knudson;Daffy Duck (with Bugs Bunny and Mr. Pluto)
+EMAIL;TYPE=INTERNET:daffy gmail com
 NICKNAME:gnat and gnu and pluto
 BDAY;value=date:02-10
 TEL;type=HOME:+01-(0)2-765.43.21



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]