conduit r1927 - in trunk: . conduit/modules/GoogleModule/atom



Author: jstowers
Date: Tue Mar 17 09:19:36 2009
New Revision: 1927
URL: http://svn.gnome.org/viewvc/conduit?rev=1927&view=rev

Log:
2009-03-17  John Stowers  <john stowers gmail com>

	* conduit/modules/GoogleModule/atom/*:
	Update to python-gdata 1.2.4


Added:
   trunk/conduit/modules/GoogleModule/atom/auth.py
   trunk/conduit/modules/GoogleModule/atom/client.py   (contents, props changed)
   trunk/conduit/modules/GoogleModule/atom/core.py   (contents, props changed)
   trunk/conduit/modules/GoogleModule/atom/http.py
   trunk/conduit/modules/GoogleModule/atom/http_core.py
   trunk/conduit/modules/GoogleModule/atom/http_interface.py
   trunk/conduit/modules/GoogleModule/atom/mock_http.py
   trunk/conduit/modules/GoogleModule/atom/mock_http_core.py
   trunk/conduit/modules/GoogleModule/atom/token_store.py
   trunk/conduit/modules/GoogleModule/atom/url.py
Modified:
   trunk/ChangeLog

Added: trunk/conduit/modules/GoogleModule/atom/auth.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/atom/auth.py	Tue Mar 17 09:19:36 2009
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+#
+#    Copyright (C) 2009 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 is used for version 2 of the Google Data APIs.
+
+
+__author__ = 'j s google com (Jeff Scudder)'
+
+
+import base64
+
+
+class BasicAuth(object):
+  """Sets the Authorization header as defined in RFC1945"""
+
+  def __init__(self, user_id, password):
+    self.basic_cookie = base64.encodestring(
+        '%s:%s' % (user_id, password)).strip()
+
+  def modify_request(self, http_request):
+    http_request.headers['Authorization'] = 'Basic %s' % self.basic_cookie
+
+  ModifyRequest = modify_request

Added: trunk/conduit/modules/GoogleModule/atom/client.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/atom/client.py	Tue Mar 17 09:19:36 2009
@@ -0,0 +1,111 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2009 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.
+
+
+"""AtomPubClient provides CRUD ops. in line with the Atom Publishing Protocol.
+
+"""
+
+__author__ = 'j s google com (Jeff Scudder)'
+
+
+import atom.http_core
+
+
+class AtomPubClient(object):
+  host = None
+  auth_token = None
+
+  def __init__(self, http_client=None, host=None, auth_token=None, **kwargs):
+    self.http_client = http_client or atom.http_core.HttpClient()
+    if host is not None:
+      self.host = host
+    if auth_token is not None:
+      self.auth_token = auth_token
+
+  def request(self, method=None, uri=None, auth_token=None,
+              http_request=None, **kwargs):
+    """Performs an HTTP request to the server indicated.
+
+    Uses the http_client instance to make the request.
+
+    Args:
+      method: The HTTP method as a string, usually one of 'GET', 'POST',
+              'PUT', or 'DELETE'
+      uri: The URI desired as a string or atom.http_core.Uri. 
+      http_request: 
+      auth_token: An authorization token object whose modify_request method
+                  sets the HTTP Authorization header.
+    """
+    if http_request is None:
+      http_request = atom.http_core.HttpRequest()
+    # If the http_request didn't specify the target host, use the client's
+    # default host (if set).
+    if self.host is not None and http_request.host is None:
+      http_request.host = self.host
+    # Modify the request based on the AtomPubClient settings and parameters
+    # passed in to the request.
+    if isinstance(uri, (str, unicode)):
+      uri = atom.http_core.parse_uri(uri)
+    if uri is not None:
+      uri.modify_request(http_request)
+    if isinstance(method, (str, unicode)):
+      http_request.method = method
+    # Any unrecognized arguments are assumed to be capable of modifying the
+    # HTTP request.
+    for name, value in kwargs.iteritems():
+      if value is not None:
+        value.modify_request(http_request)
+    # Default to an http request if the protocol scheme is not set.
+    if http_request.scheme is None:
+      http_request.scheme = 'http'
+    # Add the Authorization header at the very end. The Authorization header
+    # value may need to be calculated using information in the request.
+    if auth_token:
+      auth_token.modify_request(http_request)
+    elif self.auth_token:
+      self.auth_token.modify_request(http_request)
+    # Perform the fully specified request using the http_client instance. 
+    # Sends the request to the server and returns the server's response.
+    return self.http_client.request(http_request)
+
+  Request = request
+
+  def get(self, uri=None, auth_token=None, http_request=None, **kwargs):
+    return self.request(method='GET', uri=uri, auth_token=auth_token, 
+                        http_request=http_request, **kwargs)
+
+  Get = get
+
+  def post(self, uri=None, data=None, auth_token=None, http_request=None, 
+           **kwargs):
+    return self.request(method='POST', uri=uri, auth_token=auth_token, 
+                        http_request=http_request, data=data, **kwargs)
+
+  Post = post
+
+  def put(self, uri=None, data=None, auth_token=None, http_request=None, 
+          **kwargs):
+    return self.request(method='PUT', uri=uri, auth_token=auth_token, 
+                        http_request=http_request, data=data, **kwargs)
+
+  Put = put
+
+  def delete(self, uri=None, auth_token=None, http_request=None, **kwargs):
+    return self.request(method='DELETE', uri=uri, auth_token=auth_token, 
+                        http_request=http_request, **kwargs)
+
+  Delete = delete

Added: trunk/conduit/modules/GoogleModule/atom/core.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/atom/core.py	Tue Mar 17 09:19:36 2009
@@ -0,0 +1,395 @@
+#!/usr/bin/env python
+#
+#    Copyright (C) 2008 Google Inc.
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+
+# This module is used for version 2 of the Google Data APIs.
+# TODO: handle UTF-8 and unicode as done in src/atom/__init__.py
+
+
+__author__ = 'j s google com (Jeff Scudder)'
+
+
+import inspect
+try:
+  from xml.etree import cElementTree as ElementTree
+except ImportError:
+  try:
+    import cElementTree as ElementTree
+  except ImportError:
+    try:
+      from xml.etree import ElementTree
+    except ImportError:
+      from elementtree import ElementTree
+
+
+class XmlElement(object):
+  _qname = None
+  _other_elements = None
+  _other_attributes = None
+  _rule_set = None
+  _members = None
+  text = None
+  
+  def __init__(self, text=None, *args, **kwargs):
+    if ('_members' not in self.__class__.__dict__ 
+        or self.__class__._members is None):
+      self.__class__._members = tuple(self.__class__._list_xml_members())
+    for member_name, member_type in self.__class__._members:
+      if member_name in kwargs:
+        setattr(self, member_name, kwargs[member_name])
+      else:
+        if isinstance(member_type, list):
+          setattr(self, member_name, [])
+        else:
+          setattr(self, member_name, None)
+    self._other_elements = []
+    self._other_attributes = {}
+    if text is not None:
+      self.text = text
+    
+  def _list_xml_members(cls):
+    """Generator listing all members which are XML elements or attributes.
+    
+    The following members would be considered XML members:
+    foo = 'abc' - indicates an XML attribute with the qname abc
+    foo = SomeElement - indicates an XML child element
+    foo = [AnElement] - indicates a repeating XML child element, each instance
+        will be stored in a list in this member
+    foo = ('att1', '{http://example.com/namespace}att2') - indicates an XML
+        attribute which has different parsing rules in different versions of 
+        the protocol. Version 1 of the XML parsing rules will look for an
+        attribute with the qname 'att1' but verion 2 of the parsing rules will
+        look for a namespaced attribute with the local name of 'att2' and an
+        XML namespace of 'http://example.com/namespace'.
+    """
+    members = []
+    for pair in inspect.getmembers(cls):
+      if not pair[0].startswith('_') and pair[0] != 'text':
+        member_type = pair[1]
+        if (isinstance(member_type, tuple) or isinstance(member_type, list) 
+            or isinstance(member_type, (str, unicode)) 
+            or (inspect.isclass(member_type) 
+                and issubclass(member_type, XmlElement))):
+          members.append(pair)
+    return members
+
+  _list_xml_members = classmethod(_list_xml_members)
+
+  def _get_rules(cls, version):
+    # Initialize the _rule_set to make sure there is a slot available to store
+    # the parsing rules for this version of the XML schema.
+    # Look for rule set in the class __dict__ proxy so that only the 
+    # _rule_set for this class will be found. By using the dict proxy
+    # we avoid finding rule_sets defined in superclasses.
+    # The four lines below provide support for any number of versions, but it
+    # runs a bit slower then hard coding slots for two versions, so I'm using
+    # the below two lines.
+    #if '_rule_set' not in cls.__dict__ or cls._rule_set is None:
+    #  cls._rule_set = []
+    #while len(cls.__dict__['_rule_set']) < version:
+    #  cls._rule_set.append(None)
+    # If there is no rule set cache in the class, provide slots for two XML
+    # versions. If and when there is a version 3, this list will need to be
+    # expanded.
+    if '_rule_set' not in cls.__dict__ or cls._rule_set is None:
+      cls._rule_set = [None, None]
+    # If a version higher than 2 is requested, fall back to version 2 because
+    # 2 is currently the highest supported version.
+    if version > 2:
+      return cls._get_rules(2)
+    # Check the dict proxy for the rule set to avoid finding any rule sets 
+    # which belong to the superclass. We only want rule sets for this class.
+    if cls._rule_set[version-1] is None:
+      # The rule set for each version consists of the qname for this element 
+      # ('{namespace}tag'), a dictionary (elements) for looking up the 
+      # corresponding class member when given a child element's qname, and a 
+      # dictionary (attributes) for looking up the corresponding class member
+      # when given an XML attribute's qname.
+      elements = {}
+      attributes = {}
+      if ('_members' not in cls.__dict__ or cls._members is None):
+        cls._members = tuple(cls._list_xml_members())
+      for member_name, target in cls._members:
+        if isinstance(target, list):
+          # This member points to a repeating element.
+          elements[_get_qname(target[0], version)] = (member_name, target[0], 
+              True)
+        elif isinstance(target, tuple):
+          # This member points to a versioned XML attribute.
+          if version <= len(target):
+            attributes[target[version-1]] = member_name
+          else:
+            attributes[target[-1]] = member_name
+        elif isinstance(target, (str, unicode)):
+          # This member points to an XML attribute.
+          attributes[target] = member_name
+        elif issubclass(target, XmlElement):
+          # This member points to a single occurance element.
+          elements[_get_qname(target, version)] = (member_name, target, False)
+      version_rules = (_get_qname(cls, version), elements, attributes)
+      cls._rule_set[version-1] = version_rules
+      return version_rules
+    else:
+      return cls._rule_set[version-1]
+
+  _get_rules = classmethod(_get_rules)
+  
+  def get_elements(self, tag=None, namespace=None, version=1):
+    """Find all sub elements which match the tag and namespace.
+
+    To find all elements in this object, call get_elements with the tag and
+    namespace both set to None (the default). This method searches through
+    the object's members and the elements stored in _other_elements which
+    did not match any of the XML parsing rules for this class.
+
+    Args:
+      tag: str
+      namespace: str
+      version: int Specifies the version of the XML rules to be used when 
+               searching for matching elements.
+
+    Returns:
+      A list of the matching XmlElements.
+    """
+    matches = []
+    ignored1, elements, ignored2 = self.__class__._get_rules(version)
+    if elements:
+      for qname, element_def in elements.iteritems():
+        member = getattr(self, element_def[0])
+        if member:
+          if _qname_matches(tag, namespace, qname):
+            if element_def[2]:
+              # If this is a repeating element, copy all instances into the 
+              # result list.
+              matches.extend(member)
+            else:
+              matches.append(member)
+    for element in self._other_elements:
+      if _qname_matches(tag, namespace, element._qname):
+        matches.append(element)
+    return matches
+
+  GetElements = get_elements
+    
+  def get_attributes(self, tag=None, namespace=None, version=1):
+    """Find all attributes which match the tag and namespace.
+
+    To find all attributes in this object, call get_attributes with the tag
+    and namespace both set to None (the default). This method searches 
+    through the object's members and the attributes stored in 
+    _other_attributes which did not fit any of the XML parsing rules for this
+    class.
+
+    Args:
+      tag: str
+      namespace: str
+      version: int Specifies the version of the XML rules to be used when 
+               searching for matching attributes.
+
+    Returns:
+      A list of XmlAttribute objects for the matching attributes.
+    """
+    matches = []
+    ignored1, ignored2, attributes = self.__class__._get_rules(version)
+    if attributes:
+      for qname, attribute_def in attributes.iteritems():
+        member = getattr(self, attribute_def[0])
+        if member:
+          if _qname_matches(tag, namespace, qname):
+            matches.append(XmlAttribute(qname, member))
+    for qname, value in self._other_attributes.iteritems():
+      if _qname_matches(tag, namespace, qname):
+        matches.append(XmlAttribute(qname, value))
+    return matches
+
+  GetAttributes = get_attributes
+  
+  def _harvest_tree(self, tree, version=1):
+    """Populates object members from the data in the tree Element."""
+    qname, elements, attributes = self.__class__._get_rules(version)
+    for element in tree:
+      if elements and element.tag in elements:
+        definition = elements[element.tag]
+        # If this is a repeating element, make sure the member is set to a 
+        # list.
+        if definition[2]:
+          if getattr(self, definition[0]) is None:
+            setattr(self, definition[0], [])
+          getattr(self, definition[0]).append(_xml_element_from_tree(element,
+              definition[1]))
+        else:
+          setattr(self, definition[0], _xml_element_from_tree(element, 
+              definition[1]))
+      else:
+        self._other_elements.append(_xml_element_from_tree(element, XmlElement))
+    for attrib, value in tree.attrib.iteritems():
+      if attributes and attrib in attributes:
+        setattr(self, attributes[attrib], value)
+      else:
+        self._other_attributes[attrib] = value
+    if tree.text:
+      self.text = tree.text
+
+  def _to_tree(self, version=1):
+    new_tree = ElementTree.Element(_get_qname(self, version))
+    self._attach_members(new_tree, version)
+    return new_tree
+
+  def _attach_members(self, tree, version=1):
+    """Convert members to XML elements/attributes and add them to the tree.
+    
+    Args:
+      tree: An ElementTree.Element which will be modified. The members of
+            this object will be added as child elements or attributes 
+            according to the rules described in _expected_elements and 
+            _expected_attributes. The elements and attributes stored in
+            other_attributes and other_elements are also added a children
+            of this tree.
+      version: int Ingnored in this method but used by VersionedElement.
+    """
+    qname, elements, attributes = self.__class__._get_rules(version)
+    # Add the expected elements and attributes to the tree.
+    if elements:
+      for tag, element_def in elements.iteritems():
+        member = getattr(self, element_def[0])
+        # If this is a repeating element and there are members in the list.
+        if member and element_def[2]:
+          for instance in member:
+            instance._become_child(tree, version)
+        elif member:
+          member._become_child(tree, version)
+    if attributes:
+      for attribute_tag, member_name in attributes.iteritems():
+        value = getattr(self, member_name)
+        if value:
+          tree.attrib[attribute_tag] = value
+    # Add the unexpected (other) elements and attributes to the tree.
+    for element in self._other_elements:
+      element._become_child(tree, version)
+    for key, value in self._other_attributes.iteritems():
+      tree.attrib[key] = value
+    if self.text:
+      tree.text = self.text
+
+  def to_string(self, version=1):
+    """Converts this object to XML."""
+    return ElementTree.tostring(self._to_tree(version))
+
+  ToString = to_string
+
+  def _become_child(self, tree, version=1):
+    """Adds a child element to tree with the XML data in self."""
+    new_child = ElementTree.Element('')
+    tree.append(new_child)
+    new_child.tag = _get_qname(self, version)
+    self._attach_members(new_child, version)
+
+
+def _get_qname(element, version):
+  if isinstance(element._qname, tuple):
+    if version <= len(element._qname):
+      return element._qname[version-1]
+    else:
+      return element._qname[-1]
+  else:
+    return element._qname
+
+
+def _qname_matches(tag, namespace, qname):
+  """Logic determines if a QName matches the desired local tag and namespace.
+  
+  This is used in XmlElement.get_elements and XmlElement.get_attributes to
+  find matches in the element's members (among all expected-and-unexpected
+  elements-and-attributes).
+  
+  Args:
+    expected_tag: string
+    expected_namespace: string
+    qname: string in the form '{xml_namespace}localtag' or 'tag' if there is
+           no namespace.
+  
+  Returns:
+    boolean True if the member's tag and namespace fit the expected tag and
+    namespace.
+  """
+  # If there is no expected namespace or tag, then everything will match.
+  if qname is None:
+    member_tag = None
+    member_namespace = None
+  else:
+    if qname.startswith('{'):
+      member_namespace = qname[1:qname.index('}')]
+      member_tag = qname[qname.index('}') + 1:]
+    else:
+      member_namespace = None
+      member_tag = qname
+  return ((tag is None and namespace is None)
+      # If there is a tag, but no namespace, see if the local tag matches.
+      or (namespace is None and member_tag == tag)
+      # There was no tag, but there was a namespace so see if the namespaces
+      # match.
+      or (tag is None and member_namespace == namespace)
+      # There was no tag, and the desired elements have no namespace, so check
+      # to see that the member's namespace is None.
+      or (tag is None and namespace == ''
+          and member_namespace is None)
+      # The tag and the namespace both match.
+      or (tag == member_tag
+          and namespace == member_namespace)
+      # The tag matches, and the expected namespace is the empty namespace,
+      # check to make sure the member's namespace is None.
+      or (tag == member_tag and namespace == ''
+          and member_namespace is None))
+
+
+def xml_element_from_string(xml_string, target_class, 
+    version=1, encoding='UTF-8'):
+  """Parses the XML string according to the rules for the target_class.
+
+  Args:
+    xml_string: str or unicode
+    target_class: XmlElement or a subclass.
+    version: int (optional) The version of the schema which should be used when
+             converting the XML into an object. The default is 1.
+  """
+  tree = ElementTree.fromstring(xml_string)
+  return _xml_element_from_tree(tree, target_class, version)
+
+
+XmlElementFromString = xml_element_from_string
+
+
+def _xml_element_from_tree(tree, target_class, version=1):
+  if target_class._qname is None:
+    instance = target_class()
+    instance._qname = tree.tag
+    instance._harvest_tree(tree, version)
+    return instance
+  # TODO handle the namespace-only case
+  # Namespace only will be used with Google Spreadsheets rows and 
+  # Google Base item attributes.
+  elif tree.tag == target_class._qname:
+    instance = target_class()
+    instance._harvest_tree(tree, version)
+    return instance
+  return None
+
+
+class XmlAttribute(object):
+
+  def __init__(self, qname, value):
+    self._qname = qname
+    self.value = value

Added: trunk/conduit/modules/GoogleModule/atom/http.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/atom/http.py	Tue Mar 17 09:19:36 2009
@@ -0,0 +1,286 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""HttpClients in this module use httplib to make HTTP requests.
+
+This module make HTTP requests based on httplib, but there are environments
+in which an httplib based approach will not work (if running in Google App
+Engine for example). In those cases, higher level classes (like AtomService
+and GDataService) can swap out the HttpClient to transparently use a 
+different mechanism for making HTTP requests.
+
+  HttpClient: Contains a request method which performs an HTTP call to the 
+      server.
+      
+  ProxiedHttpClient: Contains a request method which connects to a proxy using
+      settings stored in operating system environment variables then 
+      performs an HTTP call to the endpoint server.
+"""
+
+
+__author__ = 'api.jscudder (Jeff Scudder)'
+
+
+import types
+import os
+import httplib
+import atom.url
+import atom.http_interface
+import socket
+import base64
+
+
+class ProxyError(atom.http_interface.Error):
+  pass
+
+
+DEFAULT_CONTENT_TYPE = 'application/atom+xml'
+
+
+class HttpClient(atom.http_interface.GenericHttpClient):
+  def __init__(self, headers=None):
+    self.debug = False
+    self.headers = headers or {}
+
+  def request(self, operation, url, data=None, headers=None):
+    """Performs an HTTP call to the server, supports GET, POST, PUT, and 
+    DELETE.
+
+    Usage example, perform and HTTP GET on http://www.google.com/:
+      import atom.http
+      client = atom.http.HttpClient()
+      http_response = client.request('GET', 'http://www.google.com/')
+
+    Args:
+      operation: str The HTTP operation to be performed. This is usually one
+          of 'GET', 'POST', 'PUT', or 'DELETE'
+      data: filestream, list of parts, or other object which can be converted
+          to a string. Should be set to None when performing a GET or DELETE.
+          If data is a file-like object which can be read, this method will 
+          read a chunk of 100K bytes at a time and send them. 
+          If the data is a list of parts to be sent, each part will be 
+          evaluated and sent.
+      url: The full URL to which the request should be sent. Can be a string
+          or atom.url.Url.
+      headers: dict of strings. HTTP headers which should be sent
+          in the request. 
+    """
+    if not isinstance(url, atom.url.Url):
+      if isinstance(url, types.StringTypes):
+        url = atom.url.parse_url(url)
+      else:
+        raise atom.http_interface.UnparsableUrlObject('Unable to parse url '
+            'parameter because it was not a string or atom.url.Url')
+    
+    all_headers = self.headers.copy()
+    if headers:
+      all_headers.update(headers) 
+
+    connection = self._prepare_connection(url, all_headers)
+
+    if self.debug:
+      connection.debuglevel = 1
+
+    connection.putrequest(operation, self._get_access_url(url), 
+        skip_host=True)
+    connection.putheader('Host', url.host)
+
+    # Overcome a bug in Python 2.4 and 2.5
+    # httplib.HTTPConnection.putrequest adding
+    # HTTP request header 'Host: www.google.com:443' instead of
+    # 'Host: www.google.com', and thus resulting the error message
+    # 'Token invalid - AuthSub token has wrong scope' in the HTTP response.
+    if (url.protocol == 'https' and int(url.port or 443) == 443 and
+        hasattr(connection, '_buffer') and
+        isinstance(connection._buffer, list)):
+      header_line = 'Host: %s:443' % url.host
+      replacement_header_line = 'Host: %s' % url.host
+      try:
+        connection._buffer[connection._buffer.index(header_line)] = (
+            replacement_header_line)
+      except ValueError:  # header_line missing from connection._buffer
+        pass
+
+    # If the list of headers does not include a Content-Length, attempt to
+    # calculate it based on the data object.
+    if data and 'Content-Length' not in all_headers:
+      if isinstance(data, types.StringTypes):
+        all_headers['Content-Length'] = len(data)
+      else:
+        raise atom.http_interface.ContentLengthRequired('Unable to calculate '
+            'the length of the data parameter. Specify a value for '
+            'Content-Length')
+
+    # Set the content type to the default value if none was set.
+    if 'Content-Type' not in all_headers:
+      all_headers['Content-Type'] = DEFAULT_CONTENT_TYPE
+
+    # Send the HTTP headers.
+    for header_name in all_headers:
+      connection.putheader(header_name, all_headers[header_name])
+    connection.endheaders()
+
+    # If there is data, send it in the request.
+    if data:
+      if isinstance(data, list):
+        for data_part in data:
+          _send_data_part(data_part, connection)
+      else:
+        _send_data_part(data, connection)
+
+    # Return the HTTP Response from the server.
+    return connection.getresponse()
+    
+  def _prepare_connection(self, url, headers):
+    if not isinstance(url, atom.url.Url):
+      if isinstance(url, types.StringTypes):
+        url = atom.url.parse_url(url)
+      else:
+        raise atom.http_interface.UnparsableUrlObject('Unable to parse url '
+            'parameter because it was not a string or atom.url.Url')
+    if url.protocol == 'https':
+      if not url.port:
+        return httplib.HTTPSConnection(url.host)
+      return httplib.HTTPSConnection(url.host, int(url.port))
+    else:
+      if not url.port:
+        return httplib.HTTPConnection(url.host)
+      return httplib.HTTPConnection(url.host, int(url.port))
+
+  def _get_access_url(self, url):
+    return url.to_string()
+
+
+class ProxiedHttpClient(HttpClient):
+  """Performs an HTTP request through a proxy.
+  
+  The proxy settings are obtained from enviroment variables. The URL of the 
+  proxy server is assumed to be stored in the environment variables 
+  'https_proxy' and 'http_proxy' respectively. If the proxy server requires
+  a Basic Auth authorization header, the username and password are expected to 
+  be in the 'proxy-username' or 'proxy_username' variable and the 
+  'proxy-password' or 'proxy_password' variable.
+  
+  After connecting to the proxy server, the request is completed as in 
+  HttpClient.request.
+  """
+  def _prepare_connection(self, url, headers):
+    proxy_auth = _get_proxy_auth()
+    if url.protocol == 'https':
+      # destination is https
+      proxy = os.environ.get('https_proxy')
+      if proxy:
+        # Set any proxy auth headers 
+        if proxy_auth:
+          proxy_auth = 'Proxy-authorization: %s' % proxy_auth
+          
+        # Construct the proxy connect command.
+        port = url.port
+        if not port:
+          port = '443'
+        proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % (url.host, port)
+        
+        # Set the user agent to send to the proxy
+        if headers and 'User-Agent' in headers:
+          user_agent = 'User-Agent: %s\r\n' % (headers['User-Agent'])
+        else:
+          user_agent = ''
+        
+        proxy_pieces = '%s%s%s\r\n' % (proxy_connect, proxy_auth, user_agent)
+        
+        # Find the proxy host and port.
+        proxy_url = atom.url.parse_url(proxy)
+        if not proxy_url.port:
+          proxy_url.port = '80'
+        
+        # Connect to the proxy server, very simple recv and error checking
+        p_sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
+        p_sock.connect((proxy_url.host, int(proxy_url.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 ProxyError('Error status=%s' % 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(proxy_url.host)
+        connection.sock=fake_sock
+        return connection
+      else:
+        # The request was HTTPS, but there was no https_proxy set.
+        return HttpClient._prepare_connection(self, url, headers)
+    else:
+      proxy = os.environ.get('http_proxy')
+      if proxy:
+        # Find the proxy host and port.
+        proxy_url = atom.url.parse_url(proxy)
+        if not proxy_url.port:
+          proxy_url.port = '80'
+        
+        if proxy_auth:
+          headers['Proxy-Authorization'] = proxy_auth.strip()
+
+        return httplib.HTTPConnection(proxy_url.host, int(proxy_url.port))
+      else:
+        # The request was HTTP, but there was no http_proxy set.
+        return HttpClient._prepare_connection(self, url, headers)
+
+  def _get_access_url(self, url):
+    return url.to_string()
+
+
+def _get_proxy_auth():
+  proxy_username = os.environ.get('proxy-username')
+  if not proxy_username:
+    proxy_username = os.environ.get('proxy_username')
+  proxy_password = os.environ.get('proxy-password')
+  if not proxy_password:
+    proxy_password = os.environ.get('proxy_password')
+  if proxy_username:
+    user_auth = base64.encodestring('%s:%s' % (proxy_username,
+                                               proxy_password))
+    return 'Basic %s\r\n' % (user_auth.strip())
+  else:
+    return ''
+
+
+def _send_data_part(data, connection):
+  if isinstance(data, types.StringTypes):
+    connection.send(data)
+    return
+  # Check to see if data is a file-like object that has a read method.
+  elif hasattr(data, 'read'):
+    # Read the file and send it a chunk at a time.
+    while 1:
+      binarydata = data.read(100000)
+      if binarydata == '': break
+      connection.send(binarydata)
+    return
+  else:
+    # The data object was not a file.
+    # Try to convert to a string and send the data.
+    connection.send(str(data))
+    return

Added: trunk/conduit/modules/GoogleModule/atom/http_core.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/atom/http_core.py	Tue Mar 17 09:19:36 2009
@@ -0,0 +1,407 @@
+#!/usr/bin/env python
+#
+#    Copyright (C) 2009 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 is used for version 2 of the Google Data APIs.
+# TODO: add proxy handling.
+
+
+__author__ = 'j s google com (Jeff Scudder)'
+
+
+import StringIO
+import urlparse
+import urllib
+import httplib
+
+
+class Error(Exception):
+  pass
+
+
+class UnknownSize(Error):
+  pass
+
+
+MIME_BOUNDARY = 'END_OF_PART'
+
+
+class HttpRequest(object):
+  """Contains all of the parameters for an HTTP 1.1 request.
+ 
+  The HTTP headers are represented by a dictionary, and it is the
+  responsibility of the user to ensure that duplicate field names are combined
+  into one header value according to the rules in section 4.2 of RFC 2616.
+  """
+  scheme = None
+  host = None
+  port = None
+  method = None
+  uri = None
+ 
+  def __init__(self, scheme=None, host=None, port=None, method=None, uri=None,
+      headers=None):
+    """Construct an HTTP request.
+
+    Args:
+      scheme: str The protocol to be used, usually this is 'http' or 'https'
+      host: str The name or IP address string of the server.
+      port: int The port number to connect to on the server.
+      method: The HTTP method for the request, examples include 'GET', 'POST',
+              etc.
+      uri: str The relative path inclusing escaped query parameters.
+      headers: dict of strings The HTTP headers to include in the request.
+    """
+    self.headers = headers or {}
+    self._body_parts = []
+    if scheme is not None:
+      self.scheme = scheme
+    if host is not None:
+      self.host = host
+    if port is not None:
+      self.port = port
+    if method is not None:
+      self.method = method
+    if uri is not None:
+      self.uri = uri
+
+  def add_body_part(self, data, mime_type, size=None):
+    """Adds data to the HTTP request body.
+   
+    If more than one part is added, this is assumed to be a mime-multipart
+    request. This method is designed to create MIME 1.0 requests as specified
+    in RFC 1341.
+
+    Args:
+      data: str or a file-like object containing a part of the request body.
+      mime_type: str The MIME type describing the data
+      size: int Required if the data is a file like object. If the data is a
+            string, the size is calculated so this parameter is ignored.
+    """
+    if isinstance(data, str):
+      size = len(data)
+    if size is None:
+      # TODO: support chunked transfer if some of the body is of unknown size.
+      raise UnknownSize('Each part of the body must have a known size.')
+    if 'Content-Length' in self.headers:
+      content_length = int(self.headers['Content-Length'])
+    else:
+      content_length = 0
+    # If this is the first part added to the body, then this is not a multipart
+    # request.
+    if len(self._body_parts) == 0:
+      self.headers['Content-Type'] = mime_type
+      content_length = size
+      self._body_parts.append(data)
+    elif len(self._body_parts) == 1:
+      # This is the first member in a mime-multipart request, so change the
+      # _body_parts list to indicate a multipart payload.
+      self._body_parts.insert(0, 'Media multipart posting')
+      boundary_string = '\r\n--%s\r\n' % (MIME_BOUNDARY,)
+      content_length += len(boundary_string) + size
+      self._body_parts.insert(1, boundary_string)
+      content_length += len('Media multipart posting')
+      # Put the content type of the first part of the body into the multipart
+      # payload.
+      original_type_string = 'Content-Type: %s\r\n\r\n' % (
+          self.headers['Content-Type'],)
+      self._body_parts.insert(2, original_type_string)
+      content_length += len(original_type_string)
+      boundary_string = '\r\n--%s\r\n' % (MIME_BOUNDARY,)
+      self._body_parts.append(boundary_string)
+      content_length += len(boundary_string)
+      # Change the headers to indicate this is now a mime multipart request.
+      self.headers['Content-Type'] = 'multipart/related; boundary="%s"' % (
+          MIME_BOUNDARY,)
+      self.headers['MIME-version'] = '1.0'
+      # Include the mime type of this part.
+      type_string = 'Content-Type: %s\r\n\r\n' % (mime_type)
+      self._body_parts.append(type_string)
+      content_length += len(type_string)
+      self._body_parts.append(data)
+      ending_boundary_string = '\r\n--%s--' % (MIME_BOUNDARY,)
+      self._body_parts.append(ending_boundary_string)
+      content_length += len(ending_boundary_string)
+    else:
+      # This is a mime multipart request.
+      boundary_string = '\r\n--%s\r\n' % (MIME_BOUNDARY,)
+      self._body_parts.insert(-1, boundary_string)
+      content_length += len(boundary_string) + size
+      # Include the mime type of this part.
+      type_string = 'Content-Type: %s\r\n\r\n' % (mime_type)
+      self._body_parts.insert(-1, type_string)
+      content_length += len(type_string)
+      self._body_parts.insert(-1, data)
+    self.headers['Content-Length'] = str(content_length)
+  # I could add an "append_to_body_part" method as well.
+
+  AddBodyPart = add_body_part
+
+  def add_form_inputs(self, form_data,
+                      mime_type='application/x-www-form-urlencoded'):
+    body = urllib.urlencode(form_data)
+    self.add_body_part(body, mime_type)
+
+  AddFormInputs = add_form_inputs
+
+  def _copy(self):
+    new_request = HttpRequest(scheme=self.scheme, host=self.host,
+        port=self.port, method=self.method, uri=self.uri,
+        headers=self.headers.copy())
+    new_request._body_parts = self._body_parts[:]
+    return new_request
+
+
+def _apply_defaults(http_request):
+  if http_request.scheme is None:
+    if http_request.port == 443:
+      http_request.scheme = 'https'
+    else:
+      http_request.scheme = 'http'
+
+
+class Uri(object):
+  """A URI as used in HTTP 1.1"""
+  scheme = None
+  host = None
+  port = None
+  path = None
+ 
+  def __init__(self, scheme=None, host=None, port=None, path=None, query=None):
+    """Constructor for a URI.
+
+    Args:
+      scheme: str This is usually 'http' or 'https'.
+      ... TODO
+      query: dict of strings The URL query parameters. The keys and values are
+             both escaped so this dict should contain the unescaped values.
+
+    """
+    self.query = query or {}
+    if scheme is not None:
+      self.scheme = scheme
+    if host is not None:
+      self.host = host
+    if port is not None:
+      self.port = port
+    if path:
+      self.path = path
+     
+  def _get_query_string(self):
+    param_pairs = []
+    for key, value in self.query.iteritems():
+      param_pairs.append('='.join((urllib.quote_plus(key),
+          urllib.quote_plus(str(value)))))
+    return '&'.join(param_pairs)
+
+  def _get_relative_path(self):
+    """Returns the path with the query parameters escaped and appended."""
+    param_string = self._get_query_string()
+    if self.path is None:
+      path = '/'
+    else:
+      path = self.path
+    if param_string:
+      return '?'.join([path, param_string])
+    else:
+      return path
+     
+  def _to_string(self):
+    if self.scheme is None and self.port == 443:
+      scheme = 'https'
+    elif self.scheme is None:
+      scheme = 'http'
+    else:
+      scheme = self.scheme
+    if self.path is None:
+      path = '/'
+    else:
+      path = self.path
+    if self.port is None:
+      return '%s://%s%s' % (scheme, self.host, self._get_relative_path())
+    else:
+      return '%s://%s:%s%s' % (scheme, self.host, str(self.port),
+                               self._get_relative_path())
+     
+  def modify_request(self, http_request=None):
+    """Sets HTTP request components based on the URI."""
+    if http_request is None:
+      http_request = HttpRequest()
+    # Determine the correct scheme.
+    if self.scheme:
+      http_request.scheme = self.scheme
+    if self.port:
+      http_request.port = self.port
+    if self.host:
+      http_request.host = self.host
+    # Set the relative uri path
+    if self.path:
+      http_request.uri = self._get_relative_path()
+    elif not self.path and self.query:
+      http_request.uri = '/%s' % self._get_relative_path()
+    elif not self.path and not self.query and not http_request.uri:
+      http_request.uri = '/'
+    return http_request
+
+  ModifyRequest = modify_request
+
+ 
+def parse_uri(uri_string):
+  """Creates a Uri object which corresponds to the URI string.
+ 
+  This method can accept partial URIs, but it will leave missing
+  members of the Uri unset.
+  """
+  parts = urlparse.urlparse(uri_string)
+  uri = Uri()
+  if parts[0]:
+    uri.scheme = parts[0]
+  if parts[1]:
+    host_parts = parts[1].split(':')
+    if host_parts[0]:
+      uri.host = host_parts[0]
+    if len(host_parts) > 1:
+      uri.port = int(host_parts[1])
+  if parts[2]:
+    uri.path = parts[2]
+  if parts[4]:
+    param_pairs = parts[4].split('&')
+    for pair in param_pairs:
+      pair_parts = pair.split('=')
+      if len(pair_parts) > 1:
+        uri.query[urllib.unquote_plus(pair_parts[0])] = (
+            urllib.unquote_plus(pair_parts[1]))
+      elif len(pair_parts) == 1:
+        uri.query[urllib.unquote_plus(pair_parts[0])] = None
+  return uri
+
+
+ParseUri = parse_uri
+
+
+class HttpResponse(object):
+  status = None
+  reason = None
+  _body = None
+ 
+  def __init__(self, status=None, reason=None, headers=None, body=None):
+    self._headers = headers or {}
+    if status is not None:
+      self.status = status
+    if reason is not None:
+      self.reason = reason
+    if body is not None:
+      if hasattr(body, 'read'):
+        self._body = body
+      else:
+        self._body = StringIO.StringIO(body)
+         
+  def getheader(self, name, default=None):
+    if name in self._headers:
+      return self._headers[name]
+    else:
+      return default
+   
+  def read(self, amt=None):
+    if not amt:
+      return self._body.read()
+    else:
+      return self._body.read(amt)
+
+
+class HttpClient(object):
+  """Performs HTTP requests using httplib."""
+  debug = None
+ 
+  def request(self, http_request):
+    return self._http_request(http_request.host, http_request.method,
+        http_request.uri, http_request.scheme, http_request.port,
+        http_request.headers, http_request._body_parts)
+
+  Request = request
+ 
+  def _http_request(self, host, method, uri, scheme=None,  port=None,
+      headers=None, body_parts=None):
+    """Makes an HTTP request using httplib.
+   
+    Args:
+      uri: str
+    """
+    if scheme == 'https':
+      if not port:
+        connection = httplib.HTTPSConnection(host)
+      else:
+        connection = httplib.HTTPSConnection(host, int(port))
+    else:
+      if not port:
+        connection = httplib.HTTPConnection(host)
+      else:
+        connection = httplib.HTTPConnection(host, int(port))
+   
+    if self.debug:
+      connection.debuglevel = 1
+
+    connection.putrequest(method, uri)
+
+    # Overcome a bug in Python 2.4 and 2.5
+    # httplib.HTTPConnection.putrequest adding
+    # HTTP request header 'Host: www.google.com:443' instead of
+    # 'Host: www.google.com', and thus resulting the error message
+    # 'Token invalid - AuthSub token has wrong scope' in the HTTP response.
+    if (scheme == 'https' and int(port or 443) == 443 and
+        hasattr(connection, '_buffer') and
+        isinstance(connection._buffer, list)):
+      header_line = 'Host: %s:443' % host
+      replacement_header_line = 'Host: %s' % host
+      try:
+        connection._buffer[connection._buffer.index(header_line)] = (
+            replacement_header_line)
+      except ValueError:  # header_line missing from connection._buffer
+        pass
+
+    # Send the HTTP headers.
+    for header_name, value in headers.iteritems():
+      connection.putheader(header_name, value)
+    connection.endheaders()
+
+    # If there is data, send it in the request.
+    if body_parts:
+      for part in body_parts:
+        _send_data_part(part, connection)
+   
+    # Return the HTTP Response from the server.
+    return connection.getresponse()
+
+
+def _send_data_part(data, connection):
+  if isinstance(data, (str, unicode)):
+    # I might want to just allow str, not unicode.
+    connection.send(data)
+    return
+  # Check to see if data is a file-like object that has a read method.
+  elif hasattr(data, 'read'):
+    # Read the file and send it a chunk at a time.
+    while 1:
+      binarydata = data.read(100000)
+      if binarydata == '': break
+      connection.send(binarydata)
+    return
+  else:
+    # The data object was not a file.
+    # Try to convert to a string and send the data.
+    connection.send(str(data))
+    return
+

Added: trunk/conduit/modules/GoogleModule/atom/http_interface.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/atom/http_interface.py	Tue Mar 17 09:19:36 2009
@@ -0,0 +1,158 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This module provides a common interface for all HTTP requests.
+
+  HttpResponse: Represents the server's response to an HTTP request. Provides
+      an interface identical to httplib.HTTPResponse which is the response
+      expected from higher level classes which use HttpClient.request.
+
+  GenericHttpClient: Provides an interface (superclass) for an object 
+      responsible for making HTTP requests. Subclasses of this object are
+      used in AtomService and GDataService to make requests to the server. By
+      changing the http_client member object, the AtomService is able to make
+      HTTP requests using different logic (for example, when running on 
+      Google App Engine, the http_client makes requests using the App Engine
+      urlfetch API). 
+"""
+
+
+__author__ = 'api.jscudder (Jeff Scudder)'
+
+
+import StringIO
+
+
+USER_AGENT = '%s GData-Python/1.2.2'
+
+
+class Error(Exception):
+  pass
+
+
+class UnparsableUrlObject(Error):
+  pass
+
+
+class ContentLengthRequired(Error):
+  pass
+  
+
+class HttpResponse(object):
+  def __init__(self, body=None, status=None, reason=None, headers=None):
+    """Constructor for an HttpResponse object. 
+
+    HttpResponse represents the server's response to an HTTP request from
+    the client. The HttpClient.request method returns a httplib.HTTPResponse
+    object and this HttpResponse class is designed to mirror the interface
+    exposed by httplib.HTTPResponse.
+
+    Args:
+      body: A file like object, with a read() method. The body could also
+          be a string, and the constructor will wrap it so that 
+          HttpResponse.read(self) will return the full string.
+      status: The HTTP status code as an int. Example: 200, 201, 404.
+      reason: The HTTP status message which follows the code. Example: 
+          OK, Created, Not Found
+      headers: A dictionary containing the HTTP headers in the server's 
+          response. A common header in the response is Content-Length.
+    """
+    if body:
+      if hasattr(body, 'read'):
+        self._body = body
+      else:
+        self._body = StringIO.StringIO(body)
+    else:
+      self._body = None
+    if status is not None:
+      self.status = int(status)
+    else:
+      self.status = None
+    self.reason = reason
+    self._headers = headers or {}
+
+  def getheader(self, name, default=None):
+    if name in self._headers:
+      return self._headers[name]
+    else:
+      return default
+    
+  def read(self, amt=None):
+    if not amt:
+      return self._body.read()
+    else:
+      return self._body.read(amt)
+
+
+class GenericHttpClient(object):
+  debug = False
+
+  def __init__(self, http_client, headers=None):
+    """
+    
+    Args:
+      http_client: An object which provides a request method to make an HTTP 
+          request. The request method in GenericHttpClient performs a 
+          call-through to the contained HTTP client object.
+      headers: A dictionary containing HTTP headers which should be included
+          in every HTTP request. Common persistent headers include 
+          'User-Agent'.
+    """
+    self.http_client = http_client
+    self.headers = headers or {}
+
+  def request(self, operation, url, data=None, headers=None):
+    all_headers = self.headers.copy()
+    if headers:
+      all_headers.update(headers)
+    return self.http_client.request(operation, url, data=data, 
+        headers=all_headers)
+
+  def get(self, url, headers=None):
+    return self.request('GET', url, headers=headers)
+
+  def post(self, url, data, headers=None):
+    return self.request('POST', url, data=data, headers=headers)
+
+  def put(self, url, data, headers=None):
+    return self.request('PUT', url, data=data, headers=headers)
+
+  def delete(self, url, headers=None):
+    return self.request('DELETE', url, headers=headers)
+
+
+class GenericToken(object):
+  """Represents an Authorization token to be added to HTTP requests.
+  
+  Some Authorization headers included calculated fields (digital
+  signatures for example) which are based on the parameters of the HTTP
+  request. Therefore the token is responsible for signing the request
+  and adding the Authorization header. 
+  """
+  def perform_request(self, http_client, operation, url, data=None, 
+                      headers=None):
+    """For the GenericToken, no Authorization token is set."""
+    return http_client.request(operation, url, data=data, headers=headers)
+
+  def valid_for_scope(self, url):
+    """Tells the caller if the token authorizes access to the desired URL.
+    
+    Since the generic token doesn't add an auth header, it is not valid for
+    any scope.
+    """
+    return False
+
+

Added: trunk/conduit/modules/GoogleModule/atom/mock_http.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/atom/mock_http.py	Tue Mar 17 09:19:36 2009
@@ -0,0 +1,132 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+__author__ = 'api.jscudder (Jeff Scudder)'
+
+
+import atom.http_interface
+import atom.url
+
+
+class Error(Exception):
+  pass
+
+
+class NoRecordingFound(Error):
+  pass
+
+
+class MockRequest(object):
+  """Holds parameters of an HTTP request for matching against future requests.
+  """
+  def __init__(self, operation, url, data=None, headers=None):
+    self.operation = operation
+    if isinstance(url, (str, unicode)):
+      url = atom.url.parse_url(url)
+    self.url = url
+    self.data = data
+    self.headers = headers
+
+
+class MockResponse(atom.http_interface.HttpResponse):
+  """Simulates an httplib.HTTPResponse object."""
+  def __init__(self, body=None, status=None, reason=None, headers=None):
+    if body and hasattr(body, 'read'):
+      self.body = body.read()
+    else:
+      self.body = body
+    if status is not None:
+      self.status = int(status)
+    else:
+      self.status = None
+    self.reason = reason
+    self._headers = headers or {}
+
+  def read(self):
+    return self.body
+
+
+class MockHttpClient(atom.http_interface.GenericHttpClient):
+  def __init__(self, headers=None, recordings=None, real_client=None):
+    """An HttpClient which responds to request with stored data.
+
+    The request-response pairs are stored as tuples in a member list named
+    recordings.
+
+    The MockHttpClient can be switched from replay mode to record mode by
+    setting the real_client member to an instance of an HttpClient which will
+    make real HTTP requests and store the server's response in list of 
+    recordings.
+    
+    Args:
+      headers: dict containing HTTP headers which should be included in all
+          HTTP requests.
+      recordings: The initial recordings to be used for responses. This list
+          contains tuples in the form: (MockRequest, MockResponse)
+      real_client: An HttpClient which will make a real HTTP request. The 
+          response will be converted into a MockResponse and stored in 
+          recordings.
+    """
+    self.recordings = recordings or []
+    self.real_client = real_client
+    self.headers = headers or {}
+
+  def add_response(self, response, operation, url, data=None, headers=None):
+    """Adds a request-response pair to the recordings list.
+    
+    After the recording is added, future matching requests will receive the
+    response.
+    
+    Args:
+      response: MockResponse
+      operation: str
+      url: str
+      data: str, Currently the data is ignored when looking for matching
+          requests.
+      headers: dict of strings: Currently the headers are ignored when
+          looking for matching requests.
+    """
+    request = MockRequest(operation, url, data=data, headers=headers)
+    self.recordings.append((request, response))
+
+  def request(self, operation, url, data=None, headers=None):
+    """Returns a matching MockResponse from the recordings.
+    
+    If the real_client is set, the request will be passed along and the 
+    server's response will be added to the recordings and also returned. 
+
+    If there is no match, a NoRecordingFound error will be raised.
+    """
+    if self.real_client is None:
+      if isinstance(url, (str, unicode)):
+        url = atom.url.parse_url(url)
+      for recording in self.recordings:
+        if recording[0].operation == operation and recording[0].url == url:
+          return recording[1]
+      raise NoRecordingFound('No recodings found for %s %s' % (
+          operation, url))
+    else:
+      # There is a real HTTP client, so make the request, and record the 
+      # response.
+      response = self.real_client.request(operation, url, data=data, 
+          headers=headers)
+      # TODO: copy the headers
+      stored_response = MockResponse(body=response, status=response.status,
+          reason=response.reason)
+      self.add_response(stored_response, operation, url, data=data, 
+          headers=headers)
+      return stored_response

Added: trunk/conduit/modules/GoogleModule/atom/mock_http_core.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/atom/mock_http_core.py	Tue Mar 17 09:19:36 2009
@@ -0,0 +1,141 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2009 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 is used for version 2 of the Google Data APIs.
+
+
+__author__ = 'j s google com (Jeff Scudder)'
+
+
+import StringIO
+import pickle
+import os.path
+import tempfile
+import atom.http_core
+
+
+class MockHttpClient(object):
+
+  real_client = None
+
+  def __init__(self, recordings=None, real_client=None):
+    self._recordings = recordings or []
+    if real_client is not None:
+      self.real_client = real_client
+
+  def add_response(self, http_request, status, reason, headers=None, 
+      body=None):
+    if body is not None:
+      if hasattr(body, 'read'):
+        copied_body = body.read()
+      else:
+        copied_body = body
+    response = atom.http_core.HttpResponse(status, reason, headers, 
+                                           copied_body)
+    # TODO Scrub the request and the response.
+    self._recordings.append((http_request._copy(), response))
+  
+  def request(self, http_request):
+    """Provide a recorded response, or record a response for replay.
+
+    If the real_client is set, the request will be made using the
+    real_client, and the response from the server will be recorded.
+    If the real_client is None (the default), this method will examine
+    the recordings and find the first which matches. 
+    """
+    request = http_request._copy()
+    _scrub_request(request)
+    if self.real_client is None:
+      for recording in self._recordings:
+        if _match_request(recording[0], request):
+          return recording[1]
+    else:
+      response = self.real_client.request(http_request)
+      _scrub_response(response)
+      self.add_response(request, response.status, response.reason, 
+          dict(response.getheaders()), response.read())
+      # Return the recording which we just added.
+      return self._recordings[-1][1]
+    return None
+    
+  def _save_recordings(self, filename):
+    recording_file = open(os.path.join(tempfile.gettempdir(), filename), 
+                          'wb')
+    pickle.dump(self._recordings, recording_file)
+
+  def _load_recordings(self, filename):
+    recording_file = open(os.path.join(tempfile.gettempdir(), filename), 
+                          'rb')
+    self._recordings = pickle.load(recording_file)
+
+  def _load_or_use_client(self, filename, http_client):
+    if os.path.exists(os.path.join(tempfile.gettempdir(), filename)):
+      self._load_recordings(filename)
+    else:
+      self.real_client = http_client
+
+def _match_request(http_request, stored_request):
+  """Determines whether a request is similar enough to a stored request 
+     to cause the stored response to be returned."""
+  return True
+
+def _scrub_request(http_request):
+  pass
+
+def _scrub_response(http_response):
+  pass
+
+    
+class EchoHttpClient(object):
+  """Sends the request data back in the response.
+
+  Used to check the formatting of the request as it was sent. Always responds
+  with a 200 OK, and some information from the HTTP request is returned in
+  special Echo-X headers in the response. The following headers are added
+  in the response:
+  'Echo-Host': The host name and port number to which the HTTP connection is
+               made. If no port was passed in, the header will contain
+               host:None.
+  'Echo-Uri': The path portion of the URL being requested. /example?x=1&y=2
+  'Echo-Scheme': The beginning of the URL, usually 'http' or 'https'
+  'Echo-Method': The HTTP method being used, 'GET', 'POST', 'PUT', etc.
+  """
+  
+  def request(self, http_request):
+    return self._http_request(http_request.host, http_request.method, 
+        http_request.uri, http_request.scheme, http_request.port, 
+        http_request.headers, http_request._body_parts)
+
+  def _http_request(self, host, method, uri, scheme=None,  port=None, 
+      headers=None, body_parts=None):
+    body = StringIO.StringIO()
+    response = atom.http_core.HttpResponse(status=200, reason='OK', body=body)
+    if headers is None:
+      response._headers = {}
+    else:
+      response._headers = headers.copy()
+    response._headers['Echo-Host'] = '%s:%s' % (host, str(port))
+    response._headers['Echo-Uri'] = uri
+    response._headers['Echo-Scheme'] = scheme
+    response._headers['Echo-Method'] = method
+    for part in body_parts:
+      if isinstance(part, str):
+        body.write(part)
+      elif hasattr(part, 'read'):
+        body.write(part.read())
+    body.seek(0)
+    return response

Added: trunk/conduit/modules/GoogleModule/atom/token_store.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/atom/token_store.py	Tue Mar 17 09:19:36 2009
@@ -0,0 +1,117 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This module provides a TokenStore class which is designed to manage
+auth tokens required for different services.
+
+Each token is valid for a set of scopes which is the start of a URL. An HTTP
+client will use a token store to find a valid Authorization header to send
+in requests to the specified URL. If the HTTP client determines that a token
+has expired or been revoked, it can remove the token from the store so that
+it will not be used in future requests.
+"""
+
+
+__author__ = 'api.jscudder (Jeff Scudder)'
+
+
+import atom.http_interface
+import atom.url
+
+
+SCOPE_ALL = 'http'
+
+
+class TokenStore(object):
+  """Manages Authorization tokens which will be sent in HTTP headers."""
+  def __init__(self, scoped_tokens=None):
+    self._tokens = scoped_tokens or {}
+
+  def add_token(self, token):
+    """Adds a new token to the store (replaces tokens with the same scope).
+
+    Args:
+      token: A subclass of http_interface.GenericToken. The token object is 
+          responsible for adding the Authorization header to the HTTP request.
+          The scopes defined in the token are used to determine if the token
+          is valid for a requested scope when find_token is called.
+
+    Returns:
+      True if the token was added, False if the token was not added becase
+      no scopes were provided.
+    """
+    if not hasattr(token, 'scopes') or not token.scopes:
+      return False
+
+    for scope in token.scopes:
+      self._tokens[str(scope)] = token
+    return True  
+
+  def find_token(self, url):
+    """Selects an Authorization header token which can be used for the URL.
+
+    Args:
+      url: str or atom.url.Url or a list containing the same.
+          The URL which is going to be requested. All
+          tokens are examined to see if any scopes begin match the beginning
+          of the URL. The first match found is returned.
+
+    Returns:
+      The token object which should execute the HTTP request. If there was
+      no token for the url (the url did not begin with any of the token
+      scopes available), then the atom.http_interface.GenericToken will be 
+      returned because the GenericToken calls through to the http client
+      without adding an Authorization header.
+    """
+    if url is None:
+      return None
+    if isinstance(url, (str, unicode)):
+      url = atom.url.parse_url(url)
+    if url in self._tokens:
+      token = self._tokens[url]
+      if token.valid_for_scope(url):
+        return token
+      else:
+        del self._tokens[url]
+    for scope, token in self._tokens.iteritems():
+      if token.valid_for_scope(url):
+        return token
+    return atom.http_interface.GenericToken()
+
+  def remove_token(self, token):
+    """Removes the token from the token_store.
+
+    This method is used when a token is determined to be invalid. If the
+    token was found by find_token, but resulted in a 401 or 403 error stating
+    that the token was invlid, then the token should be removed to prevent
+    future use.
+
+    Returns:
+      True if a token was found and then removed from the token
+      store. False if the token was not in the TokenStore.
+    """
+    token_found = False
+    scopes_to_delete = []
+    for scope, stored_token in self._tokens.iteritems():
+      if stored_token == token:
+        scopes_to_delete.append(scope)
+        token_found = True
+    for scope in scopes_to_delete:
+      del self._tokens[scope]
+    return token_found
+
+  def remove_all_tokens(self):
+    self._tokens = {} 

Added: trunk/conduit/modules/GoogleModule/atom/url.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/atom/url.py	Tue Mar 17 09:19:36 2009
@@ -0,0 +1,139 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+__author__ = 'api.jscudder (Jeff Scudder)'
+
+
+import urlparse
+import urllib
+
+
+DEFAULT_PROTOCOL = 'http'
+DEFAULT_PORT = 80
+
+
+def parse_url(url_string):
+  """Creates a Url object which corresponds to the URL string.
+  
+  This method can accept partial URLs, but it will leave missing
+  members of the Url unset.
+  """
+  parts = urlparse.urlparse(url_string)
+  url = Url()
+  if parts[0]:
+    url.protocol = parts[0]
+  if parts[1]:
+    host_parts = parts[1].split(':')
+    if host_parts[0]:
+      url.host = host_parts[0]
+    if len(host_parts) > 1:
+      url.port = host_parts[1]
+  if parts[2]:
+    url.path = parts[2]
+  if parts[4]:
+    param_pairs = parts[4].split('&')
+    for pair in param_pairs:
+      pair_parts = pair.split('=')
+      if len(pair_parts) > 1:
+        url.params[urllib.unquote_plus(pair_parts[0])] = (
+            urllib.unquote_plus(pair_parts[1]))
+      elif len(pair_parts) == 1:
+        url.params[urllib.unquote_plus(pair_parts[0])] = None
+  return url
+   
+class Url(object):
+  """Represents a URL and implements comparison logic.
+  
+  URL strings which are not identical can still be equivalent, so this object
+  provides a better interface for comparing and manipulating URLs than 
+  strings. URL parameters are represented as a dictionary of strings, and
+  defaults are used for the protocol (http) and port (80) if not provided.
+  """
+  def __init__(self, protocol=None, host=None, port=None, path=None, 
+               params=None):
+    self.protocol = protocol
+    self.host = host
+    self.port = port
+    self.path = path
+    self.params = params or {}
+
+  def to_string(self):
+    url_parts = ['', '', '', '', '', '']
+    if self.protocol:
+      url_parts[0] = self.protocol
+    if self.host:
+      if self.port:
+        url_parts[1] = ':'.join((self.host, str(self.port)))
+      else:
+        url_parts[1] = self.host
+    if self.path:
+      url_parts[2] = self.path
+    if self.params:
+      url_parts[4] = self.get_param_string()
+    return urlparse.urlunparse(url_parts)
+
+  def get_param_string(self):
+    param_pairs = []
+    for key, value in self.params.iteritems():
+      param_pairs.append('='.join((urllib.quote_plus(key), 
+          urllib.quote_plus(str(value)))))
+    return '&'.join(param_pairs)
+
+  def get_request_uri(self):
+    """Returns the path with the parameters escaped and appended."""
+    param_string = self.get_param_string()
+    if param_string:
+      return '?'.join([self.path, param_string])
+    else:
+      return self.path
+
+  def __cmp__(self, other):
+    if not isinstance(other, Url):
+      return cmp(self.to_string(), str(other))
+    difference = 0
+    # Compare the protocol
+    if self.protocol and other.protocol:
+      difference = cmp(self.protocol, other.protocol)
+    elif self.protocol and not other.protocol:
+      difference = cmp(self.protocol, DEFAULT_PROTOCOL)
+    elif not self.protocol and other.protocol:
+      difference = cmp(DEFAULT_PROTOCOL, other.protocol)
+    if difference != 0:
+      return difference
+    # Compare the host
+    difference = cmp(self.host, other.host)
+    if difference != 0:
+      return difference
+    # Compare the port
+    if self.port and other.port:
+      difference = cmp(self.port, other.port)
+    elif self.port and not other.port:
+      difference = cmp(self.port, DEFAULT_PORT)
+    elif not self.port and other.port:
+      difference = cmp(DEFAULT_PORT, other.port)
+    if difference != 0:
+      return difference
+    # Compare the path
+    difference = cmp(self.path, other.path)
+    if difference != 0:
+      return difference
+    # Compare the parameters
+    return cmp(self.params, other.params)
+
+  def __str__(self):
+    return self.to_string()
+    



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