conduit r1250 - in trunk: . conduit/modules conduit/modules/GoogleModule conduit/modules/GoogleModule/libgdata conduit/modules/GoogleModule/libgdata/atom conduit/modules/GoogleModule/libgdata/gdata conduit/modules/GoogleModule/libgdata/gdata/calendar conduit/modules/GoogleModule/libgdata/gdata/exif conduit/modules/GoogleModule/libgdata/gdata/geo conduit/modules/GoogleModule/libgdata/gdata/media conduit/modules/GoogleModule/libgdata/gdata/photos conduit/modules/PicasaModule test/python-tests



Author: thomasvm
Date: Sat Jan 19 23:45:12 2008
New Revision: 1250
URL: http://svn.gnome.org/viewvc/conduit?rev=1250&view=rev

Log:
Fix #510129, use gdata instead of our own picasaweb implementation

Added:
   trunk/conduit/modules/GoogleModule/calendar-config.glade
      - copied unchanged from r1249, /trunk/conduit/modules/GoogleModule/config.glade
   trunk/conduit/modules/GoogleModule/libgdata/
   trunk/conduit/modules/GoogleModule/libgdata/Makefile.am
   trunk/conduit/modules/GoogleModule/libgdata/atom/
   trunk/conduit/modules/GoogleModule/libgdata/atom/Makefile.am
   trunk/conduit/modules/GoogleModule/libgdata/atom/__init__.py   (contents, props changed)
   trunk/conduit/modules/GoogleModule/libgdata/atom/service.py   (contents, props changed)
   trunk/conduit/modules/GoogleModule/libgdata/gdata/
   trunk/conduit/modules/GoogleModule/libgdata/gdata/Makefile.am
   trunk/conduit/modules/GoogleModule/libgdata/gdata/__init__.py   (contents, props changed)
   trunk/conduit/modules/GoogleModule/libgdata/gdata/auth.py
   trunk/conduit/modules/GoogleModule/libgdata/gdata/calendar/
   trunk/conduit/modules/GoogleModule/libgdata/gdata/calendar/Makefile.am
   trunk/conduit/modules/GoogleModule/libgdata/gdata/calendar/__init__.py   (contents, props changed)
   trunk/conduit/modules/GoogleModule/libgdata/gdata/calendar/service.py   (contents, props changed)
   trunk/conduit/modules/GoogleModule/libgdata/gdata/exif/
   trunk/conduit/modules/GoogleModule/libgdata/gdata/exif/Makefile.am
   trunk/conduit/modules/GoogleModule/libgdata/gdata/exif/__init__.py
   trunk/conduit/modules/GoogleModule/libgdata/gdata/geo/
   trunk/conduit/modules/GoogleModule/libgdata/gdata/geo/Makefile.am
   trunk/conduit/modules/GoogleModule/libgdata/gdata/geo/__init__.py
   trunk/conduit/modules/GoogleModule/libgdata/gdata/media/
   trunk/conduit/modules/GoogleModule/libgdata/gdata/media/Makefile.am
   trunk/conduit/modules/GoogleModule/libgdata/gdata/media/__init__.py
   trunk/conduit/modules/GoogleModule/libgdata/gdata/photos/
   trunk/conduit/modules/GoogleModule/libgdata/gdata/photos/Makefile.am
   trunk/conduit/modules/GoogleModule/libgdata/gdata/photos/__init__.py
   trunk/conduit/modules/GoogleModule/libgdata/gdata/photos/service.py   (contents, props changed)
   trunk/conduit/modules/GoogleModule/libgdata/gdata/service.py   (contents, props changed)
Removed:
   trunk/conduit/modules/GoogleModule/config.glade
   trunk/conduit/modules/PicasaModule/
Modified:
   trunk/ChangeLog
   trunk/conduit/modules/GoogleModule/GoogleModule.py
   trunk/conduit/modules/GoogleModule/Makefile.am
   trunk/conduit/modules/Makefile.am
   trunk/configure.ac
   trunk/test/python-tests/TestDataProviderPicasa.py

Modified: trunk/conduit/modules/GoogleModule/GoogleModule.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/GoogleModule.py	(original)
+++ trunk/conduit/modules/GoogleModule/GoogleModule.py	Sat Jan 19 23:45:12 2008
@@ -7,10 +7,12 @@
 
 import conduit
 import conduit.dataproviders.DataProvider as DataProvider
+import conduit.dataproviders.Image as Image
 import conduit.Utils as Utils
 import conduit.Exceptions as Exceptions
 from conduit.datatypes import Rid
 import conduit.datatypes.Event as Event
+import conduit.datatypes.Photo as Photo
 
 from gettext import gettext as _
 
@@ -19,14 +21,27 @@
     import gdata.calendar.service
     import gdata.service
     import gdata.calendar
+    import gdata.photos
+    import gdata.photos.service
     import atom
-    MODULES = {
-        "GoogleCalendarTwoWay" : { "type": "dataprovider" },
-    }
 except ImportError:
-    MODULES = {
-    }
-    log.warn("Skipping GoogleCalendarTwoWay - GDATA is not available")
+    Utils.dataprovider_add_dir_to_path(__file__, "libgdata")
+    import vobject
+    import gdata.calendar.service
+    import gdata.service
+    import gdata.calendar
+    import gdata.photos
+    import gdata.photos.service
+    import atom
+
+# time format
+FORMAT_STRING = "%Y-%m-%dT%H:%M:%S"
+
+MODULES = {
+    "GoogleCalendarTwoWay" : { "type": "dataprovider" },
+    "PicasaTwoWay" :          { "type": "dataprovider" }        
+}
+
 
 class GoogleConnection(object):
     def __init__(self):
@@ -381,7 +396,7 @@
         import gtk
         tree = Utils.dataprovider_glade_get_widget(
                         __file__, 
-                        "config.glade",
+                        "calendar-config.glade",
                         "GoogleCalendarConfigDialog"
                         )
 
@@ -502,4 +517,174 @@
          
     def get_UID(self):
         return self.google.GetUsername()
+
+class PicasaTwoWay(Image.ImageTwoWay):
+
+    _name_ = _("Picasa")
+    _description_ = _("Sync your Google Picasa photos")
+    _icon_ = "picasa"
+
+    def __init__(self, *args):
+        Image.ImageTwoWay.__init__(self)
+        self.need_configuration(True)
+        
+        self.username = ""
+        self.password = ""
+        self.album = ""
+        self.imageSize = "None"
+
+        self.pws = gdata.photos.service.PhotosService()
+        self.galbum = None
+        self.gphoto_dict = None
+
+    def _get_raw_photo_url(self, photoInfo):
+        return photoInfo.GetMediaURL()
+
+    def _get_photo_info (self, id):
+        if self.gphoto_dict.has_key(id):
+            return self.gphoto_dict[id]
+        else:
+            return None
+            
+    def _get_photo_formats (self):
+        return ("image/jpeg", )
+        
+    def refresh(self):
+        Image.ImageTwoWay.refresh(self)
+        self._login()
+        self._get_album()
+        self._get_photos()
+
+    def get_all (self):
+        return self.gphoto_dict.keys()
+        
+    def get (self, LUID):
+        Image.ImageTwoWay.get (self, LUID)
+
+        gphoto = self.gphoto_dict[LUID]
+        url = gphoto.GetMediaURL()
+
+        f = Photo.Photo (URI=url)
+        f.force_new_mtime(self._get_photo_timestamp(gphoto))
+        f.set_open_URI(url)
+        f.set_UID(LUID)
+
+        return f
+
+    def delete(self, LUID):
+        if not self.gphoto_dict.has_key(LUID):
+            log.warn("Photo does not exit")
+            return
+
+        self.pws.Delete(self.gphoto_dict[LUID])
+        del self.gphoto_dict[LUID]
+
+    def _upload_photo (self, uploadInfo):
+        try:
+            gphoto = self.pws.InsertPhotoSimple (self.galbum, uploadInfo.name, '', uploadInfo.url)
+
+            for tag in uploadInfo.tags:
+                self.pws.InsertTag(gphoto, str(tag))
+
+            return Rid(uid=gphoto.gphoto_id.text)
+        except Exception, e:
+            raise Exceptions.SyncronizeError("Picasa Upload Error.")
+
+    def _login(self):
+        self.pws.ClientLogin(self.username, self.password)
+
+    def _get_album(self):
+        configured_album = None
+
+        albums = self.pws.GetUserFeed().entry
+
+        for album in albums:
+            if album.title.text != self.album:
+                continue
+
+            log.debug("Found album %s" % self.album)
+            configured_album = album
+            break
+
+        if not configured_album:
+            log.debug("Creating new album %s." % self.album)
+            configured_album = self.pws.InsertAlbum (self.album, '')
+
+        self.galbum = configured_album
+
+    def _get_photos(self):
+        self.gphoto_dict = {}
+
+        for photo in self.pws.GetFeed(self.galbum.GetPhotosUri()).entry:
+            self.gphoto_dict[photo.gphoto_id.text] = photo
+
+    def _get_photo_timestamp(self, gphoto):
+        from datetime import datetime
+        timestamp = gphoto.updated.text[0:-5]
+        try:
+            return datetime.strptime(timestamp, FORMAT_STRING)
+        except AttributeError:
+            import time
+            return datetime(*(time.strptime(timestamp, FORMAT_STRING)[0:6]))
+
+    def configure(self, window):
+        """
+        Configures the PicasaTwoWay
+        """
+        widget = Utils.dataprovider_glade_get_widget(
+                        __file__, 
+                        "picasa-config.glade", 
+                        "PicasaTwoWayConfigDialog")
+                        
+        #get a whole bunch of widgets
+        username = widget.get_widget("username")
+        password = widget.get_widget("password")
+        album = widget.get_widget("album")                
+        
+        #preload the widgets        
+        username.set_text(self.username)
+        password.set_text(self.password)
+        album.set_text (self.album)
+
+        resizecombobox = widget.get_widget("combobox1")
+        self._resize_combobox_build(resizecombobox, self.imageSize)
+        
+        dlg = widget.get_widget("PicasaTwoWayConfigDialog")
+        
+        response = Utils.run_dialog (dlg, window)
+
+        if response == True:
+            self.username = username.get_text()
+            self.password = password.get_text()
+            self.album = album.get_text()
+
+            self.imageSize = self._resize_combobox_get_active(resizecombobox)
+
+            self.set_configured(self.is_configured())
+
+        dlg.destroy()    
+        
+    def get_configuration(self):
+        return {
+            "imageSize" : self.imageSize,
+            "username" : self.username,
+            "password" : self.password,
+            "album" : self.album
+            }
+            
+    def is_configured (self):
+        if len(self.username) < 1:
+            return False
+        
+        if len(self.password) < 1:
+            return False
+            
+        if len(self.album) < 1:
+            return False
+            
+        return True
+
+    def get_UID(self):
+        return self.username
+            
 		

Modified: trunk/conduit/modules/GoogleModule/Makefile.am
==============================================================================
--- trunk/conduit/modules/GoogleModule/Makefile.am	(original)
+++ trunk/conduit/modules/GoogleModule/Makefile.am	Sat Jan 19 23:45:12 2008
@@ -1,8 +1,10 @@
+SUBDIRS = libgdata
+
 conduit_handlersdir = $(libdir)/conduit/modules/GoogleModule
 conduit_handlers_PYTHON = GoogleModule.py
 
-conduit_handlers_DATA = config.glade
-EXTRA_DIST = config.glade
+conduit_handlers_DATA = calendar-config.glade picasa-config.glade 
+EXTRA_DIST = calendar-config.glade picasa-config.glade 
 
 clean-local:
 	rm -rf *.pyc *.pyo

Added: trunk/conduit/modules/GoogleModule/libgdata/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/Makefile.am	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,2 @@
+SUBDIRS = atom gdata
+

Added: trunk/conduit/modules/GoogleModule/libgdata/atom/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/atom/Makefile.am	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,5 @@
+conduit_handlersdir = $(libdir)/conduit/modules/GoogleModule/libgdata/atom
+conduit_handlers_PYTHON = __init__.py service.py
+
+clean-local:
+	rm -rf *.pyc *.pyo

Added: trunk/conduit/modules/GoogleModule/libgdata/atom/__init__.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/atom/__init__.py	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,1385 @@
+#!/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 classes representing Atom elements.
+
+  Module objective: provide data classes for Atom constructs. These classes hide
+  the XML-ness of Atom and provide a set of native Python classes to interact 
+  with.
+
+  Conversions to and from XML should only be necessary when the Atom classes
+  "touch the wire" and are sent over HTTP. For this reason this module 
+  provides  methods and functions to convert Atom classes to and from strings.
+
+  For more information on the Atom data model, see RFC 4287 
+  (http://www.ietf.org/rfc/rfc4287.txt)
+
+  AtomBase: A foundation class on which Atom classes are built. It 
+      handles the parsing of attributes and children which are common to all
+      Atom classes. By default, the AtomBase class translates all XML child 
+      nodes into ExtensionElements.
+
+  ExtensionElement: Atom allows Atom objects to contain XML which is not part 
+      of the Atom specification, these are called extension elements. If a 
+      classes parser encounters an unexpected XML construct, it is translated
+      into an ExtensionElement instance. ExtensionElement is designed to fully
+      capture the information in the XML. Child nodes in an XML extension are
+      turned into ExtensionElements as well.
+"""
+
+
+__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
+
+
+# XML namespaces which are often used in Atom entities.
+ATOM_NAMESPACE = 'http://www.w3.org/2005/Atom'
+ELEMENT_TEMPLATE = '{http://www.w3.org/2005/Atom}%s'
+APP_NAMESPACE = 'http://purl.org/atom/app#'
+APP_TEMPLATE = '{http://purl.org/atom/app#}%s'
+
+# This encoding is used for converting strings before translating the XML
+# into an object.
+XML_STRING_ENCODING = 'utf-8'
+# The desired string encoding for object members.
+MEMBER_STRING_ENCODING = 'utf-8'
+
+
+def CreateClassFromXMLString(target_class, xml_string, string_encoding=None):
+  """Creates an instance of the target class from the string contents.
+  
+  Args:
+    target_class: class The class which will be instantiated and populated
+        with the contents of the XML. This class must have a _tag and a
+        _namespace class variable.
+    xml_string: str A string which contains valid XML. The root element
+        of the XML string should match the tag and namespace of the desired
+        class.
+    string_encoding: str The character encoding which the xml_string should
+        be converted to before it is interpreted and translated into 
+        objects. The default is None in which case the string encoding
+        is not changed.
+
+  Returns:
+    An instance of the target class with members assigned according to the
+    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)
+  return _CreateClassFromElementTree(target_class, tree)
+
+
+def _CreateClassFromElementTree(target_class, tree, namespace=None, tag=None):
+  """Instantiates the class and populates members according to the tree.
+
+  Note: Only use this function with classes that have _namespace and _tag
+  class members.
+
+  Args:
+    target_class: class The class which will be instantiated and populated
+        with the contents of the XML.
+    tree: ElementTree An element tree whose contents will be converted into
+        members of the new target_class instance.
+    namespace: str (optional) The namespace which the XML tree's root node must
+        match. If omitted, the namespace defaults to the _namespace of the 
+        target class.
+    tag: str (optional) The tag which the XML tree's root node must match. If
+        omitted, the tag defaults to the _tag class member of the target 
+        class.
+
+    Returns:
+      An instance of the target class - or None if the tag and namespace of 
+      the XML tree's root node did not match the desired namespace and tag.
+  """
+  if namespace is None:
+    namespace = target_class._namespace
+  if tag is None:
+    tag = target_class._tag
+  if tree.tag == '{%s}%s' % (namespace, tag):
+    target = target_class()
+    target._HarvestElementTree(tree)
+    return target
+  else:
+    return None
+
+
+class ExtensionContainer(object):
+  
+  def __init__(self, extension_elements=None, extension_attributes=None,
+      text=None):
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+    self.text = text
+ 
+  # Three methods to create an object from an ElementTree
+  def _HarvestElementTree(self, tree):
+    # 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)
+    # Encode the text string according to the desired encoding type. (UTF-8)
+    if tree.text:
+      self.text = tree.text.encode(MEMBER_STRING_ENCODING)
+    
+  def _ConvertElementTreeToMember(self, child_tree, current_class=None):
+    self.extension_elements.append(_ExtensionElementFromElementTree(
+        child_tree))
+
+  def _ConvertElementAttributeToMember(self, attribute, value):
+    # Encode the attribute value's string with the desired type Default UTF-8
+    if value:
+      self.extension_attributes[attribute] = value.encode(
+          MEMBER_STRING_ENCODING)
+
+  # One method to create an ElementTree from an object
+  def _AddMembersToElementTree(self, tree):
+    for child in self.extension_elements:
+      child._BecomeChildElement(tree)
+    for attribute, value in self.extension_attributes.iteritems():
+      if value:
+        # Encode the value in the desired type (default UTF-8).
+        tree.attrib[attribute] = value.encode(MEMBER_STRING_ENCODING)
+    tree.text = self.text
+
+  def FindExtensions(self, tag=None, namespace=None):
+    """Searches extension elements for child nodes with the desired name.
+
+    Returns a list of extension elements within this object whose tag
+    and/or namespace match those passed in. To find all extensions in
+    a particular namespace, specify the namespace but not the tag name.
+    If you specify only the tag, the result list may contain extension
+    elements in multiple namespaces.
+
+    Args:
+      tag: str (optional) The desired tag
+      namespace: str (optional) The desired namespace
+
+    Returns:
+      A list of elements whose tag and/or namespace match the parameters
+      values
+    """
+
+    results = []
+
+    if tag and namespace:
+      for element in self.extension_elements:
+        if element.tag == tag and element.namespace == namespace:
+          results.append(element)
+    elif tag and not namespace:
+      for element in self.extension_elements:
+        if element.tag == tag:
+          results.append(element)
+    elif namespace and not tag:
+      for element in self.extension_elements:
+        if element.namespace == namespace:
+          results.append(element)
+    else:
+      for element in self.extension_elements:
+        results.append(element)
+
+    return results
+  
+
+class AtomBase(ExtensionContainer):
+
+  _children = {}
+  _attributes = {}
+
+  def __init__(self, extension_elements=None, extension_attributes=None,
+      text=None):
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+    self.text = text
+      
+  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(_CreateClassFromElementTree(
+            member_class[0], child_tree))
+      else:
+        setattr(self, member_name, 
+                _CreateClassFromElementTree(member_class, child_tree))
+    else:
+      ExtensionContainer._ConvertElementTreeToMember(self, child_tree)      
+
+  def _ConvertElementAttributeToMember(self, attribute, value):
+    # Find the attribute in this class's list of attributes. 
+    if self.__class__._attributes.has_key(attribute):
+      # Find the member of this class which corresponds to the XML attribute
+      # (lookup in current_class._attributes) and set this member to the
+      # desired value (using self.__dict__).
+      if value:
+        # Encode the string to capture non-ascii characters (default UTF-8)
+        setattr(self, self.__class__._attributes[attribute], 
+                value.encode(MEMBER_STRING_ENCODING))
+    else:
+      ExtensionContainer._ConvertElementAttributeToMember(self, attribute, value)
+
+  # Three methods to create an ElementTree from an object
+  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
+    # Lastly, call the ExtensionContainers's _AddMembersToElementTree to 
+    # convert any extension attributes.
+    ExtensionContainer._AddMembersToElementTree(self, tree)
+    
+  
+  def _BecomeChildElement(self, tree):
+    """
+
+    Note: Only for use with classes that have a _tag and _namespace class 
+    member. It is in AtomBase so that it can be inherited but it should
+    not be called on instances of AtomBase.
+    
+    """
+    new_child = ElementTree.Element('')
+    tree.append(new_child)
+    new_child.tag = '{%s}%s' % (self.__class__._namespace, 
+                                self.__class__._tag)
+    self._AddMembersToElementTree(new_child)
+
+  def _ToElementTree(self):
+    """
+
+    Note, this method is designed to be used only with classes that have a 
+    _tag and _namespace. It is placed in AtomBase for inheritance but should
+    not be called on this class.
+
+    """
+    new_tree = ElementTree.Element('{%s}%s' % (self.__class__._namespace,
+                                               self.__class__._tag))
+    self._AddMembersToElementTree(new_tree)
+    return new_tree
+
+  def ToString(self, string_encoding='UTF-8'):
+    """Converts the Atom object to a string containing XML."""
+    return ElementTree.tostring(self._ToElementTree(), encoding=string_encoding)
+
+  def __str__(self):
+    return self.ToString()
+
+    
+class Name(AtomBase):
+  """The atom:name element"""
+
+  _tag = 'name'
+  _namespace = ATOM_NAMESPACE
+  _children = AtomBase._children.copy()
+  _attributes = AtomBase._attributes.copy()
+
+  def __init__(self, text=None, extension_elements=None,
+      extension_attributes=None):
+    """Constructor for Name
+
+    Args:
+      text: str The text data in the this element
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+    """
+
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def NameFromString(xml_string):
+  return CreateClassFromXMLString(Name, xml_string)
+
+
+class Email(AtomBase):
+  """The atom:email element"""
+
+  _tag = 'email'
+  _namespace = ATOM_NAMESPACE
+  _children = AtomBase._children.copy()
+  _attributes = AtomBase._attributes.copy()
+  
+  def __init__(self, text=None, extension_elements=None,
+      extension_attributes=None):
+    """Constructor for Email
+
+    Args:
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+      text: str The text data in the this element
+    """
+
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def EmailFromString(xml_string):
+  return CreateClassFromXMLString(Email, xml_string)
+
+
+class Uri(AtomBase):
+  """The atom:uri element"""
+
+  _tag = 'uri'
+  _namespace = ATOM_NAMESPACE
+  _children = AtomBase._children.copy()
+  _attributes = AtomBase._attributes.copy()
+
+  def __init__(self, text=None, extension_elements=None,
+      extension_attributes=None):
+    """Constructor for Uri
+
+    Args:
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+      text: str The text data in the this element
+    """
+
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def UriFromString(xml_string):
+  return CreateClassFromXMLString(Uri, xml_string)
+
+  
+class Person(AtomBase):
+  """A foundation class from which atom:author and atom:contributor extend.
+  
+  A person contains information like name, email address, and web page URI for
+  an author or contributor to an Atom feed. 
+  """
+
+  _children = AtomBase._children.copy()
+  _attributes = AtomBase._attributes.copy()
+  _children['{%s}name' % (ATOM_NAMESPACE)] = ('name', Name)
+  _children['{%s}email' % (ATOM_NAMESPACE)] = ('email', Email)
+  _children['{%s}uri' % (ATOM_NAMESPACE)] = ('uri', Uri)
+
+  def __init__(self, name=None, email=None, uri=None,
+      extension_elements=None, extension_attributes=None, text=None):
+    """Foundation from which author and contributor are derived.
+
+    The constructor is provided for illustrative purposes, you should not
+    need to instantiate a Person.
+
+    Args:
+      name: Name The person's name
+      email: Email The person's email address
+      uri: Uri The URI of the person's webpage
+      extension_elements: list A list of ExtensionElement instances which are
+          children of this element.
+      extension_attributes: dict A dictionary of strings which are the values
+          for additional XML attributes of this element.
+      text: String The text contents of the element. This is the contents
+          of the Entry's XML text node. (Example: <foo>This is the text</foo>)
+    """
+
+    self.name = name
+    self.email = email
+    self.uri = uri
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+    self.text = text
+
+
+class Author(Person):
+  """The atom:author element
+  
+  An author is a required element in Feed. 
+  """
+
+  _tag = 'author'
+  _namespace = ATOM_NAMESPACE
+  _children = Person._children.copy()
+  _attributes = Person._attributes.copy()
+  #_children = {}
+  #_attributes = {}
+
+  def __init__(self, name=None, email=None, uri=None, 
+      extension_elements=None, extension_attributes=None, text=None):
+    """Constructor for Author
+    
+    Args:
+      name: Name
+      email: Email
+      uri: Uri
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+      text: str The text data in the this element
+    """
+    
+    self.name = name
+    self.email = email
+    self.uri = uri
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+    self.text = text
+
+    
+def AuthorFromString(xml_string):
+  return CreateClassFromXMLString(Author, xml_string)
+  
+
+class Contributor(Person):
+  """The atom:contributor element"""
+
+  _tag = 'contributor'
+  _namespace = ATOM_NAMESPACE
+  _children = Person._children.copy()
+  _attributes = Person._attributes.copy()
+
+  def __init__(self, name=None, email=None, uri=None,
+      extension_elements=None, extension_attributes=None, text=None):
+    """Constructor for Contributor
+    
+    Args:
+      name: Name
+      email: Email
+      uri: Uri
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+      text: str The text data in the this element
+    """
+
+    self.name = name
+    self.email = email
+    self.uri = uri
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+    self.text = text
+
+
+def ContributorFromString(xml_string):
+  return CreateClassFromXMLString(Contributor, xml_string)
+  
+  
+class Link(AtomBase):
+  """The atom:link element"""
+
+  _tag = 'link'
+  _namespace = ATOM_NAMESPACE
+  _children = AtomBase._children.copy()
+  _attributes = AtomBase._attributes.copy()
+  _attributes['rel'] = 'rel'
+  _attributes['href'] = 'href'
+  _attributes['type'] = 'type'
+  _attributes['title'] = 'title'
+  _attributes['length'] = 'length'
+  _attributes['hreflang'] = 'hreflang'
+  
+  def __init__(self, href=None, rel=None, link_type=None, hreflang=None, 
+      title=None, length=None, text=None, extension_elements=None, 
+      extension_attributes=None):
+    """Constructor for Link
+    
+    Args:
+      href: string The href attribute of the link
+      rel: string
+      type: string
+      hreflang: string The language for the href
+      title: string
+      length: string The length of the href's destination
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+      text: str The text data in the this element
+    """
+
+    self.href = href
+    self.rel = rel
+    self.type = link_type
+    self.hreflang = hreflang
+    self.title = title
+    self.length = length
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def LinkFromString(xml_string):
+  return CreateClassFromXMLString(Link, xml_string)
+  
+
+class Generator(AtomBase):
+  """The atom:generator element"""
+
+  _tag = 'generator'
+  _namespace = ATOM_NAMESPACE
+  _children = AtomBase._children.copy()
+  _attributes = AtomBase._attributes.copy()
+  _attributes['uri'] = 'uri'
+  _attributes['version'] = 'version'
+
+  def __init__(self, uri=None, version=None, text=None, 
+      extension_elements=None, extension_attributes=None):
+    """Constructor for Generator
+    
+    Args:
+      uri: string
+      version: string
+      text: str The text data in the this element
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+    """
+
+    self.uri = uri
+    self.version = version
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+def GeneratorFromString(xml_string):
+  return CreateClassFromXMLString(Generator, xml_string)
+
+
+class Text(AtomBase):
+  """A foundation class from which atom:title, summary, etc. extend.
+  
+  This class should never be instantiated.
+  """
+
+  _children = AtomBase._children.copy()
+  _attributes = AtomBase._attributes.copy()
+  _attributes['type'] = 'type'
+
+  def __init__(self, text_type=None, text=None, extension_elements=None,
+      extension_attributes=None):
+    """Constructor for Text
+    
+    Args:
+      text_type: string
+      text: str The text data in the this element
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+    """
+    
+    self.type = text_type
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+class Title(Text):
+  """The atom:title element"""
+
+  _tag = 'title'
+  _namespace = ATOM_NAMESPACE
+  _children = Text._children.copy()
+  _attributes = Text._attributes.copy()
+
+  def __init__(self, title_type=None, text=None, extension_elements=None,
+      extension_attributes=None):
+    """Constructor for Title
+    
+    Args:
+      title_type: string
+      text: str The text data in the this element
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+    """
+
+    self.type = title_type
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def TitleFromString(xml_string):
+  return CreateClassFromXMLString(Title, xml_string)
+
+
+class Subtitle(Text):
+  """The atom:subtitle element"""
+
+  _tag = 'subtitle'
+  _namespace = ATOM_NAMESPACE
+  _children = Text._children.copy()
+  _attributes = Text._attributes.copy()
+
+  def __init__(self, subtitle_type=None, text=None, extension_elements=None,
+      extension_attributes=None):
+    """Constructor for Subtitle
+    
+    Args:
+      subtitle_type: string
+      text: str The text data in the this element
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+    """
+
+    self.type = subtitle_type
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def SubtitleFromString(xml_string):
+  return CreateClassFromXMLString(Subtitle, xml_string)
+
+
+class Rights(Text):
+  """The atom:rights element"""
+
+  _tag = 'rights'
+  _namespace = ATOM_NAMESPACE
+  _children = Text._children.copy()
+  _attributes = Text._attributes.copy()
+
+  def __init__(self, rights_type=None, text=None, extension_elements=None,
+      extension_attributes=None):
+    """Constructor for Rights
+    
+    Args:
+      rights_type: string
+      text: str The text data in the this element
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+    """
+
+    self.type = rights_type
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def RightsFromString(xml_string):
+  return CreateClassFromXMLString(Rights, xml_string)
+
+
+class Summary(Text):
+  """The atom:summary element"""
+
+  _tag = 'summary'
+  _namespace = ATOM_NAMESPACE
+  _children = Text._children.copy()
+  _attributes = Text._attributes.copy()
+
+  def __init__(self, summary_type=None, text=None, extension_elements=None,
+      extension_attributes=None):
+    """Constructor for Summary
+    
+    Args:
+      summary_type: string
+      text: str The text data in the this element
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+    """
+
+    self.type = summary_type
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def SummaryFromString(xml_string):
+  return CreateClassFromXMLString(Summary, xml_string)
+
+
+class Content(Text):
+  """The atom:content element"""
+
+  _tag = 'content'
+  _namespace = ATOM_NAMESPACE
+  _children = Text._children.copy()
+  _attributes = Text._attributes.copy()
+  _attributes['src'] = 'src'
+
+  def __init__(self, content_type=None, src=None, text=None, extension_elements=None,
+      extension_attributes=None):
+    """Constructor for Content
+    
+    Args:
+      content_type: string
+      src: string
+      text: str The text data in the this element
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+    """
+    
+    self.type = content_type
+    self.src = src
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+def ContentFromString(xml_string):
+  return CreateClassFromXMLString(Content, xml_string)
+
+
+class Category(AtomBase):
+  """The atom:category element"""
+
+  _tag = 'category'
+  _namespace = ATOM_NAMESPACE
+  _children = AtomBase._children.copy()
+  _attributes = AtomBase._attributes.copy()
+  _attributes['term'] = 'term'
+  _attributes['scheme'] = 'scheme'
+  _attributes['label'] = 'label'
+
+  def __init__(self, term=None, scheme=None, label=None, text=None, 
+      extension_elements=None, extension_attributes=None):
+    """Constructor for Category
+    
+    Args:
+      term: str
+      scheme: str
+      label: str
+      text: str The text data in the this element
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+    """
+    
+    self.term = term
+    self.scheme = scheme
+    self.label = label
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def CategoryFromString(xml_string):
+  return CreateClassFromXMLString(Category, xml_string)
+
+
+class Id(AtomBase):
+  """The atom:id element."""
+
+  _tag = 'id'
+  _namespace = ATOM_NAMESPACE
+  _children = AtomBase._children.copy()
+  _attributes = AtomBase._attributes.copy()
+
+  def __init__(self, text=None, extension_elements=None,
+      extension_attributes=None):
+    """Constructor for Id
+    
+    Args:
+      text: str The text data in the this element
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+    """
+
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def IdFromString(xml_string):
+  return CreateClassFromXMLString(Id, xml_string)
+
+
+class Icon(AtomBase):
+  """The atom:icon element."""
+  
+  _tag = 'icon'
+  _namespace = ATOM_NAMESPACE
+  _children = AtomBase._children.copy()
+  _attributes = AtomBase._attributes.copy()
+
+  def __init__(self, text=None, extension_elements=None,
+      extension_attributes=None):
+    """Constructor for Icon
+    
+    Args:
+      text: str The text data in the this element
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+    """
+
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def IconFromString(xml_string):
+  return CreateClassFromXMLString(Icon, xml_string)
+
+
+class Logo(AtomBase):
+  """The atom:logo element."""
+
+  _tag = 'logo'
+  _namespace = ATOM_NAMESPACE
+  _children = AtomBase._children.copy()
+  _attributes = AtomBase._attributes.copy()
+
+  def __init__(self, text=None, extension_elements=None,
+      extension_attributes=None):
+    """Constructor for Logo
+    
+    Args:
+      text: str The text data in the this element
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+    """
+    
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def LogoFromString(xml_string):
+  return CreateClassFromXMLString(Logo, xml_string)
+
+
+class Draft(AtomBase):
+  """The app:draft element which indicates if this entry should be public."""
+
+  _tag = 'draft'
+  _namespace = APP_NAMESPACE
+  _children = AtomBase._children.copy()
+  _attributes = AtomBase._attributes.copy()
+
+  def __init__(self, text=None, extension_elements=None,
+      extension_attributes=None):
+    """Constructor for app:draft
+
+    Args:
+      text: str The text data in the this element
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+    """
+
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def DraftFromString(xml_string):
+  return CreateClassFromXMLString(Draft, xml_string)
+
+
+class Control(AtomBase):
+  """The app:control element indicating restrictions on publication.
+  
+  The APP control element may contain a draft element indicating whether or
+  not this entry should be publicly available.
+  """
+
+  _tag = 'control'
+  _namespace = APP_NAMESPACE
+  _children = AtomBase._children.copy()
+  _attributes = AtomBase._attributes.copy()
+  _children['{%s}draft' % APP_NAMESPACE] = ('draft', Draft)
+
+  def __init__(self, draft=None, text=None, extension_elements=None,
+        extension_attributes=None):
+    """Constructor for app:control"""
+    
+    self.draft = draft
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def ControlFromString(xml_string):
+  return CreateClassFromXMLString(Control, xml_string)
+
+
+class Date(AtomBase):
+  """A parent class for atom:updated, published, etc."""
+
+  #TODO Add text to and from time conversion methods to allow users to set
+  # the contents of a Date to a python DateTime object.
+
+  _children = AtomBase._children.copy()
+  _attributes = 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 Updated(Date):
+  """The atom:updated element."""
+
+  _tag = 'updated'
+  _namespace = ATOM_NAMESPACE
+  _children = Date._children.copy()
+  _attributes = Date._attributes.copy()
+
+  def __init__(self, text=None, extension_elements=None,
+      extension_attributes=None):
+    """Constructor for Updated
+    
+    Args:
+      text: str The text data in the this element
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+    """
+
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def UpdatedFromString(xml_string):
+  return CreateClassFromXMLString(Updated, xml_string)
+
+
+class Published(Date):
+  """The atom:published element."""
+
+  _tag = 'published'
+  _namespace = ATOM_NAMESPACE
+  _children = Date._children.copy()
+  _attributes = Date._attributes.copy()
+
+  def __init__(self, text=None, extension_elements=None,
+      extension_attributes=None):
+    """Constructor for Published
+    
+    Args:
+      text: str The text data in the this element
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+    """
+
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def PublishedFromString(xml_string):
+  return CreateClassFromXMLString(Published, xml_string)
+
+
+class LinkFinder(object):
+  """An "interface" providing methods to find link elements
+
+  Entry elements often contain multiple links which differ in the rel
+  attribute or content type. Often, developers are interested in a specific
+  type of link so this class provides methods to find specific classes of
+  links.
+
+  This class is used as a mixin in Atom entries and feeds.
+  """
+
+  def GetSelfLink(self):
+    """Find the first link with rel set to 'self'
+
+    Returns:
+      An atom.Link or none if none of the links had rel equal to 'self'
+    """
+
+    for a_link in self.link:
+      if a_link.rel == 'self':
+        return a_link
+    return None
+
+  def GetEditLink(self):
+    for a_link in self.link:
+      if a_link.rel == 'edit':
+        return a_link
+    return None
+
+  def GetNextLink(self):
+    for a_link in self.link:
+      if a_link.rel == 'next':
+        return a_link
+    return None
+
+  def GetLicenseLink(self):
+    for a_link in self.link:
+      if a_link.rel == 'license':
+        return a_link
+    return None
+
+  def GetAlternateLink(self):
+    for a_link in self.link:
+      if a_link.rel == 'alternate':
+        return a_link
+    return None
+    
+ 
+class FeedEntryParent(AtomBase, LinkFinder):
+  """A super class for atom:feed and entry, contains shared attributes"""
+
+  _children = AtomBase._children.copy()
+  _attributes = AtomBase._attributes.copy()
+  _children['{%s}author' % ATOM_NAMESPACE] = ('author', [Author])
+  _children['{%s}category' % ATOM_NAMESPACE] = ('category', [Category])
+  _children['{%s}contributor' % ATOM_NAMESPACE] = ('contributor', [Contributor])
+  _children['{%s}id' % ATOM_NAMESPACE] = ('id', Id)
+  _children['{%s}link' % ATOM_NAMESPACE] = ('link', [Link])
+  _children['{%s}rights' % ATOM_NAMESPACE] = ('rights', Rights)
+  _children['{%s}title' % ATOM_NAMESPACE] = ('title', Title)
+  _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):
+    self.author = author or []
+    self.category = category or []
+    self.contributor = contributor or []
+    self.id = atom_id
+    self.link = link or []
+    self.rights = rights
+    self.title = title
+    self.updated = updated
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+class Source(FeedEntryParent):
+  """The atom:source element"""
+
+  _tag = 'source'
+  _namespace = ATOM_NAMESPACE
+  _children = FeedEntryParent._children.copy()
+  _attributes = FeedEntryParent._attributes.copy()
+  _children['{%s}generator' % ATOM_NAMESPACE] = ('generator', Generator)
+  _children['{%s}icon' % ATOM_NAMESPACE] = ('icon', Icon)
+  _children['{%s}logo' % ATOM_NAMESPACE] = ('logo', Logo)
+  _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,
+      extension_elements=None, extension_attributes=None):
+    """Constructor for Source
+
+    Args:
+      author: list (optional) A list of Author instances which belong to this
+          class.
+      category: list (optional) A list of Category instances
+      contributor: list (optional) A list on Contributor instances
+      generator: Generator (optional)
+      icon: Icon (optional)
+      id: Id (optional) The entry's Id element
+      link: list (optional) A list of Link instances
+      logo: Logo (optional)
+      rights: Rights (optional) The entry's Rights element
+      subtitle: Subtitle (optional) The entry's subtitle element
+      title: Title (optional) the entry's title element
+      updated: Updated (optional) the entry's updated element
+      text: String (optional) The text contents of the element. This is the
+          contents of the Entry's XML text node.
+          (Example: <foo>This is the text</foo>)
+      extension_elements: list (optional) A list of ExtensionElement instances
+          which are children of this element.
+      extension_attributes: dict (optional) A dictionary of strings which are
+          the values for additional XML attributes of this element.
+    """
+
+    self.author = author or []
+    self.category = category or []
+    self.contributor = contributor or []
+    self.generator = generator
+    self.icon = icon
+    self.id = atom_id
+    self.link = link or []
+    self.logo = logo
+    self.rights = rights
+    self.subtitle = subtitle
+    self.title = title
+    self.updated = updated
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def SourceFromString(xml_string):
+  return CreateClassFromXMLString(Source, xml_string)
+
+
+class Entry(FeedEntryParent):
+  """The atom:entry element"""
+
+  _tag = 'entry'
+  _namespace = ATOM_NAMESPACE
+  _children = FeedEntryParent._children.copy()
+  _attributes = FeedEntryParent._attributes.copy()
+  _children['{%s}content' % ATOM_NAMESPACE] = ('content', Content)
+  _children['{%s}published' % ATOM_NAMESPACE] = ('published', Published)
+  _children['{%s}source' % ATOM_NAMESPACE] = ('source', Source)
+  _children['{%s}summary' % ATOM_NAMESPACE] = ('summary', Summary)
+  _children['{%s}control' % APP_NAMESPACE] = ('control', Control)
+
+  def __init__(self, author=None, category=None, content=None, 
+      contributor=None, atom_id=None, link=None, published=None, rights=None,
+      source=None, summary=None, control=None, title=None, updated=None,
+      extension_elements=None, extension_attributes=None, text=None):
+    """Constructor for atom:entry
+
+    Args:
+      author: list A list of Author instances which belong to this class.
+      category: list A list of Category instances
+      content: Content The entry's Content
+      contributor: list A list on Contributor instances
+      id: Id The entry's Id element
+      link: list A list of Link instances
+      published: Published The entry's Published element
+      rights: Rights The entry's Rights element
+      source: Source the entry's source element
+      summary: Summary the entry's summary element
+      title: Title the entry's title element
+      updated: Updated the entry's updated element
+      control: The entry's app:control element which can be used to mark an 
+          entry as a draft which should not be publicly viewable.
+      text: String The text contents of the element. This is the contents
+          of the Entry's XML text node. (Example: <foo>This is the text</foo>)
+      extension_elements: list A list of ExtensionElement instances which are
+          children of this element.
+      extension_attributes: dict A dictionary of strings which are the values
+          for additional XML attributes of this element.
+    """
+
+    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.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def EntryFromString(xml_string):
+  return CreateClassFromXMLString(Entry, xml_string)
+
+
+class Feed(Source):
+  """The atom:feed element"""
+
+  _tag = 'feed'
+  _namespace = ATOM_NAMESPACE
+  _children = Source._children.copy()
+  _attributes = Source._attributes.copy()
+  _children['{%s}entry' % ATOM_NAMESPACE] = ('entry', [Entry])
+
+  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, 
+      text=None, extension_elements=None, extension_attributes=None):
+    """Constructor for Source
+    
+    Args:
+      author: list (optional) A list of Author instances which belong to this
+          class.
+      category: list (optional) A list of Category instances
+      contributor: list (optional) A list on Contributor instances
+      generator: Generator (optional) 
+      icon: Icon (optional) 
+      id: Id (optional) The entry's Id element
+      link: list (optional) A list of Link instances
+      logo: Logo (optional) 
+      rights: Rights (optional) The entry's Rights element
+      subtitle: Subtitle (optional) The entry's subtitle element
+      title: Title (optional) the entry's title element
+      updated: Updated (optional) the entry's updated element
+      entry: list (optional) A list of the Entry instances contained in the 
+          feed.
+      text: String (optional) The text contents of the element. This is the 
+          contents of the Entry's XML text node. 
+          (Example: <foo>This is the text</foo>)
+      extension_elements: list (optional) A list of ExtensionElement instances
+          which are children of this element.
+      extension_attributes: dict (optional) A dictionary of strings which are 
+          the values for additional XML attributes of this element.
+    """
+
+    self.author = author or []
+    self.category = category or []
+    self.contributor = contributor or []
+    self.generator = generator
+    self.icon = icon
+    self.id = atom_id
+    self.link = link or []
+    self.logo = logo
+    self.rights = rights
+    self.subtitle = subtitle
+    self.title = title
+    self.updated = updated
+    self.entry = entry or []
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def FeedFromString(xml_string):
+  return CreateClassFromXMLString(Feed, xml_string)
+  
+  
+class ExtensionElement(object):
+  """Represents extra XML elements contained in Atom classes."""
+  
+  def __init__(self, tag, namespace=None, attributes=None, 
+      children=None, text=None):
+    """Constructor for EtensionElement
+
+    Args:
+      namespace: string (optional) The XML namespace for this element.
+      tag: string (optional) The tag (without the namespace qualifier) for
+          this element. To reconstruct the full qualified name of the element,
+          combine this tag with the namespace.
+      attributes: dict (optinal) The attribute value string pairs for the XML 
+          attributes of this element.
+      children: list (optional) A list of ExtensionElements which represent 
+          the XML child nodes of this element.
+    """
+
+    self.namespace = namespace
+    self.tag = tag
+    self.attributes = attributes or {}
+    self.children = children or []
+    self.text = text
+    
+  def ToString(self):
+    element_tree = self._TransferToElementTree(ElementTree.Element(''))
+    return ElementTree.tostring(element_tree, encoding="UTF-8")
+    
+  def _TransferToElementTree(self, element_tree):
+    if self.tag is None:
+      return None
+      
+    if self.namespace is not None:
+      element_tree.tag = '{%s}%s' % (self.namespace, self.tag)
+    else:
+      element_tree.tag = self.tag
+      
+    for key, value in self.attributes.iteritems():
+      element_tree.attrib[key] = value
+      
+    for child in self.children:
+      child._BecomeChildElement(element_tree)
+      
+    element_tree.text = self.text
+      
+    return element_tree
+
+  def _BecomeChildElement(self, element_tree):
+    """Converts this object into an etree element and adds it as a child node.
+
+    Adds self to the ElementTree. This method is required to avoid verbose XML
+    which constantly redefines the namespace.
+
+    Args:
+      element_tree: ElementTree._Element The element to which this object's XML
+          will be added.
+    """
+    new_element = ElementTree.Element('')
+    element_tree.append(new_element)
+    self._TransferToElementTree(new_element)
+
+  def FindChildren(self, tag=None, namespace=None):
+    """Searches child nodes for objects with the desired tag/namespace.
+
+    Returns a list of extension elements within this object whose tag
+    and/or namespace match those passed in. To find all children in
+    a particular namespace, specify the namespace but not the tag name.
+    If you specify only the tag, the result list may contain extension
+    elements in multiple namespaces.
+
+    Args:
+      tag: str (optional) The desired tag
+      namespace: str (optional) The desired namespace
+
+    Returns:
+      A list of elements whose tag and/or namespace match the parameters
+      values
+    """
+
+    results = []
+
+    if tag and namespace:
+      for element in self.children:
+        if element.tag == tag and element.namespace == namespace:
+          results.append(element)
+    elif tag and not namespace:
+      for element in self.children:
+        if element.tag == tag:
+          results.append(element)
+    elif namespace and not tag:
+      for element in self.children:
+        if element.namespace == namespace:
+          results.append(element)
+    else:
+      for element in self.children:
+        results.append(element)
+
+    return results
+ 
+    
+def ExtensionElementFromString(xml_string):
+  element_tree = ElementTree.fromstring(xml_string)
+  return _ExtensionElementFromElementTree(element_tree)
+
+
+def _ExtensionElementFromElementTree(element_tree):
+  element_tag = element_tree.tag
+  if '}' in element_tag:
+    namespace = element_tag[1:element_tag.index('}')]
+    tag = element_tag[element_tag.index('}')+1:]
+  else: 
+    namespace = None
+    tag = element_tag
+  extension = ExtensionElement(namespace=namespace, tag=tag)
+  for key, value in element_tree.attrib.iteritems():
+    extension.attributes[key] = value
+  for child in element_tree:
+    extension.children.append(_ExtensionElementFromElementTree(child))
+  extension.text = element_tree.text
+  return extension

