conduit r1927 - in trunk: . conduit/modules/GoogleModule/atom
- From: jstowers svn gnome org
- To: svn-commits-list gnome org
- Subject: conduit r1927 - in trunk: . conduit/modules/GoogleModule/atom
- Date: Tue, 17 Mar 2009 09:19:36 +0000 (UTC)
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]