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
- From: thomasvm svn gnome org
- To: svn-commits-list gnome org
- Subject: 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
- Date: Sat, 19 Jan 2008 23:45:12 +0000 (GMT)
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]