Added: trunk/conduit/modules/GoogleModule/libgdata/atom/service.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/atom/service.py	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,428 @@
+#!/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.
+
+
+"""AtomService provides CRUD ops. in line with the Atom Publishing Protocol.
+
+  AtomService: Encapsulates the ability to perform insert, update and delete
+               operations with the Atom Publishing Protocol on which GData is
+               based. An instance can perform query, insertion, deletion, and
+               update.
+"""
+
+__author__ = 'api.jscudder (Jeffrey Scudder)'
+
+import os
+import httplib
+import urllib
+import re
+import base64
+import socket
+try:
+  from xml.etree import cElementTree as ElementTree
+except ImportError:
+  try:
+    import cElementTree as ElementTree
+  except ImportError:
+    from elementtree import ElementTree
+
+URL_REGEX = re.compile('http(s)?\://([\w\.-]*)(\:(\d+))?(/.*)?')
+
+class AtomService(object):
+  """Performs Atom Publishing Protocol CRUD operations.
+  
+  The AtomService contains methods to perform HTTP CRUD operations. 
+  """
+
+  # Default values for members
+  port = 80
+  ssl = False
+  # If debug is True, the HTTPConnection will display debug information
+  debug = False
+
+  def __init__(self, server=None, additional_headers=None):
+    """Creates a new AtomService client.
+    
+    Args:
+      server: string (optional) The start of a URL for the server
+              to which all operations should be directed. Example: 
+              'www.google.com'
+      additional_headers: dict (optional) Any additional HTTP headers which
+                          should be included with CRUD operations.
+    """
+
+    self.server = server
+    self.additional_headers = additional_headers or {}
+
+    self.additional_headers['User-Agent'] = 'Python Google Data Client Lib'
+
+  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)
+
+  def UseBasicAuth(self, 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.
+
+    Args:
+      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'
+    self.additional_headers[header_name] = 'Basic %s' % (base_64_string,)
+
+  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
+
+      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) = 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
+
+    return (connection, full_uri)
+ 
+  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
+
+    The uri is the portion of the URI after the server value 
+    (server example: 'www.google.com').
+
+    Example use:
+    To perform a query against Google Base, set the server to 
+    'base.google.com' and set the uri to '/base/feeds/...', where ... is 
+    your query. For example, to find snippets for all digital cameras uri 
+    should be set to: '/base/feeds/snippets?bq=digital+camera'
+
+    Args:
+      uri: string The query in the form of a URI. Example:
+           '/base/feeds/snippets?bq=digital+camera'.
+      extra_headers: dicty (optional) Extra HTTP headers to be included
+                     in the GET request. These headers are in addition to 
+                     those stored in the client's additional_headers property.
+                     The client automatically sets the Content-Type and 
+                     Authorization headers.
+      url_params: dict (optional) Additional URL parameters to be included
+                  in the query. 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.
+
+    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()
+
+  def Post(self, data, uri, extra_headers=None, url_params=None, 
+           escape_params=True, content_type='application/atom+xml'):
+    """Insert data into an APP server at the given URI.
+
+    Args:
+      data: string, ElementTree._Element, or something with a __str__ method 
+            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.
+
+    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()
+
+  def Put(self, data, uri, extra_headers=None, url_params=None, 
+           escape_params=True, content_type='application/atom+xml'):
+    """Updates an entry at the given URI.
+     
+    Args:
+      data: string, ElementTree._Element, or xml_wrapper.ElementWrapper The 
+            XML containing the updated data.
+      uri: string A URI indicating entry to which the update will be applied.
+           Example: '/base/feeds/items/ITEM-ID'
+      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.
+  
+    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()
+
+  def Delete(self, uri, extra_headers=None, url_params=None, 
+             escape_params=True):
+    """Deletes the entry at the given URI.
+
+    Args:
+      uri: string The URI of the entry to be deleted. Example: 
+           '/base/feeds/items/ITEM-ID'
+      extra_headers: dict (optional) HTTP headers which are to be included.
+                     The client automatically sets the Content-Type and
+                     Authorization 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.
+
+    Returns:
+      httplib.HTTPResponse Server's response to the DELETE request.
+    """
+    delete_connection = self._CreateConnection(uri, 'DELETE', extra_headers,
+        url_params, escape_params)
+
+    return delete_connection.getresponse()
+
+def DictionaryToParamList(url_parameters, escape_params=True):
+  """Convert a dictionary of URL arguments into a URL parameter string.
+
+  Args:
+    url_parameters: The dictionaty of key-value pairs which will be converted
+                    into URL parameters. For example,
+                    {'dry-run': 'true', 'foo': 'bar'}
+                    will become ['dry-run=true', 'foo=bar'].
+
+  Returns:
+    A list which contains a string for each key-value pair. The strings are
+    ready to be incorporated into a URL by using '&'.join([] + parameter_list)
+  """
+  # Choose which function to use when modifying the query and parameters.
+  # Use quote_plus when escape_params is true.
+  transform_op = [str, urllib.quote_plus][bool(escape_params)]
+  # Create a list of tuples containing the escaped version of the
+  # parameter-value pairs.
+  parameter_tuples = [(transform_op(param), transform_op(value))
+                     for param, value in (url_parameters or {}).items()]
+  # Turn parameter-value tuples into a list of strings in the form
+  # 'PARAMETER=VALUE'.
+
+  return ['='.join(x) for x in parameter_tuples]
+
+
+def BuildUri(uri, url_params=None, escape_params=True):
+  """Converts a uri string and a collection of parameters into a URI.
+
+  Args:
+    uri: string
+    url_params: dict (optional)
+    escape_params: boolean (optional)
+    uri: string The start of the desired URI. This string can alrady contain
+         URL parameters. Examples: '/base/feeds/snippets', 
+         '/base/feeds/snippets?bq=digital+camera'
+    url_parameters: dict (optional) Additional URL parameters to be included
+                    in the query. 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.
+
+  Returns:
+    string The URI consisting of the escaped URL parameters appended to the
+    initial uri string.
+  """
+  # Prepare URL parameters for inclusion into the GET request.
+  parameter_list = DictionaryToParamList(url_params, escape_params)
+
+  # Append the URL parameters to the URL.
+  if parameter_list:
+    if uri.find('?') != -1:
+      # If there are already URL parameters in the uri string, add the
+      # parameters after a new & character.
+      full_uri = '&'.join([uri] + parameter_list)
+    else:
+      # The uri string did not have any URL parameters (no ? character)
+      # so put a ? between the uri and URL parameters.
+      full_uri = '%s%s' % (uri, '?%s' % ('&'.join([] + parameter_list)))  
+  else:
+    full_uri = uri
+        
+  return full_uri

Added: trunk/conduit/modules/GoogleModule/libgdata/gdata/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/gdata/Makefile.am	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,7 @@
+SUBDIRS = calendar exif geo media photos
+
+conduit_handlersdir = $(libdir)/conduit/modules/GoogleModule/libgdata/gdata
+conduit_handlers_PYTHON = __init__.py auth.py service.py
+
+clean-local:
+	rm -rf *.pyc *.pyo

Added: trunk/conduit/modules/GoogleModule/libgdata/gdata/__init__.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/gdata/__init__.py	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,751 @@
+#!/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 classes representing Google Data elements.
+
+  Extends Atom classes to add Google Data specific elements.
+"""
+
+
+__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
+
+
+# XML namespaces which are often used in GData entities.
+GDATA_NAMESPACE = 'http://schemas.google.com/g/2005'
+GDATA_TEMPLATE = '{http://schemas.google.com/g/2005}%s'
+OPENSEARCH_NAMESPACE = 'http://a9.com/-/spec/opensearchrss/1.0/'
+OPENSEARCH_TEMPLATE = '{http://a9.com/-/spec/opensearchrss/1.0/}%s'
+BATCH_NAMESPACE = 'http://schemas.google.com/gdata/batch'
+GACL_NAMESPACE = 'http://schemas.google.com/acl/2007'
+GACL_TEMPLATE = '{http://schemas.google.com/acl/2007}%s'
+
+
+# Labels used in batch request entries to specify the desired CRUD operation.
+BATCH_INSERT = 'insert'
+BATCH_UPDATE = 'update'
+BATCH_DELETE = 'delete'
+BATCH_QUERY = 'query'
+
+class Error(Exception):
+  pass
+
+
+class MissingRequiredParameters(Error):
+  pass
+
+
+class MediaSource(object):
+  """GData Entries can refer to media sources, so this class provides a
+  place to store references to these objects along with some metadata.
+  """
+  
+  def __init__(self, file_handle=None, content_type=None, content_length=None,
+      file_path=None, file_name=None):
+    """Creates an object of type MediaSource.
+
+    Args:
+      file_handle: A file handle pointing to the file to be encapsulated in the
+                   MediaSource
+      content_type: string The MIME type of the file. Required if a file_handle
+                    is given.
+      content_length: int The size of the file. Required if a file_handle is
+                      given.
+      file_path: string (optional) A full path name to the file. Used in
+                    place of a file_handle.
+      file_name: string The name of the file without any path information.
+                 Required if a file_handle is given.
+    """
+    self.file_handle = file_handle
+    self.content_type = content_type
+    self.content_length = content_length
+    self.file_name = file_name
+
+    if (file_handle is None and content_type is not None and
+        file_path is not None):
+
+        self.setFile(file_path, content_type)
+
+  def setFile(self, file_name, content_type):
+    """A helper function which can create a file handle from a given filename
+    and set the content type and length all at once.
+
+    Args:
+      file_name: string The path and file name to the file containing the media
+      content_type: string A MIME type representing the type of the media
+    """
+
+    self.file_handle = open(file_name, 'rb')
+    self.content_type = content_type
+    self.content_length = os.path.getsize(file_name)
+    self.file_name = os.path.basename(file_name)
+  
+
+class LinkFinder(atom.LinkFinder):
+  """An "interface" providing methods to find link elements
+
+  GData Entry elements often contain multiple links which differ in the rel
+  attribute or content type. Often, developers are interested in a specific
+  type of link so this class provides methods to find specific classes of
+  links.
+
+  This class is used as a mixin in GData entries.
+  """
+
+  def GetSelfLink(self):
+    """Find the first link with rel set to 'self'
+
+    Returns:
+      An atom.Link or none if none of the links had rel equal to 'self'
+    """
+
+    for a_link in self.link:
+      if a_link.rel == 'self':
+        return a_link
+    return None
+
+  def GetEditLink(self):
+    for a_link in self.link:
+      if a_link.rel == 'edit':
+        return a_link
+    return None
+    
+  def GetEditMediaLink(self):
+    """The Picasa API mistakenly returns media-edit rather than edit-media, but
+    this may change soon.
+    """
+    for a_link in self.link:
+      if a_link.rel == 'edit-media':
+        return a_link
+      if a_link.rel == 'media-edit':
+        return a_link
+    return None
+
+  def GetHtmlLink(self):
+    """Find the first link with rel of alternate and type of text/html
+
+    Returns:
+      An atom.Link or None if no links matched
+    """
+    for a_link in self.link:
+      if a_link.rel == 'alternate' and a_link.type == 'text/html':
+        return a_link
+    return None
+
+  def GetPostLink(self):
+    """Get a link containing the POST target URL.
+    
+    The POST target URL is used to insert new entries.
+
+    Returns:
+      A link object with a rel matching the POST type.
+    """
+    for a_link in self.link:
+      if a_link.rel == 'http://schemas.google.com/g/2005#post':
+        return a_link
+    return None
+
+  def GetAclLink(self):
+    for a_link in self.link:
+      if a_link.rel == 'http://schemas.google.com/acl/2007#accessControlList':
+        return a_link
+    return None
+
+  def GetFeedLink(self):
+    for a_link in self.link:
+      if a_link.rel == 'http://schemas.google.com/g/2005#feed':
+        return a_link
+    return None
+
+  def GetNextLink(self):
+    for a_link in self.link:
+      if a_link.rel == 'next':
+        return a_link
+    return None
+
+
+class TotalResults(atom.AtomBase):
+  """opensearch:TotalResults for a GData feed"""
+  
+  _tag = 'totalResults'
+  _namespace = OPENSEARCH_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  def __init__(self, extension_elements=None,
+     extension_attributes=None, text=None):
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def TotalResultsFromString(xml_string):
+  return atom.CreateClassFromXMLString(TotalResults, xml_string)
+
+  
+class StartIndex(atom.AtomBase):
+  """The opensearch:startIndex element in GData feed"""
+
+  _tag = 'startIndex'
+  _namespace = OPENSEARCH_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  def __init__(self, extension_elements=None, 
+      extension_attributes=None, text=None):
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def StartIndexFromString(xml_string):
+  return atom.CreateClassFromXMLString(StartIndex, xml_string)
+
+
+class ItemsPerPage(atom.AtomBase):
+  """The opensearch:itemsPerPage element in GData feed"""
+
+  _tag = 'itemsPerPage'
+  _namespace = OPENSEARCH_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  def __init__(self, extension_elements=None,
+      extension_attributes=None, text=None):
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def ItemsPerPageFromString(xml_string):
+  return atom.CreateClassFromXMLString(ItemsPerPage, xml_string)
+
+
+class GDataEntry(atom.Entry, LinkFinder):
+  """Extends Atom Entry to provide data processing"""
+
+  _tag = atom.Entry._tag
+  _namespace = atom.Entry._namespace
+  _children = atom.Entry._children.copy()
+  _attributes = atom.Entry._attributes.copy()
+
+  def __GetId(self):
+    return self.__id
+
+  # This method was created to strip the unwanted whitespace from the id's 
+  # text node.
+  def __SetId(self, id):
+    self.__id = id
+    if id is not None:
+      self.__id.text = id.text.strip()
+
+  id = property(__GetId, __SetId)
+  
+  def IsMedia(self):
+    """Determines whether or not an entry is a GData Media entry.
+    """
+    if (self.GetEditMediaLink()):
+      return True
+    else:
+      return False
+  
+  def GetMediaURL(self):
+    """Returns the URL to the media content, if the entry is a media entry.
+    Otherwise returns None.
+    """
+    if not self.IsMedia():
+      return None
+    else:
+      return self.content.src
+
+
+def GDataEntryFromString(xml_string):
+  """Creates a new GDataEntry instance given a string of XML."""
+  return atom.CreateClassFromXMLString(GDataEntry, xml_string)
+
+
+class GDataFeed(atom.Feed, LinkFinder):
+  """A Feed from a GData service"""
+
+  _tag = 'feed'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = atom.Feed._children.copy()
+  _attributes = atom.Feed._attributes.copy()
+  _children['{%s}totalResults' % OPENSEARCH_NAMESPACE] = ('total_results', 
+                                                          TotalResults)
+  _children['{%s}startIndex' % OPENSEARCH_NAMESPACE] = ('start_index', 
+                                                        StartIndex)
+  _children['{%s}itemsPerPage' % OPENSEARCH_NAMESPACE] = ('items_per_page',
+                                                          ItemsPerPage)
+               # Add a conversion rule for atom:entry to make it into a GData
+               # Entry.
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [GDataEntry])
+
+  def __GetId(self):
+    return self.__id
+
+  def __SetId(self, id):
+    self.__id = id
+    if id is not None:
+      self.__id.text = id.text.strip()
+
+  id = property(__GetId, __SetId)
+
+  def __GetGenerator(self):
+    return self.__generator
+
+  def __SetGenerator(self, generator):
+    self.__generator = generator
+    if generator is not None:
+      self.__generator.text = generator.text.strip()
+
+  generator = property(__GetGenerator, __SetGenerator)
+
+  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):
+    """Constructor for Source
+    
+    Args:
+      author: list (optional) A list of Author instances which belong to this
+          class.
+      category: list (optional) A list of Category instances
+      contributor: list (optional) A list on Contributor instances
+      generator: Generator (optional) 
+      icon: Icon (optional) 
+      id: Id (optional) The entry's Id element
+      link: list (optional) A list of Link instances
+      logo: Logo (optional) 
+      rights: Rights (optional) The entry's Rights element
+      subtitle: Subtitle (optional) The entry's subtitle element
+      title: Title (optional) the entry's title element
+      updated: Updated (optional) the entry's updated element
+      entry: list (optional) A list of the Entry instances contained in the 
+          feed.
+      text: String (optional) The text contents of the element. This is the 
+          contents of the Entry's XML text node. 
+          (Example: <foo>This is the text</foo>)
+      extension_elements: list (optional) A list of ExtensionElement instances
+          which are children of this element.
+      extension_attributes: dict (optional) A dictionary of strings which are 
+          the values for additional XML attributes of this element.
+    """
+
+    self.author = author or []
+    self.category = category or []
+    self.contributor = contributor or []
+    self.generator = generator
+    self.icon = icon
+    self.id = atom_id
+    self.link = link or []
+    self.logo = logo
+    self.rights = rights
+    self.subtitle = subtitle
+    self.title = title
+    self.updated = updated
+    self.entry = entry or []
+    self.total_results = total_results
+    self.start_index = start_index
+    self.items_per_page = items_per_page
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def GDataFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(GDataFeed, xml_string)
+
+
+class BatchId(atom.AtomBase):
+  _tag = 'id'
+  _namespace = BATCH_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+
+def BatchIdFromString(xml_string):
+  return atom.CreateClassFromXMLString(BatchId, xml_string)
+
+
+class BatchOperation(atom.AtomBase):
+  _tag = 'operation'
+  _namespace = BATCH_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['type'] = 'type'
+
+  def __init__(self, op_type=None, extension_elements=None, 
+               extension_attributes=None,
+               text=None):
+    self.type = op_type
+    atom.AtomBase.__init__(self, 
+                           extension_elements=extension_elements, 
+                           extension_attributes=extension_attributes, 
+                           text=text)
+
+
+def BatchOperationFromString(xml_string):
+  return atom.CreateClassFromXMLString(BatchOperation, xml_string)
+
+
+class BatchStatus(atom.AtomBase):
+  """The batch:status element present in a batch response entry.
+  
+  A status element contains the code (HTTP response code) and 
+  reason as elements. In a single request these fields would
+  be part of the HTTP response, but in a batch request each
+  Entry operation has a corresponding Entry in the response
+  feed which includes status information.
+
+  See http://code.google.com/apis/gdata/batch.html#Handling_Errors
+  """
+
+  _tag = 'status'
+  _namespace = BATCH_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['code'] = 'code'
+  _attributes['reason'] = 'reason'
+  _attributes['content-type'] = 'content_type'
+
+  def __init__(self, code=None, reason=None, content_type=None, 
+               extension_elements=None, extension_attributes=None, text=None):
+    self.code = code
+    self.reason = reason
+    self.content_type = content_type
+    atom.AtomBase.__init__(self, extension_elements=extension_elements,
+                           extension_attributes=extension_attributes, 
+                           text=text)
+
+
+def BatchStatusFromString(xml_string):
+  return atom.CreateClassFromXMLString(BatchStatus, xml_string)
+
+
+class BatchEntry(GDataEntry):
+  """An atom:entry for use in batch requests.
+
+  The BatchEntry contains additional members to specify the operation to be
+  performed on this entry and a batch ID so that the server can reference
+  individual operations in the response feed. For more information, see:
+  http://code.google.com/apis/gdata/batch.html
+  """
+
+  _tag = GDataEntry._tag
+  _namespace = GDataEntry._namespace
+  _children = GDataEntry._children.copy()
+  _children['{%s}operation' % BATCH_NAMESPACE] = ('batch_operation', BatchOperation) 
+  _children['{%s}id' % BATCH_NAMESPACE] = ('batch_id', BatchId)
+  _children['{%s}status' % BATCH_NAMESPACE] = ('batch_status', BatchStatus)
+  _attributes = 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, control=None, title=None, updated=None,
+      batch_operation=None, batch_id=None, batch_status=None,
+      extension_elements=None, extension_attributes=None, text=None):
+    self.batch_operation = batch_operation
+    self.batch_id = batch_id
+    self.batch_status = batch_status
+    GDataEntry.__init__(self, author=author, category=category, 
+        content=content, contributor=contributor, atom_id=atom_id, link=link, 
+        published=published, rights=rights, source=source, summary=summary, 
+        control=control, title=title, updated=updated, 
+        extension_elements=extension_elements, 
+        extension_attributes=extension_attributes, text=text)
+
+
+def BatchEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(BatchEntry, xml_string)
+
+
+class BatchInterrupted(atom.AtomBase):
+  """The batch:interrupted element sent if batch request was interrupted.
+  
+  Only appears in a feed if some of the batch entries could not be processed.
+  See: http://code.google.com/apis/gdata/batch.html#Handling_Errors
+  """
+
+  _tag = 'interrupted'
+  _namespace = BATCH_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['reason'] = 'reason'
+  _attributes['success'] = 'success'
+  _attributes['failures'] = 'failures'
+  _attributes['parsed'] = 'parsed'
+
+  def __init__(self, reason=None, success=None, failures=None, parsed=None, 
+               extension_elements=None, extension_attributes=None, text=None):
+    self.reason = reason
+    self.success = success
+    self.failures = failures
+    self.parsed = parsed
+    atom.AtomBase.__init__(self, extension_elements=extension_elements,
+                           extension_attributes=extension_attributes, 
+                           text=text)
+
+
+def BatchInterruptedFromString(xml_string):
+  return atom.CreateClassFromXMLString(BatchInterrupted, xml_string)
+
+
+class BatchFeed(GDataFeed):
+  """A feed containing a list of batch request entries."""
+
+  _tag = GDataFeed._tag
+  _namespace = GDataFeed._namespace
+  _children = GDataFeed._children.copy()
+  _attributes = GDataFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [BatchEntry])
+  _children['{%s}interrupted' % BATCH_NAMESPACE] = ('interrupted', BatchInterrupted)
+
+  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,
+      interrupted=None,
+      extension_elements=None, extension_attributes=None, text=None):
+    self.interrupted = interrupted
+    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 AddBatchEntry(self, entry=None, id_url_string=None, 
+                     batch_id_string=None, operation_string=None):
+    """Logic for populating members of a BatchEntry and adding to the feed.
+
+    
+    If the entry is not a BatchEntry, it is converted to a BatchEntry so
+    that the batch specific members will be present. 
+
+    The id_url_string can be used in place of an entry if the batch operation
+    applies to a URL. For example query and delete operations require just
+    the URL of an entry, no body is sent in the HTTP request. If an
+    id_url_string is sent instead of an entry, a BatchEntry is created and
+    added to the feed.
+
+    This method also assigns the desired batch id to the entry so that it 
+    can be referenced in the server's response. If the batch_id_string is
+    None, this method will assign a batch_id to be the index at which this
+    entry will be in the feed's entry list.
+    
+    Args:
+      entry: BatchEntry, atom.Entry, or another Entry flavor (optional) The
+          entry which will be sent to the server as part of the batch request.
+          The item must have a valid atom id so that the server knows which 
+          entry this request references.
+      id_url_string: str (optional) The URL of the entry to be acted on. You
+          can find this URL in the text member of the atom id for an entry.
+          If an entry is not sent, this id will be used to construct a new
+          BatchEntry which will be added to the request feed.
+      batch_id_string: str (optional) The batch ID to be used to reference
+          this batch operation in the results feed. If this parameter is None,
+          the current length of the feed's entry array will be used as a
+          count. Note that batch_ids should either always be specified or
+          never, mixing could potentially result in duplicate batch ids.
+      operation_string: str (optional) The desired batch operation which will
+          set the batch_operation.type member of the entry. Options are
+          'insert', 'update', 'delete', and 'query'
+    
+    Raises:
+      MissingRequiredParameters: Raised if neither an id_ url_string nor an
+          entry are provided in the request.
+
+    Returns:
+      The added entry.
+    """
+    if entry is None and id_url_string is None:
+      raise MissingRequiredParameters('supply either an entry or URL string')
+    if entry is None and id_url_string is not None:
+      entry = BatchEntry(atom_id=atom.Id(text=id_url_string))
+    # TODO: handle cases in which the entry lacks batch_... members.
+    #if not isinstance(entry, BatchEntry):
+      # Convert the entry to a batch entry.
+    if batch_id_string is not None:
+      entry.batch_id = BatchId(text=batch_id_string)
+    elif entry.batch_id is None or entry.batch_id.text is None:
+      entry.batch_id = BatchId(text=str(len(self.entry)))
+    if operation_string is not None:
+      entry.batch_operation = BatchOperation(op_type=operation_string)
+    self.entry.append(entry)
+    return entry
+
+  def AddInsert(self, entry, batch_id_string=None):
+    """Add an insert request to the operations in this batch request feed.
+
+    If the entry doesn't yet have an operation or a batch id, these will
+    be set to the insert operation and a batch_id specified as a parameter.
+
+    Args:
+      entry: BatchEntry The entry which will be sent in the batch feed as an
+          insert request.
+      batch_id_string: str (optional) The batch ID to be used to reference
+          this batch operation in the results feed. If this parameter is None,
+          the current length of the feed's entry array will be used as a
+          count. Note that batch_ids should either always be specified or
+          never, mixing could potentially result in duplicate batch ids.
+    """
+    entry = self.AddBatchEntry(entry=entry, batch_id_string=batch_id_string,
+                               operation_string=BATCH_INSERT)
+  
+  def AddUpdate(self, entry, batch_id_string=None):
+    """Add an update request to the list of batch operations in this feed.
+
+    Sets the operation type of the entry to insert if it is not already set
+    and assigns the desired batch id to the entry so that it can be 
+    referenced in the server's response.
+
+    Args:
+      entry: BatchEntry The entry which will be sent to the server as an
+          update (HTTP PUT) request. The item must have a valid atom id
+          so that the server knows which entry to replace.
+      batch_id_string: str (optional) The batch ID to be used to reference
+          this batch operation in the results feed. If this parameter is None,
+          the current length of the feed's entry array will be used as a
+          count. See also comments for AddInsert.
+    """
+    entry = self.AddBatchEntry(entry=entry, batch_id_string=batch_id_string,
+                               operation_string=BATCH_UPDATE)
+
+  def AddDelete(self, url_string=None, entry=None, batch_id_string=None):
+    """Adds a delete request to the batch request feed.
+
+    This method takes either the url_string which is the atom id of the item
+    to be deleted, or the entry itself. The atom id of the entry must be 
+    present so that the server knows which entry should be deleted.
+
+    Args:
+      url_string: str (optional) The URL of the entry to be deleted. You can
+         find this URL in the text member of the atom id for an entry. 
+      entry: BatchEntry (optional) The entry to be deleted.
+      batch_id_string: str (optional)
+
+    Raises:
+      MissingRequiredParameters: Raised if neither a url_string nor an entry 
+          are provided in the request. 
+    """
+    entry = self.AddBatchEntry(entry=entry, id_url_string=url_string, 
+                               batch_id_string=batch_id_string, 
+                               operation_string=BATCH_DELETE)
+
+  def AddQuery(self, url_string=None, entry=None, batch_id_string=None):
+    """Adds a query request to the batch request feed.
+
+    This method takes either the url_string which is the query URL 
+    whose results will be added to the result feed. The query URL will
+    be encapsulated in a BatchEntry, and you may pass in the BatchEntry
+    with a query URL instead of sending a url_string.
+
+    Args:
+      url_string: str (optional)
+      entry: BatchEntry (optional)
+      batch_id_string: str (optional)
+
+    Raises:
+      MissingRequiredParameters
+    """
+    entry = self.AddBatchEntry(entry=entry, id_url_string=url_string,
+                               batch_id_string=batch_id_string,
+                               operation_string=BATCH_QUERY)
+
+  def GetBatchLink(self):
+    for link in self.link:
+      if link.rel == 'http://schemas.google.com/g/2005#batch':
+        return link
+    return None
+ 
+
+def BatchFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(BatchFeed, xml_string)
+  
+
+class EntryLink(atom.AtomBase):
+  """The gd:entryLink element"""
+
+  _tag = 'entryLink'
+  _namespace = GDATA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  # The entry used to be an atom.Entry, now it is a GDataEntry.
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', GDataEntry)
+  _attributes['rel'] = 'rel',
+  _attributes['readOnly'] = 'read_only'
+  _attributes['href'] = 'href'
+  
+  def __init__(self, href=None, read_only=None, rel=None,
+      entry=None, extension_elements=None, 
+      extension_attributes=None, text=None):
+    self.href = href
+    self.read_only = read_only
+    self.rel = rel
+    self.entry = entry
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+    
+
+def EntryLinkFromString(xml_string):
+  return atom.CreateClassFromXMLString(EntryLink, xml_string)
+
+
+class FeedLink(atom.AtomBase):
+  """The gd:feedLink element"""
+
+  _tag = 'feedLink'
+  _namespace = GDATA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _children['{%s}feed' % atom.ATOM_NAMESPACE] = ('feed', GDataFeed)
+  _attributes['rel'] = 'rel'
+  _attributes['readOnly'] = 'read_only'
+  _attributes['countHint'] = 'count_hint'
+  _attributes['href'] = 'href'
+
+  def __init__(self, count_hint=None, href=None, read_only=None, rel=None,
+      feed=None, extension_elements=None, extension_attributes=None,
+      text=None):
+    self.count_hint = count_hint 
+    self.href = href
+    self.read_only = read_only
+    self.rel = rel
+    self.feed = feed
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+def FeedLinkFromString(xml_string):
+  return atom.CreateClassFromXMLString(EntryLink, xml_string)

Added: trunk/conduit/modules/GoogleModule/libgdata/gdata/auth.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/gdata/auth.py	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,196 @@
+#/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.
+
+
+import re
+import urllib
+
+
+__author__ = 'api.jscudder (Jeffrey Scudder)'
+
+
+AUTH_SUB_KEY_PATTERN = re.compile('.*\?.*token=(.*)(&?)')
+
+
+def GenerateClientLoginRequestBody(email, password, service, source, 
+    account_type='HOSTED_OR_GOOGLE', captcha_token=None, 
+    captcha_response=None):
+  """Creates the body of the autentication request
+
+  See http://code.google.com/apis/accounts/AuthForInstalledApps.html#Request
+  for more details.
+
+  Args:
+    email: str
+    password: str
+    service: str
+    source: str
+    account_type: str (optional) Defaul is 'HOSTED_OR_GOOGLE', other valid
+        values are 'GOOGLE' and 'HOSTED'
+    captcha_token: str (optional)
+    captcha_response: str (optional)
+
+  Returns:
+    The HTTP body to send in a request for a client login token.
+  """
+  # Create a POST body containing the user's credentials.
+  request_fields = {'Email': email,
+                    'Passwd': password,
+                    'accountType': account_type,
+                    'service': service,
+                    'source': source}
+  if captcha_token and captcha_response:
+    # Send the captcha token and response as part of the POST body if the
+    # user is responding to a captch challenge.
+    request_fields['logintoken'] = captcha_token
+    request_fields['logincaptcha'] = captcha_response
+  return urllib.urlencode(request_fields)
+
+
+def GenerateClientLoginAuthToken(http_body):
+  """Returns the token value to use in Authorization headers.
+
+  Reads the token from the server's response to a Client Login request and
+  creates header value to use in requests.
+
+  Args:
+    http_body: str The body of the server's HTTP response to a Client Login
+        request
+ 
+  Returns:
+    The value half of an Authorization header.
+  """
+  for response_line in http_body.splitlines():
+    if response_line.startswith('Auth='):
+      return 'GoogleLogin auth=%s' % response_line.lstrip('Auth=')
+  return None
+
+
+def GetCaptchChallenge(http_body, 
+    captcha_base_url='http://www.google.com/accounts/'):
+  """Returns the URL and token for a CAPTCHA challenge issued bu the server.
+
+  Args:
+    http_body: str The body of the HTTP response from the server which 
+        contains the CAPTCHA challenge.
+    captcha_base_url: str This function returns a full URL for viewing the 
+        challenge image which is built from the server's response. This
+        base_url is used as the beginning of the URL because the server
+        only provides the end of the URL. For example the server provides
+        'Captcha?ctoken=Hi...N' and the URL for the image is
+        'http://www.google.com/accounts/Captcha?ctoken=Hi...N'
+
+  Returns:
+    A dictionary containing the information needed to repond to the CAPTCHA
+    challenge, the image URL and the ID token of the challenge. The 
+    dictionary is in the form:
+    {'token': string identifying the CAPTCHA image,
+     'url': string containing the URL of the image}
+    Returns None if there was no CAPTCHA challenge in the response.
+  """
+  contains_captcha_challenge = False
+  captcha_parameters = {}
+  for response_line in http_body.splitlines():
+    if response_line.startswith('Error=CaptchaRequired'):
+      contains_captcha_challenge = True
+    elif response_line.startswith('CaptchaToken='):
+      captcha_parameters['token'] = response_line.lstrip('CaptchaToken=')
+    elif response_line.startswith('CaptchaUrl='):
+      captcha_parameters['url'] = '%s%s' % (captcha_base_url,
+          response_line.lstrip('CaptchaUrl='))
+  if contains_captcha_challenge:
+    return captcha_parameters
+  else:
+    return None
+
+
+def GenerateAuthSubUrl(next, scope, secure=False, session=True, 
+    request_url='https://www.google.com/accounts/AuthSubRequest'):
+  """Generate a URL at which the user will login and be redirected back.
+
+  Users enter their credentials on a Google login page and a token is sent
+  to the URL specified in next. See documentation for AuthSub login at:
+  http://code.google.com/apis/accounts/AuthForWebApps.html
+
+  Args:
+    request_url: str The beginning of the request URL. This is normally
+        'http://www.google.com/accounts/AuthSubRequest' or 
+        '/accounts/AuthSubRequest'
+    next: string The URL user will be sent to after logging in.
+    scope: string The URL of the service to be accessed.
+    secure: boolean (optional) Determines whether or not the issued token
+            is a secure token.
+    session: boolean (optional) Determines whether or not the issued token
+             can be upgraded to a session token.
+  """
+  # Translate True/False values for parameters into numeric values acceoted
+  # by the AuthSub service.
+  if secure:
+    secure = 1
+  else:
+    secure = 0
+
+  if session:
+    session = 1
+  else:
+    session = 0
+
+  request_params = urllib.urlencode({'next': next, 'scope': scope,
+                                  'secure': secure, 'session': session})
+  if request_url.find('?') == -1:
+    return '%s?%s' % (request_url, request_params)
+  else:
+    # The request URL already contained url parameters so we should add
+    # the parameters using the & seperator
+    return '%s&%s' % (request_url, request_params)
+
+
+def AuthSubTokenFromUrl(url):
+  """Extracts the AuthSub token from the URL. 
+
+  Used after the AuthSub redirect has sent the user to the 'next' page and
+  appended the token to the URL.
+
+  Args:
+    url: str The URL of the current page which contains the AuthSub token as
+        a URL parameter.
+  """
+  m = AUTH_SUB_KEY_PATTERN.match(url)
+  if m:
+    return 'AuthSub token=%s' % m.group(1)
+  return None
+
+
+def AuthSubTokenFromHttpBody(http_body):
+  """Extracts the AuthSub token from an HTTP body string.
+
+  Used to find the new session token after making a request to upgrade a 
+  single use AuthSub token.
+
+  Args:
+    http_body: str The repsonse from the server which contains the AuthSub 
+        key. For example, this function would find the new session token
+        from the server's response to an upgrade token request.
+
+  Returns:
+    The header value to use for Authorization which contains the AuthSub
+    token.
+  """
+  for response_line in http_body.splitlines():
+    if response_line.startswith('Token='):
+      auth_token = response_line.lstrip('Token=')
+      return 'AuthSub token=%s' % auth_token
+  return None

Added: trunk/conduit/modules/GoogleModule/libgdata/gdata/calendar/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/gdata/calendar/Makefile.am	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,5 @@
+conduit_handlersdir = $(libdir)/conduit/modules/GoogleModule/libgdata/gdata/calendar
+conduit_handlers_PYTHON = __init__.py service.py
+
+clean-local:
+	rm -rf *.pyc *.pyo

Added: trunk/conduit/modules/GoogleModule/libgdata/gdata/calendar/__init__.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/gdata/calendar/__init__.py	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,991 @@
+#!/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.
+
+
+# TODO:
+#   add text=none to all inits
+
+
+"""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
+import atom
+import gdata
+
+
+# XML namespaces which are often used in Google Calendar entities.
+GCAL_NAMESPACE = 'http://schemas.google.com/gCal/2005'
+GCAL_TEMPLATE = '{http://schemas.google.com/gCal/2005}%s'
+WEB_CONTENT_LINK_REL = '%s/%s' % (GCAL_NAMESPACE, 'webContent')
+GACL_NAMESPACE = gdata.GACL_NAMESPACE
+GACL_TEMPLATE = gdata.GACL_TEMPLATE
+
+
+
+class ValueAttributeContainer(atom.AtomBase):
+  """A parent class for all Calendar classes which have a value attribute.
+
+  Children include Color, AccessLevel, Hidden
+  """
+
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['value'] = 'value'
+
+  def __init__(self, value=None, extension_elements=None,
+      extension_attributes=None, text=None):
+    self.value = value
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+class Color(ValueAttributeContainer):
+  """The Google Calendar color element"""
+
+  _tag = 'color'
+  _namespace = GCAL_NAMESPACE
+  _children = ValueAttributeContainer._children.copy()
+  _attributes = ValueAttributeContainer._attributes.copy()
+  
+
+
+class AccessLevel(ValueAttributeContainer):
+  """The Google Calendar accesslevel element"""
+
+  _tag = 'accesslevel'
+  _namespace = GCAL_NAMESPACE
+  _children = ValueAttributeContainer._children.copy()
+  _attributes = ValueAttributeContainer._attributes.copy()
+  
+
+class Hidden(ValueAttributeContainer):
+  """The Google Calendar hidden element"""
+
+  _tag = 'hidden'
+  _namespace = GCAL_NAMESPACE
+  _children = ValueAttributeContainer._children.copy()
+  _attributes = ValueAttributeContainer._attributes.copy()
+  
+
+class Selected(ValueAttributeContainer):
+  """The Google Calendar selected element"""
+
+  _tag = 'selected'
+  _namespace = GCAL_NAMESPACE
+  _children = ValueAttributeContainer._children.copy()
+  _attributes = ValueAttributeContainer._attributes.copy()
+
+
+class Timezone(ValueAttributeContainer):
+  """The Google Calendar timezone element"""
+
+  _tag = 'timezone'
+  _namespace = GCAL_NAMESPACE
+  _children = ValueAttributeContainer._children.copy()
+  _attributes = ValueAttributeContainer._attributes.copy()
+
+
+class Where(atom.AtomBase):
+  """The Google Calendar Where element"""
+
+  _tag = 'where'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['valueString'] = 'value_string'
+
+  def __init__(self, value_string=None, extension_elements=None,
+      extension_attributes=None, text=None):
+    self.value_string = value_string 
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+class CalendarListEntry(gdata.GDataEntry, gdata.LinkFinder):
+  """A Google Calendar meta Entry flavor of an Atom Entry """
+
+  _tag = gdata.GDataEntry._tag
+  _namespace = gdata.GDataEntry._namespace
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+  _children['{%s}color' % GCAL_NAMESPACE] = ('color', Color)
+  _children['{%s}accesslevel' % GCAL_NAMESPACE] = ('access_level', 
+                                                   AccessLevel)
+  _children['{%s}hidden' % GCAL_NAMESPACE] = ('hidden', Hidden)
+  _children['{%s}selected' % GCAL_NAMESPACE] = ('selected', Selected)
+  _children['{%s}timezone' % GCAL_NAMESPACE] = ('timezone', Timezone)
+  _children['{%s}where' % gdata.GDATA_NAMESPACE] = ('where', Where)
+  
+  def __init__(self, author=None, category=None, content=None,
+      atom_id=None, link=None, published=None, 
+      title=None, updated=None, 
+      color=None, access_level=None, hidden=None, timezone=None,
+      selected=None,
+      where=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.color = color
+    self.access_level = access_level
+    self.hidden = hidden 
+    self.selected = selected
+    self.timezone = timezone
+    self.where = where 
+
+
+class CalendarListFeed(gdata.GDataFeed, gdata.LinkFinder):
+  """A Google Calendar meta 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', [CalendarListEntry])
+  #_attributes = {}
+
+
+class Scope(atom.AtomBase):
+  """The Google ACL scope element"""
+
+  _tag = 'scope'
+  _namespace = GACL_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['value'] = 'value'
+  _attributes['type'] = 'type'
+  
+  def __init__(self, extension_elements=None, value=None, scope_type=None,
+      extension_attributes=None, text=None):
+    self.value = value
+    self.type = scope_type
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+class Role(ValueAttributeContainer):
+  """The Google Calendar timezone element"""
+
+  _tag = 'role'
+  _namespace = GACL_NAMESPACE
+  _children = ValueAttributeContainer._children.copy()
+  _attributes = ValueAttributeContainer._attributes.copy()
+
+
+class CalendarAclEntry(gdata.GDataEntry, gdata.LinkFinder):
+  """A Google Calendar ACL Entry flavor of an Atom Entry """
+
+  _tag = gdata.GDataEntry._tag
+  _namespace = gdata.GDataEntry._namespace
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+  _children['{%s}scope' % GACL_NAMESPACE] = ('scope', Scope)
+  _children['{%s}role' % GACL_NAMESPACE] = ('role', Role)
+  
+  def __init__(self, author=None, category=None, content=None,
+      atom_id=None, link=None, published=None, 
+      title=None, updated=None,
+      scope=None, role=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.scope = scope
+    self.role = role
+
+
+
+class CalendarAclFeed(gdata.GDataFeed, gdata.LinkFinder):
+  """A Google Calendar ACL 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', [CalendarAclEntry])
+
+
+class CalendarEventCommentEntry(gdata.GDataEntry, gdata.LinkFinder):
+  """A Google Calendar event comments entry flavor of an Atom Entry"""
+
+  _tag = gdata.GDataEntry._tag
+  _namespace = gdata.GDataEntry._namespace
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+
+
+class CalendarEventCommentFeed(gdata.GDataFeed, gdata.LinkFinder):
+  """A Google Calendar event comments 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', 
+      [CalendarEventCommentEntry])
+
+
+class ExtendedProperty(atom.AtomBase):
+  """The Google Calendar extendedProperty element"""
+
+  _tag = 'extendedProperty'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['name'] = 'name'
+  _attributes['value'] = 'value'
+
+  def __init__(self, name=None, value=None, extension_elements=None,
+      extension_attributes=None, text=None):
+    self.name = name 
+    self.value = value
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+    
+class Reminder(atom.AtomBase):
+  """The Google Calendar reminder element"""
+  
+  _tag = 'reminder'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['absoluteTime'] = 'absolute_time'
+  _attributes['days'] = 'days'
+  _attributes['hours'] = 'hours'
+  _attributes['minutes'] = 'minutes'
+
+  def __init__(self, absolute_time=None,
+      days=None, hours=None, minutes=None, 
+      extension_elements=None,
+      extension_attributes=None, text=None):
+    self.absolute_time = absolute_time
+    if days is not None: 
+      self.days = str(days)
+    else:
+      self.days = None
+    if hours is not None:
+      self.hours = str(hours)
+    else:
+      self.hours = None
+    if minutes is not None:
+      self.minutes = str(minutes)
+    else:
+      self.minutes = None
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+    
+
+class When(atom.AtomBase):
+  """The Google Calendar When element"""
+
+  _tag = 'when'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _children['{%s}reminder' % gdata.GDATA_NAMESPACE] = ('reminder', [Reminder])
+  _attributes['startTime'] = 'start_time'
+  _attributes['endTime'] = 'end_time'
+
+  def __init__(self, start_time=None, end_time=None, reminder=None, 
+      extension_elements=None, extension_attributes=None, text=None):
+    self.start_time = start_time 
+    self.end_time = end_time 
+    self.reminder = reminder or []
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+class Recurrence(atom.AtomBase):
+  """The Google Calendar Recurrence element"""
+
+  _tag = 'recurrence'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  
+class UriEnumElement(atom.AtomBase):
+
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  def __init__(self, tag, enum_map, attrib_name='value', 
+      extension_elements=None, extension_attributes=None, text=None):
+    self.tag=tag
+    self.enum_map=enum_map
+    self.attrib_name=attrib_name
+    self.value=None
+    self.text=text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+     
+  def findKey(self, value):
+     res=[item[0] for item in self.enum_map.items() if item[1] == value]
+     if res is None or len(res) == 0:
+       return None
+     return res[0]
+
+  def _ConvertElementAttributeToMember(self, attribute, value):
+    # Special logic to use the enum_map to set the value of the object's value member.
+    if attribute == self.attrib_name and value != '':
+      self.value = self.enum_map[value]
+      return
+    # Find the attribute in this class's list of attributes.
+    if self.__class__._attributes.has_key(attribute):
+      # Find the member of this class which corresponds to the XML attribute
+      # (lookup in current_class._attributes) and set this member to the
+      # desired value (using self.__dict__).
+      setattr(self, self.__class__._attributes[attribute], value)
+    else:
+      # The current class doesn't map this attribute, so try to parent class.
+      atom.ExtensionContainer._ConvertElementAttributeToMember(self, 
+                                                               attribute,
+                                                               value)
+ 
+  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)
+    # Special logic to set the desired XML attribute.
+    key = self.findKey(self.value)
+    if key is not None:
+      tree.attrib[self.attrib_name]=key
+    # 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
+    # Lastly, call the parent's _AddMembersToElementTree to get any
+    # extension elements.
+    atom.ExtensionContainer._AddMembersToElementTree(self, tree)
+    
+    
+
+class AttendeeStatus(UriEnumElement):
+  """The Google Calendar attendeeStatus element"""
+  
+  _tag = 'attendeeStatus'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = UriEnumElement._children.copy()
+  _attributes = UriEnumElement._attributes.copy()
+  
+  attendee_enum = { 
+      'http://schemas.google.com/g/2005#event.accepted' : 'ACCEPTED',
+      'http://schemas.google.com/g/2005#event.declined' : 'DECLINED',
+      'http://schemas.google.com/g/2005#event.invited' : 'INVITED',
+      'http://schemas.google.com/g/2005#event.tentative' : 'TENTATIVE'}
+  
+  def __init__(self, extension_elements=None, 
+      extension_attributes=None, text=None):
+    UriEnumElement.__init__(self, 'attendeeStatus', AttendeeStatus.attendee_enum,
+                            extension_elements=extension_elements,
+                            extension_attributes=extension_attributes, 
+                            text=text)
+
+
+class AttendeeType(UriEnumElement):
+  """The Google Calendar attendeeType element"""
+  
+  _tag = 'attendeeType'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = UriEnumElement._children.copy()
+  _attributes = UriEnumElement._attributes.copy()
+  
+  attendee_type_enum = { 
+      'http://schemas.google.com/g/2005#event.optional' : 'OPTIONAL',
+      'http://schemas.google.com/g/2005#event.required' : 'REQUIRED' }
+  
+  def __init__(self, extension_elements=None,
+      extension_attributes=None, text=None):
+    UriEnumElement.__init__(self, 'attendeeType', 
+        AttendeeType.attendee_type_enum,
+        extension_elements=extension_elements,
+        extension_attributes=extension_attributes,text=text)
+
+
+class Visibility(UriEnumElement):
+  """The Google Calendar Visibility element"""
+  
+  _tag = 'visibility'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = UriEnumElement._children.copy()
+  _attributes = UriEnumElement._attributes.copy()
+  
+  visibility_enum = { 
+      'http://schemas.google.com/g/2005#event.confidential' : 'CONFIDENTIAL',
+      'http://schemas.google.com/g/2005#event.default' : 'DEFAULT',
+      'http://schemas.google.com/g/2005#event.private' : 'PRIVATE',
+      'http://schemas.google.com/g/2005#event.public' : 'PUBLIC' }
+
+  def __init__(self, extension_elements=None,
+      extension_attributes=None, text=None):
+    UriEnumElement.__init__(self, 'visibility', Visibility.visibility_enum,
+                            extension_elements=extension_elements,
+                            extension_attributes=extension_attributes, 
+                            text=text)
+
+
+class Transparency(UriEnumElement):
+  """The Google Calendar Transparency element"""
+  
+  _tag = 'transparency'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = UriEnumElement._children.copy()
+  _attributes = UriEnumElement._attributes.copy()
+  
+  transparency_enum = { 
+      'http://schemas.google.com/g/2005#event.opaque' : 'OPAQUE',
+      'http://schemas.google.com/g/2005#event.transparent' : 'TRANSPARENT' }
+  
+  def __init__(self, extension_elements=None,
+      extension_attributes=None, text=None):
+    UriEnumElement.__init__(self, tag='transparency', 
+                            enum_map=Transparency.transparency_enum,
+                            extension_elements=extension_elements,
+                            extension_attributes=extension_attributes, 
+                            text=text)
+
+
+class Comments(atom.AtomBase):
+  """The Google Calendar comments element"""
+  
+  _tag = 'comments'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link', 
+                                                       gdata.FeedLink)
+  _attributes['rel'] = 'rel'
+
+  def __init__(self, rel=None, feed_link=None, extension_elements=None,
+      extension_attributes=None, text=None):
+    self.rel = rel 
+    self.feed_link = feed_link
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
+class EventStatus(UriEnumElement):
+  """The Google Calendar eventStatus element"""
+  
+  _tag = 'eventStatus'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = UriEnumElement._children.copy()
+  _attributes = UriEnumElement._attributes.copy()
+  
+  status_enum = { 'http://schemas.google.com/g/2005#event.canceled' : 'CANCELED',
+                 'http://schemas.google.com/g/2005#event.confirmed' : 'CONFIRMED',
+                 'http://schemas.google.com/g/2005#event.tentative' : 'TENTATIVE'}
+  
+  def __init__(self, extension_elements=None,
+      extension_attributes=None, text=None):
+    UriEnumElement.__init__(self, tag='eventStatus', 
+        enum_map=EventStatus.status_enum,
+        extension_elements=extension_elements,
+        extension_attributes=extension_attributes, 
+        text=text)
+    
+class Who(UriEnumElement):
+  """The Google Calendar Who element"""
+
+  _tag = 'who'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = UriEnumElement._children.copy()
+  _attributes = UriEnumElement._attributes.copy()
+  _children['{%s}attendeeStatus' % gdata.GDATA_NAMESPACE] = (
+      'attendee_status', AttendeeStatus)
+  _children['{%s}attendeeType' % gdata.GDATA_NAMESPACE] = ('attendee_type',
+                                                           AttendeeType)
+  _attributes['valueString'] = 'name'
+  _attributes['email'] = 'email'
+
+  relEnum = { 'http://schemas.google.com/g/2005#event.attendee' : 'ATTENDEE',
+              'http://schemas.google.com/g/2005#event.organizer' : 'ORGANIZER',
+              'http://schemas.google.com/g/2005#event.performer' : 'PERFORMER',
+              'http://schemas.google.com/g/2005#event.speaker' : 'SPEAKER',
+              'http://schemas.google.com/g/2005#message.bcc' : 'BCC',
+              'http://schemas.google.com/g/2005#message.cc' : 'CC',
+              'http://schemas.google.com/g/2005#message.from' : 'FROM',
+              'http://schemas.google.com/g/2005#message.reply-to' : 'REPLY_TO',
+              'http://schemas.google.com/g/2005#message.to' : 'TO' }
+  
+  def __init__(self, extension_elements=None, 
+    extension_attributes=None, text=None):
+    UriEnumElement.__init__(self, 'who', Who.relEnum, attrib_name='rel',
+                            extension_elements=extension_elements,
+                            extension_attributes=extension_attributes, 
+                            text=text)
+    self.name=None
+    self.email=None
+    self.attendee_status=None
+    self.attendee_type=None
+    self.rel=None
+
+
+class OriginalEvent(atom.AtomBase):
+  """The Google Calendar OriginalEvent element"""
+  
+  _tag = 'originalEvent'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  # TODO: The when tag used to map to a EntryLink, make sure it should really be a When.
+  _children['{%s}when' % gdata.GDATA_NAMESPACE] = ('when', When)
+  _attributes['id'] = 'id'
+  _attributes['href'] = 'href'
+
+  def __init__(self, id=None, href=None, when=None, 
+      extension_elements=None, extension_attributes=None, text=None):
+    self.id = id
+    self.href = href 
+    self.when = when
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}    
+  
+
+def GetCalendarEventEntryClass():
+  return CalendarEventEntry
+  
+# This class is not completely defined here, because of a circular reference
+# in which CalendarEventEntryLink and CalendarEventEntry refer to one another.
+class CalendarEventEntryLink(gdata.EntryLink):
+  """An entryLink which contains a calendar event entry
+  
+  Within an event's recurranceExceptions, an entry link
+  points to a calendar event entry. This class exists
+  to capture the calendar specific extensions in the entry.
+  """
+
+  _tag = 'entryLink'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = gdata.EntryLink._children.copy()
+  _attributes = gdata.EntryLink._attributes.copy()
+  # The CalendarEventEntryLink should like CalendarEventEntry as a child but
+  # that class hasn't been defined yet, so we will wait until after defining
+  # CalendarEventEntry to list it in _children.
+
+  
+class RecurrenceException(atom.AtomBase):
+  """The Google Calendar RecurrenceException element"""
+
+  _tag = 'recurrenceException'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _children['{%s}entryLink' % gdata.GDATA_NAMESPACE] = ('entry_link', 
+      CalendarEventEntryLink)
+  _children['{%s}originalEvent' % gdata.GDATA_NAMESPACE] = ('original_event',
+                                                            OriginalEvent)
+  _attributes['specialized'] = 'specialized'
+  
+  def __init__(self, specialized=None, entry_link=None, 
+      original_event=None, extension_elements=None, 
+      extension_attributes=None, text=None):
+    self.specialized = specialized
+    self.entry_link = entry_link
+    self.original_event = original_event
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+    
+ 
+class SendEventNotifications(atom.AtomBase):
+  """The Google Calendar sendEventNotifications element"""
+  
+  _tag = 'sendEventNotifications'
+  _namespace = GCAL_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['value'] = 'value'
+
+  def __init__(self, extension_elements=None,
+      value=None, extension_attributes=None, text=None):
+    self.value = value
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+    
+class QuickAdd(atom.AtomBase):
+  """The Google Calendar quickadd element"""
+  
+  _tag = 'quickadd'
+  _namespace = GCAL_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['value'] = 'value'
+
+  def __init__(self, extension_elements=None,
+      value=None, extension_attributes=None, text=None):
+    self.value = value
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+  def _TransferToElementTree(self, element_tree):
+    if self.value:
+      element_tree.attrib['value'] = self.value
+    element_tree.tag = GCAL_TEMPLATE % 'quickadd'
+    atom.AtomBase._TransferToElementTree(self, element_tree)
+    return element_tree
+
+  def _TakeAttributeFromElementTree(self, attribute, element_tree):
+    if attribute == 'value':
+      self.value = element_tree.attrib[attribute]
+      del element_tree.attrib[attribute]
+    else:
+      atom.AtomBase._TakeAttributeFromElementTree(self, attribute, 
+          element_tree)
+
+          
+class WebContentGadgetPref(atom.AtomBase):
+
+  _tag = 'webContentGadgetPref'
+  _namespace = GCAL_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['name'] = 'name'
+  _attributes['value'] = 'value'
+
+  """The Google Calendar Web Content Gadget Preferences element"""
+
+  def __init__(self, name=None, value=None, extension_elements=None,
+      extension_attributes=None, text=None):
+    self.name = name
+    self.value = value
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}          
+          
+      
+class WebContent(atom.AtomBase):
+
+  _tag = 'webContent'
+  _namespace = GCAL_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _children['{%s}webContentGadgetPref' % GCAL_NAMESPACE] = ('gadget_pref', 
+      [WebContentGadgetPref])
+  _attributes['url'] = 'url'
+  _attributes['width'] = 'width'
+  _attributes['height'] = 'height'
+
+  def __init__(self, url=None, width=None, height=None, text=None,
+      gadget_pref=None, extension_elements=None, extension_attributes=None):
+    self.url = url
+    self.width = width
+    self.height = height
+    self.text = text
+    self.gadget_pref = gadget_pref or []
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}  
+
+      
+class WebContentLink(atom.Link):
+
+  _tag = 'link'
+  _namespace = atom.ATOM_NAMESPACE
+  _children = atom.Link._children.copy()
+  _attributes = atom.Link._attributes.copy()
+  _children['{%s}webContent' % GCAL_NAMESPACE] = ('web_content', WebContent)
+    
+  def __init__(self, title=None, href=None, link_type=None, 
+        web_content=None):
+    atom.Link.__init__(self, rel=WEB_CONTENT_LINK_REL, title=title, href=href, 
+        link_type=link_type)
+    self.web_content = web_content
+
+
+class CalendarEventEntry(gdata.BatchEntry):
+  """A Google Calendar flavor of an Atom Entry """
+
+  _tag = gdata.BatchEntry._tag
+  _namespace = gdata.BatchEntry._namespace
+  _children = gdata.BatchEntry._children.copy()
+  _attributes = gdata.BatchEntry._attributes.copy()
+  # This class also contains WebContentLinks but converting those members
+  # is handled in a special version of _ConvertElementTreeToMember.
+  _children['{%s}where' % gdata.GDATA_NAMESPACE] = ('where', [Where])
+  _children['{%s}when' % gdata.GDATA_NAMESPACE] = ('when', [When])
+  _children['{%s}who' % gdata.GDATA_NAMESPACE] = ('who', [Who])
+  _children['{%s}extendedProperty' % gdata.GDATA_NAMESPACE] = (
+      'extended_property', [ExtendedProperty]) 
+  _children['{%s}visibility' % gdata.GDATA_NAMESPACE] = ('visibility', 
+                                                         Visibility)
+  _children['{%s}transparency' % gdata.GDATA_NAMESPACE] = ('transparency', 
+                                                           Transparency)
+  _children['{%s}eventStatus' % gdata.GDATA_NAMESPACE] = ('event_status', 
+                                                          EventStatus)
+  _children['{%s}recurrence' % gdata.GDATA_NAMESPACE] = ('recurrence', 
+                                                         Recurrence)
+  _children['{%s}recurrenceException' % gdata.GDATA_NAMESPACE] = (
+      'recurrence_exception', [RecurrenceException])
+  _children['{%s}sendEventNotifications' % GCAL_NAMESPACE] = (
+      'send_event_notifications', SendEventNotifications)
+  _children['{%s}quickadd' % GCAL_NAMESPACE] = ('quick_add', QuickAdd)
+  _children['{%s}comments' % gdata.GDATA_NAMESPACE] = ('comments', Comments)
+  _children['{%s}originalEvent' % gdata.GDATA_NAMESPACE] = ('original_event',
+                                                            OriginalEvent)
+  
+  def __init__(self, author=None, category=None, content=None,
+      atom_id=None, link=None, published=None, 
+      title=None, updated=None, 
+      transparency=None, comments=None, event_status=None,
+      send_event_notifications=None, visibility=None,
+      recurrence=None, recurrence_exception=None,
+      where=None, when=None, who=None, quick_add=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.event_status = event_status 
+    self.send_event_notifications = send_event_notifications
+    self.visibility = visibility
+    self.recurrence = recurrence 
+    self.recurrence_exception = recurrence_exception or []
+    self.where = where or []
+    self.when = when or []
+    self.who = who or []
+    self.quick_add = quick_add
+    self.extended_property = extended_property or []
+    self.original_event = original_event
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+  # We needed to add special logic to _ConvertElementTreeToMember because we
+  # want to make links with a rel of WEB_CONTENT_LINK_REL into a 
+  # WebContentLink
+  def _ConvertElementTreeToMember(self, child_tree):
+    # Special logic to handle Web Content links
+    if (child_tree.tag == '{%s}link' % atom.ATOM_NAMESPACE and 
+        child_tree.attrib['rel'] == WEB_CONTENT_LINK_REL):
+      if self.link is None:
+        self.link = []
+      self.link.append(atom._CreateClassFromElementTree(WebContentLink, 
+                                                        child_tree))
+      return
+    # 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))
+    else:
+      atom.ExtensionContainer._ConvertElementTreeToMember(self, child_tree)
+      
+
+  def GetWebContentLink(self):
+    """Finds the first link with rel set to WEB_CONTENT_REL
+
+    Returns:
+      A gdata.calendar.WebContentLink or none if none of the links had rel 
+      equal to WEB_CONTENT_REL
+    """
+
+    for a_link in self.link:
+      if a_link.rel == WEB_CONTENT_LINK_REL:
+        return a_link
+    return None
+    
+
+def CalendarEventEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(CalendarEventEntry, xml_string)
+
+
+def CalendarEventCommentEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(CalendarEventCommentEntry, xml_string)
+  
+
+CalendarEventEntryLink._children = {'{%s}entry' % atom.ATOM_NAMESPACE: 
+    ('entry', CalendarEventEntry)}
+  
+
+def CalendarEventEntryLinkFromString(xml_string):
+  return atom.CreateClassFromXMLString(CalendarEventEntryLink, xml_string)
+
+
+class CalendarEventFeed(gdata.BatchFeed, gdata.LinkFinder):
+  """A Google Calendar event feed flavor of an Atom Feed"""
+
+  _tag = gdata.BatchFeed._tag
+  _namespace = gdata.BatchFeed._namespace
+  _children = gdata.BatchFeed._children.copy()
+  _attributes = gdata.BatchFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', 
+                                                  [CalendarEventEntry])
+
+  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,
+      interrupted=None,
+      extension_elements=None, extension_attributes=None, text=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,
+                              interrupted=interrupted,
+                              extension_elements=extension_elements,
+                              extension_attributes=extension_attributes,
+                              text=text)
+
+
+def CalendarListEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(CalendarListEntry, xml_string)
+
+
+def CalendarAclEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(CalendarAclEntry, xml_string)
+
+
+def CalendarListFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(CalendarListFeed, xml_string)
+  
+  
+def CalendarAclFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(CalendarAclFeed, xml_string)
+
+
+def CalendarEventFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(CalendarEventFeed, xml_string)
+
+  
+def CalendarEventCommentFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(CalendarEventCommentFeed, xml_string)
+
+
+# Code to create atom feeds from element trees
+#_CalendarListFeedFromElementTree = atom._AtomInstanceFromElementTree(
+#    CalendarListFeed, 'feed', atom.ATOM_NAMESPACE)
+#_CalendarListEntryFromElementTree = atom._AtomInstanceFromElementTree(
+#    CalendarListEntry, 'entry', atom.ATOM_NAMESPACE)
+#_CalendarAclFeedFromElementTree = atom._AtomInstanceFromElementTree(
+#    CalendarAclFeed, 'feed', atom.ATOM_NAMESPACE)
+#_CalendarAclEntryFromElementTree = atom._AtomInstanceFromElementTree(
+#    CalendarAclEntry, 'entry', atom.ATOM_NAMESPACE)
+#_CalendarEventFeedFromElementTree = atom._AtomInstanceFromElementTree(
+#    CalendarEventFeed, 'feed', atom.ATOM_NAMESPACE)
+#_CalendarEventEntryFromElementTree = atom._AtomInstanceFromElementTree(
+#    CalendarEventEntry, 'entry', atom.ATOM_NAMESPACE)
+#_CalendarEventCommentFeedFromElementTree = atom._AtomInstanceFromElementTree(
+#    CalendarEventCommentFeed, 'feed', atom.ATOM_NAMESPACE)
+#_CalendarEventCommentEntryFromElementTree = atom._AtomInstanceFromElementTree(
+#    CalendarEventCommentEntry, 'entry', atom.ATOM_NAMESPACE)
+#_WhereFromElementTree = atom._AtomInstanceFromElementTree(
+#    Where, 'where', gdata.GDATA_NAMESPACE)
+#_WhenFromElementTree = atom._AtomInstanceFromElementTree(
+#    When, 'when', gdata.GDATA_NAMESPACE)
+#_WhoFromElementTree = atom._AtomInstanceFromElementTree(
+#    Who, 'who', gdata.GDATA_NAMESPACE)
+#_VisibilityFromElementTree= atom._AtomInstanceFromElementTree(
+#    Visibility, 'visibility', gdata.GDATA_NAMESPACE)
+#_TransparencyFromElementTree = atom._AtomInstanceFromElementTree(
+#    Transparency, 'transparency', gdata.GDATA_NAMESPACE)
+#_CommentsFromElementTree = atom._AtomInstanceFromElementTree(
+#    Comments, 'comments', gdata.GDATA_NAMESPACE)
+#_EventStatusFromElementTree = atom._AtomInstanceFromElementTree(
+#    EventStatus, 'eventStatus', gdata.GDATA_NAMESPACE)
+#_SendEventNotificationsFromElementTree = atom._AtomInstanceFromElementTree(
+#    SendEventNotifications, 'sendEventNotifications', GCAL_NAMESPACE)
+#_QuickAddFromElementTree = atom._AtomInstanceFromElementTree(
+#    QuickAdd, 'quickadd', GCAL_NAMESPACE)
+#_AttendeeStatusFromElementTree = atom._AtomInstanceFromElementTree(
+#    AttendeeStatus, 'attendeeStatus', gdata.GDATA_NAMESPACE)
+#_AttendeeTypeFromElementTree = atom._AtomInstanceFromElementTree(
+#    AttendeeType, 'attendeeType', gdata.GDATA_NAMESPACE)
+#_ExtendedPropertyFromElementTree = atom._AtomInstanceFromElementTree(
+#    ExtendedProperty, 'extendedProperty', gdata.GDATA_NAMESPACE)
+#_RecurrenceFromElementTree = atom._AtomInstanceFromElementTree(
+#    Recurrence, 'recurrence', gdata.GDATA_NAMESPACE)
+#_RecurrenceExceptionFromElementTree = atom._AtomInstanceFromElementTree(
+#    RecurrenceException, 'recurrenceException', gdata.GDATA_NAMESPACE)
+#_OriginalEventFromElementTree = atom._AtomInstanceFromElementTree(
+#    OriginalEvent, 'originalEvent', gdata.GDATA_NAMESPACE)
+#_ColorFromElementTree = atom._AtomInstanceFromElementTree(
+#    Color, 'color', GCAL_NAMESPACE)
+#_HiddenFromElementTree = atom._AtomInstanceFromElementTree(
+#    Hidden, 'hidden', GCAL_NAMESPACE)
+#_SelectedFromElementTree = atom._AtomInstanceFromElementTree(
+#    Selected, 'selected', GCAL_NAMESPACE)
+#_TimezoneFromElementTree = atom._AtomInstanceFromElementTree(
+#    Timezone, 'timezone', GCAL_NAMESPACE)
+#_AccessLevelFromElementTree = atom._AtomInstanceFromElementTree(
+#    AccessLevel, 'accesslevel', GCAL_NAMESPACE)
+#_ReminderFromElementTree = atom._AtomInstanceFromElementTree(
+#    Reminder, 'reminder', gdata.GDATA_NAMESPACE)
+#_ScopeFromElementTree = atom._AtomInstanceFromElementTree(
+#    Scope, 'scope', GACL_NAMESPACE)
+#_RoleFromElementTree = atom._AtomInstanceFromElementTree(
+#    Role, 'role', GACL_NAMESPACE)
+#_WebContentLinkFromElementTree = atom._AtomInstanceFromElementTree(
+#    WebContentLink, 'link', atom.ATOM_NAMESPACE)
+#_WebContentFromElementTree = atom._AtomInstanceFromElementTree(
+#    WebContent, 'webContent', GCAL_NAMESPACE)
+#_WebContentGadgetPrefFromElementTree = atom._AtomInstanceFromElementTree(
+#    WebContentGadgetPref, 'webContentGadgetPref', GCAL_NAMESPACE)

Added: trunk/conduit/modules/GoogleModule/libgdata/gdata/calendar/service.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/gdata/calendar/service.py	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,602 @@
+#!/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.
+
+"""CalendarService extends the GDataService to streamline Google Calendar operations.
+
+  CalendarService: 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.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
+import gdata.service
+import gdata.calendar
+import atom
+
+
+DEFAULT_BATCH_URL = ('http://www.google.com/calendar/feeds/default/private'
+                     '/full/batch')
+
+
+class Error(Exception):
+  pass
+
+
+class RequestError(Error):
+  pass
+
+
+class CalendarService(gdata.service.GDataService):
+  """Client for the Google Calendar 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='cl', source=source, 
+                                        server=server, 
+                                        additional_headers=additional_headers)
+
+  def GetCalendarEventFeed(self, uri='/calendar/feeds/default/private/full'):
+    return gdata.calendar.CalendarEventFeedFromString(str(self.Get(uri)))
+
+  def GetCalendarEventEntry(self, uri):
+    return gdata.calendar.CalendarEventEntryFromString(str(self.Get(uri)))
+
+  def GetCalendarListFeed(self, uri='/calendar/feeds/default/allcalendars/full'):
+    return gdata.calendar.CalendarListFeedFromString(str(self.Get(uri)))
+
+  def GetAllCalendarsFeed(self, uri='/calendar/feeds/default/allcalendars/full'):
+    return gdata.calendar.CalendarListFeedFromString(str(self.Get(uri)))
+
+  def GetOwnCalendarsFeed(self, uri='/calendar/feeds/default/owncalendars/full'):
+    return gdata.calendar.CalendarListFeedFromString(str(self.Get(uri)))
+
+  def GetCalendarListEntry(self, uri):
+    return gdata.calendar.CalendarListEntryFromString(str(self.Get(uri)))
+
+  def GetCalendarAclFeed(self, uri='/calendar/feeds/default/acl/full'):
+    return gdata.calendar.CalendarAclFeedFromString(str(self.Get(uri)))
+
+  def GetCalendarAclEntry(self, uri):
+    return gdata.calendar.CalendarAclEntryFromString(str(self.Get(uri)))
+
+  def GetCalendarEventCommentFeed(self, uri):
+    return gdata.calendar.CalendarEventCommentFeedFromString(str(self.Get(uri)))
+
+  def GetCalendarEventCommentEntry(self, uri):
+    return gdata.calendar.CalendarEventCommentEntryFromString(str(self.Get(uri)))
+ 
+  def Query(self, uri):
+    """Performs a query and returns a resulting feed or entry.
+
+    Args:
+      feed: string The feed which is to be queried
+
+    Returns:
+      On success, a GDataFeed or Entry depending on which is sent from the 
+        server.
+      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}
+    """
+
+    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())
+    elif isinstance(query, CalendarListQuery):
+      return gdata.calendar.CalendarListFeedFromString(result.ToString())
+    elif isinstance(query, CalendarEventCommentQuery):
+      return gdata.calendar.CalendarEventCommentFeedFromString(result.ToString())
+    else:
+      return result
+    
+  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 
+                Google Calendar.
+      insert_uri: the URL to post new events 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 event 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}
+    """
+
+    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
+
+  def InsertCalendarSubscription(self, calendar, url_params=None, 
+                                 escape_params=True):
+    """Subscribes the authenticated user to the provided calendar.
+    
+    Args: 
+      calendar: The calendar to which the user should be subscribed.
+      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 subscription 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}
+    """
+    
+    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
+
+  def InsertCalendar(self, new_calendar, url_params=None,
+                                 escape_params=True):
+    """Creates a new calendar.
+    
+    Args: 
+      new_calendar: The calendar to be created
+      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 calendar 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}
+    """
+
+    insert_uri = '/calendar/feeds/default/owncalendars/full'
+    response = self.Post(new_calendar, insert_uri, url_params=url_params,
+                         escape_params=escape_params, 
+                         converter=gdata.calendar.CalendarListEntryFromString)
+    return response
+
+  def UpdateCalendar(self, calendar, url_params=None,
+                                 escape_params=True):
+    """Updates a calendar.
+    
+    Args: 
+      calendar: The calendar which should be updated
+      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 calendar 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}
+    """
+
+    update_uri = calendar.GetEditLink().href
+    response = self.Put(data=calendar, uri=update_uri, url_params=url_params,
+                         escape_params=escape_params,
+                         converter=gdata.calendar.CalendarListEntryFromString)
+    return response
+
+  def InsertAclEntry(self, new_entry, insert_uri, url_params=None, 
+                  escape_params=True):
+    """Adds an ACL entry (rule) to Google Calendar.
+
+    Args: 
+      new_entry: ElementTree._Element 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
+                  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 ACL entry 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}
+    """
+
+    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
+
+  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
+                Google Calendar.
+      insert_uri: the URL to post new entrys 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 comment 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}
+    """
+
+    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
+
+  def DeleteEvent(self, edit_uri, extra_headers=None, 
+      url_params=None, escape_params=True):
+    """Removes an event with the specified ID from Google Calendar.
+
+    Args:
+      edit_uri: string The edit URL of the entry to be deleted. Example:
+               'http://www.google.com/calendar/feeds/default/private/full/abx'
+      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)
+
+  def DeleteAclEntry(self, edit_uri, extra_headers=None, 
+      url_params=None, escape_params=True):
+    """Removes an ACL entry at the given edit_uri from Google Calendar.
+
+    Args:
+      edit_uri: string The edit URL of the entry to be deleted. Example:
+               'http://www.google.com/calendar/feeds/liz%40gmail.com/acl/full/default'
+      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)
+
+  def DeleteCalendarEntry(self, edit_uri, extra_headers=None,
+      url_params=None, escape_params=True):
+    """Removes a calendar entry at the given edit_uri from Google Calendar.
+
+    Args:
+      edit_uri: string The edit URL of the entry to be deleted. Example:
+               'http://www.google.com/calendar/feeds/default/allcalendars/abcdef group calendar google com'
+      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, True is returned
+      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.Delete(edit_uri, url_params=url_params, 
+                       escape_params=escape_params)
+
+  def UpdateEvent(self, edit_uri, updated_event, url_params=None, 
+                 escape_params=True):
+    """Updates an existing event.
+
+    Args:
+      edit_uri: string The edit link URI for the element being updated
+      updated_event: string, ElementTree._Element, or ElementWrapper 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_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
+
+  def UpdateAclEntry(self, edit_uri, updated_rule, url_params=None, 
+                     escape_params=True):
+    """Updates an existing ACL rule.
+
+    Args:
+      edit_uri: string The edit link URI for the element being updated
+      updated_rule: string, ElementTree._Element, or ElementWrapper 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_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
+
+  def ExecuteBatch(self, batch_feed, url, 
+      converter=gdata.calendar.CalendarEventFeedFromString):
+    """Sends a batch request feed to the server.
+
+    The batch request needs to be sent to the batch URL for a particular 
+    calendar. You can find the URL by calling GetBatchLink().href on the 
+    CalendarEventFeed.
+
+    Args:
+      batch_feed: gdata.calendar.CalendarEventFeed A feed containing batch
+          request entries. Each entry contains the operation to be performed 
+          on the data contained in the entry. For example an entry with an 
+          operation type of insert will be used as if the individual entry 
+          had been inserted.
+      url: str The batch URL for the Calendar to which these operations should
+          be applied.
+      converter: Function (optional) The function used to convert the server's
+          response to an object. The default value is 
+          CalendarEventFeedFromString.
+     
+    Returns:
+      The results of the batch request's execution on the server. If the 
+      default converter is used, this is stored in a CalendarEventFeed.
+    """
+    return self.Post(batch_feed, url, converter=converter)
+
+
+class CalendarEventQuery(gdata.service.Query):
+
+  def __init__(self, user='default', visibility='private', projection='full',
+               text_query=None, params=None, categories=None):
+    gdata.service.Query.__init__(self, feed='http://www.google.com/calendar/feeds/'+
+                           '%s/%s/%s' % (user, visibility, projection,),
+                           text_query=text_query, params=params, 
+                           categories=categories)
+    
+  def _GetStartMin(self):
+    if 'start-min' in self.keys():
+      return self['start-min']
+    else:
+      return None
+
+  def _SetStartMin(self, val):
+    self['start-min'] = val
+
+  start_min = property(_GetStartMin, _SetStartMin, 
+      doc="""The start-min query parameter""")
+
+  def _GetStartMax(self):
+    if 'start-max' in self.keys():
+      return self['start-max']
+    else:
+      return None
+
+  def _SetStartMax(self, val):
+    self['start-max'] = val
+
+  start_max = property(_GetStartMax, _SetStartMax, 
+      doc="""The start-max query parameter""")
+
+  def _GetOrderBy(self):
+    if 'orderby' in self.keys():
+      return self['orderby']
+    else:
+      return None
+
+  def _SetOrderBy(self, val):
+    if val is not 'lastmodified' and val is not 'starttime':
+      raise Error, "Order By must be either 'lastmodified' or 'starttime'"
+    self['orderby'] = val
+
+  orderby = property(_GetOrderBy, _SetOrderBy, 
+      doc="""The orderby query parameter""")
+
+  def _GetSortOrder(self):
+    if 'sortorder' in self.keys():
+      return self['sortorder']
+    else:
+      return None
+
+  def _SetSortOrder(self, val):
+    if (val is not 'ascending' and val is not 'descending' 
+        and val is not 'a' and val is not 'd' and val is not 'ascend'
+        and val is not 'descend'):
+      raise Error, "Sort order must be either ascending, ascend, " + (
+          "a or descending, descend, or d")
+    self['sortorder'] = val
+
+  sortorder = property(_GetSortOrder, _SetSortOrder, 
+      doc="""The sortorder query parameter""")
+
+  def _GetSingleEvents(self):
+    if 'singleevents' in self.keys():
+      return self['singleevents']
+    else:
+      return None
+
+  def _SetSingleEvents(self, val):
+    self['singleevents'] = val
+
+  singleevents = property(_GetSingleEvents, _SetSingleEvents, 
+      doc="""The singleevents query parameter""")
+
+  def _GetFutureEvents(self):
+    if 'futureevents' in self.keys():
+      return self['futureevents']
+    else:
+      return None
+
+  def _SetFutureEvents(self, val):
+    self['futureevents'] = val
+
+  futureevents = property(_GetFutureEvents, _SetFutureEvents, 
+      doc="""The futureevents query parameter""")
+
+  def _GetRecurrenceExpansionStart(self):
+    if 'recurrence-expansion-start' in self.keys():
+      return self['recurrence-expansion-start']
+    else:
+      return None
+
+  def _SetRecurrenceExpansionStart(self, val):
+    self['recurrence-expansion-start'] = val
+
+  recurrence_expansion_start = property(_GetRecurrenceExpansionStart, 
+      _SetRecurrenceExpansionStart, 
+      doc="""The recurrence-expansion-start query parameter""")
+
+  def _GetRecurrenceExpansionEnd(self):
+    if 'recurrence-expansion-end' in self.keys():
+      return self['recurrence-expansion-end']
+    else:
+      return None
+
+  def _SetRecurrenceExpansionEnd(self, val):
+    self['recurrence-expansion-end'] = val
+
+  recurrence_expansion_end = property(_GetRecurrenceExpansionEnd, 
+      _SetRecurrenceExpansionEnd, 
+      doc="""The recurrence-expansion-end query parameter""")
+
+  def _SetTimezone(self, val):
+    self['ctz'] = val
+
+  def _GetTimezone(self):
+    if 'ctz' in self.keys():
+      return self['ctz']
+    else:
+      return None
+
+  ctz = property(_GetTimezone, _SetTimezone, 
+      doc="""The ctz query parameter which sets report time on the server.""")
+
+
+class CalendarListQuery(gdata.service.Query): 
+  """Queries the Google Calendar meta feed"""
+
+  def __init__(self, userId=None, text_query=None,
+               params=None, categories=None):
+    if userId is None:
+      userId = 'default'
+
+    gdata.service.Query.__init__(self, feed='http://www.google.com/calendar/feeds/'
+                           +userId,
+                           text_query=text_query, params=params,
+                           categories=categories)
+
+class CalendarEventCommentQuery(gdata.service.Query): 
+  """Queries the Google Calendar event comments feed"""
+
+  def __init__(self, feed=None):
+    gdata.service.Query.__init__(self, feed=feed)

Added: trunk/conduit/modules/GoogleModule/libgdata/gdata/exif/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/gdata/exif/Makefile.am	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,5 @@
+conduit_handlersdir = $(libdir)/conduit/modules/GoogleModule/libgdata/gdata/exif
+conduit_handlers_PYTHON = __init__.py
+
+clean-local:
+	rm -rf *.pyc *.pyo

Added: trunk/conduit/modules/GoogleModule/libgdata/gdata/exif/__init__.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/gdata/exif/__init__.py	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,223 @@
+# -*-*- encoding: utf-8 -*-*-
+#
+# This is gdata.photos.exif, implementing the exif namespace in gdata
+#
+# $Id: __init__.py 81 2007-10-03 14:41:42Z havard.gulldahl $
+#
+# Copyright 2007 HÃvard Gulldahl 
+# Portions copyright 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.
+
+"""This module maps elements from the {EXIF} namespace[1] to GData objects. 
+These elements describe image data, using exif attributes[2].
+
+Picasa Web Albums uses the exif namespace to represent Exif data encoded 
+in a photo [3].
+
+Picasa Web Albums uses the following exif elements:
+exif:distance
+exif:exposure
+exif:flash
+exif:focallength
+exif:fstop
+exif:imageUniqueID
+exif:iso
+exif:make
+exif:model
+exif:tags
+exif:time
+
+[1]: http://schemas.google.com/photos/exif/2007. 
+[2]: http://en.wikipedia.org/wiki/Exif
+[3]: http://code.google.com/apis/picasaweb/reference.html#exif_reference
+"""
+
+
+__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
+
+EXIF_NAMESPACE = 'http://schemas.google.com/photos/exif/2007'
+
+class ExifBaseElement(atom.AtomBase):
+  """Base class for elements in the EXIF_NAMESPACE (%s). To add new elements, you only need to add the element tag name to self._tag
+  """ % EXIF_NAMESPACE
+  
+  _tag = ''
+  _namespace = EXIF_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  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 {}
+
+class Distance(ExifBaseElement):
+  "(float) The distance to the subject, e.g. 0.0"
+  
+  _tag = 'distance'
+def DistanceFromString(xml_string):
+  return atom.CreateClassFromXMLString(Distance, xml_string)
+  
+class Exposure(ExifBaseElement):
+  "(float) The exposure time used, e.g. 0.025 or 8.0E4"
+  
+  _tag = 'exposure'
+def ExposureFromString(xml_string):
+  return atom.CreateClassFromXMLString(Exposure, xml_string)
+  
+class Flash(ExifBaseElement):
+  """(string) Boolean value indicating whether the flash was used.
+  The .text attribute will either be `true' or `false'
+
+  As a convenience, this object's .bool method will return what you want,
+  so you can say:
+
+  flash_used = bool(Flash)
+
+  """
+  
+  _tag = 'flash'
+  def __bool__(self):
+    if self.text.lower() in ('true','false'):
+      return self.text.lower() == 'true'
+def FlashFromString(xml_string):
+  return atom.CreateClassFromXMLString(Flash, xml_string)
+  
+class Focallength(ExifBaseElement):
+  "(float) The focal length used, e.g. 23.7"
+  
+  _tag = 'focallength'
+def FocallengthFromString(xml_string):
+  return atom.CreateClassFromXMLString(Focallength, xml_string)
+  
+class Fstop(ExifBaseElement):
+  "(float) The fstop value used, e.g. 5.0"
+  
+  _tag = 'fstop'
+def FstopFromString(xml_string):
+  return atom.CreateClassFromXMLString(Fstop, xml_string)
+  
+class ImageUniqueID(ExifBaseElement):
+  "(string) The unique image ID for the photo. Generated by Google Photo servers"
+  
+  _tag = 'imageUniqueID'
+def ImageUniqueIDFromString(xml_string):
+  return atom.CreateClassFromXMLString(ImageUniqueID, xml_string)
+  
+class Iso(ExifBaseElement):
+  "(int) The iso equivalent value used, e.g. 200"
+  
+  _tag = 'iso'
+def IsoFromString(xml_string):
+  return atom.CreateClassFromXMLString(Iso, xml_string)
+  
+class Make(ExifBaseElement):
+  "(string) The make of the camera used, e.g. Fictitious Camera Company"
+  
+  _tag = 'make'
+def MakeFromString(xml_string):
+  return atom.CreateClassFromXMLString(Make, xml_string)
+  
+class Model(ExifBaseElement):
+  "(string) The model of the camera used,e.g AMAZING-100D"
+  
+  _tag = 'model'
+def ModelFromString(xml_string):
+  return atom.CreateClassFromXMLString(Model, xml_string)
+  
+class Time(ExifBaseElement):
+  """(int) The date/time the photo was taken, e.g. 1180294337000.
+  Represented as the number of milliseconds since January 1st, 1970.
+  
+  The value of this element will always be identical to the value
+  of the <gphoto:timestamp>.
+
+  Look at this object's .isoformat() for a human friendly datetime string:
+
+  photo_epoch = Time.text # 1180294337000
+  photo_isostring = Time.isoformat() # '2007-05-27T19:32:17.000Z'
+
+  Alternatively: 
+  photo_datetime = Time.datetime() # (requires python >= 2.3)
+  """
+  
+  _tag = 'time'
+  def isoformat(self):
+    """(string) Return the timestamp as a ISO 8601 formatted string,
+    e.g. '2007-05-27T19:32:17.000Z'
+    """
+    import time
+    epoch = float(self.text)/1000
+    return time.strftime('%Y-%m-%dT%H:%M:%S.000Z', time.gmtime(epoch))
+  
+  def datetime(self):
+    """(datetime.datetime) Return the timestamp as a datetime.datetime object
+
+    Requires python 2.3
+    """
+    import datetime
+    epoch = float(self.text)/1000
+    return datetime.datetime.fromtimestamp(epoch)
+  
+def TimeFromString(xml_string):
+  return atom.CreateClassFromXMLString(Time, xml_string)
+  
+class Tags(ExifBaseElement):
+  """The container for all exif elements.
+  The <exif:tags> element can appear as a child of a photo entry.
+  """
+  
+  _tag = 'tags'
+  _children = atom.AtomBase._children.copy()
+  _children['{%s}fstop' % EXIF_NAMESPACE] = ('fstop', Fstop) 
+  _children['{%s}make' % EXIF_NAMESPACE] = ('make', Make) 
+  _children['{%s}model' % EXIF_NAMESPACE] = ('model', Model) 
+  _children['{%s}distance' % EXIF_NAMESPACE] = ('distance', Distance) 
+  _children['{%s}exposure' % EXIF_NAMESPACE] = ('exposure', Exposure) 
+  _children['{%s}flash' % EXIF_NAMESPACE] = ('flash', Flash) 
+  _children['{%s}focallength' % EXIF_NAMESPACE] = ('focallength', Focallength) 
+  _children['{%s}iso' % EXIF_NAMESPACE] = ('iso', Iso) 
+  _children['{%s}time' % EXIF_NAMESPACE] = ('time', Time) 
+  _children['{%s}imageUniqueID' % EXIF_NAMESPACE] = ('imageUniqueID', ImageUniqueID) 
+
+  def __init__(self, extension_elements=None, extension_attributes=None, text=None):
+    ExifBaseElement.__init__(self, extension_elements=extension_elements,
+                            extension_attributes=extension_attributes,
+                            text=text)
+    self.fstop=None
+    self.make=None
+    self.model=None
+    self.distance=None
+    self.exposure=None
+    self.flash=None
+    self.focallength=None
+    self.iso=None
+    self.time=None
+    self.imageUniqueID=None
+def TagsFromString(xml_string):
+  return atom.CreateClassFromXMLString(Tags, xml_string)
+  

Added: trunk/conduit/modules/GoogleModule/libgdata/gdata/geo/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/gdata/geo/Makefile.am	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,5 @@
+conduit_handlersdir = $(libdir)/conduit/modules/GoogleModule/libgdata/gdata/geo
+conduit_handlers_PYTHON = __init__.py
+
+clean-local:
+	rm -rf *.pyc *.pyo

Added: trunk/conduit/modules/GoogleModule/libgdata/gdata/geo/__init__.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/gdata/geo/__init__.py	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,187 @@
+# -*-*- encoding: utf-8 -*-*-
+#
+# This is gdata.photos.geo, implementing geological positioning in gdata structures
+#
+# $Id: __init__.py 81 2007-10-03 14:41:42Z havard.gulldahl $
+#
+# Copyright 2007 HÃvard Gulldahl 
+# Portions copyright 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.
+
+"""Picasa Web Albums uses the georss and gml namespaces for 
+elements defined in the GeoRSS and Geography Markup Language specifications.
+
+Specifically, Picasa Web Albums uses the following elements:
+
+georss:where
+gml:Point
+gml:pos
+
+http://code.google.com/apis/picasaweb/reference.html#georss_reference
+
+
+Picasa Web Albums also accepts geographic-location data in two other formats:
+W3C format and plain-GeoRSS (without GML) format. 
+"""
+# 
+#Over the wire, the Picasa Web Albums only accepts and sends the 
+#elements mentioned above, but this module will let you seamlessly convert 
+#between the different formats (TODO 2007-10-18 hg)
+
+__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
+
+GEO_NAMESPACE = 'http://www.w3.org/2003/01/geo/wgs84_pos#'
+GML_NAMESPACE = 'http://www.opengis.net/gml'
+GEORSS_NAMESPACE = 'http://www.georss.org/georss'
+
+class GeoBaseElement(atom.AtomBase):
+  """Base class for elements.
+
+  To add new elements, you only need to add the element tag name to self._tag
+  and the namespace to self._namespace
+  """
+  
+  _tag = ''
+  _namespace = GML_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  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 {}
+
+class Pos(GeoBaseElement):
+  """(string) Specifies a latitude and longitude, separated by a space,
+  e.g. `35.669998 139.770004'"""
+  
+  _tag = 'pos'
+def PosFromString(xml_string):
+  return atom.CreateClassFromXMLString(Pos, xml_string)
+
+class Point(GeoBaseElement):
+  """(container)  Specifies a particular geographical point, by means of
+  a <gml:pos> element."""
+  
+  _tag = 'Point'
+  _children = atom.AtomBase._children.copy()
+  _children['{%s}pos' % GML_NAMESPACE] = ('pos', Pos) 
+  def __init__(self, pos=None, extension_elements=None, extension_attributes=None, text=None):
+    GeoBaseElement.__init__(self, extension_elements=extension_elements,
+                            extension_attributes=extension_attributes,
+                            text=text)
+    if pos is None:
+      pos = Pos()
+    self.pos=pos
+def PointFromString(xml_string):
+  return atom.CreateClassFromXMLString(Point, xml_string)
+
+class Where(GeoBaseElement):
+  """(container) Specifies a geographical location or region.
+  A container element, containing a single <gml:Point> element.
+  (Not to be confused with <gd:where>.) 
+  
+  Note that the (only) child attribute, .Point, is title-cased.
+  This reflects the names of elements in the xml stream
+  (principle of least surprise).
+  
+  As a convenience, you can get a tuple of (lat, lon) with Where.location(),
+  and set the same data with Where.setLocation( (lat, lon) ).
+
+  Similarly, there are methods to set and get only latitude and longtitude.
+  """
+  
+  _tag = 'where'
+  _namespace = GEORSS_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _children['{%s}Point' % GML_NAMESPACE] = ('Point', Point) 
+  def __init__(self, point=None, extension_elements=None, extension_attributes=None, text=None):
+    GeoBaseElement.__init__(self, extension_elements=extension_elements,
+                            extension_attributes=extension_attributes,
+                            text=text)
+    if point is None:
+      point = Point()
+    self.Point=point
+  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(' '))
+    except AttributeError:
+      return tuple()
+  def set_location(self, latlon):
+    """(bool) Set Where.Point.pos.text from a (lat,lon) tuple.
+
+    Arguments:
+    lat (float): The latitude in degrees, from -90.0 to 90.0
+    lon (float): The longitude in degrees, from -180.0 to 180.0
+    
+    Returns True on success.
+
+    """
+    
+    assert(isinstance(latlon[0], float))
+    assert(isinstance(latlon[1], float))
+    try:
+      self.Point.pos.text = "%s %s" % (latlon[0], latlon[1])
+      return True
+    except AttributeError:
+      return False
+  def latitude(self):
+    "(float) Get the latitude value of the geo-tag. See also .location()"
+    lat, lon = self.location()
+    return lat
+  
+  def longtitude(self):
+    "(float) Get the longtitude value of the geo-tag. See also .location()"
+    lat, lon = self.location()
+    return lon
+
+  def set_latitude(self, lat):
+    """(bool) Set the latitude value of the geo-tag.
+
+    Args:
+    lat (float): The new latitude value
+
+    See also .set_location()
+    """
+    _lat, lon = self.location()
+    return self.set_location(lat, lon)
+  
+  def set_longtitude(self, lon):
+    """(bool) Set the longtitude value of the geo-tag.
+    
+    Args:
+    lat (float): The new latitude value
+
+    See also .set_location()
+    """
+    lat, _lon = self.location()
+    return self.set_location(lat, lon)
+
+def WhereFromString(xml_string):
+  return atom.CreateClassFromXMLString(Where, xml_string)
+  

Added: trunk/conduit/modules/GoogleModule/libgdata/gdata/media/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/gdata/media/Makefile.am	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,5 @@
+conduit_handlersdir = $(libdir)/conduit/modules/GoogleModule/libgdata/gdata/media
+conduit_handlers_PYTHON = __init__.py
+
+clean-local:
+	rm -rf *.pyc *.pyo

Added: trunk/conduit/modules/GoogleModule/libgdata/gdata/media/__init__.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/gdata/media/__init__.py	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,259 @@
+# -*-*- encoding: utf-8 -*-*-
+#
+# This is gdata.photos.media, implementing parts of the MediaRSS spec in gdata structures
+#
+# $Id: __init__.py 81 2007-10-03 14:41:42Z havard.gulldahl $
+#
+# Copyright 2007 HÃvard Gulldahl 
+# Portions copyright 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.
+
+
+"""Essential attributes of photos in Google Photos/Picasa Web Albums are 
+expressed using elements from the `media' namespace, defined in the 
+MediaRSS specification[1].
+
+Due to copyright issues, the elements herein are documented sparingly, please 
+consult with the Google Photos API Reference Guide[2], alternatively the 
+official MediaRSS specification[1] for details. 
+(If there is a version conflict between the two sources, stick to the 
+Google Photos API).
+
+[1]: http://search.yahoo.com/mrss (version 1.1.1)
+[2]: http://code.google.com/apis/picasaweb/reference.html#media_reference
+
+Keep in mind that Google Photos only uses a subset of the MediaRSS elements 
+(and some of the attributes are trimmed down, too): 
+
+media:content
+media:credit
+media:description
+media:group
+media:keywords
+media:thumbnail
+media:title
+"""
+
+__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
+
+MEDIA_NAMESPACE = 'http://search.yahoo.com/mrss/'
+
+class MediaBaseElement(atom.AtomBase):
+  """Base class for elements in the MEDIA_NAMESPACE. To add new elements, you only need to add the element tag name to self._tag
+  """
+  
+  _tag = ''
+  _namespace = MEDIA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  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 {}
+
+class Content(MediaBaseElement):
+  """(attribute container) This element describes the original content,
+    e.g. an image or a video. There may be multiple Content elements
+    in a media:Group.
+
+    For example, a video may have a
+    <media:content medium="image"> element that specifies a JPEG
+    representation of the video, and a <media:content medium="video">
+    element that specifies the URL of the video itself.
+  
+  Attributes:
+  url: non-ambigous reference to online object
+  width: width of the object frame, in pixels
+  height: width of the object frame, in pixels
+  medium: one of `image' or `video', allowing the api user to quickly
+    determine the object's type
+  type: Internet media Type[1] (a.k.a. mime type) of the object -- a more
+    verbose way of determining the media type
+  (optional) fileSize: the size of the object, in bytes
+  
+  [1]: http://en.wikipedia.org/wiki/Internet_media_type
+  """
+  
+  _tag = 'content'
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['url'] = 'url'
+  _attributes['width'] = 'width'
+  _attributes['height'] = 'height'
+  _attributes['medium'] = 'medium'
+  _attributes['type'] = 'type'
+  _attributes['fileSize'] = 'fileSize'
+  def __init__(self, url=None, width=None, height=None,
+      medium=None, content_type=None, fileSize=None,
+      extension_elements=None, extension_attributes=None, text=None):
+    MediaBaseElement.__init__(self, extension_elements=extension_elements,
+                            extension_attributes=extension_attributes,
+                            text=text)
+    self.url = url
+    self.width = width
+    self.height = height
+    self.medium = medium
+    self.type = content_type
+    self.fileSize = fileSize
+def ContentFromString(xml_string):
+  return atom.CreateClassFromXMLString(Content, xml_string)
+
+class Credit(MediaBaseElement):
+  """(string) Contains the nickname of the user who created the content,
+  e.g. `Liz Bennet'.
+  
+  This is a user-specified value that should be used when referring to
+  the user by name.
+
+  Note that none of the attributes from the MediaRSS spec are supported.
+  """
+  
+  _tag = 'credit'
+def CreditFromString(xml_string):
+  return atom.CreateClassFromXMLString(Credit, xml_string)
+
+class Description(MediaBaseElement):
+  """(string) A description of the media object.
+  Either plain unicode text, or entity-encoded html (look at the `type'
+  attribute).
+
+  E.g `A set of photographs I took while vacationing in Italy.'
+  
+  For `api' projections, the description is in plain text;
+  for `base' projections, the description is in HTML.
+  
+  Attributes:
+  type: either `text' or `html'. 
+  """
+  
+  _tag = 'description'
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['type'] = 'type'
+  def __init__(self, description_type=None, 
+      extension_elements=None, extension_attributes=None, text=None):
+    MediaBaseElement.__init__(self, extension_elements=extension_elements,
+                            extension_attributes=extension_attributes,
+                            text=text)
+    
+    self.type = description_type
+def DescriptionFromString(xml_string):
+  return atom.CreateClassFromXMLString(Description, xml_string)
+
+class Keywords(MediaBaseElement):
+  """(string) Lists the tags associated with the entry,
+  e.g `italy, vacation, sunset'.
+  
+  Contains a comma-separated list of tags that have been added to the photo, or
+  all tags that have been added to photos in the album.
+  """
+  
+  _tag = 'keywords'
+def KeywordsFromString(xml_string):
+  return atom.CreateClassFromXMLString(Keywords, xml_string)
+
+class Thumbnail(MediaBaseElement):
+  """(attributes) Contains the URL of a thumbnail of a photo or album cover.
+  
+  There can be multiple <media:thumbnail> elements for a given <media:group>; 
+  for example, a given item may have multiple thumbnails at different sizes. 
+  Photos generally have two thumbnails at different sizes; 
+  albums generally have one cropped thumbnail.  
+    
+  If the thumbsize parameter is set to the initial query, this element points 
+  to thumbnails of the requested sizes; otherwise the thumbnails are the 
+  default thumbnail size. 
+  
+  This element must not be confused with the <gphoto:thumbnail> element.
+  
+  Attributes:
+  url:  The URL of the thumbnail image.
+  height:  The height of the thumbnail image, in pixels.
+  width:  The width of the thumbnail image, in pixels.
+  """
+  
+  _tag = 'thumbnail'
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['url'] = 'url'
+  _attributes['width'] = 'width'
+  _attributes['height'] = 'height'
+  def __init__(self, url=None, width=None, height=None,
+      extension_attributes=None, text=None, extension_elements=None):
+    MediaBaseElement.__init__(self, extension_elements=extension_elements,
+                            extension_attributes=extension_attributes,
+                            text=text)
+    self.url = url
+    self.width = width
+    self.height = height
+def ThumbnailFromString(xml_string):
+  return atom.CreateClassFromXMLString(Thumbnail, xml_string)
+
+class Title(MediaBaseElement):
+  """(string) Contains the title of the entry's media content, in plain text.
+  
+  Attributes:
+  type: Always set to plain
+  """
+  
+  _tag = 'title'
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['type'] = 'type'
+  def __init__(self, title_type=None, 
+      extension_attributes=None, text=None, extension_elements=None):
+    MediaBaseElement.__init__(self, extension_elements=extension_elements,
+                            extension_attributes=extension_attributes,
+                            text=text)
+    self.type = title_type
+def TitleFromString(xml_string):
+  return atom.CreateClassFromXMLString(Title, xml_string)
+
+class Group(MediaBaseElement):
+  """Container element for all media elements.
+  The <media:group> element can appear as a child of an album or photo entry."""
+
+  _tag = 'group'
+  _children = atom.AtomBase._children.copy()
+  _children['{%s}content' % MEDIA_NAMESPACE] = ('content', [Content,]) 
+  _children['{%s}credit' % MEDIA_NAMESPACE] = ('credit', Credit) 
+  _children['{%s}description' % MEDIA_NAMESPACE] = ('description', Description) 
+  _children['{%s}keywords' % MEDIA_NAMESPACE] = ('keywords', Keywords) 
+  _children['{%s}thumbnail' % MEDIA_NAMESPACE] = ('thumbnail', [Thumbnail,])
+  _children['{%s}title' % MEDIA_NAMESPACE] = ('title', Title) 
+
+  def __init__(self, content=None, credit=None, description=None, keywords=None,
+      thumbnail=None, title=None,
+      extension_elements=None, extension_attributes=None, text=None):
+    MediaBaseElement.__init__(self, extension_elements=extension_elements,
+                            extension_attributes=extension_attributes,
+                            text=text)
+    self.content=content
+    self.credit=credit
+    self.description=description
+    self.keywords=keywords
+    self.thumbnail=thumbnail or []
+    self.title=title
+def GroupFromString(xml_string):
+  return atom.CreateClassFromXMLString(Group, xml_string)

Added: trunk/conduit/modules/GoogleModule/libgdata/gdata/photos/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/gdata/photos/Makefile.am	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,5 @@
+conduit_handlersdir = $(libdir)/conduit/modules/GoogleModule/libgdata/gdata/photos
+conduit_handlers_PYTHON = __init__.py service.py
+
+clean-local:
+	rm -rf *.pyc *.pyo

Added: trunk/conduit/modules/GoogleModule/libgdata/gdata/photos/__init__.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/gdata/photos/__init__.py	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,1045 @@
+# -*-*- encoding: utf-8 -*-*-
+#
+# This is the base file for the PicasaWeb python client.
+# It is used for lower level operations.
+#
+# $Id: __init__.py 148 2007-10-28 15:09:19Z havard.gulldahl $
+#
+# Copyright 2007 HÃvard Gulldahl 
+# Portions (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.
+
+"""This module provides a pythonic, gdata-centric interface to Google Photos
+(a.k.a. Picasa Web Services.
+
+It is modelled after the gdata/* interfaces from the gdata-python-client
+project[1] by Google. 
+
+You'll find the user-friendly api in photos.service. Please see the
+documentation or live help() system for available methods.
+
+[1]: http://gdata-python-client.googlecode.com/
+
+  """
+
+__author__ = u'havard gulldahl no'# (HÃvard Gulldahl)' #BUG: pydoc chokes on non-ascii chars in __author__
+__license__ = 'Apache License v2'
+__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
+import atom
+import gdata
+
+# importing google photo submodules
+import gdata.media as Media, gdata.exif as Exif, gdata.geo as Geo
+
+# XML namespaces which are often used in Google Photo elements
+PHOTOS_NAMESPACE = 'http://schemas.google.com/photos/2007'
+MEDIA_NAMESPACE = 'http://search.yahoo.com/mrss/'
+EXIF_NAMESPACE = 'http://schemas.google.com/photos/exif/2007'
+OPENSEARCH_NAMESPACE = 'http://a9.com/-/spec/opensearchrss/1.0/'
+GEO_NAMESPACE = 'http://www.w3.org/2003/01/geo/wgs84_pos#'
+GML_NAMESPACE = 'http://www.opengis.net/gml'
+GEORSS_NAMESPACE = 'http://www.georss.org/georss'
+PHEED_NAMESPACE = 'http://www.pheed.com/pheed/'
+BATCH_NAMESPACE = 'http://schemas.google.com/gdata/batch'
+
+
+class PhotosBaseElement(atom.AtomBase):
+  """Base class for elements in the PHOTO_NAMESPACE. To add new elements,
+  you only need to add the element tag name to self._tag
+  """
+  
+  _tag = ''
+  _namespace = PHOTOS_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+
+  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 __str__(self):
+    #return str(self.text)
+  #def __unicode__(self):
+    #return unicode(self.text)
+  def __int__(self):
+    return int(self.text)
+  def bool(self):
+    return self.text == 'true'
+
+class GPhotosBaseFeed(gdata.GDataFeed, gdata.LinkFinder):
+  "Base class for all Feeds in gdata.photos"
+  _tag = 'feed'
+  _namespace = atom.ATOM_NAMESPACE
+  _attributes = gdata.GDataFeed._attributes.copy()
+  _children = gdata.GDataFeed._children.copy()
+  # We deal with Entry elements ourselves
+  _children.pop('{%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,
+               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 kind(self):
+    "(string) Returns the kind"
+    try:
+      return self.category[0].term.split('#')[1]
+    except IndexError:
+      return None
+  
+  def _feedUri(self, kind):
+    "Convenience method to return a uri to a feed of a special kind"
+    assert(kind in ('album', 'tag', 'photo', 'comment', 'user'))
+    here_href = self.GetSelfLink().href
+    if 'kind=%s' % kind in here_href:
+      return here_href
+    if not 'kind=' in here_href:
+      sep = '?'
+      if '?' in here_href: sep = '&'
+      return here_href + "%skind=%s" % (sep, kind)
+    rx = re.match('.*(kind=)(album|tag|photo|comment)', here_href)
+    return here_href[:rx.end(1)] + kind + here_href[rx.end(2):]
+  
+  def _ConvertElementTreeToMember(self, child_tree):
+    """Re-implementing the method from AtomBase, since we deal with
+      Entry elements specially"""
+    category = child_tree.find('{%s}category' % atom.ATOM_NAMESPACE)
+    if category is None:
+      return atom.AtomBase._ConvertElementTreeToMember(self, child_tree)
+    namespace, kind = category.get('term').split('#')
+    if namespace != PHOTOS_NAMESPACE:
+      return atom.AtomBase._ConvertElementTreeToMember(self, child_tree)
+    ## TODO: is it safe to use getattr on gdata.photos?
+    entry_class = getattr(gdata.photos, '%sEntry' % kind.title())
+    if not hasattr(self, 'entry') or self.entry is None:
+      self.entry = []
+    self.entry.append(atom._CreateClassFromElementTree(
+        entry_class, child_tree))
+
+class GPhotosBaseEntry(gdata.GDataEntry, gdata.LinkFinder):
+  "Base class for all Entry elements in gdata.photos"
+  _tag = 'entry'
+  _kind = ''
+  _namespace = atom.ATOM_NAMESPACE
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+    
+  def __init__(self, author=None, category=None, content=None,
+      atom_id=None, link=None, published=None,
+      title=None, updated=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, text=text,
+                        extension_elements=extension_elements,
+                        extension_attributes=extension_attributes)
+    self.category.append(
+      atom.Category(scheme='http://schemas.google.com/g/2005#kind', 
+              term = 'http://schemas.google.com/photos/2007#%s' % self._kind))
+
+  def kind(self):
+    "(string) Returns the kind"
+    try:
+      return self.category[0].term.split('#')[1]
+    except IndexError:
+      return None
+  
+  def _feedUri(self, kind):
+    "Convenience method to get the uri to this entry's feed of the some kind"
+    try:
+      href = self.GetFeedLink().href
+    except AttributeError:
+      return None
+    sep = '?'
+    if '?' in href: sep = '&'
+    return '%s%skind=%s' % (href, sep, kind)
+
+
+class PhotosBaseEntry(GPhotosBaseEntry):
+  pass
+
+class PhotosBaseFeed(GPhotosBaseFeed):
+  pass
+
+class GPhotosBaseData(object):
+  pass
+
+class Access(PhotosBaseElement):
+  """The Google Photo `Access' element.
+
+  The album's access level. Valid values are `public' or `private'.
+  In documentation, access level is also referred to as `visibility.'"""
+  
+  _tag = 'access'
+def AccessFromString(xml_string):
+  return atom.CreateClassFromXMLString(Access, xml_string)
+
+class Albumid(PhotosBaseElement):
+  "The Google Photo `Albumid' element"
+  
+  _tag = 'albumid'
+def AlbumidFromString(xml_string):
+  return atom.CreateClassFromXMLString(Albumid, xml_string)
+
+class BytesUsed(PhotosBaseElement):
+  "The Google Photo `BytesUsed' element"
+  
+  _tag = 'bytesUsed'
+def BytesUsedFromString(xml_string):
+  return atom.CreateClassFromXMLString(BytesUsed, xml_string)
+
+class Client(PhotosBaseElement):
+  "The Google Photo `Client' element"
+  
+  _tag = 'client'
+def ClientFromString(xml_string):
+  return atom.CreateClassFromXMLString(Client, xml_string)
+
+class Checksum(PhotosBaseElement):
+  "The Google Photo `Checksum' element"
+  
+  _tag = 'checksum'
+def ChecksumFromString(xml_string):
+  return atom.CreateClassFromXMLString(Checksum, xml_string)
+
+class CommentCount(PhotosBaseElement):
+  "The Google Photo `CommentCount' element"
+  
+  _tag = 'commentCount'
+def CommentCountFromString(xml_string):
+  return atom.CreateClassFromXMLString(CommentCount, xml_string)
+
+class CommentingEnabled(PhotosBaseElement):
+  "The Google Photo `CommentingEnabled' element"
+  
+  _tag = 'commentingEnabled'
+def CommentingEnabledFromString(xml_string):
+  return atom.CreateClassFromXMLString(CommentingEnabled, xml_string)
+
+class Height(PhotosBaseElement):
+  "The Google Photo `Height' element"
+  
+  _tag = 'height'
+def HeightFromString(xml_string):
+  return atom.CreateClassFromXMLString(Height, xml_string)
+
+class Id(PhotosBaseElement):
+  "The Google Photo `Id' element"
+  
+  _tag = 'id'
+def IdFromString(xml_string):
+  return atom.CreateClassFromXMLString(Id, xml_string)
+
+class Location(PhotosBaseElement):
+  "The Google Photo `Location' element"
+  
+  _tag = 'location'
+def LocationFromString(xml_string):
+  return atom.CreateClassFromXMLString(Location, xml_string)
+
+class MaxPhotosPerAlbum(PhotosBaseElement):
+  "The Google Photo `MaxPhotosPerAlbum' element"
+  
+  _tag = 'maxPhotosPerAlbum'
+def MaxPhotosPerAlbumFromString(xml_string):
+  return atom.CreateClassFromXMLString(MaxPhotosPerAlbum, xml_string)
+
+class Name(PhotosBaseElement):
+  "The Google Photo `Name' element"
+  
+  _tag = 'name'
+def NameFromString(xml_string):
+  return atom.CreateClassFromXMLString(Name, xml_string)
+
+class Nickname(PhotosBaseElement):
+  "The Google Photo `Nickname' element"
+  
+  _tag = 'nickname'
+def NicknameFromString(xml_string):
+  return atom.CreateClassFromXMLString(Nickname, xml_string)
+
+class Numphotos(PhotosBaseElement):
+  "The Google Photo `Numphotos' element"
+  
+  _tag = 'numphotos'
+def NumphotosFromString(xml_string):
+  return atom.CreateClassFromXMLString(Numphotos, xml_string)
+
+class Numphotosremaining(PhotosBaseElement):
+  "The Google Photo `Numphotosremaining' element"
+  
+  _tag = 'numphotosremaining'
+def NumphotosremainingFromString(xml_string):
+  return atom.CreateClassFromXMLString(Numphotosremaining, xml_string)
+
+class Position(PhotosBaseElement):
+  "The Google Photo `Position' element"
+  
+  _tag = 'position'
+def PositionFromString(xml_string):
+  return atom.CreateClassFromXMLString(Position, xml_string)
+
+class Photoid(PhotosBaseElement):
+  "The Google Photo `Photoid' element"
+  
+  _tag = 'photoid'
+def PhotoidFromString(xml_string):
+  return atom.CreateClassFromXMLString(Photoid, xml_string)
+
+class Quotacurrent(PhotosBaseElement):
+  "The Google Photo `Quotacurrent' element"
+  
+  _tag = 'quotacurrent'
+def QuotacurrentFromString(xml_string):
+  return atom.CreateClassFromXMLString(Quotacurrent, xml_string)
+
+class Quotalimit(PhotosBaseElement):
+  "The Google Photo `Quotalimit' element"
+  
+  _tag = 'quotalimit'
+def QuotalimitFromString(xml_string):
+  return atom.CreateClassFromXMLString(Quotalimit, xml_string)
+
+class Rotation(PhotosBaseElement):
+  "The Google Photo `Rotation' element"
+  
+  _tag = 'rotation'
+def RotationFromString(xml_string):
+  return atom.CreateClassFromXMLString(Rotation, xml_string)
+
+class Size(PhotosBaseElement):
+  "The Google Photo `Size' element"
+  
+  _tag = 'size'
+def SizeFromString(xml_string):
+  return atom.CreateClassFromXMLString(Size, xml_string)
+
+class Thumbnail(PhotosBaseElement):
+  """The Google Photo `Thumbnail' element
+
+  Used to display user's photo thumbnail (hackergotchi).
+  
+  (Not to be confused with the <media:thumbnail> element, which gives you
+  small versions of the photo object.)"""
+  
+  _tag = 'thumbnail'
+def ThumbnailFromString(xml_string):
+  return atom.CreateClassFromXMLString(Thumbnail, xml_string)
+
+class Timestamp(PhotosBaseElement):
+  """The Google Photo `Timestamp' element
+  Represented as the number of milliseconds since January 1st, 1970.
+  
+  
+  Take a look at the convenience methods .isoformat() and .datetime():
+
+  photo_epoch     = Time.text        # 1180294337000
+  photo_isostring = Time.isoformat() # '2007-05-27T19:32:17.000Z'
+
+  Alternatively: 
+  photo_datetime  = Time.datetime()  # (requires python >= 2.3)
+  """
+  
+  _tag = 'timestamp'
+  def isoformat(self):
+    """(string) Return the timestamp as a ISO 8601 formatted string,
+    e.g. '2007-05-27T19:32:17.000Z'
+    """
+    import time
+    epoch = float(self.text)/1000
+    return time.strftime('%Y-%m-%dT%H:%M:%S.000Z', time.gmtime(epoch))
+  
+  def datetime(self):
+    """(datetime.datetime) Return the timestamp as a datetime.datetime object
+
+    Requires python 2.3
+    """
+    import datetime
+    epoch = float(self.text)/1000
+    return datetime.datetime.fromtimestamp(epoch)
+def TimestampFromString(xml_string):
+  return atom.CreateClassFromXMLString(Timestamp, xml_string)
+
+class User(PhotosBaseElement):
+  "The Google Photo `User' element"
+  
+  _tag = 'user'
+def UserFromString(xml_string):
+  return atom.CreateClassFromXMLString(User, xml_string)
+
+class Version(PhotosBaseElement):
+  "The Google Photo `Version' element"
+  
+  _tag = 'version'
+def VersionFromString(xml_string):
+  return atom.CreateClassFromXMLString(Version, xml_string)
+
+class Width(PhotosBaseElement):
+  "The Google Photo `Width' element"
+  
+  _tag = 'width'
+def WidthFromString(xml_string):
+  return atom.CreateClassFromXMLString(Width, xml_string)
+
+class Weight(PhotosBaseElement):
+  """The Google Photo `Weight' element.
+
+  The weight of the tag is the number of times the tag
+  appears in the collection of tags currently being viewed.
+  The default weight is 1, in which case this tags is omitted."""
+  _tag = 'weight'
+def WeightFromString(xml_string):
+  return atom.CreateClassFromXMLString(Weight, xml_string)
+
+class CommentAuthor(atom.Author):
+  """The Atom `Author' element in CommentEntry entries is augmented to
+  contain elements from the PHOTOS_NAMESPACE
+
+  http://groups.google.com/group/Google-Picasa-Data-API/msg/819b0025b5ff5e38
+  """
+  _children = atom.Author._children.copy()
+  _children['{%s}user' % PHOTOS_NAMESPACE] = ('user', User)
+  _children['{%s}nickname' % PHOTOS_NAMESPACE] = ('nickname', Nickname)
+  _children['{%s}thumbnail' % PHOTOS_NAMESPACE] = ('thumbnail', Thumbnail)
+def CommentAuthorFromString(xml_string):
+  return atom.CreateClassFromXMLString(CommentAuthor, xml_string)
+
+########################## ################################
+
+class AlbumData(object):
+  _children = {}
+  _children['{%s}id' % PHOTOS_NAMESPACE] = ('gphoto_id', Id) 
+  _children['{%s}name' % PHOTOS_NAMESPACE] = ('name', Name)
+  _children['{%s}location' % PHOTOS_NAMESPACE] = ('location', Location)
+  _children['{%s}access' % PHOTOS_NAMESPACE] = ('access', Access)
+  _children['{%s}bytesUsed' % PHOTOS_NAMESPACE] = ('bytesUsed', BytesUsed)
+  _children['{%s}timestamp' % PHOTOS_NAMESPACE] = ('timestamp', Timestamp)
+  _children['{%s}numphotos' % PHOTOS_NAMESPACE] = ('numphotos', Numphotos)
+  _children['{%s}numphotosremaining' % PHOTOS_NAMESPACE] = \
+    ('numphotosremaining', Numphotosremaining)
+  _children['{%s}user' % PHOTOS_NAMESPACE] = ('user', User)
+  _children['{%s}nickname' % PHOTOS_NAMESPACE] = ('nickname', Nickname)
+  _children['{%s}commentingEnabled' % PHOTOS_NAMESPACE] = \
+    ('commentingEnabled', CommentingEnabled)
+  _children['{%s}commentCount' % PHOTOS_NAMESPACE] = \
+    ('commentCount', CommentCount)
+  ## NOTE: storing media:group as self.media, to create a self-explaining api
+  gphoto_id = None
+  name = None
+  location = None
+  access = None
+  bytesUsed = None
+  timestamp = None
+  numphotos = None
+  numphotosremaining = None
+  user = None
+  nickname = None
+  commentingEnabled = None
+  commentCount = None
+
+class AlbumEntry(GPhotosBaseEntry, AlbumData):
+  """All metadata for a Google Photos Album
+
+  Take a look at AlbumData for metadata accessible as attributes to this object.
+
+  Notes:
+    To avoid name clashes, and to create a more sensible api, some
+    objects have names that differ from the original elements:
+  
+    o media:group -> self.media,
+    o geo:where -> self.geo,
+    o photo:id -> self.gphoto_id
+  """
+  
+  _kind = 'album'
+  _children = GPhotosBaseEntry._children.copy()
+  _children.update(AlbumData._children.copy())
+  # child tags only for Album entries, not feeds
+  _children['{%s}where' % GEORSS_NAMESPACE] = ('geo', Geo.Where)
+  _children['{%s}group' % MEDIA_NAMESPACE] = ('media', Media.Group)
+  media = Media.Group()
+  geo = Geo.Where()
+  
+  def __init__(self, author=None, category=None, content=None,
+      atom_id=None, link=None, published=None,
+      title=None, updated=None,
+      #GPHOTO NAMESPACE:
+      gphoto_id=None, name=None, location=None, access=None, 
+      timestamp=None, numphotos=None, user=None, nickname=None,
+      commentingEnabled=None, commentCount=None, thumbnail=None,
+      # MEDIA NAMESPACE:
+      media=None,
+      # GEORSS NAMESPACE:
+      geo=None,
+      extended_property=None,
+      extension_elements=None, extension_attributes=None, text=None):
+    GPhotosBaseEntry.__init__(self, author=author, category=category,
+                        content=content, atom_id=atom_id, link=link,
+                        published=published, title=title,
+                        updated=updated, text=text,
+                        extension_elements=extension_elements,
+                        extension_attributes=extension_attributes)
+
+    ## NOTE: storing photo:id as self.gphoto_id, to avoid name clash with atom:id
+    self.gphoto_id = gphoto_id 
+    self.name = name
+    self.location = location
+    self.access = access
+    self.timestamp = timestamp
+    self.numphotos = numphotos
+    self.user = user
+    self.nickname = nickname
+    self.commentingEnabled = commentingEnabled
+    self.commentCount = commentCount
+    self.thumbnail = thumbnail
+    self.extended_property = extended_property or []
+    self.text = text
+    ## NOTE: storing media:group as self.media, and geo:where as geo,
+    ## to create a self-explaining api
+    self.media = media or Media.Group()
+    self.geo = geo or Geo.Where()
+
+  def GetAlbumId(self):
+    "Return the id of this album"
+    
+    return self.GetFeedLink().href.split('/')[-1]
+          
+  def GetPhotosUri(self):
+    "(string) Return the uri to this albums feed of the PhotoEntry kind"
+    return self._feedUri('photo')
+  
+  def GetCommentsUri(self):
+    "(string) Return the uri to this albums feed of the CommentEntry kind"
+    return self._feedUri('comment')
+  
+  def GetTagsUri(self):
+    "(string) Return the uri to this albums feed of the TagEntry kind"
+    return self._feedUri('tag')
+
+def AlbumEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(AlbumEntry, xml_string)
+  
+class AlbumFeed(GPhotosBaseFeed, AlbumData):
+  """All metadata for a Google Photos Album, including its sub-elements
+  
+  This feed represents an album as the container for other objects.
+
+  A Album feed contains entries of
+  PhotoEntry, CommentEntry or TagEntry,
+  depending on the `kind' parameter in the original query.
+
+  Take a look at AlbumData for accessible attributes.
+  
+  """
+  
+  _children = GPhotosBaseFeed._children.copy()
+  _children.update(AlbumData._children.copy())
+
+  def GetPhotosUri(self):
+    "(string) Return the uri to the same feed, but of the PhotoEntry kind"
+    
+    return self._feedUri('photo')
+         
+  def GetTagsUri(self):
+    "(string) Return the uri to the same feed, but of the TagEntry kind"
+
+    return self._feedUri('tag')
+    
+  def GetCommentsUri(self):
+    "(string) Return the uri to the same feed, but of the CommentEntry kind"
+
+    return self._feedUri('comment')
+  
+def AlbumFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(AlbumFeed, xml_string)
+
+
+class PhotoData(object):
+  _children = {}
+  ## NOTE: storing photo:id as self.gphoto_id, to avoid name clash with atom:id
+  _children['{%s}id' % PHOTOS_NAMESPACE] = ('gphoto_id', Id) 
+  _children['{%s}albumid' % PHOTOS_NAMESPACE] = ('albumid', Albumid)
+  _children['{%s}checksum' % PHOTOS_NAMESPACE] = ('checksum', Checksum)
+  _children['{%s}client' % PHOTOS_NAMESPACE] = ('client', Client)
+  _children['{%s}height' % PHOTOS_NAMESPACE] = ('height', Height)
+  _children['{%s}position' % PHOTOS_NAMESPACE] = ('position', Position)
+  _children['{%s}rotation' % PHOTOS_NAMESPACE] = ('rotation', Rotation)
+  _children['{%s}size' % PHOTOS_NAMESPACE] = ('size', Size)
+  _children['{%s}timestamp' % PHOTOS_NAMESPACE] = ('timestamp', Timestamp)
+  _children['{%s}version' % PHOTOS_NAMESPACE] = ('version', Version)
+  _children['{%s}width' % PHOTOS_NAMESPACE] = ('width', Width)
+  _children['{%s}commentingEnabled' % PHOTOS_NAMESPACE] = \
+    ('commentingEnabled', CommentingEnabled)
+  _children['{%s}commentCount' % PHOTOS_NAMESPACE] = \
+    ('commentCount', CommentCount)
+  ## NOTE: storing media:group as self.media, exif:tags as self.exif, and
+  ## geo:where as self.geo, to create a self-explaining api
+  _children['{%s}tags' % EXIF_NAMESPACE] = ('exif', Exif.Tags)
+  _children['{%s}where' % GEORSS_NAMESPACE] = ('geo', Geo.Where)
+  _children['{%s}group' % MEDIA_NAMESPACE] = ('media', Media.Group)
+  gphoto_id = None
+  albumid = None
+  checksum = None
+  client = None
+  height = None
+  position = None
+  rotation = None
+  size = None
+  timestamp = None
+  version = None
+  width = None
+  commentingEnabled = None
+  commentCount = None
+  media = Media.Group()
+  geo = Geo.Where()
+  tags = Exif.Tags()
+
+class PhotoEntry(GPhotosBaseEntry, PhotoData):
+  """All metadata for a Google Photos Photo
+
+  Take a look at PhotoData for metadata accessible as attributes to this object.
+
+  Notes:
+    To avoid name clashes, and to create a more sensible api, some
+    objects have names that differ from the original elements:
+  
+    o media:group -> self.media,
+    o exif:tags -> self.exif,
+    o geo:where -> self.geo,
+    o photo:id -> self.gphoto_id
+  """
+
+  _kind = 'photo'
+  _children = GPhotosBaseEntry._children.copy()
+  _children.update(PhotoData._children.copy())
+  
+  def __init__(self, author=None, category=None, content=None,
+      atom_id=None, link=None, published=None, 
+      title=None, updated=None, text=None,
+      # GPHOTO NAMESPACE:
+      gphoto_id=None, albumid=None, checksum=None, client=None, height=None,
+      position=None, rotation=None, size=None, timestamp=None, version=None,
+      width=None, commentCount=None, commentingEnabled=None,
+      # MEDIARSS NAMESPACE:
+      media=None,
+      # EXIF_NAMESPACE:
+      exif=None,
+      # GEORSS NAMESPACE:
+      geo=None,
+      extension_elements=None, extension_attributes=None):
+    GPhotosBaseEntry.__init__(self, author=author, category=category,
+                              content=content,
+                              atom_id=atom_id, link=link, published=published,
+                              title=title, updated=updated, text=text,
+                              extension_elements=extension_elements,
+                              extension_attributes=extension_attributes)
+                              
+
+    ## NOTE: storing photo:id as self.gphoto_id, to avoid name clash with atom:id
+    self.gphoto_id = gphoto_id
+    self.albumid = albumid
+    self.checksum = checksum
+    self.client = client
+    self.height = height
+    self.position = position
+    self.rotation = rotation
+    self.size = size
+    self.timestamp = timestamp
+    self.version = version
+    self.width = width
+    self.commentingEnabled = commentingEnabled
+    self.commentCount = commentCount
+    ## NOTE: storing media:group as self.media, to create a self-explaining api
+    self.media = media or Media.Group()
+    self.exif = exif or Exif.Tags()
+    self.geo = geo or Geo.Where()
+
+  def GetPostLink(self):
+    "Return the uri to this photo's `POST' link (use it for updates of the object)"
+
+    return self.GetFeedLink()
+
+  def GetCommentsUri(self):
+    "Return the uri to this photo's feed of CommentEntry comments"
+    return self._feedUri('comment')    
+
+  def GetTagsUri(self):
+    "Return the uri to this photo's feed of TagEntry tags"
+    return self._feedUri('tag')    
+
+  def GetAlbumUri(self):
+    """Return the uri to the AlbumEntry containing this photo"""
+
+    href = self.GetSelfLink().href
+    return href[:href.find('/photoid')]
+
+def PhotoEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(PhotoEntry, xml_string)
+
+class PhotoFeed(GPhotosBaseFeed, PhotoData):
+  """All metadata for a Google Photos Photo, including its sub-elements
+  
+  This feed represents a photo as the container for other objects.
+
+  A Photo feed contains entries of
+  CommentEntry or TagEntry,
+  depending on the `kind' parameter in the original query.
+
+  Take a look at PhotoData for metadata accessible as attributes to this object.
+  
+  """
+  _children = GPhotosBaseFeed._children.copy()
+  _children.update(PhotoData._children.copy())
+
+  def GetTagsUri(self):
+    "(string) Return the uri to the same feed, but of the TagEntry kind"
+    
+    return self._feedUri('tag')
+  
+  def GetCommentsUri(self):
+    "(string) Return the uri to the same feed, but of the CommentEntry kind"
+    
+    return self._feedUri('comment')
+
+def PhotoFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(PhotoFeed, xml_string)
+
+class TagData(GPhotosBaseData):
+  _children = {}
+  _children['{%s}weight' % PHOTOS_NAMESPACE] = ('weight', Weight)
+  weight=None
+
+class TagEntry(GPhotosBaseEntry, TagData):
+  """All metadata for a Google Photos Tag
+
+  The actual tag is stored in the .title.text attribute
+
+  """
+
+  _kind = 'tag'
+  _children = GPhotosBaseEntry._children.copy()
+  _children.update(TagData._children.copy())
+
+  def __init__(self, author=None, category=None, content=None,
+               atom_id=None, link=None, published=None,
+               title=None, updated=None,
+               # GPHOTO NAMESPACE:
+               weight=None,
+               extended_property=None,
+               extension_elements=None, extension_attributes=None, text=None):
+    GPhotosBaseEntry.__init__(self, author=author, category=category,
+                              content=content,
+                              atom_id=atom_id, link=link, published=published,
+                              title=title, updated=updated, text=text,
+                              extension_elements=extension_elements,
+                              extension_attributes=extension_attributes)
+    
+    self.weight = weight
+
+  def GetAlbumUri(self):
+    """Return the uri to the AlbumEntry containing this tag"""
+
+    href = self.GetSelfLink().href
+    pos = href.find('/photoid')
+    if pos == -1:
+      return None
+    return href[:pos]
+
+  def GetPhotoUri(self):
+    """Return the uri to the PhotoEntry containing this tag"""
+
+    href = self.GetSelfLink().href
+    pos = href.find('/tag')
+    if pos == -1:
+      return None
+    return href[:pos]
+
+def TagEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(TagEntry, xml_string)
+
+
+class TagFeed(GPhotosBaseFeed, TagData):
+  """All metadata for a Google Photos Tag, including its sub-elements"""
+  
+  _children = GPhotosBaseFeed._children.copy()
+  _children.update(TagData._children.copy())
+  
+def TagFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(TagFeed, xml_string)
+
+class CommentData(GPhotosBaseData):
+  _children = {}
+  ## NOTE: storing photo:id as self.gphoto_id, to avoid name clash with atom:id
+  _children['{%s}id' % PHOTOS_NAMESPACE] = ('gphoto_id', Id)
+  _children['{%s}albumid' % PHOTOS_NAMESPACE] = ('albumid', Albumid)
+  _children['{%s}photoid' % PHOTOS_NAMESPACE] = ('photoid', Photoid)
+  _children['{%s}author' % atom.ATOM_NAMESPACE] = ('author', [CommentAuthor,])
+  gphoto_id=None
+  albumid=None
+  photoid=None
+  author=None
+
+class CommentEntry(GPhotosBaseEntry, CommentData):
+  """All metadata for a Google Photos Comment
+   
+  The comment is stored in the .content.text attribute,
+  with a content type in .content.type.
+
+
+  """
+  
+  _kind = 'comment'
+  _children = GPhotosBaseEntry._children.copy()
+  _children.update(CommentData._children.copy())
+  def __init__(self, author=None, category=None, content=None,
+               atom_id=None, link=None, published=None,
+               title=None, updated=None,
+               # GPHOTO NAMESPACE:
+               gphoto_id=None, albumid=None, photoid=None,
+               extended_property=None,
+               extension_elements=None, extension_attributes=None, text=None):
+    
+    GPhotosBaseEntry.__init__(self, author=author, category=category,
+                              content=content,
+                              atom_id=atom_id, link=link, published=published,
+                              title=title, updated=updated,
+                              extension_elements=extension_elements,
+                              extension_attributes=extension_attributes,
+                              text=text)
+    
+    self.gphoto_id = gphoto_id
+    self.albumid = albumid
+    self.photoid = photoid
+
+  def GetCommentId(self):
+    """Return the globally unique id of this comment"""
+    return self.GetSelfLink().href.split('/')[-1]
+
+  def GetAlbumUri(self):
+    """Return the uri to the AlbumEntry containing this comment"""
+
+    href = self.GetSelfLink().href
+    return href[:href.find('/photoid')]
+
+  def GetPhotoUri(self):
+    """Return the uri to the PhotoEntry containing this comment"""
+
+    href = self.GetSelfLink().href
+    return href[:href.find('/commentid')]
+
+def CommentEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(CommentEntry, xml_string)
+
+class CommentFeed(GPhotosBaseFeed, CommentData):
+  """All metadata for a Google Photos Comment, including its sub-elements"""
+  
+  _children = GPhotosBaseFeed._children.copy()
+  _children.update(CommentData._children.copy())
+
+def CommentFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(CommentFeed, xml_string)
+
+class UserData(GPhotosBaseData):
+  _children = {}
+  _children['{%s}maxPhotosPerAlbum' % PHOTOS_NAMESPACE] = ('maxPhotosPerAlbum', MaxPhotosPerAlbum)
+  _children['{%s}nickname' % PHOTOS_NAMESPACE] = ('nickname', Nickname)
+  _children['{%s}quotalimit' % PHOTOS_NAMESPACE] = ('quotalimit', Quotalimit)
+  _children['{%s}quotacurrent' % PHOTOS_NAMESPACE] = ('quotacurrent', Quotacurrent)
+  _children['{%s}thumbnail' % PHOTOS_NAMESPACE] = ('thumbnail', Thumbnail)
+  _children['{%s}user' % PHOTOS_NAMESPACE] = ('user', User)
+  _children['{%s}id' % PHOTOS_NAMESPACE] = ('gphoto_id', Id)   
+
+  maxPhotosPerAlbum=None
+  nickname=None
+  quotalimit=None
+  quotacurrent=None
+  thumbnail=None
+  user=None
+  gphoto_id=None
+  
+
+class UserEntry(GPhotosBaseEntry, UserData):
+  """All metadata for a Google Photos User
+
+  This entry represents an album owner and all appropriate metadata.
+
+  Take a look at at the attributes of the UserData for metadata available.
+  """
+  _children = GPhotosBaseEntry._children.copy()
+  _children.update(UserData._children.copy())
+  _kind = 'user'
+  
+  def __init__(self, author=None, category=None, content=None,
+               atom_id=None, link=None, published=None,
+               title=None, updated=None,
+               # GPHOTO NAMESPACE:
+               gphoto_id=None, maxPhotosPerAlbum=None, nickname=None, quotalimit=None,
+               quotacurrent=None, thumbnail=None, user=None,
+               extended_property=None,
+               extension_elements=None, extension_attributes=None, text=None):
+    
+    GPhotosBaseEntry.__init__(self, author=author, category=category,
+                              content=content,
+                              atom_id=atom_id, link=link, published=published,
+                              title=title, updated=updated,
+                              extension_elements=extension_elements,
+                              extension_attributes=extension_attributes,
+                              text=text)
+                              
+    
+    self.gphoto_id=gphoto_id
+    self.maxPhotosPerAlbum=maxPhotosPerAlbum
+    self.nickname=nickname
+    self.quotalimit=quotalimit
+    self.quotacurrent=quotacurrent
+    self.thumbnail=thumbnail
+    self.user=user
+
+  def GetAlbumsUri(self):
+    "(string) Return the uri to this user's feed of the AlbumEntry kind"
+    return self._feedUri('album')
+  
+  def GetPhotosUri(self):
+    "(string) Return the uri to this user's feed of the PhotoEntry kind"
+    return self._feedUri('photo')
+  
+  def GetCommentsUri(self):
+    "(string) Return the uri to this user's feed of the CommentEntry kind"
+    return self._feedUri('comment')
+  
+  def GetTagsUri(self):
+    "(string) Return the uri to this user's feed of the TagEntry kind"
+    return self._feedUri('tag')
+
+def UserEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(UserEntry, xml_string)
+    
+class UserFeed(GPhotosBaseFeed, UserData):
+  """Feed for a User in the google photos api.
+
+  This feed represents a user as the container for other objects.
+
+  A User feed contains entries of
+  AlbumEntry, PhotoEntry, CommentEntry, UserEntry or TagEntry,
+  depending on the `kind' parameter in the original query.
+
+  The user feed itself also contains all of the metadata available
+  as part of a UserData object."""
+  _children = GPhotosBaseFeed._children.copy()
+  _children.update(UserData._children.copy())
+
+  def GetAlbumsUri(self):
+    """Get the uri to this feed, but with entries of the AlbumEntry kind."""
+    return self._feedUri('album')
+
+  def GetTagsUri(self):
+    """Get the uri to this feed, but with entries of the TagEntry kind."""
+    return self._feedUri('tag')
+
+  def GetPhotosUri(self):
+    """Get the uri to this feed, but with entries of the PhotosEntry kind."""
+    return self._feedUri('photo')
+
+  def GetCommentsUri(self):
+    """Get the uri to this feed, but with entries of the CommentsEntry kind."""
+    return self._feedUri('comment')
+
+def UserFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(UserFeed, xml_string)
+  
+
+  
+def AnyFeedFromString(xml_string):
+  """Creates an instance of the appropriate feed class from the
+    xml string contents.
+
+  Args:
+    xml_string: str A string which contains valid XML. The root element
+        of the XML string should match the tag and namespace of the desired
+        class.
+
+  Returns:
+    An instance of the target class with members assigned according to the
+    contents of the XML - or a basic gdata.GDataFeed instance if it is
+    impossible to determine the appropriate class (look for extra elements
+    in GDataFeed's .FindExtensions() and extension_elements[] ).
+  """
+  tree = ElementTree.fromstring(xml_string)
+  category = tree.find('{%s}category' % atom.ATOM_NAMESPACE)
+  if category is None:
+    # TODO: is this the best way to handle this?
+    return atom._CreateClassFromElementTree(GPhotosBaseFeed, tree)
+  namespace, kind = category.get('term').split('#')
+  if namespace != PHOTOS_NAMESPACE:
+    # TODO: is this the best way to handle this?
+    return atom._CreateClassFromElementTree(GPhotosBaseFeed, tree)
+  ## TODO: is getattr safe this way?
+  feed_class = getattr(gdata.photos, '%sFeed' % kind.title())
+  return atom._CreateClassFromElementTree(feed_class, tree)
+
+def AnyEntryFromString(xml_string):
+  """Creates an instance of the appropriate entry class from the
+    xml string contents.
+
+  Args:
+    xml_string: str A string which contains valid XML. The root element
+        of the XML string should match the tag and namespace of the desired
+        class.
+
+  Returns:
+    An instance of the target class with members assigned according to the
+    contents of the XML - or a basic gdata.GDataEndry instance if it is
+    impossible to determine the appropriate class (look for extra elements
+    in GDataEntry's .FindExtensions() and extension_elements[] ).
+  """
+  tree = ElementTree.fromstring(xml_string)
+  category = tree.find('{%s}category' % atom.ATOM_NAMESPACE)
+  if category is None:
+    # TODO: is this the best way to handle this?
+    return atom._CreateClassFromElementTree(GPhotosBaseEntry, tree)
+  namespace, kind = category.get('term').split('#')
+  if namespace != PHOTOS_NAMESPACE:
+    # TODO: is this the best way to handle this?
+    return atom._CreateClassFromElementTree(GPhotosBaseEntry, tree)
+  ## TODO: is getattr safe this way?
+  feed_class = getattr(gdata.photos, '%sEntry' % kind.title())
+  return atom._CreateClassFromElementTree(feed_class, tree)
+

Added: trunk/conduit/modules/GoogleModule/libgdata/gdata/photos/service.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/gdata/photos/service.py	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,700 @@
+#!/usr/bin/env python
+# -*-*- encoding: utf-8 -*-*-
+#
+# This is the service file for the Google Photo python client.
+# It is used for higher level operations.
+#
+# $Id: service.py 144 2007-10-25 21:03:34Z havard.gulldahl $
+#
+# Copyright 2007 HÃvard Gulldahl 
+#
+# 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.
+
+"""Google PhotoService provides a human-friendly interface to
+Google Photo (a.k.a Picasa Web) services[1].
+
+It extends gdata.service.GDataService and as such hides all the
+nasty details about authenticating, parsing and communicating with
+Google Photos. 
+
+[1]: http://code.google.com/apis/picasaweb/gdata.html
+
+Example:
+  import gdata.photos, gdata.photos.service
+  pws = gdata.photos.service.PhotosService()
+  pws.ClientLogin(username, password)
+  #Get all albums
+  albums = pws.GetUserFeed().entry
+  # Get all photos in second album
+  photos = pws.GetFeed(albums[1].GetPhotosUri()).entry
+  # Get all tags for photos in second album and print them
+  tags = pws.GetFeed(albums[1].GetTagsUri()).entry
+  print [ tag.summary.text for tag in tags ]
+  # Get all comments for the first photos in list and print them
+  comments = pws.GetCommentFeed(photos[0].GetCommentsUri()).entry
+  print [ c.summary.text for c in comments ]
+
+  # Get a photo to work with
+  photo = photos[0]
+  # Update metadata
+
+  # Attributes from the <gphoto:*> namespace
+  photo.summary.text = u'A nice view from my veranda'
+  photo.title.text = u'Verandaview.jpg'
+
+  # Attributes from the <media:*> namespace
+  photo.media.keywords.text = u'Home, Long-exposure, Sunset' # Comma-separated
+
+  # Adding attributes to media object
+
+  # Rotate 90 degrees clockwise
+  photo.rotation = gdata.photos.Rotation(text='90') 
+
+  # Submit modified photo object
+  photo = pws.UpdatePhotoMetadata(photo)
+  
+  # Make sure you only modify the newly returned object, else you'll get
+  # versioning errors. See Optimistic-concurrency
+
+  # Add comment to a picture
+  comment = pws.InsertComment(photo, u'I wish the water always was this warm')
+
+  # Remove comment because it was silly
+  print "*blush*"
+  pws.Delete(comment.GetEditLink().href)
+
+"""
+
+__author__ = u'havard gulldahl no'# (HÃvard Gulldahl)' #BUG: pydoc chokes on non-ascii chars in __author__
+__license__ = 'Apache License v2'
+__version__ = '$Revision: 176 $'[11:-2] 
+
+
+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
+import atom
+import gdata.photos
+
+SUPPORTED_UPLOAD_TYPES = ('bmp', 'jpeg', 'jpg', 'gif', 'png')
+
+UNKOWN_ERROR=1000
+GPHOTOS_BAD_REQUEST=400
+GPHOTOS_CONFLICT=409
+GPHOTOS_INTERNAL_SERVER_ERROR=500
+GPHOTOS_INVALID_ARGUMENT=601
+GPHOTOS_INVALID_CONTENT_TYPE=602
+GPHOTOS_NOT_AN_IMAGE=603
+GPHOTOS_INVALID_KIND=604
+
+class GooglePhotosException(Exception):
+  def __init__(self, response):
+
+    self.error_code = response['status']
+    self.reason = response['reason'].strip()
+    if '<html>' in str(response['body']): #general html message, discard it
+      response['body'] = ""
+    self.body = response['body'].strip()
+    self.message = "(%(status)s) %(body)s -- %(reason)s" % response
+
+    #return explicit error codes
+    error_map = { '(12) Not an image':GPHOTOS_NOT_AN_IMAGE,
+                  'kind: That is not one of the acceptable values':
+                      GPHOTOS_INVALID_KIND,
+                  
+                }
+    for msg, code in error_map.iteritems():
+      if self.body == msg:
+        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'
+  
+  def __init__(self, email=None, password=None, 
+    source=None, server='picasaweb.google.com', additional_headers=None):
+    """ GooglePhotosService constructor.
+      
+    Arguments:
+    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.
+
+    Returns:
+    A PhotosService object used to communicate with the Google Photos
+    service.
+    """
+    self.email = email
+    self.client = source
+    gdata.service.GDataService.__init__(self, email=self.email, password=password,
+                                        service='lh2', source=source,
+                                        server=server,
+                                        additional_headers=additional_headers)
+
+  def GetFeed(self, uri, limit=None, start_index=None):
+    """Get a feed.
+
+     The results are ordered by the values of their `updated' elements,
+     with the most recently updated entry appearing first in the feed.
+    
+    Arguments:
+    uri: the uri to fetch
+    limit (optional): the maximum number of entries to return. Defaults to what
+      the server returns.
+     
+    Returns:
+    one of gdata.photos.AlbumFeed,
+           gdata.photos.UserFeed,
+           gdata.photos.PhotoFeed,
+           gdata.photos.CommentFeed,
+           gdata.photos.TagFeed,
+      depending on the results of the query.
+    Raises:
+    GooglePhotosException
+
+    See:
+    http://code.google.com/apis/picasaweb/gdata.html#Get_Album_Feed_Manual
+    """
+    if limit is not None:
+      uri += '&max-results=%s' % limit
+    if start_index is not None:
+      uri += '&start-index=%s' % start_index
+    try:
+      return self.Get(uri, converter=gdata.photos.AnyFeedFromString)
+    except gdata.service.RequestError, e:
+      raise GooglePhotosException(e.args[0])
+
+  def GetEntry(self, uri, limit=None, start_index=None):
+    """Get an Entry.
+
+    Arguments:
+    uri: the uri to the entry
+    limit (optional): the maximum number of entries to return. Defaults to what
+      the server returns.
+     
+    Returns:
+    one of gdata.photos.AlbumEntry,
+           gdata.photos.UserEntry,
+           gdata.photos.PhotoEntry,
+           gdata.photos.CommentEntry,
+           gdata.photos.TagEntry,
+      depending on the results of the query.
+    Raises:
+    GooglePhotosException
+    """
+    if limit is not None:
+      uri += '&max-results=%s' % limit
+    if start_index is not None:
+      uri += '&start-index=%s' % start_index
+    try:
+      return self.Get(uri, converter=gdata.photos.AnyEntryFromString)
+    except gdata.service.RequestError, e:
+      raise GooglePhotosException(e.args[0])
+      
+  def GetUserFeed(self, kind='album', user='default', limit=None):
+    """Get user-based feed, containing albums, photos, comments or tags;
+      defaults to albums.
+
+    The entries are ordered by the values of their `updated' elements,
+    with the most recently updated entry appearing first in the feed.
+    
+    Arguments:
+    kind: the kind of entries to get, either `album', `photo',
+      `comment' or `tag', or a python list of these. Defaults to `album'.
+    user (optional): whose albums we're querying. Defaults to current user.
+    limit (optional): the maximum number of entries to return.
+      Defaults to everything the server returns.
+
+     
+    Returns:
+    gdata.photos.UserFeed, containing appropriate Entry elements
+
+    See:
+    http://code.google.com/apis/picasaweb/gdata.html#Get_Album_Feed_Manual
+    http://googledataapis.blogspot.com/2007/07/picasa-web-albums-adds-new-api-features.html
+    """
+    if isinstance(kind, (list, tuple) ):
+      kind = ",".join(kind)
+    
+    uri = '/data/feed/api/user/%s?kind=%s' % (user, kind)
+    return self.GetFeed(uri, limit=limit)
+  
+  def GetTaggedPhotos(self, tag, user='default', limit=None):
+    """Get all photos belonging to a specific user, tagged by the given keyword
+
+    Arguments:
+    tag: The tag you're looking for, e.g. `dog'
+    user (optional): Whose images/videos you want to search, defaults
+      to current user
+    limit (optional): the maximum number of entries to return.
+      Defaults to everything the server returns.
+
+    Returns:
+    gdata.photos.UserFeed containing PhotoEntry elements
+    """
+    # Lower-casing because of
+    #   http://code.google.com/p/gdata-issues/issues/detail?id=194
+    uri = '/data/feed/api/user/%s?kind=photo&tag=%s' % (user, tag.lower())
+    return self.GetFeed(uri, limit)
+
+  def SearchUserPhotos(self, query, user='default', limit=100):
+    """Search through all photos for a specific user and return a feed.
+    This will look for matches in file names and image tags (a.k.a. keywords)
+
+    Arguments:
+    query: The string you're looking for, e.g. `vacation'
+    user (optional): The username of whose photos you want to search, defaults
+      to current user.
+    limit (optional): Don't return more than `limit' hits, defaults to 100
+
+    Only public photos are searched, unless you are authenticated and
+    searching through your own photos.
+
+    Returns:
+    gdata.photos.UserFeed with PhotoEntry elements
+    """
+    uri = '/data/feed/api/user/%s?kind=photo&q=%s' % (user, query)
+    return self.GetFeed(uri, limit=limit)
+
+  def SearchCommunityPhotos(self, query, limit=100):
+    """Search through all public photos and return a feed.
+    This will look for matches in file names and image tags (a.k.a. keywords)
+
+    Arguments:
+    query: The string you're looking for, e.g. `vacation'
+    limit (optional): Don't return more than `limit' hits, defaults to 100
+
+    Returns:
+    gdata.GDataFeed with PhotoEntry elements
+    """
+    uri='/data/feed/api/all?q=%s' % query
+    return self.GetFeed(uri, limit=limit)
+
+  def GetContacts(self, user='default', limit=None):
+    """Retrieve a feed that contains a list of your contacts
+
+    Arguments:
+    user: Username of the user whose contacts you want
+
+    Returns
+    gdata.photos.UserFeed, with UserEntry entries
+
+    See:
+    http://groups.google.com/group/Google-Picasa-Data-API/msg/819b0025b5ff5e38
+    """
+    uri = '/data/feed/api/user/%s/contacts?kind=user' % user
+    return self.GetFeed(uri, limit=limit)
+
+  def SearchContactsPhotos(self, user='default', search=None, limit=None):
+    """Search over your contacts' photos and return a feed
+
+    Arguments:
+    user: Username of the user whose contacts you want
+    search (optional): What to search for (photo title, description and keywords)
+
+    Returns
+    gdata.photos.UserFeed, with PhotoEntry elements
+
+    See:
+    http://groups.google.com/group/Google-Picasa-Data-API/msg/819b0025b5ff5e38
+    """
+
+    uri = '/data/feed/api/user/%s/contacts?kind=photo&q=%s' % (user, search)
+    return self.GetFeed(uri, limit=limit)
+
+  def InsertAlbum(self, title, summary, location=None, access='public',
+    commenting_enabled='true', timestamp=None):
+    """Add an album.
+
+    Needs authentication, see self.ClientLogin()
+
+    Arguments:
+    title: Album title 
+    summary: Album summary / description
+    access (optional): `private' or `public'. Public albums are searchable
+      by everyone on the internet. Defaults to `public'
+    commenting_enabled (optional): `true' or `false'. Defaults to `true'.
+    timestamp (optional): A date and time for the album, in milliseconds since
+      Unix epoch[1] UTC. Defaults to now.
+
+    Returns:
+    The newly created gdata.photos.AlbumEntry
+
+    See:
+    http://code.google.com/apis/picasaweb/gdata.html#Add_Album_Manual_Installed
+
+    [1]: http://en.wikipedia.org/wiki/Unix_epoch
+    """
+    album = gdata.photos.AlbumEntry()
+    album.title = atom.Title(text=title, title_type='text')
+    album.summary = atom.Summary(text=summary, summary_type='text')
+    if location is not None:
+      album.location = gdata.photos.Location(text=location)
+    album.access = gdata.photos.Access(text=access)
+    if commenting_enabled in ('true', 'false'):
+      album.commentingEnabled = gdata.photos.CommentingEnabled(text=commenting_enabled)
+    if timestamp is None:
+      timestamp = '%i' % int(time.time() * 1000)
+    album.timestamp = gdata.photos.Timestamp(text=timestamp)
+    try:
+      return self.Post(album, uri=self.userUri % self.email,
+      converter=gdata.photos.AlbumEntryFromString)
+    except gdata.service.RequestError, e:
+      raise GooglePhotosException(e.args[0])
+
+  def InsertPhoto(self, album_or_uri, photo, filename_or_handle,
+    content_type='image/jpeg'):
+    """Add a PhotoEntry
+
+    Needs authentication, see self.ClientLogin()
+
+    Arguments:
+    album_or_uri: AlbumFeed or uri of the album where the photo should go
+    photo: PhotoEntry to add
+    filename_or_handle: A file-like object or file name where the image/video
+      will be read from
+    content_type (optional): Internet media type (a.k.a. mime type) of
+      media object. Currently Google Photos supports these types:
+       o image/bmp
+       o image/gif
+       o image/jpeg
+       o image/png
+       
+      Images will be converted to jpeg on upload. Defaults to `image/jpeg'
+
+    """
+
+    try:
+      assert(isinstance(photo, gdata.photos.PhotoEntry))
+    except AssertionError:
+      raise GooglePhotosException({'status':GPHOTOS_INVALID_ARGUMENT,
+        'body':'`photo` must be a gdata.photos.PhotoEntry instance',
+        'reason':'Found %s, not PhotoEntry' % type(photo)
+        })
+    try:
+      majtype, mintype = content_type.split('/')
+      assert(mintype in SUPPORTED_UPLOAD_TYPES)
+    except (ValueError, AssertionError):
+      raise GooglePhotosException({'status':GPHOTOS_INVALID_CONTENT_TYPE,
+        'body':'This is not a valid content type: %s' % content_type,
+        'reason':'Accepted content types: %s' % \
+          ['image/'+t for t in SUPPORTED_UPLOAD_TYPES]
+        })
+    if isinstance(filename_or_handle, (str, unicode)) and \
+      os.path.exists(filename_or_handle): # it's a file name
+      mediasource = gdata.MediaSource()
+      mediasource.setFile(filename_or_handle, content_type)
+    elif hasattr(filename_or_handle, 'read'):# it's a file-like resource
+      if hasattr(filename_or_handle, 'seek'):
+        filename_or_handle.seek(0) # rewind pointer to the start of the file
+      # gdata.MediaSource needs the content length, so read the whole image 
+      file_handle = StringIO.StringIO(filename_or_handle.read()) 
+      name = 'image'
+      if hasattr(filename_or_handle, 'name'):
+        name = filename_or_handle.name
+      mediasource = gdata.MediaSource(file_handle, content_type,
+        content_length=file_handle.len, file_name=name)
+    else: #filename_or_handle is not valid
+      raise GooglePhotosException({'status':GPHOTOS_INVALID_ARGUMENT,
+        'body':'`filename_or_handle` must be a path name or a file-like object',
+        'reason':'Found %s, not path name or object with a .read() method' % \
+          type(filename_or_handle)
+        })
+    
+    if isinstance(album_or_uri, (str, unicode)): # it's a uri
+      feed_uri = album_or_uri
+    elif hasattr(album_or_uri, 'GetFeedLink'): # it's a AlbumFeed object
+      feed_uri = album_or_uri.GetFeedLink().href
+  
+    try:
+      return self.Post(photo, uri=feed_uri, media_source=mediasource,
+        converter=gdata.photos.PhotoEntryFromString)
+    except gdata.service.RequestError, e:
+      raise GooglePhotosException(e.args[0])
+  
+  def InsertPhotoSimple(self, album_or_uri, title, summary, filename_or_handle,
+      content_type='image/jpeg', keywords=None):
+    """Add a photo without constructing a PhotoEntry.
+
+    Needs authentication, see self.ClientLogin()
+
+    Arguments:
+    album_or_uri: AlbumFeed or uri of the album where the photo should go
+    title: Photo title
+    summary: Photo summary / description
+    filename_or_handle: A file-like object or file name where the image/video
+      will be read from
+    content_type (optional): Internet media type (a.k.a. mime type) of
+      media object. Currently Google Photos supports these types:
+       o image/bmp
+       o image/gif
+       o image/jpeg
+       o image/png
+       
+      Images will be converted to jpeg on upload. Defaults to `image/jpeg'
+    keywords (optional): a 1) comma separated string or 2) a python list() of
+      keywords (a.k.a. tags) to add to the image.
+      E.g. 1) `dog, vacation, happy' 2) ['dog', 'happy', 'vacation']
+    
+    Returns:
+    The newly created gdata.photos.PhotoEntry or GooglePhotosException on errors
+
+    See:
+    http://code.google.com/apis/picasaweb/gdata.html#Add_Album_Manual_Installed
+    [1]: http://en.wikipedia.org/wiki/Unix_epoch
+    """
+    
+    metadata = gdata.photos.PhotoEntry()
+    metadata.title=atom.Title(text=title)
+    metadata.summary = atom.Summary(text=summary, summary_type='text')
+    if keywords is not None:
+      if isinstance(keywords, list):
+        keywords = ','.join(keywords)
+      metadata.media.keywords = gdata.photos.media.Keywords(text=keywords)
+    return self.InsertPhoto(album_or_uri, metadata, filename_or_handle,
+      content_type)
+
+  def UpdatePhotoMetadata(self, photo):
+    """Update a photo's metadata. 
+
+     Needs authentication, see self.ClientLogin()
+
+     You can update any or all of the following metadata properties:
+      * <title>
+      * <media:description>
+      * <gphoto:checksum>
+      * <gphoto:client>
+      * <gphoto:rotation>
+      * <gphoto:timestamp>
+      * <gphoto:commentingEnabled>
+
+      Arguments:
+      photo: a gdata.photos.PhotoEntry object with updated elements
+
+      Returns:
+      The modified gdata.photos.PhotoEntry
+
+      Example:
+      p = GetFeed(uri).entry[0]
+      p.title.text = u'My new text'
+      p.commentingEnabled.text = 'false'
+      p = UpdatePhotoMetadata(p)
+
+      It is important that you don't keep the old object around, once
+      it has been updated. See
+      http://code.google.com/apis/gdata/reference.html#Optimistic-concurrency
+      """
+    try:
+      return self.Put(data=photo, uri=photo.GetEditLink().href,
+      converter=gdata.photos.PhotoEntryFromString)
+    except gdata.service.RequestError, e:
+      raise GooglePhotosException(e.args[0])
+
+  
+  def UpdatePhotoBlob(self, photo_or_uri, filename_or_handle,
+                      content_type = 'image/jpeg'):
+    """Update a photo's binary data.
+
+    Needs authentication, see self.ClientLogin()
+
+    Arguments:
+    photo_or_uri: a gdata.photos.PhotoEntry that will be updated, or a
+      `edit-media' uri pointing to it
+    filename_or_handle:  A file-like object or file name where the image/video
+      will be read from
+    content_type (optional): Internet media type (a.k.a. mime type) of
+      media object. Currently Google Photos supports these types:
+       o image/bmp
+       o image/gif
+       o image/jpeg
+       o image/png
+    Images will be converted to jpeg on upload. Defaults to `image/jpeg'
+
+    Returns:
+    The modified gdata.photos.PhotoEntry
+
+    Example:
+    p = GetFeed(PhotoUri)
+    p = UpdatePhotoBlob(p, '/tmp/newPic.jpg')
+
+    It is important that you don't keep the old object around, once
+    it has been updated. See
+    http://code.google.com/apis/gdata/reference.html#Optimistic-concurrency
+    """
+
+    try:  
+      majtype, mintype = content_type.split('/')
+      assert(mintype in SUPPORTED_UPLOAD_TYPES)
+    except (ValueError, AssertionError):
+      raise GooglePhotosException({'status':GPHOTOS_INVALID_CONTENT_TYPE,
+        'body':'This is not a valid content type: %s' % content_type,
+        'reason':'Accepted content types: %s' % \
+          ['image/'+t for t in SUPPORTED_UPLOAD_TYPES]
+        })
+    
+    if isinstance(filename_or_handle, (str, unicode)) and \
+      os.path.exists(filename_or_handle): # it's a file name
+      photoblob = gdata.MediaSource()
+      photoblob.setFile(filename_or_handle, content_type)
+    elif hasattr(filename_or_handle, 'read'):# it's a file-like resource
+      if hasattr(filename_or_handle, 'seek'):
+        filename_or_handle.seek(0) # rewind pointer to the start of the file
+      # gdata.MediaSource needs the content length, so read the whole image 
+      file_handle = StringIO.StringIO(filename_or_handle.read()) 
+      name = 'image'
+      if hasattr(filename_or_handle, 'name'):
+        name = filename_or_handle.name
+      mediasource = gdata.MediaSource(file_handle, content_type,
+        content_length=file_handle.len, file_name=name)
+    else: #filename_or_handle is not valid
+      raise GooglePhotosException({'status':GPHOTOS_INVALID_ARGUMENT,
+        'body':'`filename_or_handle` must be a path name or a file-like object',
+        'reason':'Found %s, not path name or an object with .read() method' % \
+          type(filename_or_handle)
+        })
+    
+    if isinstance(photo_or_uri, (str, unicode)):
+      entry_uri = photo_or_uri # it's a uri
+    elif hasattr(photo_or_uri, 'GetEditMediaLink'):
+      entry_uri = photo_or_uri.GetEditMediaLink().href
+    try:
+      return self.Put(photoblob, entry_uri,
+      converter=gdata.photos.PhotoEntryFromString)
+    except gdata.service.RequestError, e:
+      raise GooglePhotosException(e.args[0])
+
+  def InsertTag(self, photo_or_uri, tag):
+    """Add a tag (a.k.a. keyword) to a photo.
+
+    Needs authentication, see self.ClientLogin()
+
+    Arguments:
+    photo_or_uri: a gdata.photos.PhotoEntry that will be tagged, or a
+      `post' uri pointing to it
+    (string) tag: The tag/keyword
+
+    Returns:
+    The new gdata.photos.TagEntry
+
+    Example:
+    p = GetFeed(PhotoUri)
+    tag = InsertTag(p, 'Beautiful sunsets')
+
+    """
+    tag = gdata.photos.TagEntry(title=atom.Title(text=tag))
+    if isinstance(photo_or_uri, (str, unicode)):
+      post_uri = photo_or_uri # it's a uri
+    elif hasattr(photo_or_uri, 'GetEditMediaLink'):
+      post_uri = photo_or_uri.GetPostLink().href
+    try:
+      return self.Post(data=tag, uri=post_uri,
+      converter=gdata.photos.TagEntryFromString)
+    except gdata.service.RequestError, e:
+      raise GooglePhotosException(e.args[0])
+
+                  
+  def InsertComment(self, photo_or_uri, comment):
+    """Add a comment to a photo.
+
+    Needs authentication, see self.ClientLogin()
+
+    Arguments:
+    photo_or_uri: a gdata.photos.PhotoEntry that is about to be commented
+      , or a `post' uri pointing to it
+    (string) comment: The actual comment
+
+    Returns:
+    The new gdata.photos.CommentEntry
+
+    Example:
+    p = GetFeed(PhotoUri)
+    tag = InsertComment(p, 'OOOH! I would have loved to be there.
+      Who's that in the back?')
+
+    """
+    comment = gdata.photos.CommentEntry(content=atom.Content(text=comment))
+    if isinstance(photo_or_uri, (str, unicode)):
+      post_uri = photo_or_uri # it's a uri
+    elif hasattr(photo_or_uri, 'GetEditMediaLink'):
+      post_uri = photo_or_uri.GetPostLink().href
+    try:
+      return self.Post(data=comment, uri=post_uri,
+        converter=gdata.photos.CommentEntryFromString)
+    except gdata.service.RequestError, e:
+      raise GooglePhotosException(e.args[0])
+
+  def Delete(self, object_or_uri, *args, **kwargs):
+    """Delete an object.
+
+    Re-implementing the GDataService.Delete method, to add some
+    convenience.
+
+    Arguments:
+    object_or_uri: Any object that has a GetEditLink() method that
+      returns a link, or a uri to that object.
+
+    Returns:
+    ? or GooglePhotosException on errors
+    """
+    try:
+      uri = object_or_uri.GetEditLink().href
+    except AttributeError:
+      uri = object_or_uri
+    try:
+      return gdata.service.GDataService.Delete(self, uri, *args, **kwargs)
+    except gdata.service.RequestError, e:
+      raise GooglePhotosException(e.args[0])
+
+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 """
+  r = {}
+  for thumb in media_thumbnail_list:
+      r[int(thumb.width)*int(thumb.height)] = thumb
+  keys = r.keys()
+  keys.sort()
+  return r[keys[0]]
+
+def ConvertAtomTimestampToEpoch(timestamp):
+  """Helper function to convert a timestamp string, for instance
+    from atom:updated or atom:published, to milliseconds since Unix epoch
+    (a.k.a. POSIX time).
+
+    `2007-07-22T00:45:10.000Z' -> """
+  return time.mktime(time.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.000Z'))
+  ## TODO: Timezone aware

Added: trunk/conduit/modules/GoogleModule/libgdata/gdata/service.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/libgdata/gdata/service.py	Sat Jan 19 23:45:12 2008
@@ -0,0 +1,1121 @@
+#!/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.
+
+
+"""GDataService provides CRUD ops. and programmatic login for GData services.
+
+  Error: A base exception class for all exceptions in the gdata_client
+         module.
+
+  CaptchaRequired: This exception is thrown when a login attempt results in a
+                   captcha challenge from the ClientLogin service. When this
+                   exception is thrown, the captcha_token and captcha_url are
+                   set to the values provided in the server's response.
+
+  BadAuthentication: Raised when a login attempt is made with an incorrect
+                     username or password.
+
+  NotAuthenticated: Raised if an operation requiring authentication is called
+                    before a user has authenticated.
+
+  NonAuthSubToken: Raised if a method to modify an AuthSub token is used when
+                   the user is either not authenticated or is authenticated
+                   through programmatic login.
+
+  RequestError: Raised if a CRUD request returned a non-success code. 
+
+  UnexpectedReturnType: Raised if the response from the server was not of the
+                        desired type. For example, this would be raised if the
+                        server sent a feed when the client requested an entry.
+
+  GDataService: Encapsulates user credentials needed to perform insert, update
+                and delete operations with the GData API. An instance can
+                perform user authentication, query, insertion, deletion, and 
+                update.
+
+  Query: Eases query URI creation by allowing URI parameters to be set as 
+         dictionary attributes. For example a query with a feed of 
+         '/base/feeds/snippets' and ['bq'] set to 'digital camera' will 
+         produce '/base/feeds/snippets?bq=digital+camera' when .ToUri() is 
+         called on it.
+"""
+
+__author__ = 'api.jscudder (Jeffrey Scudder)'
+
+
+import re
+import httplib
+import urllib
+try:
+  from xml.etree import cElementTree as ElementTree
+except ImportError:
+  try:
+    import cElementTree as ElementTree
+  except ImportError:
+    from elementtree import ElementTree
+import atom.service
+import gdata
+import atom
+import gdata.auth
+
+
+PROGRAMMATIC_AUTH_LABEL = 'GoogleLogin auth'
+AUTHSUB_AUTH_LABEL = 'AuthSub token'
+AUTH_SERVER_HOST = 'https://www.google.com/'
+
+
+class Error(Exception):
+  pass
+
+
+class CaptchaRequired(Error):
+  pass
+
+
+class BadAuthentication(Error):
+  pass
+
+
+class NotAuthenticated(Error):
+  pass
+
+
+class NonAuthSubToken(Error):
+  pass
+
+
+class RequestError(Error):
+  pass
+
+
+class UnexpectedReturnType(Error):
+  pass
+
+
+class GDataService(atom.service.AtomService):
+  """Contains elements needed for GData login and CRUD request headers.
+
+  Maintains additional headers (tokens for example) needed for the GData 
+  services to allow a user to perform inserts, updates, and deletes.
+  """
+
+  def __init__(self, email=None, password=None, account_type='HOSTED_OR_GOOGLE',
+               service=None, source=None, server=None, 
+               additional_headers=None):
+    """Creates an object of type GDataService.
+
+    Args:
+      email: string (optional) The user's email address, used for
+             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'.
+      service: string (optional) The desired service for which credentials
+               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'.
+      additional_headers: dictionary (optional) Any additional headers which 
+                          should be included with CRUD operations.
+    """
+
+    self.email = email
+    self.password = password
+    self.account_type = account_type
+    self.service = service
+    self.server = server
+    self.additional_headers = additional_headers or {}
+    self.__SetSource(source)
+    self.__auth_token = None
+    self.__captcha_token = None
+    self.__captcha_url = None
+    self.__gsessionid = None
+ 
+  # Define properties for GDataService
+  def _SetAuthSubToken(self, auth_token):
+    """Sets the token sent in requests to an AuthSub token.
+
+    Only use this method if you have received a token from the AuthSub 
+    service. The auth_token is set automatically when ProgrammaticLogin()
+    is used. See documentation for Google AuthSub here:
+    http://code.google.com/apis/accounts/AuthForWebApps.html .
+
+    Args:
+      auth_token: string The token returned by the AuthSub service.
+    """
+
+    self.__auth_token = '%s=%s' % (AUTHSUB_AUTH_LABEL, auth_token)
+    # The auth token is only set externally when using AuthSub authentication,
+    # so set the auth_type to indicate AuthSub.
+
+  def __SetAuthSubToken(self, auth_token):
+    self._SetAuthSubToken(auth_token)
+
+  def _GetAuthToken(self):
+    """Returns the auth token used for authenticating requests.
+
+    Returns:
+      string
+    """
+
+    return self.__auth_token
+
+  def __GetAuthToken(self):
+    return self._GetAuthToken()
+
+  auth_token = property(__GetAuthToken, __SetAuthSubToken,
+      doc="""Get or set the token used for authentication.""")
+
+  def _GetCaptchaToken(self):
+    """Returns a captcha token if the most recent login attempt generated one.
+
+    The captcha token is only set if the Programmatic Login attempt failed 
+    because the Google service issued a captcha challenge.
+
+    Returns:
+      string
+    """
+
+    return self.__captcha_token
+
+  def __GetCaptchaToken(self):
+    return self._GetCaptchaToken()
+
+  captcha_token = property(__GetCaptchaToken,
+      doc="""Get the captcha token for a login request.""")
+
+  def _GetCaptchaURL(self):
+    """Returns the URL of the captcha image if a login attempt generated one.
+     
+    The captcha URL is only set if the Programmatic Login attempt failed
+    because the Google service issued a captcha challenge.
+
+    Returns:
+      string
+    """
+
+    return self.__captcha_url
+
+  def __GetCaptchaURL(self):
+    return self._GetCaptchaURL()
+
+  captcha_url = property(__GetCaptchaURL,
+      doc="""Get the captcha URL for a login request.""")
+
+  def GetAuthSubToken(self):
+    if self.__auth_token.startswith(AUTHSUB_AUTH_LABEL):
+      return self.__auth_token.lstrip(AUTHSUB_AUTH_LABEL + '=')
+    else:
+      return None
+
+  def SetAuthSubToken(self, token):
+    self.__auth_token = '%s=%s' % (AUTHSUB_AUTH_LABEL, token)
+
+  def GetClientLoginToken(self):
+    if self.__auth_token.startswith(PROGRAMMATIC_AUTH_LABEL):
+      return self.__auth_token.lstrip(PROGRAMMATIC_AUTH_LABEL + '=')
+    else:
+      return None
+
+  def SetClientLoginToken(self, token):
+    self.__auth_token = '%s=%s' % (PROGRAMMATIC_AUTH_LABEL, token)
+
+  # Private methods to create the source property.
+  def __GetSource(self):
+    return self.__source
+
+  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
+
+  source = property(__GetSource, __SetSource, 
+      doc="""The source is the name of the application making the request. 
+             It should be in the form company_id-app_name-app_version""")
+
+  # Authentication operations
+
+  def ProgrammaticLogin(self, captcha_token=None, captcha_response=None):
+    """Authenticates the user and sets the GData Auth token.
+
+    Login retreives a temporary auth token which must be used with all
+    requests to GData services. The auth token is stored in the GData client
+    object.
+
+    Login is also used to respond to a captcha challenge. If the user's login
+    attempt failed with a CaptchaRequired error, the user can respond by
+    calling Login with the captcha token and the answer to the challenge.
+
+    Args:
+      captcha_token: string (optional) The identifier for the captcha challenge
+                     which was presented to the user.
+      captcha_response: string (optional) The user's answer to the captch 
+                        challenge.
+
+    Raises:
+      CaptchaRequired if the login service will require a captcha response
+      BadAuthentication if the login service rejected the username or password
+      Error if the login service responded with a 403 different from the above
+    """
+    request_body = gdata.auth.GenerateClientLoginRequestBody(self.email, 
+        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()
+    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, 
+          captcha_base_url='%saccounts/' % AUTH_SERVER_HOST)
+      if captcha_parameters:
+        self.__captcha_token = captcha_parameters['token']
+        self.__captcha_url = captcha_parameters['url']
+        raise CaptchaRequired, 'Captcha Required'
+      elif response_body.splitlines()[0] == 'Error=BadAuthentication':
+        self.__captcha_token = None
+        self.__captcha_url = None
+        raise BadAuthentication, 'Incorrect username or password'
+      else:
+        self.__captcha_token = None
+        self.__captcha_url = None
+        raise Error, 'Server responded with a 403 code'
+
+  def ClientLogin(self, username, password, account_type=None, service=None,
+      source=None, captcha_token=None, captcha_response=None):
+    """Convenience method for authenticating using ProgrammaticLogin. 
+    
+    Sets values for email, password, and other optional members.
+
+    Args:
+      username:
+      password:
+      account_type: string (optional)
+      service: string (optional)
+      captcha_token: string (optional)
+      captcha_response: string (optional)
+    """
+    self.email = username
+    self.password = password
+
+    if account_type:
+      self.account_type = account_type
+    if service:
+      self.service = service
+    if source:
+      self.source = source
+
+    self.ProgrammaticLogin(captcha_token, captcha_response)
+
+  def GenerateAuthSubURL(self, next, scope, secure=False, session=True):
+    """Generate a URL at which the user will login and be redirected back.
+
+    Users enter their credentials on a Google login page and a token is sent
+    to the URL specified in next. See documentation for AuthSub login at:
+    http://code.google.com/apis/accounts/AuthForWebApps.html
+
+    Args:
+      next: string The URL user will be sent to after logging in.
+      scope: string The URL of the service to be accessed.
+      secure: boolean (optional) Determines whether or not the issued token
+              is a secure token.
+      session: boolean (optional) Determines whether or not the issued token
+               can be upgraded to a session token.
+    """
+
+    # Translate True/False values for parameters into numeric values acceoted
+    # by the AuthSub service.
+    if secure:
+      secure = 1
+    else:
+      secure = 0
+
+    if session:
+      session = 1
+    else:
+      session = 0
+
+    request_params = urllib.urlencode({'next': next, 'scope': scope,
+                                    'secure': secure, 'session': session})
+    return '%saccounts/AuthSubRequest?%s' % (AUTH_SERVER_HOST, request_params)
+
+  def UpgradeToSessionToken(self):
+    """Upgrades a single use AuthSub token to a session token.
+
+    Raises:
+      NonAuthSubToken if the user's auth token is not an AuthSub token
+    """
+   
+    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_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=')
+
+  def RevokeAuthSubToken(self):
+    """Revokes an existing AuthSub token.
+
+    Raises:
+      NonAuthSubToken if the user's auth token is not an AuthSub token
+    """
+
+    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()
+    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):
+    """Query the GData API with the given URI
+
+    The uri is the portion of the URI after the server value 
+    (ex: www.google.com).
+
+    To perform a query against Google Base, set the server to 
+    'base.google.com' and set the uri to '/base/feeds/...', where ... is 
+    your query. For example, to find snippets for all digital cameras uri 
+    should be set to: '/base/feeds/snippets?bq=digital+camera'
+
+    Args:
+      uri: string The query in the form of a URI. Example:
+           '/base/feeds/snippets?bq=digital+camera'.
+      extra_headers: dictionary (optional) Extra HTTP headers to be included
+                     in the GET request. These headers are in addition to 
+                     those stored in the client's additional_headers property.
+                     The client automatically sets the Content-Type and 
+                     Authorization headers.
+      redirects_remaining: int (optional) Tracks the number of additional
+          redirects this method will allow. If the service object receives
+          a redirect and remaining is 0, it will not follow the redirect. 
+          This was added to avoid infinite redirect loops.
+      encoding: string (optional) The character encoding for the server's
+          response. Default is UTF-8
+      converter: func (optional) A function which will transform
+          the server's results before it is returned. Example: use 
+          GDataFeedFromString to parse the server response as if it
+          were a GDataFeed.
+
+    Returns:
+      If there is no ResultsTransformer specified in the call, a GDataFeed 
+      or GDataEntry depending on which is sent from the server. If the 
+      response is niether a feed or entry and there is no ResultsTransformer,
+      return a string. If there is a ResultsTransformer, the returned value 
+      will be that of the ResultsTransformer function.
+    """
+
+    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,)
+
+    server_response = atom.service.AtomService.Get(self, uri, extra_headers)
+    result_body = server_response.read()
+
+    if server_response.status == 200:
+      if converter:
+        return converter(result_body)
+      # There was no ResultsTransformer specified, so try to convert the
+      # server's response into a GDataFeed.
+      feed = gdata.GDataFeedFromString(result_body)
+      if not feed:
+        # If conversion to a GDataFeed failed, try to convert the server's
+        # response to a GDataEntry.
+        entry = gdata.GDataEntryFromString(result_body)
+        if not entry:
+          # The server's response wasn't a feed, or an entry, so return the
+          # response body as a string.
+          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.Get(location, extra_headers, redirects_remaining - 1, 
+              encoding=encoding, 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}
+
+  def GetMedia(self, uri, extra_headers=None):
+    """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()
+    return gdata.MediaSource(response_handle, response_handle.getheader('Content-Type'),
+        response_handle.getheader('Content-Length'))
+
+  def GetEntry(self, uri, extra_headers=None):
+    """Query the GData API with the given URI and receive an Entry.
+    
+    See also documentation for gdata.service.Get
+
+    Args:
+      uri: string The query in the form of a URI. Example:
+           '/base/feeds/snippets?bq=digital+camera'.
+      extra_headers: dictionary (optional) Extra HTTP headers to be included
+                     in the GET request. These headers are in addition to
+                     those stored in the client's additional_headers property.
+                     The client automatically sets the Content-Type and
+                     Authorization headers.
+
+    Returns:
+      A GDataEntry built from the XML in the server's response.
+    """
+
+    result = self.Get(uri, extra_headers, converter=atom.EntryFromString)
+    if isinstance(result, atom.Entry):
+      return result
+    else:
+      raise UnexpectedReturnType, 'Server did not send an entry' 
+
+  def GetFeed(self, uri, extra_headers=None, 
+              converter=gdata.GDataFeedFromString):
+    """Query the GData API with the given URI and receive a Feed.
+
+    See also documentation for gdata.service.Get
+
+    Args:
+      uri: string The query in the form of a URI. Example:
+           '/base/feeds/snippets?bq=digital+camera'.
+      extra_headers: dictionary (optional) Extra HTTP headers to be included
+                     in the GET request. These headers are in addition to
+                     those stored in the client's additional_headers property.
+                     The client automatically sets the Content-Type and
+                     Authorization headers.
+
+    Returns:
+      A GDataFeed built from the XML in the server's response.
+    """
+
+    result = self.Get(uri, extra_headers, converter=converter)
+    if isinstance(result, atom.Feed):
+      return result
+    else:
+      raise UnexpectedReturnType, 'Server did not send a feed'  
+
+  def GetNext(self, feed):
+    """Requests the next 'page' of results in the feed.
+    
+    This method uses the feed's next link to request an additional feed
+    and uses the class of the feed to convert the results of the GET request.
+
+    Args:
+      feed: atom.Feed or a subclass. The feed should contain a next link and
+          the type of the feed will be applied to the results from the 
+          server. The new feed which is returned will be of the same class
+          as this feed which was passed in.
+
+    Returns:
+      A new feed representing the next set of results in the server's feed.
+      The type of this feed will match that of the feed argument.
+    """
+    next_link = feed.GetNextLink()
+    # Create a closure which will convert an XML string to the class of
+    # the feed object passed in.
+    def ConvertToFeedClass(xml_string):
+      return atom.CreateClassFromXMLString(feed.__class__, xml_string)
+    # Make a GET request on the next link and use the above closure for the
+    # converted which processes the XML string from the server.
+    if next_link and next_link.href:
+      return self.Get(next_link.href, converter=ConvertToFeedClass)
+    else:
+      return None
+
+  def Post(self, 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:
+      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).
+    """
+    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 data and media_source:
+      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, '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()
+      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,
+          '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()
+      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)
+      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:
+      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.Post(data, location, extra_headers, url_params,
+              escape_params, redirects_remaining - 1, 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}
+
+  def Put(self, data, uri, extra_headers=None, url_params=None, 
+          escape_params=True, redirects_remaining=3, media_source=None,
+          converter=None):
+    """Updates an entry at the given URI.
+     
+    Args:
+      data: string, ElementTree._Element, or xml_wrapper.ElementWrapper The 
+            XML containing the updated data.
+      uri: string A URI indicating entry to which the update will be applied.
+           Example: '/base/feeds/items/ITEM-ID'
+      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.
+      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 put succeeded, this method will return a GDataFeed, GDataEntry,
+      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}
+
+  def Delete(self, uri, extra_headers=None, url_params=None, 
+             escape_params=True, redirects_remaining=4):
+    """Deletes the entry at the given URI.
+
+    Args:
+      uri: string The URI of the entry to be deleted. Example: 
+           '/base/feeds/items/ITEM-ID'
+      extra_headers: dict (optional) HTTP headers which are to be included.
+                     The client automatically sets the Content-Type and
+                     Authorization 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.
+
+    Returns:
+      True if the entry was deleted.
+    """
+    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,)
+                                                  
+    server_response = atom.service.AtomService.Delete(self, uri,
+        extra_headers, url_params, escape_params)
+    result_body = server_response.read()
+
+    if server_response.status == 200:
+      return True
+    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.Delete(location, extra_headers, url_params,
+              escape_params, redirects_remaining - 1)
+        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}
+
+
+class Query(dict):
+  """Constructs a query URL to be used in GET requests
+  
+  Url parameters are created by adding key-value pairs to this object as a 
+  dict. For example, to add &max-results=25 to the URL do
+  my_query['max-results'] = 25
+
+  Category queries are created by adding category strings to the categories
+  member. All items in the categories list will be concatenated with the /
+  symbol (symbolizing a category x AND y restriction). If you would like to OR
+  2 categories, append them as one string with a | between the categories. 
+  For example, do query.categories.append('Fritz|Laurie') to create a query
+  like this feed/-/Fritz%7CLaurie . This query will look for results in both
+  categories.
+  """
+
+  def __init__(self, feed=None, text_query=None, params=None, 
+      categories=None):
+    """Constructor for Query
+    
+    Args:
+      feed: str (optional) The path for the feed (Examples: 
+          '/base/feeds/snippets' or 'calendar/feeds/jo gmail com/private/full'
+      text_query: str (optional) The contents of the q query parameter. The
+          contents of the text_query are 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 (key-value pairs).
+      categories: list (optional) List of category strings which should be
+          included as query categories. See 
+          http://code.google.com/apis/gdata/reference.html#Queries for 
+          details. If you want to get results from category A or B (both 
+          categories), specify a single list item 'A|B'. 
+    """
+    
+    self.feed = feed
+    self.categories = []
+    if text_query:
+      self.text_query = text_query
+    if isinstance(params, dict):
+      for param in params:
+        self[param] = params[param]
+    if isinstance(categories, list):
+      for category in categories:
+        self.categories.append(category)
+
+  def _GetTextQuery(self):
+    if 'q' in self.keys():
+      return self['q']
+    else:
+      return None
+
+  def _SetTextQuery(self, query):
+    self['q'] = query
+
+  text_query = property(_GetTextQuery, _SetTextQuery, 
+      doc="""The feed query's q parameter""")
+
+  def _GetAuthor(self):
+    if 'author' in self.keys():
+      return self['author']
+    else:
+      return None
+
+  def _SetAuthor(self, query):
+    self['author'] = query
+
+  author = property(_GetAuthor, _SetAuthor,
+      doc="""The feed query's author parameter""")
+
+  def _GetAlt(self):
+    if 'alt' in self.keys():
+      return self['alt']
+    else:
+      return None
+
+  def _SetAlt(self, query):
+    self['alt'] = query
+
+  alt = property(_GetAlt, _SetAlt,
+      doc="""The feed query's alt parameter""")
+
+  def _GetUpdatedMin(self):
+    if 'updated-min' in self.keys():
+      return self['updated-min']
+    else:
+      return None
+
+  def _SetUpdatedMin(self, query):
+    self['updated-min'] = query
+
+  updated_min = property(_GetUpdatedMin, _SetUpdatedMin,
+      doc="""The feed query's updated-min parameter""")
+
+  def _GetUpdatedMax(self):
+    if 'updated-max' in self.keys():
+      return self['updated-max']
+    else:
+      return None
+
+  def _SetUpdatedMax(self, query):
+    self['updated-max'] = query
+
+  updated_max = property(_GetUpdatedMax, _SetUpdatedMax,
+      doc="""The feed query's updated-max parameter""")
+
+  def _GetPublishedMin(self):
+    if 'published-min' in self.keys():
+      return self['published-min']
+    else:
+      return None
+
+  def _SetPublishedMin(self, query):
+    self['published-min'] = query
+
+  published_min = property(_GetPublishedMin, _SetPublishedMin,
+      doc="""The feed query's published-min parameter""")
+
+  def _GetPublishedMax(self):
+    if 'published-max' in self.keys():
+      return self['published-max']
+    else:
+      return None
+
+  def _SetPublishedMax(self, query):
+    self['published-max'] = query
+
+  published_max = property(_GetPublishedMax, _SetPublishedMax,
+      doc="""The feed query's published-max parameter""")
+
+  def _GetStartIndex(self):
+    if 'start-index' in self.keys():
+      return self['start-index']
+    else:
+      return None
+
+  def _SetStartIndex(self, query):
+    if not isinstance(query, str):
+      query = str(query)
+    self['start-index'] = query
+
+  start_index = property(_GetStartIndex, _SetStartIndex,
+      doc="""The feed query's start-index parameter""")
+
+  def _GetMaxResults(self):
+    if 'max-results' in self.keys():
+      return self['max-results']
+    else:
+      return None
+
+  def _SetMaxResults(self, query):
+    if not isinstance(query, str):
+      query = str(query)
+    self['max-results'] = query
+
+  max_results = property(_GetMaxResults, _SetMaxResults,
+      doc="""The feed query's max-results parameter""")
+
+
+  def ToUri(self):
+    q_feed = self.feed or ''
+    category_string = '/'.join(
+        [urllib.quote_plus(c) for c in self.categories])
+    # Add categories to the feed if there are any.
+    if len(self.categories) > 0:
+      q_feed = q_feed + '/-/' + category_string
+    return atom.service.BuildUri(q_feed, self)
+
+  

Modified: trunk/conduit/modules/Makefile.am
==============================================================================
--- trunk/conduit/modules/Makefile.am	(original)
+++ trunk/conduit/modules/Makefile.am	Sat Jan 19 23:45:12 2008
@@ -8,7 +8,6 @@
 	FlickrModule \
 	FspotModule \
 	GmailModule \
-	PicasaModule \
 	SmugMugModule \
 	EvolutionModule \
 	YouTubeModule \

Modified: trunk/configure.ac
==============================================================================
--- trunk/configure.ac	(original)
+++ trunk/configure.ac	Sat Jan 19 23:45:12 2008
@@ -140,12 +140,18 @@
 conduit/modules/FspotModule/Makefile
 conduit/modules/GmailModule/Makefile
 conduit/modules/GmailModule/libgmail-0.1.6.2/Makefile
-conduit/modules/PicasaModule/Makefile
-conduit/modules/PicasaModule/PicasaAPI/Makefile
 conduit/modules/SmugMugModule/Makefile
 conduit/modules/SmugMugModule/SmugMugAPI/Makefile
 conduit/modules/YouTubeModule/Makefile
 conduit/modules/GoogleModule/Makefile
+conduit/modules/GoogleModule/libgdata/Makefile
+conduit/modules/GoogleModule/libgdata/atom/Makefile
+conduit/modules/GoogleModule/libgdata/gdata/Makefile
+conduit/modules/GoogleModule/libgdata/gdata/calendar/Makefile
+conduit/modules/GoogleModule/libgdata/gdata/exif/Makefile
+conduit/modules/GoogleModule/libgdata/gdata/geo/Makefile
+conduit/modules/GoogleModule/libgdata/gdata/media/Makefile
+conduit/modules/GoogleModule/libgdata/gdata/photos/Makefile
 conduit/modules/ShutterflyModule/Makefile
 conduit/modules/ShutterflyModule/shutterfly/Makefile
 conduit/modules/RhythmboxModule/Makefile

Modified: trunk/test/python-tests/TestDataProviderPicasa.py
==============================================================================
--- trunk/test/python-tests/TestDataProviderPicasa.py	(original)
+++ trunk/test/python-tests/TestDataProviderPicasa.py	Sat Jan 19 23:45:12 2008
@@ -40,13 +40,13 @@
 ok("Loaded album", picasa.galbum != None)
 
 # Correct name?
-if picasa.galbum.name == SAFE_ALBUM_NAME:
-    ok("Album name is ok: expected '%s', received '%s'" % (SAFE_ALBUM_NAME, picasa.galbum.name), True)
+if picasa.galbum.title.text == SAFE_ALBUM_NAME:
+    ok("Album name is ok: expected '%s', received '%s'" % (SAFE_ALBUM_NAME, picasa.galbum.title.text), True)
 else:
-    ok("Album name is not ok: expected '%s', received '%s'" % (SAFE_ALBUM_NAME, picasa.galbum.name), False)
+    ok("Album name is not ok: expected '%s', received '%s'" % (SAFE_ALBUM_NAME, picasa.galbum.title.text), False)
 
 # Expected id?
-if picasa.galbum.id == SAFE_ALBUM_ID:
+if picasa.galbum.gphoto_id.text == SAFE_ALBUM_ID:
     ok("Album equals the one we're expecting: %s" % SAFE_ALBUM_ID, True)
 else:
     ok("Album has an unexpected id: %s instead of %s" % (picasa.galbum.id, SAFE_ALBUM_ID), False)



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