From 71750f8e0b013ac569ac0f48d42e874f3a9e86a1 Mon Sep 17 00:00:00 2001 From: Masci Date: Tue, 31 Aug 2010 22:00:32 +0200 Subject: [PATCH] added external libs --- atom/__init__.py | 1484 ++++++++++++++ atom/__init__.pyc | Bin 0 -> 52794 bytes atom/auth.py | 43 + atom/client.py | 182 ++ atom/core.py | 545 ++++++ atom/data.py | 339 ++++ atom/http.py | 318 +++ atom/http.pyc | Bin 0 -> 9445 bytes atom/http_core.py | 597 ++++++ atom/http_core.pyc | Bin 0 -> 19145 bytes atom/http_interface.py | 158 ++ atom/http_interface.pyc | Bin 0 -> 6912 bytes atom/mock_http.py | 132 ++ atom/mock_http_core.py | 323 ++++ atom/mock_service.py | 243 +++ atom/service.py | 740 +++++++ atom/service.pyc | Bin 0 -> 26804 bytes atom/token_store.py | 117 ++ atom/token_store.pyc | Bin 0 -> 4380 bytes atom/url.py | 139 ++ atom/url.pyc | Bin 0 -> 4206 bytes gdata/__init__.py | 835 ++++++++ gdata/__init__.pyc | Bin 0 -> 33483 bytes gdata/auth.py | 952 +++++++++ gdata/auth.pyc | Bin 0 -> 38540 bytes gdata/calendar/__init__.py | 1044 ++++++++++ gdata/calendar/__init__.pyc | Bin 0 -> 39599 bytes gdata/calendar/data.py | 300 +++ gdata/calendar/service.py | 595 ++++++ gdata/calendar/service.pyc | Bin 0 -> 27086 bytes gdata/core.py | 279 +++ gdata/data.py | 1186 ++++++++++++ gdata/gauth.py | 1306 +++++++++++++ gdata/gauth.pyc | Bin 0 -> 49614 bytes gdata/oauth/CHANGES.txt | 17 + gdata/oauth/__init__.py | 529 +++++ gdata/oauth/__init__.pyc | Bin 0 -> 21643 bytes gdata/oauth/rsa.py | 120 ++ gdata/oauth/rsa.pyc | Bin 0 -> 4668 bytes gdata/service.py | 1717 +++++++++++++++++ gdata/service.pyc | Bin 0 -> 67524 bytes gdata/tlslite/BaseDB.py | 120 ++ gdata/tlslite/Checker.py | 146 ++ gdata/tlslite/FileObject.py | 220 +++ gdata/tlslite/HandshakeSettings.py | 159 ++ gdata/tlslite/Session.py | 131 ++ gdata/tlslite/SessionCache.py | 103 + gdata/tlslite/SharedKeyDB.py | 58 + gdata/tlslite/TLSConnection.py | 1600 +++++++++++++++ gdata/tlslite/TLSRecordLayer.py | 1123 +++++++++++ gdata/tlslite/VerifierDB.py | 90 + gdata/tlslite/X509.py | 133 ++ gdata/tlslite/X509CertChain.py | 181 ++ gdata/tlslite/__init__.py | 39 + gdata/tlslite/__init__.pyc | Bin 0 -> 1232 bytes gdata/tlslite/api.py | 75 + gdata/tlslite/constants.py | 225 +++ gdata/tlslite/errors.py | 149 ++ .../tlslite/integration/AsyncStateMachine.py | 235 +++ gdata/tlslite/integration/ClientHelper.py | 163 ++ .../tlslite/integration/HTTPTLSConnection.py | 169 ++ gdata/tlslite/integration/IMAP4_TLS.py | 132 ++ .../tlslite/integration/IntegrationHelper.py | 52 + gdata/tlslite/integration/POP3_TLS.py | 142 ++ gdata/tlslite/integration/SMTP_TLS.py | 114 ++ .../integration/TLSAsyncDispatcherMixIn.py | 139 ++ .../integration/TLSSocketServerMixIn.py | 59 + .../integration/TLSTwistedProtocolWrapper.py | 196 ++ gdata/tlslite/integration/XMLRPCTransport.py | 137 ++ gdata/tlslite/integration/__init__.py | 17 + gdata/tlslite/mathtls.py | 170 ++ gdata/tlslite/messages.py | 561 ++++++ gdata/tlslite/utils/AES.py | 31 + gdata/tlslite/utils/ASN1Parser.py | 34 + gdata/tlslite/utils/ASN1Parser.pyc | Bin 0 -> 1522 bytes gdata/tlslite/utils/Cryptlib_AES.py | 34 + gdata/tlslite/utils/Cryptlib_RC4.py | 28 + gdata/tlslite/utils/Cryptlib_TripleDES.py | 35 + gdata/tlslite/utils/OpenSSL_AES.py | 49 + gdata/tlslite/utils/OpenSSL_RC4.py | 25 + gdata/tlslite/utils/OpenSSL_RSAKey.py | 148 ++ gdata/tlslite/utils/OpenSSL_TripleDES.py | 44 + gdata/tlslite/utils/PyCrypto_AES.py | 22 + gdata/tlslite/utils/PyCrypto_RC4.py | 22 + gdata/tlslite/utils/PyCrypto_RSAKey.py | 61 + gdata/tlslite/utils/PyCrypto_TripleDES.py | 22 + gdata/tlslite/utils/Python_AES.py | 68 + gdata/tlslite/utils/Python_RC4.py | 39 + gdata/tlslite/utils/Python_RSAKey.py | 209 ++ gdata/tlslite/utils/Python_RSAKey.pyc | Bin 0 -> 8333 bytes gdata/tlslite/utils/RC4.py | 17 + gdata/tlslite/utils/RSAKey.py | 264 +++ gdata/tlslite/utils/RSAKey.pyc | Bin 0 -> 10159 bytes gdata/tlslite/utils/TripleDES.py | 26 + gdata/tlslite/utils/__init__.py | 31 + gdata/tlslite/utils/__init__.pyc | Bin 0 -> 843 bytes gdata/tlslite/utils/cipherfactory.py | 111 ++ gdata/tlslite/utils/codec.py | 94 + gdata/tlslite/utils/codec.pyc | Bin 0 -> 4621 bytes gdata/tlslite/utils/compat.py | 140 ++ gdata/tlslite/utils/compat.pyc | Bin 0 -> 7114 bytes gdata/tlslite/utils/cryptomath.py | 404 ++++ gdata/tlslite/utils/cryptomath.pyc | Bin 0 -> 11744 bytes gdata/tlslite/utils/dateFuncs.py | 75 + gdata/tlslite/utils/entropy.c | 173 ++ gdata/tlslite/utils/hmac.py | 104 + gdata/tlslite/utils/jython_compat.py | 195 ++ gdata/tlslite/utils/keyfactory.py | 243 +++ gdata/tlslite/utils/keyfactory.pyc | Bin 0 -> 9296 bytes gdata/tlslite/utils/rijndael.py | 392 ++++ gdata/tlslite/utils/win32prng.c | 63 + gdata/tlslite/utils/xmltools.py | 202 ++ gdata/tlslite/utils/xmltools.pyc | Bin 0 -> 8607 bytes gdata/urlfetch.py | 247 +++ tweepy/__init__.py | 27 + tweepy/__init__.pyc | Bin 0 -> 1280 bytes tweepy/api.py | 735 +++++++ tweepy/api.pyc | Bin 0 -> 14949 bytes tweepy/auth.py | 163 ++ tweepy/auth.pyc | Bin 0 -> 6931 bytes tweepy/binder.py | 191 ++ tweepy/binder.pyc | Bin 0 -> 5054 bytes tweepy/cache.py | 264 +++ tweepy/cache.pyc | Bin 0 -> 9605 bytes tweepy/cursor.py | 128 ++ tweepy/cursor.pyc | Bin 0 -> 5906 bytes tweepy/error.py | 14 + tweepy/error.pyc | Bin 0 -> 806 bytes tweepy/models.py | 313 +++ tweepy/models.pyc | Bin 0 -> 14462 bytes tweepy/oauth.py | 655 +++++++ tweepy/oauth.pyc | Bin 0 -> 26883 bytes tweepy/parsers.py | 84 + tweepy/parsers.pyc | Bin 0 -> 3463 bytes tweepy/streaming.py | 200 ++ tweepy/streaming.pyc | Bin 0 -> 7493 bytes tweepy/utils.py | 98 + tweepy/utils.pyc | Bin 0 -> 3222 bytes 138 files changed, 27602 insertions(+) create mode 100755 atom/__init__.py create mode 100644 atom/__init__.pyc create mode 100644 atom/auth.py create mode 100755 atom/client.py create mode 100644 atom/core.py create mode 100644 atom/data.py create mode 100644 atom/http.py create mode 100644 atom/http.pyc create mode 100644 atom/http_core.py create mode 100644 atom/http_core.pyc create mode 100644 atom/http_interface.py create mode 100644 atom/http_interface.pyc create mode 100644 atom/mock_http.py create mode 100644 atom/mock_http_core.py create mode 100755 atom/mock_service.py create mode 100755 atom/service.py create mode 100644 atom/service.pyc create mode 100644 atom/token_store.py create mode 100644 atom/token_store.pyc create mode 100644 atom/url.py create mode 100644 atom/url.pyc create mode 100755 gdata/__init__.py create mode 100644 gdata/__init__.pyc create mode 100644 gdata/auth.py create mode 100644 gdata/auth.pyc create mode 100755 gdata/calendar/__init__.py create mode 100644 gdata/calendar/__init__.pyc create mode 100644 gdata/calendar/data.py create mode 100755 gdata/calendar/service.py create mode 100644 gdata/calendar/service.pyc create mode 100644 gdata/core.py create mode 100644 gdata/data.py create mode 100644 gdata/gauth.py create mode 100644 gdata/gauth.pyc create mode 100755 gdata/oauth/CHANGES.txt create mode 100755 gdata/oauth/__init__.py create mode 100644 gdata/oauth/__init__.pyc create mode 100755 gdata/oauth/rsa.py create mode 100644 gdata/oauth/rsa.pyc create mode 100755 gdata/service.py create mode 100644 gdata/service.pyc create mode 100755 gdata/tlslite/BaseDB.py create mode 100755 gdata/tlslite/Checker.py create mode 100755 gdata/tlslite/FileObject.py create mode 100755 gdata/tlslite/HandshakeSettings.py create mode 100755 gdata/tlslite/Session.py create mode 100755 gdata/tlslite/SessionCache.py create mode 100755 gdata/tlslite/SharedKeyDB.py create mode 100755 gdata/tlslite/TLSConnection.py create mode 100755 gdata/tlslite/TLSRecordLayer.py create mode 100755 gdata/tlslite/VerifierDB.py create mode 100755 gdata/tlslite/X509.py create mode 100755 gdata/tlslite/X509CertChain.py create mode 100755 gdata/tlslite/__init__.py create mode 100644 gdata/tlslite/__init__.pyc create mode 100755 gdata/tlslite/api.py create mode 100755 gdata/tlslite/constants.py create mode 100755 gdata/tlslite/errors.py create mode 100755 gdata/tlslite/integration/AsyncStateMachine.py create mode 100755 gdata/tlslite/integration/ClientHelper.py create mode 100755 gdata/tlslite/integration/HTTPTLSConnection.py create mode 100755 gdata/tlslite/integration/IMAP4_TLS.py create mode 100755 gdata/tlslite/integration/IntegrationHelper.py create mode 100755 gdata/tlslite/integration/POP3_TLS.py create mode 100755 gdata/tlslite/integration/SMTP_TLS.py create mode 100755 gdata/tlslite/integration/TLSAsyncDispatcherMixIn.py create mode 100755 gdata/tlslite/integration/TLSSocketServerMixIn.py create mode 100755 gdata/tlslite/integration/TLSTwistedProtocolWrapper.py create mode 100755 gdata/tlslite/integration/XMLRPCTransport.py create mode 100755 gdata/tlslite/integration/__init__.py create mode 100755 gdata/tlslite/mathtls.py create mode 100755 gdata/tlslite/messages.py create mode 100755 gdata/tlslite/utils/AES.py create mode 100755 gdata/tlslite/utils/ASN1Parser.py create mode 100644 gdata/tlslite/utils/ASN1Parser.pyc create mode 100755 gdata/tlslite/utils/Cryptlib_AES.py create mode 100755 gdata/tlslite/utils/Cryptlib_RC4.py create mode 100755 gdata/tlslite/utils/Cryptlib_TripleDES.py create mode 100755 gdata/tlslite/utils/OpenSSL_AES.py create mode 100755 gdata/tlslite/utils/OpenSSL_RC4.py create mode 100755 gdata/tlslite/utils/OpenSSL_RSAKey.py create mode 100755 gdata/tlslite/utils/OpenSSL_TripleDES.py create mode 100755 gdata/tlslite/utils/PyCrypto_AES.py create mode 100755 gdata/tlslite/utils/PyCrypto_RC4.py create mode 100755 gdata/tlslite/utils/PyCrypto_RSAKey.py create mode 100755 gdata/tlslite/utils/PyCrypto_TripleDES.py create mode 100755 gdata/tlslite/utils/Python_AES.py create mode 100755 gdata/tlslite/utils/Python_RC4.py create mode 100755 gdata/tlslite/utils/Python_RSAKey.py create mode 100644 gdata/tlslite/utils/Python_RSAKey.pyc create mode 100755 gdata/tlslite/utils/RC4.py create mode 100755 gdata/tlslite/utils/RSAKey.py create mode 100644 gdata/tlslite/utils/RSAKey.pyc create mode 100755 gdata/tlslite/utils/TripleDES.py create mode 100755 gdata/tlslite/utils/__init__.py create mode 100644 gdata/tlslite/utils/__init__.pyc create mode 100755 gdata/tlslite/utils/cipherfactory.py create mode 100755 gdata/tlslite/utils/codec.py create mode 100644 gdata/tlslite/utils/codec.pyc create mode 100755 gdata/tlslite/utils/compat.py create mode 100644 gdata/tlslite/utils/compat.pyc create mode 100755 gdata/tlslite/utils/cryptomath.py create mode 100644 gdata/tlslite/utils/cryptomath.pyc create mode 100755 gdata/tlslite/utils/dateFuncs.py create mode 100755 gdata/tlslite/utils/entropy.c create mode 100755 gdata/tlslite/utils/hmac.py create mode 100755 gdata/tlslite/utils/jython_compat.py create mode 100755 gdata/tlslite/utils/keyfactory.py create mode 100644 gdata/tlslite/utils/keyfactory.pyc create mode 100755 gdata/tlslite/utils/rijndael.py create mode 100755 gdata/tlslite/utils/win32prng.c create mode 100755 gdata/tlslite/utils/xmltools.py create mode 100644 gdata/tlslite/utils/xmltools.pyc create mode 100644 gdata/urlfetch.py create mode 100644 tweepy/__init__.py create mode 100644 tweepy/__init__.pyc create mode 100644 tweepy/api.py create mode 100644 tweepy/api.pyc create mode 100644 tweepy/auth.py create mode 100644 tweepy/auth.pyc create mode 100644 tweepy/binder.py create mode 100644 tweepy/binder.pyc create mode 100644 tweepy/cache.py create mode 100644 tweepy/cache.pyc create mode 100644 tweepy/cursor.py create mode 100644 tweepy/cursor.pyc create mode 100644 tweepy/error.py create mode 100644 tweepy/error.pyc create mode 100644 tweepy/models.py create mode 100644 tweepy/models.pyc create mode 100644 tweepy/oauth.py create mode 100644 tweepy/oauth.pyc create mode 100644 tweepy/parsers.py create mode 100644 tweepy/parsers.pyc create mode 100644 tweepy/streaming.py create mode 100644 tweepy/streaming.pyc create mode 100644 tweepy/utils.py create mode 100644 tweepy/utils.pyc diff --git a/atom/__init__.py b/atom/__init__.py new file mode 100755 index 0000000..6aa96c1 --- /dev/null +++ b/atom/__init__.py @@ -0,0 +1,1484 @@ +#!/usr/bin/python +# +# Copyright (C) 2006 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Contains classes representing Atom elements. + + Module objective: provide data classes for Atom constructs. These classes hide + the XML-ness of Atom and provide a set of native Python classes to interact + with. + + Conversions to and from XML should only be necessary when the Atom classes + "touch the wire" and are sent over HTTP. For this reason this module + provides methods and functions to convert Atom classes to and from strings. + + For more information on the Atom data model, see RFC 4287 + (http://www.ietf.org/rfc/rfc4287.txt) + + AtomBase: A foundation class on which Atom classes are built. It + handles the parsing of attributes and children which are common to all + Atom classes. By default, the AtomBase class translates all XML child + nodes into ExtensionElements. + + ExtensionElement: Atom allows Atom objects to contain XML which is not part + of the Atom specification, these are called extension elements. If a + classes parser encounters an unexpected XML construct, it is translated + into an ExtensionElement instance. ExtensionElement is designed to fully + capture the information in the XML. Child nodes in an XML extension are + turned into ExtensionElements as well. +""" + + +__author__ = 'api.jscudder (Jeffrey Scudder)' + +try: + from xml.etree import cElementTree as ElementTree +except ImportError: + try: + import cElementTree as ElementTree + except ImportError: + try: + from xml.etree import ElementTree + except ImportError: + from elementtree import ElementTree +import warnings + + +# XML namespaces which are often used in Atom entities. +ATOM_NAMESPACE = 'http://www.w3.org/2005/Atom' +ELEMENT_TEMPLATE = '{http://www.w3.org/2005/Atom}%s' +APP_NAMESPACE = 'http://purl.org/atom/app#' +APP_TEMPLATE = '{http://purl.org/atom/app#}%s' + +# This encoding is used for converting strings before translating the XML +# into an object. +XML_STRING_ENCODING = 'utf-8' +# The desired string encoding for object members. set or monkey-patch to +# unicode if you want object members to be Python unicode strings, instead of +# encoded strings +MEMBER_STRING_ENCODING = 'utf-8' +#MEMBER_STRING_ENCODING = unicode + +# If True, all methods which are exclusive to v1 will raise a +# DeprecationWarning +ENABLE_V1_WARNINGS = False + + +def v1_deprecated(warning=None): + """Shows a warning if ENABLE_V1_WARNINGS is True. + + Function decorator used to mark methods used in v1 classes which + may be removed in future versions of the library. + """ + warning = warning or '' + # This closure is what is returned from the deprecated function. + def mark_deprecated(f): + # The deprecated_function wraps the actual call to f. + def optional_warn_function(*args, **kwargs): + if ENABLE_V1_WARNINGS: + warnings.warn(warning, DeprecationWarning, stacklevel=2) + return f(*args, **kwargs) + # Preserve the original name to avoid masking all decorated functions as + # 'deprecated_function' + try: + optional_warn_function.func_name = f.func_name + except TypeError: + pass # In Python2.3 we can't set the func_name + return optional_warn_function + return mark_deprecated + + +def CreateClassFromXMLString(target_class, xml_string, string_encoding=None): + """Creates an instance of the target class from the string contents. + + Args: + target_class: class The class which will be instantiated and populated + with the contents of the XML. This class must have a _tag and a + _namespace class variable. + xml_string: str A string which contains valid XML. The root element + of the XML string should match the tag and namespace of the desired + class. + string_encoding: str The character encoding which the xml_string should + be converted to before it is interpreted and translated into + objects. The default is None in which case the string encoding + is not changed. + + Returns: + An instance of the target class with members assigned according to the + contents of the XML - or None if the root XML tag and namespace did not + match those of the target class. + """ + encoding = string_encoding or XML_STRING_ENCODING + if encoding and isinstance(xml_string, unicode): + xml_string = xml_string.encode(encoding) + tree = ElementTree.fromstring(xml_string) + return _CreateClassFromElementTree(target_class, tree) + + +CreateClassFromXMLString = v1_deprecated( + 'Please use atom.core.parse with atom.data classes instead.')( + CreateClassFromXMLString) + + +def _CreateClassFromElementTree(target_class, tree, namespace=None, tag=None): + """Instantiates the class and populates members according to the tree. + + Note: Only use this function with classes that have _namespace and _tag + class members. + + Args: + target_class: class The class which will be instantiated and populated + with the contents of the XML. + tree: ElementTree An element tree whose contents will be converted into + members of the new target_class instance. + namespace: str (optional) The namespace which the XML tree's root node must + match. If omitted, the namespace defaults to the _namespace of the + target class. + tag: str (optional) The tag which the XML tree's root node must match. If + omitted, the tag defaults to the _tag class member of the target + class. + + Returns: + An instance of the target class - or None if the tag and namespace of + the XML tree's root node did not match the desired namespace and tag. + """ + if namespace is None: + namespace = target_class._namespace + if tag is None: + tag = target_class._tag + if tree.tag == '{%s}%s' % (namespace, tag): + target = target_class() + target._HarvestElementTree(tree) + return target + else: + return None + + +class ExtensionContainer(object): + + def __init__(self, extension_elements=None, extension_attributes=None, + text=None): + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + self.text = text + + __init__ = v1_deprecated( + 'Please use data model classes in atom.data instead.')( + __init__) + + # Three methods to create an object from an ElementTree + def _HarvestElementTree(self, tree): + # Fill in the instance members from the contents of the XML tree. + for child in tree: + self._ConvertElementTreeToMember(child) + for attribute, value in tree.attrib.iteritems(): + self._ConvertElementAttributeToMember(attribute, value) + # Encode the text string according to the desired encoding type. (UTF-8) + if tree.text: + if MEMBER_STRING_ENCODING is unicode: + self.text = tree.text + else: + self.text = tree.text.encode(MEMBER_STRING_ENCODING) + + def _ConvertElementTreeToMember(self, child_tree, current_class=None): + self.extension_elements.append(_ExtensionElementFromElementTree( + child_tree)) + + def _ConvertElementAttributeToMember(self, attribute, value): + # Encode the attribute value's string with the desired type Default UTF-8 + if value: + if MEMBER_STRING_ENCODING is unicode: + self.extension_attributes[attribute] = value + else: + self.extension_attributes[attribute] = value.encode( + MEMBER_STRING_ENCODING) + + # One method to create an ElementTree from an object + def _AddMembersToElementTree(self, tree): + for child in self.extension_elements: + child._BecomeChildElement(tree) + for attribute, value in self.extension_attributes.iteritems(): + if value: + if isinstance(value, unicode) or MEMBER_STRING_ENCODING is unicode: + tree.attrib[attribute] = value + else: + # Decode the value from the desired encoding (default UTF-8). + tree.attrib[attribute] = value.decode(MEMBER_STRING_ENCODING) + if self.text: + if isinstance(self.text, unicode) or MEMBER_STRING_ENCODING is unicode: + tree.text = self.text + else: + tree.text = self.text.decode(MEMBER_STRING_ENCODING) + + def FindExtensions(self, tag=None, namespace=None): + """Searches extension elements for child nodes with the desired name. + + Returns a list of extension elements within this object whose tag + and/or namespace match those passed in. To find all extensions in + a particular namespace, specify the namespace but not the tag name. + If you specify only the tag, the result list may contain extension + elements in multiple namespaces. + + Args: + tag: str (optional) The desired tag + namespace: str (optional) The desired namespace + + Returns: + A list of elements whose tag and/or namespace match the parameters + values + """ + + results = [] + + if tag and namespace: + for element in self.extension_elements: + if element.tag == tag and element.namespace == namespace: + results.append(element) + elif tag and not namespace: + for element in self.extension_elements: + if element.tag == tag: + results.append(element) + elif namespace and not tag: + for element in self.extension_elements: + if element.namespace == namespace: + results.append(element) + else: + for element in self.extension_elements: + results.append(element) + + return results + + +class AtomBase(ExtensionContainer): + + _children = {} + _attributes = {} + + def __init__(self, extension_elements=None, extension_attributes=None, + text=None): + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + self.text = text + + __init__ = v1_deprecated( + 'Please use data model classes in atom.data instead.')( + __init__) + + def _ConvertElementTreeToMember(self, child_tree): + # Find the element's tag in this class's list of child members + if self.__class__._children.has_key(child_tree.tag): + member_name = self.__class__._children[child_tree.tag][0] + member_class = self.__class__._children[child_tree.tag][1] + # If the class member is supposed to contain a list, make sure the + # matching member is set to a list, then append the new member + # instance to the list. + if isinstance(member_class, list): + if getattr(self, member_name) is None: + setattr(self, member_name, []) + getattr(self, member_name).append(_CreateClassFromElementTree( + member_class[0], child_tree)) + else: + setattr(self, member_name, + _CreateClassFromElementTree(member_class, child_tree)) + else: + ExtensionContainer._ConvertElementTreeToMember(self, child_tree) + + def _ConvertElementAttributeToMember(self, attribute, value): + # Find the attribute in this class's list of attributes. + if self.__class__._attributes.has_key(attribute): + # Find the member of this class which corresponds to the XML attribute + # (lookup in current_class._attributes) and set this member to the + # desired value (using self.__dict__). + if value: + # Encode the string to capture non-ascii characters (default UTF-8) + if MEMBER_STRING_ENCODING is unicode: + setattr(self, self.__class__._attributes[attribute], value) + else: + setattr(self, self.__class__._attributes[attribute], + value.encode(MEMBER_STRING_ENCODING)) + else: + ExtensionContainer._ConvertElementAttributeToMember( + self, attribute, value) + + # Three methods to create an ElementTree from an object + def _AddMembersToElementTree(self, tree): + # Convert the members of this class which are XML child nodes. + # This uses the class's _children dictionary to find the members which + # should become XML child nodes. + member_node_names = [values[0] for tag, values in + self.__class__._children.iteritems()] + for member_name in member_node_names: + member = getattr(self, member_name) + if member is None: + pass + elif isinstance(member, list): + for instance in member: + instance._BecomeChildElement(tree) + else: + member._BecomeChildElement(tree) + # Convert the members of this class which are XML attributes. + for xml_attribute, member_name in self.__class__._attributes.iteritems(): + member = getattr(self, member_name) + if member is not None: + if isinstance(member, unicode) or MEMBER_STRING_ENCODING is unicode: + tree.attrib[xml_attribute] = member + else: + tree.attrib[xml_attribute] = member.decode(MEMBER_STRING_ENCODING) + # Lastly, call the ExtensionContainers's _AddMembersToElementTree to + # convert any extension attributes. + ExtensionContainer._AddMembersToElementTree(self, tree) + + + def _BecomeChildElement(self, tree): + """ + + Note: Only for use with classes that have a _tag and _namespace class + member. It is in AtomBase so that it can be inherited but it should + not be called on instances of AtomBase. + + """ + new_child = ElementTree.Element('') + tree.append(new_child) + new_child.tag = '{%s}%s' % (self.__class__._namespace, + self.__class__._tag) + self._AddMembersToElementTree(new_child) + + def _ToElementTree(self): + """ + + Note, this method is designed to be used only with classes that have a + _tag and _namespace. It is placed in AtomBase for inheritance but should + not be called on this class. + + """ + new_tree = ElementTree.Element('{%s}%s' % (self.__class__._namespace, + self.__class__._tag)) + self._AddMembersToElementTree(new_tree) + return new_tree + + def ToString(self, string_encoding='UTF-8'): + """Converts the Atom object to a string containing XML.""" + return ElementTree.tostring(self._ToElementTree(), encoding=string_encoding) + + def __str__(self): + return self.ToString() + + +class Name(AtomBase): + """The atom:name element""" + + _tag = 'name' + _namespace = ATOM_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Name + + Args: + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def NameFromString(xml_string): + return CreateClassFromXMLString(Name, xml_string) + + +class Email(AtomBase): + """The atom:email element""" + + _tag = 'email' + _namespace = ATOM_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Email + + Args: + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + text: str The text data in the this element + """ + + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def EmailFromString(xml_string): + return CreateClassFromXMLString(Email, xml_string) + + +class Uri(AtomBase): + """The atom:uri element""" + + _tag = 'uri' + _namespace = ATOM_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Uri + + Args: + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + text: str The text data in the this element + """ + + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def UriFromString(xml_string): + return CreateClassFromXMLString(Uri, xml_string) + + +class Person(AtomBase): + """A foundation class from which atom:author and atom:contributor extend. + + A person contains information like name, email address, and web page URI for + an author or contributor to an Atom feed. + """ + + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + _children['{%s}name' % (ATOM_NAMESPACE)] = ('name', Name) + _children['{%s}email' % (ATOM_NAMESPACE)] = ('email', Email) + _children['{%s}uri' % (ATOM_NAMESPACE)] = ('uri', Uri) + + def __init__(self, name=None, email=None, uri=None, + extension_elements=None, extension_attributes=None, text=None): + """Foundation from which author and contributor are derived. + + The constructor is provided for illustrative purposes, you should not + need to instantiate a Person. + + Args: + name: Name The person's name + email: Email The person's email address + uri: Uri The URI of the person's webpage + extension_elements: list A list of ExtensionElement instances which are + children of this element. + extension_attributes: dict A dictionary of strings which are the values + for additional XML attributes of this element. + text: String The text contents of the element. This is the contents + of the Entry's XML text node. (Example: This is the text) + """ + + self.name = name + self.email = email + self.uri = uri + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + self.text = text + + +class Author(Person): + """The atom:author element + + An author is a required element in Feed. + """ + + _tag = 'author' + _namespace = ATOM_NAMESPACE + _children = Person._children.copy() + _attributes = Person._attributes.copy() + #_children = {} + #_attributes = {} + + def __init__(self, name=None, email=None, uri=None, + extension_elements=None, extension_attributes=None, text=None): + """Constructor for Author + + Args: + name: Name + email: Email + uri: Uri + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + text: str The text data in the this element + """ + + self.name = name + self.email = email + self.uri = uri + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + self.text = text + + +def AuthorFromString(xml_string): + return CreateClassFromXMLString(Author, xml_string) + + +class Contributor(Person): + """The atom:contributor element""" + + _tag = 'contributor' + _namespace = ATOM_NAMESPACE + _children = Person._children.copy() + _attributes = Person._attributes.copy() + + def __init__(self, name=None, email=None, uri=None, + extension_elements=None, extension_attributes=None, text=None): + """Constructor for Contributor + + Args: + name: Name + email: Email + uri: Uri + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + text: str The text data in the this element + """ + + self.name = name + self.email = email + self.uri = uri + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + self.text = text + + +def ContributorFromString(xml_string): + return CreateClassFromXMLString(Contributor, xml_string) + + +class Link(AtomBase): + """The atom:link element""" + + _tag = 'link' + _namespace = ATOM_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + _attributes['rel'] = 'rel' + _attributes['href'] = 'href' + _attributes['type'] = 'type' + _attributes['title'] = 'title' + _attributes['length'] = 'length' + _attributes['hreflang'] = 'hreflang' + + def __init__(self, href=None, rel=None, link_type=None, hreflang=None, + title=None, length=None, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Link + + Args: + href: string The href attribute of the link + rel: string + type: string + hreflang: string The language for the href + title: string + length: string The length of the href's destination + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + text: str The text data in the this element + """ + + self.href = href + self.rel = rel + self.type = link_type + self.hreflang = hreflang + self.title = title + self.length = length + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def LinkFromString(xml_string): + return CreateClassFromXMLString(Link, xml_string) + + +class Generator(AtomBase): + """The atom:generator element""" + + _tag = 'generator' + _namespace = ATOM_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + _attributes['uri'] = 'uri' + _attributes['version'] = 'version' + + def __init__(self, uri=None, version=None, text=None, + extension_elements=None, extension_attributes=None): + """Constructor for Generator + + Args: + uri: string + version: string + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.uri = uri + self.version = version + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + +def GeneratorFromString(xml_string): + return CreateClassFromXMLString(Generator, xml_string) + + +class Text(AtomBase): + """A foundation class from which atom:title, summary, etc. extend. + + This class should never be instantiated. + """ + + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + _attributes['type'] = 'type' + + def __init__(self, text_type=None, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Text + + Args: + text_type: string + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.type = text_type + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class Title(Text): + """The atom:title element""" + + _tag = 'title' + _namespace = ATOM_NAMESPACE + _children = Text._children.copy() + _attributes = Text._attributes.copy() + + def __init__(self, title_type=None, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Title + + Args: + title_type: string + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.type = title_type + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def TitleFromString(xml_string): + return CreateClassFromXMLString(Title, xml_string) + + +class Subtitle(Text): + """The atom:subtitle element""" + + _tag = 'subtitle' + _namespace = ATOM_NAMESPACE + _children = Text._children.copy() + _attributes = Text._attributes.copy() + + def __init__(self, subtitle_type=None, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Subtitle + + Args: + subtitle_type: string + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.type = subtitle_type + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def SubtitleFromString(xml_string): + return CreateClassFromXMLString(Subtitle, xml_string) + + +class Rights(Text): + """The atom:rights element""" + + _tag = 'rights' + _namespace = ATOM_NAMESPACE + _children = Text._children.copy() + _attributes = Text._attributes.copy() + + def __init__(self, rights_type=None, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Rights + + Args: + rights_type: string + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.type = rights_type + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def RightsFromString(xml_string): + return CreateClassFromXMLString(Rights, xml_string) + + +class Summary(Text): + """The atom:summary element""" + + _tag = 'summary' + _namespace = ATOM_NAMESPACE + _children = Text._children.copy() + _attributes = Text._attributes.copy() + + def __init__(self, summary_type=None, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Summary + + Args: + summary_type: string + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.type = summary_type + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def SummaryFromString(xml_string): + return CreateClassFromXMLString(Summary, xml_string) + + +class Content(Text): + """The atom:content element""" + + _tag = 'content' + _namespace = ATOM_NAMESPACE + _children = Text._children.copy() + _attributes = Text._attributes.copy() + _attributes['src'] = 'src' + + def __init__(self, content_type=None, src=None, text=None, + extension_elements=None, extension_attributes=None): + """Constructor for Content + + Args: + content_type: string + src: string + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.type = content_type + self.src = src + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + +def ContentFromString(xml_string): + return CreateClassFromXMLString(Content, xml_string) + + +class Category(AtomBase): + """The atom:category element""" + + _tag = 'category' + _namespace = ATOM_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + _attributes['term'] = 'term' + _attributes['scheme'] = 'scheme' + _attributes['label'] = 'label' + + def __init__(self, term=None, scheme=None, label=None, text=None, + extension_elements=None, extension_attributes=None): + """Constructor for Category + + Args: + term: str + scheme: str + label: str + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.term = term + self.scheme = scheme + self.label = label + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def CategoryFromString(xml_string): + return CreateClassFromXMLString(Category, xml_string) + + +class Id(AtomBase): + """The atom:id element.""" + + _tag = 'id' + _namespace = ATOM_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Id + + Args: + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def IdFromString(xml_string): + return CreateClassFromXMLString(Id, xml_string) + + +class Icon(AtomBase): + """The atom:icon element.""" + + _tag = 'icon' + _namespace = ATOM_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Icon + + Args: + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def IconFromString(xml_string): + return CreateClassFromXMLString(Icon, xml_string) + + +class Logo(AtomBase): + """The atom:logo element.""" + + _tag = 'logo' + _namespace = ATOM_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Logo + + Args: + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def LogoFromString(xml_string): + return CreateClassFromXMLString(Logo, xml_string) + + +class Draft(AtomBase): + """The app:draft element which indicates if this entry should be public.""" + + _tag = 'draft' + _namespace = APP_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for app:draft + + Args: + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def DraftFromString(xml_string): + return CreateClassFromXMLString(Draft, xml_string) + + +class Control(AtomBase): + """The app:control element indicating restrictions on publication. + + The APP control element may contain a draft element indicating whether or + not this entry should be publicly available. + """ + + _tag = 'control' + _namespace = APP_NAMESPACE + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + _children['{%s}draft' % APP_NAMESPACE] = ('draft', Draft) + + def __init__(self, draft=None, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for app:control""" + + self.draft = draft + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def ControlFromString(xml_string): + return CreateClassFromXMLString(Control, xml_string) + + +class Date(AtomBase): + """A parent class for atom:updated, published, etc.""" + + #TODO Add text to and from time conversion methods to allow users to set + # the contents of a Date to a python DateTime object. + + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class Updated(Date): + """The atom:updated element.""" + + _tag = 'updated' + _namespace = ATOM_NAMESPACE + _children = Date._children.copy() + _attributes = Date._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Updated + + Args: + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def UpdatedFromString(xml_string): + return CreateClassFromXMLString(Updated, xml_string) + + +class Published(Date): + """The atom:published element.""" + + _tag = 'published' + _namespace = ATOM_NAMESPACE + _children = Date._children.copy() + _attributes = Date._attributes.copy() + + def __init__(self, text=None, extension_elements=None, + extension_attributes=None): + """Constructor for Published + + Args: + text: str The text data in the this element + extension_elements: list A list of ExtensionElement instances + extension_attributes: dict A dictionary of attribute value string pairs + """ + + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def PublishedFromString(xml_string): + return CreateClassFromXMLString(Published, xml_string) + + +class LinkFinder(object): + """An "interface" providing methods to find link elements + + Entry elements often contain multiple links which differ in the rel + attribute or content type. Often, developers are interested in a specific + type of link so this class provides methods to find specific classes of + links. + + This class is used as a mixin in Atom entries and feeds. + """ + + def GetSelfLink(self): + """Find the first link with rel set to 'self' + + Returns: + An atom.Link or none if none of the links had rel equal to 'self' + """ + + for a_link in self.link: + if a_link.rel == 'self': + return a_link + return None + + def GetEditLink(self): + for a_link in self.link: + if a_link.rel == 'edit': + return a_link + return None + + def GetEditMediaLink(self): + for a_link in self.link: + if a_link.rel == 'edit-media': + return a_link + return None + + def GetNextLink(self): + for a_link in self.link: + if a_link.rel == 'next': + return a_link + return None + + def GetLicenseLink(self): + for a_link in self.link: + if a_link.rel == 'license': + return a_link + return None + + def GetAlternateLink(self): + for a_link in self.link: + if a_link.rel == 'alternate': + return a_link + return None + + +class FeedEntryParent(AtomBase, LinkFinder): + """A super class for atom:feed and entry, contains shared attributes""" + + _children = AtomBase._children.copy() + _attributes = AtomBase._attributes.copy() + _children['{%s}author' % ATOM_NAMESPACE] = ('author', [Author]) + _children['{%s}category' % ATOM_NAMESPACE] = ('category', [Category]) + _children['{%s}contributor' % ATOM_NAMESPACE] = ('contributor', [Contributor]) + _children['{%s}id' % ATOM_NAMESPACE] = ('id', Id) + _children['{%s}link' % ATOM_NAMESPACE] = ('link', [Link]) + _children['{%s}rights' % ATOM_NAMESPACE] = ('rights', Rights) + _children['{%s}title' % ATOM_NAMESPACE] = ('title', Title) + _children['{%s}updated' % ATOM_NAMESPACE] = ('updated', Updated) + + def __init__(self, author=None, category=None, contributor=None, + atom_id=None, link=None, rights=None, title=None, updated=None, + text=None, extension_elements=None, extension_attributes=None): + self.author = author or [] + self.category = category or [] + self.contributor = contributor or [] + self.id = atom_id + self.link = link or [] + self.rights = rights + self.title = title + self.updated = updated + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +class Source(FeedEntryParent): + """The atom:source element""" + + _tag = 'source' + _namespace = ATOM_NAMESPACE + _children = FeedEntryParent._children.copy() + _attributes = FeedEntryParent._attributes.copy() + _children['{%s}generator' % ATOM_NAMESPACE] = ('generator', Generator) + _children['{%s}icon' % ATOM_NAMESPACE] = ('icon', Icon) + _children['{%s}logo' % ATOM_NAMESPACE] = ('logo', Logo) + _children['{%s}subtitle' % ATOM_NAMESPACE] = ('subtitle', Subtitle) + + def __init__(self, author=None, category=None, contributor=None, + generator=None, icon=None, atom_id=None, link=None, logo=None, + rights=None, subtitle=None, title=None, updated=None, text=None, + extension_elements=None, extension_attributes=None): + """Constructor for Source + + Args: + author: list (optional) A list of Author instances which belong to this + class. + category: list (optional) A list of Category instances + contributor: list (optional) A list on Contributor instances + generator: Generator (optional) + icon: Icon (optional) + id: Id (optional) The entry's Id element + link: list (optional) A list of Link instances + logo: Logo (optional) + rights: Rights (optional) The entry's Rights element + subtitle: Subtitle (optional) The entry's subtitle element + title: Title (optional) the entry's title element + updated: Updated (optional) the entry's updated element + text: String (optional) The text contents of the element. This is the + contents of the Entry's XML text node. + (Example: This is the text) + extension_elements: list (optional) A list of ExtensionElement instances + which are children of this element. + extension_attributes: dict (optional) A dictionary of strings which are + the values for additional XML attributes of this element. + """ + + self.author = author or [] + self.category = category or [] + self.contributor = contributor or [] + self.generator = generator + self.icon = icon + self.id = atom_id + self.link = link or [] + self.logo = logo + self.rights = rights + self.subtitle = subtitle + self.title = title + self.updated = updated + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def SourceFromString(xml_string): + return CreateClassFromXMLString(Source, xml_string) + + +class Entry(FeedEntryParent): + """The atom:entry element""" + + _tag = 'entry' + _namespace = ATOM_NAMESPACE + _children = FeedEntryParent._children.copy() + _attributes = FeedEntryParent._attributes.copy() + _children['{%s}content' % ATOM_NAMESPACE] = ('content', Content) + _children['{%s}published' % ATOM_NAMESPACE] = ('published', Published) + _children['{%s}source' % ATOM_NAMESPACE] = ('source', Source) + _children['{%s}summary' % ATOM_NAMESPACE] = ('summary', Summary) + _children['{%s}control' % APP_NAMESPACE] = ('control', Control) + + def __init__(self, author=None, category=None, content=None, + contributor=None, atom_id=None, link=None, published=None, rights=None, + source=None, summary=None, control=None, title=None, updated=None, + extension_elements=None, extension_attributes=None, text=None): + """Constructor for atom:entry + + Args: + author: list A list of Author instances which belong to this class. + category: list A list of Category instances + content: Content The entry's Content + contributor: list A list on Contributor instances + id: Id The entry's Id element + link: list A list of Link instances + published: Published The entry's Published element + rights: Rights The entry's Rights element + source: Source the entry's source element + summary: Summary the entry's summary element + title: Title the entry's title element + updated: Updated the entry's updated element + control: The entry's app:control element which can be used to mark an + entry as a draft which should not be publicly viewable. + text: String The text contents of the element. This is the contents + of the Entry's XML text node. (Example: This is the text) + extension_elements: list A list of ExtensionElement instances which are + children of this element. + extension_attributes: dict A dictionary of strings which are the values + for additional XML attributes of this element. + """ + + self.author = author or [] + self.category = category or [] + self.content = content + self.contributor = contributor or [] + self.id = atom_id + self.link = link or [] + self.published = published + self.rights = rights + self.source = source + self.summary = summary + self.title = title + self.updated = updated + self.control = control + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + __init__ = v1_deprecated('Please use atom.data.Entry instead.')(__init__) + + +def EntryFromString(xml_string): + return CreateClassFromXMLString(Entry, xml_string) + + +class Feed(Source): + """The atom:feed element""" + + _tag = 'feed' + _namespace = ATOM_NAMESPACE + _children = Source._children.copy() + _attributes = Source._attributes.copy() + _children['{%s}entry' % ATOM_NAMESPACE] = ('entry', [Entry]) + + def __init__(self, author=None, category=None, contributor=None, + generator=None, icon=None, atom_id=None, link=None, logo=None, + rights=None, subtitle=None, title=None, updated=None, entry=None, + text=None, extension_elements=None, extension_attributes=None): + """Constructor for Source + + Args: + author: list (optional) A list of Author instances which belong to this + class. + category: list (optional) A list of Category instances + contributor: list (optional) A list on Contributor instances + generator: Generator (optional) + icon: Icon (optional) + id: Id (optional) The entry's Id element + link: list (optional) A list of Link instances + logo: Logo (optional) + rights: Rights (optional) The entry's Rights element + subtitle: Subtitle (optional) The entry's subtitle element + title: Title (optional) the entry's title element + updated: Updated (optional) the entry's updated element + entry: list (optional) A list of the Entry instances contained in the + feed. + text: String (optional) The text contents of the element. This is the + contents of the Entry's XML text node. + (Example: This is the text) + extension_elements: list (optional) A list of ExtensionElement instances + which are children of this element. + extension_attributes: dict (optional) A dictionary of strings which are + the values for additional XML attributes of this element. + """ + + self.author = author or [] + self.category = category or [] + self.contributor = contributor or [] + self.generator = generator + self.icon = icon + self.id = atom_id + self.link = link or [] + self.logo = logo + self.rights = rights + self.subtitle = subtitle + self.title = title + self.updated = updated + self.entry = entry or [] + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + __init__ = v1_deprecated('Please use atom.data.Feed instead.')(__init__) + + +def FeedFromString(xml_string): + return CreateClassFromXMLString(Feed, xml_string) + + +class ExtensionElement(object): + """Represents extra XML elements contained in Atom classes.""" + + def __init__(self, tag, namespace=None, attributes=None, + children=None, text=None): + """Constructor for EtensionElement + + Args: + namespace: string (optional) The XML namespace for this element. + tag: string (optional) The tag (without the namespace qualifier) for + this element. To reconstruct the full qualified name of the element, + combine this tag with the namespace. + attributes: dict (optinal) The attribute value string pairs for the XML + attributes of this element. + children: list (optional) A list of ExtensionElements which represent + the XML child nodes of this element. + """ + + self.namespace = namespace + self.tag = tag + self.attributes = attributes or {} + self.children = children or [] + self.text = text + + def ToString(self): + element_tree = self._TransferToElementTree(ElementTree.Element('')) + return ElementTree.tostring(element_tree, encoding="UTF-8") + + def _TransferToElementTree(self, element_tree): + if self.tag is None: + return None + + if self.namespace is not None: + element_tree.tag = '{%s}%s' % (self.namespace, self.tag) + else: + element_tree.tag = self.tag + + for key, value in self.attributes.iteritems(): + element_tree.attrib[key] = value + + for child in self.children: + child._BecomeChildElement(element_tree) + + element_tree.text = self.text + + return element_tree + + def _BecomeChildElement(self, element_tree): + """Converts this object into an etree element and adds it as a child node. + + Adds self to the ElementTree. This method is required to avoid verbose XML + which constantly redefines the namespace. + + Args: + element_tree: ElementTree._Element The element to which this object's XML + will be added. + """ + new_element = ElementTree.Element('') + element_tree.append(new_element) + self._TransferToElementTree(new_element) + + def FindChildren(self, tag=None, namespace=None): + """Searches child nodes for objects with the desired tag/namespace. + + Returns a list of extension elements within this object whose tag + and/or namespace match those passed in. To find all children in + a particular namespace, specify the namespace but not the tag name. + If you specify only the tag, the result list may contain extension + elements in multiple namespaces. + + Args: + tag: str (optional) The desired tag + namespace: str (optional) The desired namespace + + Returns: + A list of elements whose tag and/or namespace match the parameters + values + """ + + results = [] + + if tag and namespace: + for element in self.children: + if element.tag == tag and element.namespace == namespace: + results.append(element) + elif tag and not namespace: + for element in self.children: + if element.tag == tag: + results.append(element) + elif namespace and not tag: + for element in self.children: + if element.namespace == namespace: + results.append(element) + else: + for element in self.children: + results.append(element) + + return results + + +def ExtensionElementFromString(xml_string): + element_tree = ElementTree.fromstring(xml_string) + return _ExtensionElementFromElementTree(element_tree) + + +def _ExtensionElementFromElementTree(element_tree): + element_tag = element_tree.tag + if '}' in element_tag: + namespace = element_tag[1:element_tag.index('}')] + tag = element_tag[element_tag.index('}')+1:] + else: + namespace = None + tag = element_tag + extension = ExtensionElement(namespace=namespace, tag=tag) + for key, value in element_tree.attrib.iteritems(): + extension.attributes[key] = value + for child in element_tree: + extension.children.append(_ExtensionElementFromElementTree(child)) + extension.text = element_tree.text + return extension + + +def deprecated(warning=None): + """Decorator to raise warning each time the function is called. + + Args: + warning: The warning message to be displayed as a string (optinoal). + """ + warning = warning or '' + # This closure is what is returned from the deprecated function. + def mark_deprecated(f): + # The deprecated_function wraps the actual call to f. + def deprecated_function(*args, **kwargs): + warnings.warn(warning, DeprecationWarning, stacklevel=2) + return f(*args, **kwargs) + # Preserve the original name to avoid masking all decorated functions as + # 'deprecated_function' + try: + deprecated_function.func_name = f.func_name + except TypeError: + # Setting the func_name is not allowed in Python2.3. + pass + return deprecated_function + return mark_deprecated diff --git a/atom/__init__.pyc b/atom/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..576a914d05de7db946907311deaba61d5c37a909 GIT binary patch literal 52794 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 literal 0 HcmV?d00001 diff --git a/atom/auth.py b/atom/auth.py new file mode 100644 index 0000000..1d84175 --- /dev/null +++ b/atom/auth.py @@ -0,0 +1,43 @@ +#!/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 + + +class NoAuth(object): + + def modify_request(self, http_request): + pass diff --git a/atom/client.py b/atom/client.py new file mode 100755 index 0000000..d9d4ef0 --- /dev/null +++ b/atom/client.py @@ -0,0 +1,182 @@ +#!/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 Error(Exception): + pass + + +class MissingHost(Error): + pass + + +class AtomPubClient(object): + host = None + auth_token = None + ssl = False # Whether to force all requests over https + + def __init__(self, http_client=None, host=None, + auth_token=None, source=None, **kwargs): + """Creates a new AtomPubClient instance. + + Args: + source: The name of your application. + http_client: An object capable of performing HTTP requests through a + request method. This object is used to perform the request + when the AtomPubClient's request method is called. Used to + allow HTTP requests to be directed to a mock server, or use + an alternate library instead of the default of httplib to + make HTTP requests. + host: str The default host name to use if a host is not specified in the + requested URI. + auth_token: An object which sets the HTTP Authorization header when its + modify_request method is called. + """ + self.http_client = http_client or atom.http_core.ProxiedHttpClient() + if host is not None: + self.host = host + if auth_token is not None: + self.auth_token = auth_token + self.source = source + + 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. + + Returns: + The results of calling self.http_client.request. With the default + http_client, this is an HTTP response object. + """ + # Modify the request based on the AtomPubClient settings and parameters + # passed in to the request. + http_request = self.modify_request(http_request) + if isinstance(uri, (str, unicode)): + uri = atom.http_core.Uri.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.uri.scheme is None: + http_request.uri.scheme = 'http' + # Override scheme. Force requests over https. + if self.ssl: + http_request.uri.scheme = 'https' + if http_request.uri.path is None: + http_request.uri.path = '/' + # 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) + # Check to make sure there is a host in the http_request. + if http_request.uri.host is None: + raise MissingHost('No host provided in request %s %s' % ( + http_request.method, str(http_request.uri))) + # 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): + """Performs a request using the GET method, returns an HTTP response.""" + 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): + """Sends data using the POST method, returns an HTTP response.""" + 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): + """Sends data using the PUT method, returns an HTTP response.""" + 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): + """Performs a request using the DELETE method, returns an HTTP response.""" + return self.request(method='DELETE', uri=uri, auth_token=auth_token, + http_request=http_request, **kwargs) + + Delete = delete + + def modify_request(self, http_request): + """Changes the HTTP request before sending it to the server. + + Sets the User-Agent HTTP header and fills in the HTTP host portion + of the URL if one was not included in the request (for this it uses + the self.host member if one is set). This method is called in + self.request. + + Args: + http_request: An atom.http_core.HttpRequest() (optional) If one is + not provided, a new HttpRequest is instantiated. + + Returns: + An atom.http_core.HttpRequest() with the User-Agent header set and + if this client has a value in its host member, the host in the request + URL is set. + """ + if http_request is None: + http_request = atom.http_core.HttpRequest() + + if self.host is not None and http_request.uri.host is None: + http_request.uri.host = self.host + + # Set the user agent header for logging purposes. + if self.source: + http_request.headers['User-Agent'] = '%s gdata-py/2.0.11' % self.source + else: + http_request.headers['User-Agent'] = 'gdata-py/2.0.11' + + return http_request + + ModifyRequest = modify_request diff --git a/atom/core.py b/atom/core.py new file mode 100644 index 0000000..4fc93bb --- /dev/null +++ b/atom/core.py @@ -0,0 +1,545 @@ +#!/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. + + +__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 + + +try: + from xml.dom.minidom import parseString as xmlString +except ImportError: + xmlString = None + +STRING_ENCODING = 'utf-8' + + +class XmlElement(object): + """Represents an element node in an XML document. + + The text member is a UTF-8 encoded str or unicode. + """ + _qname = None + _other_elements = None + _other_attributes = None + # The rule set contains mappings for XML qnames to child members and the + # appropriate member classes. + _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): + """Initializes the _rule_set for the class which is used when parsing XML. + + This method is used internally for parsing and generating XML for an + XmlElement. It is not recommended that you call this method directly. + + Returns: + A tuple containing the XML parsing rules for the appropriate version. + + The tuple looks like: + (qname, {sub_element_qname: (member_name, member_class, repeating), ..}, + {attribute_qname: member_name}) + + To give a couple of concrete example, the atom.data.Control _get_rules + with version of 2 will return: + ('{http://www.w3.org/2007/app}control', + {'{http://www.w3.org/2007/app}draft': ('draft', + , + False)}, + {}) + Calling _get_rules with version 1 on gdata.data.FeedLink will produce: + ('{http://schemas.google.com/g/2005}feedLink', + {'{http://www.w3.org/2005/Atom}feed': ('feed', + , + False)}, + {'href': 'href', 'readOnly': 'read_only', 'countHint': 'count_hint', + 'rel': 'rel'}) + """ + # 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 + # FindExtensions and FindChildren are provided for backwards compatibility + # to the atom.AtomBase class. + # However, FindExtensions may return more results than the v1 atom.AtomBase + # method does, because get_elements searches both the expected children + # and the unexpected "other elements". The old AtomBase.FindExtensions + # method searched only "other elements" AKA extension_elements. + FindExtensions = get_elements + FindChildren = 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(): + if isinstance(attribute_def, (list, tuple)): + attribute_def = attribute_def[0] + member = getattr(self, attribute_def) + # TODO: ensure this hasn't broken existing behavior. + #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], version)) + else: + setattr(self, definition[0], _xml_element_from_tree(element, + definition[1], version)) + else: + self._other_elements.append(_xml_element_from_tree(element, XmlElement, + version)) + 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, encoding=None): + new_tree = ElementTree.Element(_get_qname(self, version)) + self._attach_members(new_tree, version, encoding) + return new_tree + + def _attach_members(self, tree, version=1, encoding=None): + """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. + encoding: str (optional) + """ + qname, elements, attributes = self.__class__._get_rules(version) + encoding = encoding or STRING_ENCODING + # 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(): + # I'm not sure if unicode can be used in the attribute name, so for now + # we assume the encoding is correct for the attribute name. + if not isinstance(value, unicode): + value = value.decode(encoding) + tree.attrib[key] = value + if self.text: + if isinstance(self.text, unicode): + tree.text = self.text + else: + tree.text = self.text.decode(encoding) + + def to_string(self, version=1, encoding=None, pretty_print=None): + """Converts this object to XML.""" + + tree_string = ElementTree.tostring(self._to_tree(version, encoding)) + + if pretty_print and xmlString is not None: + return xmlString(tree_string).toprettyxml() + + return tree_string + + ToString = to_string + + def __str__(self): + return self.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_extension_elements(self): + return self._other_elements + + def __set_extension_elements(self, elements): + self._other_elements = elements + + extension_elements = property(__get_extension_elements, + __set_extension_elements, + """Provides backwards compatibility for v1 atom.AtomBase classes.""") + + def __get_extension_attributes(self): + return self._other_attributes + + def __set_extension_attributes(self, attributes): + self._other_attributes = attributes + + extension_attributes = property(__get_extension_attributes, + __set_extension_attributes, + """Provides backwards compatibility for v1 atom.AtomBase classes.""") + + def _get_tag(self, version=1): + qname = _get_qname(self, version) + return qname[qname.find('}')+1:] + + def _get_namespace(self, version=1): + qname = _get_qname(self, version) + if qname.startswith('{'): + return qname[1:qname.find('}')] + else: + return None + + def _set_tag(self, tag): + if isinstance(self._qname, tuple): + self._qname = self._qname.copy() + if self._qname[0].startswith('{'): + self._qname[0] = '{%s}%s' % (self._get_namespace(1), tag) + else: + self._qname[0] = tag + else: + if self._qname.startswith('{'): + self._qname = '{%s}%s' % (self._get_namespace(), tag) + else: + self._qname = tag + + def _set_namespace(self, namespace): + if isinstance(self._qname, tuple): + self._qname = self._qname.copy() + if namespace: + self._qname[0] = '{%s}%s' % (namespace, self._get_tag(1)) + else: + self._qname[0] = self._get_tag(1) + else: + if namespace: + self._qname = '{%s}%s' % (namespace, self._get_tag(1)) + else: + self._qname = self._get_tag(1) + + tag = property(_get_tag, _set_tag, + """Provides backwards compatibility for v1 atom.AtomBase classes.""") + + namespace = property(_get_namespace, _set_namespace, + """Provides backwards compatibility for v1 atom.AtomBase classes.""") + + # Provided for backwards compatibility to atom.ExtensionElement + children = extension_elements + attributes = extension_attributes + + +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 parse(xml_string, target_class=None, version=1, encoding=None): + """Parses the XML string according to the rules for the target_class. + + Args: + xml_string: str or unicode + target_class: XmlElement or a subclass. If None is specified, the + XmlElement class is used. + version: int (optional) The version of the schema which should be used when + converting the XML into an object. The default is 1. + encoding: str (optional) The character encoding of the bytes in the + xml_string. Default is 'UTF-8'. + """ + if target_class is None: + target_class = XmlElement + if isinstance(xml_string, unicode): + if encoding is None: + xml_string = xml_string.encode(STRING_ENCODING) + else: + xml_string = xml_string.encode(encoding) + tree = ElementTree.fromstring(xml_string) + return _xml_element_from_tree(tree, target_class, version) + + +Parse = parse +xml_element_from_string = parse +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 == _get_qname(target_class, version): + 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 + diff --git a/atom/data.py b/atom/data.py new file mode 100644 index 0000000..602a8be --- /dev/null +++ b/atom/data.py @@ -0,0 +1,339 @@ +#!/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 atom.core + + +ATOM_TEMPLATE = '{http://www.w3.org/2005/Atom}%s' +APP_TEMPLATE_V1 = '{http://purl.org/atom/app#}%s' +APP_TEMPLATE_V2 = '{http://www.w3.org/2007/app}%s' + + +class Name(atom.core.XmlElement): + """The atom:name element.""" + _qname = ATOM_TEMPLATE % 'name' + + +class Email(atom.core.XmlElement): + """The atom:email element.""" + _qname = ATOM_TEMPLATE % 'email' + + +class Uri(atom.core.XmlElement): + """The atom:uri element.""" + _qname = ATOM_TEMPLATE % 'uri' + + +class Person(atom.core.XmlElement): + """A foundation class which atom:author and atom:contributor extend. + + A person contains information like name, email address, and web page URI for + an author or contributor to an Atom feed. + """ + name = Name + email = Email + uri = Uri + + +class Author(Person): + """The atom:author element. + + An author is a required element in Feed unless each Entry contains an Author. + """ + _qname = ATOM_TEMPLATE % 'author' + + +class Contributor(Person): + """The atom:contributor element.""" + _qname = ATOM_TEMPLATE % 'contributor' + + +class Link(atom.core.XmlElement): + """The atom:link element.""" + _qname = ATOM_TEMPLATE % 'link' + href = 'href' + rel = 'rel' + type = 'type' + hreflang = 'hreflang' + title = 'title' + length = 'length' + + +class Generator(atom.core.XmlElement): + """The atom:generator element.""" + _qname = ATOM_TEMPLATE % 'generator' + uri = 'uri' + version = 'version' + + +class Text(atom.core.XmlElement): + """A foundation class from which atom:title, summary, etc. extend. + + This class should never be instantiated. + """ + type = 'type' + + +class Title(Text): + """The atom:title element.""" + _qname = ATOM_TEMPLATE % 'title' + + +class Subtitle(Text): + """The atom:subtitle element.""" + _qname = ATOM_TEMPLATE % 'subtitle' + + +class Rights(Text): + """The atom:rights element.""" + _qname = ATOM_TEMPLATE % 'rights' + + +class Summary(Text): + """The atom:summary element.""" + _qname = ATOM_TEMPLATE % 'summary' + + +class Content(Text): + """The atom:content element.""" + _qname = ATOM_TEMPLATE % 'content' + src = 'src' + + +class Category(atom.core.XmlElement): + """The atom:category element.""" + _qname = ATOM_TEMPLATE % 'category' + term = 'term' + scheme = 'scheme' + label = 'label' + + +class Id(atom.core.XmlElement): + """The atom:id element.""" + _qname = ATOM_TEMPLATE % 'id' + + +class Icon(atom.core.XmlElement): + """The atom:icon element.""" + _qname = ATOM_TEMPLATE % 'icon' + + +class Logo(atom.core.XmlElement): + """The atom:logo element.""" + _qname = ATOM_TEMPLATE % 'logo' + + +class Draft(atom.core.XmlElement): + """The app:draft element which indicates if this entry should be public.""" + _qname = (APP_TEMPLATE_V1 % 'draft', APP_TEMPLATE_V2 % 'draft') + + +class Control(atom.core.XmlElement): + """The app:control element indicating restrictions on publication. + + The APP control element may contain a draft element indicating whether or + not this entry should be publicly available. + """ + _qname = (APP_TEMPLATE_V1 % 'control', APP_TEMPLATE_V2 % 'control') + draft = Draft + + +class Date(atom.core.XmlElement): + """A parent class for atom:updated, published, etc.""" + + +class Updated(Date): + """The atom:updated element.""" + _qname = ATOM_TEMPLATE % 'updated' + + +class Published(Date): + """The atom:published element.""" + _qname = ATOM_TEMPLATE % 'published' + + +class LinkFinder(object): + """An "interface" providing methods to find link elements + + Entry elements often contain multiple links which differ in the rel + attribute or content type. Often, developers are interested in a specific + type of link so this class provides methods to find specific classes of + links. + + This class is used as a mixin in Atom entries and feeds. + """ + + def find_url(self, rel): + """Returns the URL in a link with the desired rel value.""" + for link in self.link: + if link.rel == rel and link.href: + return link.href + return None + + FindUrl = find_url + + def get_link(self, rel): + """Returns a link object which has the desired rel value. + + If you are interested in the URL instead of the link object, + consider using find_url instead. + """ + for link in self.link: + if link.rel == rel and link.href: + return link + return None + + GetLink = get_link + + def find_self_link(self): + """Find the first link with rel set to 'self' + + Returns: + A str containing the link's href or None if none of the links had rel + equal to 'self' + """ + return self.find_url('self') + + FindSelfLink = find_self_link + + def get_self_link(self): + return self.get_link('self') + + GetSelfLink = get_self_link + + def find_edit_link(self): + return self.find_url('edit') + + FindEditLink = find_edit_link + + def get_edit_link(self): + return self.get_link('edit') + + GetEditLink = get_edit_link + + def find_edit_media_link(self): + link = self.find_url('edit-media') + # Search for media-edit as well since Picasa API used media-edit instead. + if link is None: + return self.find_url('media-edit') + return link + + FindEditMediaLink = find_edit_media_link + + def get_edit_media_link(self): + link = self.get_link('edit-media') + if link is None: + return self.get_link('media-edit') + return link + + GetEditMediaLink = get_edit_media_link + + def find_next_link(self): + return self.find_url('next') + + FindNextLink = find_next_link + + def get_next_link(self): + return self.get_link('next') + + GetNextLink = get_next_link + + def find_license_link(self): + return self.find_url('license') + + FindLicenseLink = find_license_link + + def get_license_link(self): + return self.get_link('license') + + GetLicenseLink = get_license_link + + def find_alternate_link(self): + return self.find_url('alternate') + + FindAlternateLink = find_alternate_link + + def get_alternate_link(self): + return self.get_link('alternate') + + GetAlternateLink = get_alternate_link + + +class FeedEntryParent(atom.core.XmlElement, LinkFinder): + """A super class for atom:feed and entry, contains shared attributes""" + author = [Author] + category = [Category] + contributor = [Contributor] + id = Id + link = [Link] + rights = Rights + title = Title + updated = Updated + + def __init__(self, atom_id=None, text=None, *args, **kwargs): + if atom_id is not None: + self.id = atom_id + atom.core.XmlElement.__init__(self, text=text, *args, **kwargs) + + +class Source(FeedEntryParent): + """The atom:source element.""" + _qname = ATOM_TEMPLATE % 'source' + generator = Generator + icon = Icon + logo = Logo + subtitle = Subtitle + + +class Entry(FeedEntryParent): + """The atom:entry element.""" + _qname = ATOM_TEMPLATE % 'entry' + content = Content + published = Published + source = Source + summary = Summary + control = Control + + +class Feed(Source): + """The atom:feed element which contains entries.""" + _qname = ATOM_TEMPLATE % 'feed' + entry = [Entry] + + +class ExtensionElement(atom.core.XmlElement): + """Provided for backwards compatibility to the v1 atom.ExtensionElement.""" + + def __init__(self, tag=None, namespace=None, attributes=None, + children=None, text=None, *args, **kwargs): + if namespace: + self._qname = '{%s}%s' % (namespace, tag) + else: + self._qname = tag + self.children = children or [] + self.attributes = attributes or {} + self.text = text + + _BecomeChildElement = atom.core.XmlElement._become_child + + diff --git a/atom/http.py b/atom/http.py new file mode 100644 index 0000000..df36693 --- /dev/null +++ b/atom/http.py @@ -0,0 +1,318 @@ +#!/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 +import atom.http_core +ssl_imported = False +ssl = None +try: + import ssl + ssl_imported = True +except ImportError: + pass + + + +class ProxyError(atom.http_interface.Error): + pass + + +class TestConfigurationError(Exception): + pass + + +DEFAULT_CONTENT_TYPE = 'application/atom+xml' + + +class HttpClient(atom.http_interface.GenericHttpClient): + # Added to allow old v1 HttpClient objects to use the new + # http_code.HttpClient. Used in unit tests to inject a mock client. + v2_http_client = None + + 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. + """ + all_headers = self.headers.copy() + if headers: + all_headers.update(headers) + + # 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'] = str(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 + + if self.v2_http_client is not None: + http_request = atom.http_core.HttpRequest(method=operation) + atom.http_core.Uri.parse_uri(str(url)).modify_request(http_request) + http_request.headers = all_headers + if data: + http_request._body_parts.append(data) + return self.v2_http_client.request(http_request=http_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') + + connection = self._prepare_connection(url, all_headers) + + if self.debug: + connection.debuglevel = 1 + + connection.putrequest(operation, self._get_access_url(url), + skip_host=True) + if url.port is not None: + connection.putheader('Host', '%s:%s' % (url.host, url.port)) + else: + 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 + + # 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. + sslobj = None + if ssl_imported: + sslobj = ssl.wrap_socket(p_sock, None, None) + else: + sock_ssl = socket.ssl(p_sock, None, None) + sslobj = httplib.FakeSocket(p_sock, sock_ssl) + + # Initalize httplib and replace with the proxy socket. + connection = httplib.HTTPConnection(proxy_url.host) + connection.sock = sslobj + 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 diff --git a/atom/http.pyc b/atom/http.pyc new file mode 100644 index 0000000000000000000000000000000000000000..24a3f94d1ac8133fae5ca031cd21d36f14394385 GIT binary patch literal 9445 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 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 + + parse_uri = staticmethod(parse_uri) + + ParseUri = parse_uri + + +parse_uri = Uri.parse_uri + + +ParseUri = Uri.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 getheaders(self): + return self._headers + + def read(self, amt=None): + if self._body is None: + return None + if not amt: + return self._body.read() + else: + return self._body.read(amt) + + +def _dump_response(http_response): + """Converts to a string for printing debug messages. + + Does not read the body since that may consume the content. + """ + output = 'HttpResponse\n status: %s\n reason: %s\n headers:' % ( + http_response.status, http_response.reason) + headers = get_headers(http_response) + if isinstance(headers, dict): + for header, value in headers.iteritems(): + output += ' %s: %s\n' % (header, value) + else: + for pair in headers: + output += ' %s: %s\n' % (pair[0], pair[1]) + return output + + +class HttpClient(object): + """Performs HTTP requests using httplib.""" + debug = None + + def request(self, http_request): + return self._http_request(http_request.method, http_request.uri, + http_request.headers, http_request._body_parts) + + Request = request + + def _get_connection(self, uri, headers=None): + """Opens a socket connection to the server to set up an HTTP request. + + Args: + uri: The full URL for the request as a Uri object. + headers: A dict of string pairs containing the HTTP headers for the + request. + """ + connection = None + if uri.scheme == 'https': + if not uri.port: + connection = httplib.HTTPSConnection(uri.host) + else: + connection = httplib.HTTPSConnection(uri.host, int(uri.port)) + else: + if not uri.port: + connection = httplib.HTTPConnection(uri.host) + else: + connection = httplib.HTTPConnection(uri.host, int(uri.port)) + return connection + + def _http_request(self, method, uri, headers=None, body_parts=None): + """Makes an HTTP request using httplib. + + Args: + method: str example: 'GET', 'POST', 'PUT', 'DELETE', etc. + uri: str or atom.http_core.Uri + headers: dict of strings mapping to strings which will be sent as HTTP + headers in the request. + body_parts: list of strings, objects with a read method, or objects + which can be converted to strings using str. Each of these + will be sent in order as the body of the HTTP request. + """ + if isinstance(uri, (str, unicode)): + uri = Uri.parse_uri(uri) + + connection = self._get_connection(uri, headers=headers) + + if self.debug: + connection.debuglevel = 1 + + if connection.host != uri.host: + connection.putrequest(method, str(uri)) + else: + connection.putrequest(method, uri._get_relative_path()) + + # 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 (uri.scheme == 'https' and int(uri.port or 443) == 443 and + hasattr(connection, '_buffer') and + isinstance(connection._buffer, list)): + header_line = 'Host: %s:443' % uri.host + replacement_header_line = 'Host: %s' % uri.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 + + +class ProxiedHttpClient(HttpClient): + + def _get_connection(self, uri, headers=None): + # Check to see if there are proxy settings required for this request. + proxy = None + if uri.scheme == 'https': + proxy = os.environ.get('https_proxy') + elif uri.scheme == 'http': + proxy = os.environ.get('http_proxy') + if not proxy: + return HttpClient._get_connection(self, uri, headers=headers) + # Now we have the URL of the appropriate proxy server. + # Get a username and password for the proxy if required. + proxy_auth = _get_proxy_auth() + if uri.scheme == 'https': + import socket + if proxy_auth: + proxy_auth = 'Proxy-authorization: %s' % proxy_auth + # Construct the proxy connect command. + port = uri.port + if not port: + port = 443 + proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % (uri.host, port) + # Set the user agent to send to the proxy + user_agent = '' + if headers and 'User-Agent' in headers: + user_agent = 'User-Agent: %s\r\n' % (headers['User-Agent']) + proxy_pieces = '%s%s%s\r\n' % (proxy_connect, proxy_auth, user_agent) + # Find the proxy host and port. + proxy_uri = Uri.parse_uri(proxy) + if not proxy_uri.port: + proxy_uri.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_uri.host, int(proxy_uri.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. + sslobj = None + if ssl is not None: + sslobj = ssl.wrap_socket(p_sock, None, None) + else: + sock_ssl = socket.ssl(p_sock, None, Nonesock_) + sslobj = httplib.FakeSocket(p_sock, sock_ssl) + # Initalize httplib and replace with the proxy socket. + connection = httplib.HTTPConnection(proxy_uri.host) + connection.sock = sslobj + return connection + elif uri.scheme == 'http': + proxy_uri = Uri.parse_uri(proxy) + if not proxy_uri.port: + proxy_uri.port = '80' + if proxy_auth: + headers['Proxy-Authorization'] = proxy_auth.strip() + return httplib.HTTPConnection(proxy_uri.host, int(proxy_uri.port)) + return None + + +def _get_proxy_auth(): + import base64 + 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.b64encode('%s:%s' % (proxy_username, + proxy_password)) + return 'Basic %s\r\n' % (user_auth.strip()) + else: + return '' diff --git a/atom/http_core.pyc b/atom/http_core.pyc new file mode 100644 index 0000000000000000000000000000000000000000..05741a0af3040d3016e8ce64fe4c5f38c015a09e GIT binary patch literal 19145 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 literal 0 HcmV?d00001 diff --git a/atom/http_interface.py b/atom/http_interface.py new file mode 100644 index 0000000..1c2a52b --- /dev/null +++ b/atom/http_interface.py @@ -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/2.0.11' + + +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 + + diff --git a/atom/http_interface.pyc b/atom/http_interface.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa92074fc6afb9230f211ae45aef0dd2e921272a GIT binary patch literal 6912 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 I{cG3%1-4klU;qFB literal 0 HcmV?d00001 diff --git a/atom/mock_http.py b/atom/mock_http.py new file mode 100644 index 0000000..c420f37 --- /dev/null +++ b/atom/mock_http.py @@ -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 diff --git a/atom/mock_http_core.py b/atom/mock_http_core.py new file mode 100644 index 0000000..f55cdc5 --- /dev/null +++ b/atom/mock_http_core.py @@ -0,0 +1,323 @@ +#!/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 Error(Exception): + pass + + +class NoRecordingFound(Error): + pass + + +class MockHttpClient(object): + debug = None + real_client = None + last_request_was_live = False + + # The following members are used to construct the session cache temp file + # name. + # These are combined to form the file name + # /tmp/cache_prefix.cache_case_name.cache_test_name + cache_name_prefix = 'gdata_live_test' + cache_case_name = '' + cache_test_name = '' + + 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): + response = MockHttpResponse(status, reason, headers, body) + # TODO Scrub the request and the response. + self._recordings.append((http_request._copy(), response)) + + AddResponse = add_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: + self.last_request_was_live = False + for recording in self._recordings: + if _match_request(recording[0], request): + return recording[1] + else: + # Pass along the debug settings to the real client. + self.real_client.debug = self.debug + # Make an actual request since we can use the real HTTP client. + self.last_request_was_live = True + response = self.real_client.request(http_request) + scrubbed_response = _scrub_response(response) + self.add_response(request, scrubbed_response.status, + scrubbed_response.reason, + dict(atom.http_core.get_headers(scrubbed_response)), + scrubbed_response.read()) + # Return the recording which we just added. + return self._recordings[-1][1] + raise NoRecordingFound('No recoding was found for request: %s %s' % ( + request.method, str(request.uri))) + + Request = request + + def _save_recordings(self, filename): + recording_file = open(os.path.join(tempfile.gettempdir(), filename), + 'wb') + pickle.dump(self._recordings, recording_file) + recording_file.close() + + def _load_recordings(self, filename): + recording_file = open(os.path.join(tempfile.gettempdir(), filename), + 'rb') + self._recordings = pickle.load(recording_file) + recording_file.close() + + def _delete_recordings(self, filename): + full_path = os.path.join(tempfile.gettempdir(), filename) + if os.path.exists(full_path): + os.remove(full_path) + + 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 use_cached_session(self, name=None, real_http_client=None): + """Attempts to load recordings from a previous live request. + + If a temp file with the recordings exists, then it is used to fulfill + requests. If the file does not exist, then a real client is used to + actually make the desired HTTP requests. Requests and responses are + recorded and will be written to the desired temprary cache file when + close_session is called. + + Args: + name: str (optional) The file name of session file to be used. The file + is loaded from the temporary directory of this machine. If no name + is passed in, a default name will be constructed using the + cache_name_prefix, cache_case_name, and cache_test_name of this + object. + real_http_client: atom.http_core.HttpClient the real client to be used + if the cached recordings are not found. If the default + value is used, this will be an + atom.http_core.HttpClient. + """ + if real_http_client is None: + real_http_client = atom.http_core.HttpClient() + if name is None: + self._recordings_cache_name = self.get_cache_file_name() + else: + self._recordings_cache_name = name + self._load_or_use_client(self._recordings_cache_name, real_http_client) + + def close_session(self): + """Saves recordings in the temporary file named in use_cached_session.""" + if self.real_client is not None: + self._save_recordings(self._recordings_cache_name) + + def delete_session(self, name=None): + """Removes recordings from a previous live request.""" + if name is None: + self._delete_recordings(self._recordings_cache_name) + else: + self._delete_recordings(name) + + def get_cache_file_name(self): + return '%s.%s.%s' % (self.cache_name_prefix, self.cache_case_name, + self.cache_test_name) + + def _dump(self): + """Provides debug information in a string.""" + output = 'MockHttpClient\n real_client: %s\n cache file name: %s\n' % ( + self.real_client, self.get_cache_file_name()) + output += ' recordings:\n' + i = 0 + for recording in self._recordings: + output += ' recording %i is for: %s %s\n' % ( + i, recording[0].method, str(recording[0].uri)) + i += 1 + return output + + +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.""" + # Check to see if the host names match. + if (http_request.uri.host is not None + and http_request.uri.host != stored_request.uri.host): + return False + # Check the request path in the URL (/feeds/private/full/x) + elif http_request.uri.path != stored_request.uri.path: + return False + # Check the method used in the request (GET, POST, etc.) + elif http_request.method != stored_request.method: + return False + # If there is a gsession ID in either request, make sure that it is matched + # exactly. + elif ('gsessionid' in http_request.uri.query + or 'gsessionid' in stored_request.uri.query): + if 'gsessionid' not in stored_request.uri.query: + return False + elif 'gsessionid' not in http_request.uri.query: + return False + elif (http_request.uri.query['gsessionid'] + != stored_request.uri.query['gsessionid']): + return False + # Ignores differences in the query params (?start-index=5&max-results=20), + # the body of the request, the port number, HTTP headers, just to name a + # few. + return True + + +def _scrub_request(http_request): + """ Removes email address and password from a client login request. + + Since the mock server saves the request and response in plantext, sensitive + information like the password should be removed before saving the + recordings. At the moment only requests sent to a ClientLogin url are + scrubbed. + """ + if (http_request and http_request.uri and http_request.uri.path and + http_request.uri.path.endswith('ClientLogin')): + # Remove the email and password from a ClientLogin request. + http_request._body_parts = [] + http_request.add_form_inputs( + {'form_data': 'client login request has been scrubbed'}) + else: + # We can remove the body of the post from the recorded request, since + # the request body is not used when finding a matching recording. + http_request._body_parts = [] + return http_request + + +def _scrub_response(http_response): + return http_response + + +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.uri, http_request.method, + http_request.headers, http_request._body_parts) + + def _http_request(self, uri, method, 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: + # Copy headers from the request to the response but convert values to + # strings. Server response headers always come in as strings, so an int + # should be converted to a corresponding string when echoing. + for header, value in headers.iteritems(): + response._headers[header] = str(value) + response._headers['Echo-Host'] = '%s:%s' % (uri.host, str(uri.port)) + response._headers['Echo-Uri'] = uri._get_relative_path() + response._headers['Echo-Scheme'] = uri.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 + + +class SettableHttpClient(object): + """An HTTP Client which responds with the data given in set_response.""" + + def __init__(self, status, reason, body, headers): + """Configures the response for the server. + + See set_response for details on the arguments to the constructor. + """ + self.set_response(status, reason, body, headers) + self.last_request = None + + def set_response(self, status, reason, body, headers): + """Determines the response which will be sent for each request. + + Args: + status: An int for the HTTP status code, example: 200, 404, etc. + reason: String for the HTTP reason, example: OK, NOT FOUND, etc. + body: The body of the HTTP response as a string or a file-like + object (something with a read method). + headers: dict of strings containing the HTTP headers in the response. + """ + self.response = atom.http_core.HttpResponse(status=status, reason=reason, + body=body) + self.response._headers = headers.copy() + + def request(self, http_request): + self.last_request = http_request + return self.response + + +class MockHttpResponse(atom.http_core.HttpResponse): + + 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: + # Instead of using a file-like object for the body, store as a string + # so that reads can be repeated. + if hasattr(body, 'read'): + self._body = body.read() + else: + self._body = body + + def read(self): + return self._body diff --git a/atom/mock_service.py b/atom/mock_service.py new file mode 100755 index 0000000..601b68a --- /dev/null +++ b/atom/mock_service.py @@ -0,0 +1,243 @@ +#!/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. + + +"""MockService provides CRUD ops. for mocking calls to AtomPub services. + + MockService: Exposes the publicly used methods of AtomService to provide + a mock interface which can be used in unit tests. +""" + +import atom.service +import pickle + + +__author__ = 'api.jscudder (Jeffrey Scudder)' + + +# Recordings contains pairings of HTTP MockRequest objects with MockHttpResponse objects. +recordings = [] +# If set, the mock service HttpRequest are actually made through this object. +real_request_handler = None + +def ConcealValueWithSha(source): + import sha + return sha.new(source[:-5]).hexdigest() + +def DumpRecordings(conceal_func=ConcealValueWithSha): + if conceal_func: + for recording_pair in recordings: + recording_pair[0].ConcealSecrets(conceal_func) + return pickle.dumps(recordings) + +def LoadRecordings(recordings_file_or_string): + if isinstance(recordings_file_or_string, str): + atom.mock_service.recordings = pickle.loads(recordings_file_or_string) + elif hasattr(recordings_file_or_string, 'read'): + atom.mock_service.recordings = pickle.loads( + recordings_file_or_string.read()) + +def HttpRequest(service, operation, data, uri, extra_headers=None, + url_params=None, escape_params=True, content_type='application/atom+xml'): + """Simulates an HTTP call to the server, makes an actual HTTP request if + real_request_handler is set. + + This function operates in two different modes depending on if + real_request_handler is set or not. If real_request_handler is not set, + HttpRequest will look in this module's recordings list to find a response + which matches the parameters in the function call. If real_request_handler + is set, this function will call real_request_handler.HttpRequest, add the + response to the recordings list, and respond with the actual response. + + Args: + service: atom.AtomService object which contains some of the parameters + needed to make the request. The following members are used to + construct the HTTP call: server (str), additional_headers (dict), + port (int), and ssl (bool). + operation: str The HTTP operation to be performed. This is usually one of + 'GET', 'POST', 'PUT', or 'DELETE' + data: ElementTree, filestream, list of parts, or other object which can be + converted to a string. + Should be set to None when performing a GET or PUT. + If data is a file-like object which can be read, this method will read + a chunk of 100K bytes at a time and send them. + If the data is a list of parts to be sent, each part will be evaluated + and sent. + uri: The beginning of the URL to which the request should be sent. + Examples: '/', '/base/feeds/snippets', + '/m8/feeds/contacts/default/base' + extra_headers: dict of strings. HTTP headers which should be sent + in the request. These headers are in addition to those stored in + service.additional_headers. + url_params: dict of strings. Key value pairs to be added to the URL as + URL parameters. For example {'foo':'bar', 'test':'param'} will + become ?foo=bar&test=param. + escape_params: bool default True. If true, the keys and values in + url_params will be URL escaped when the form is constructed + (Special characters converted to %XX form.) + content_type: str The MIME type for the data being sent. Defaults to + 'application/atom+xml', this is only used if data is set. + """ + full_uri = atom.service.BuildUri(uri, url_params, escape_params) + (server, port, ssl, uri) = atom.service.ProcessUrl(service, uri) + current_request = MockRequest(operation, full_uri, host=server, ssl=ssl, + data=data, extra_headers=extra_headers, url_params=url_params, + escape_params=escape_params, content_type=content_type) + # If the request handler is set, we should actually make the request using + # the request handler and record the response to replay later. + if real_request_handler: + response = real_request_handler.HttpRequest(service, operation, data, uri, + extra_headers=extra_headers, url_params=url_params, + escape_params=escape_params, content_type=content_type) + # TODO: need to copy the HTTP headers from the real response into the + # recorded_response. + recorded_response = MockHttpResponse(body=response.read(), + status=response.status, reason=response.reason) + # Insert a tuple which maps the request to the response object returned + # when making an HTTP call using the real_request_handler. + recordings.append((current_request, recorded_response)) + return recorded_response + else: + # Look through available recordings to see if one matches the current + # request. + for request_response_pair in recordings: + if request_response_pair[0].IsMatch(current_request): + return request_response_pair[1] + return None + + +class MockRequest(object): + """Represents a request made to an AtomPub server. + + These objects are used to determine if a client request matches a recorded + HTTP request to determine what the mock server's response will be. + """ + + def __init__(self, operation, uri, host=None, ssl=False, port=None, + data=None, extra_headers=None, url_params=None, escape_params=True, + content_type='application/atom+xml'): + """Constructor for a MockRequest + + Args: + operation: str One of 'GET', 'POST', 'PUT', or 'DELETE' this is the + HTTP operation requested on the resource. + uri: str The URL describing the resource to be modified or feed to be + retrieved. This should include the protocol (http/https) and the host + (aka domain). For example, these are some valud full_uris: + 'http://example.com', 'https://www.google.com/accounts/ClientLogin' + host: str (optional) The server name which will be placed at the + beginning of the URL if the uri parameter does not begin with 'http'. + Examples include 'example.com', 'www.google.com', 'www.blogger.com'. + ssl: boolean (optional) If true, the request URL will begin with https + instead of http. + data: ElementTree, filestream, list of parts, or other object which can be + converted to a string. (optional) + Should be set to None when performing a GET or PUT. + If data is a file-like object which can be read, the constructor + will read the entire file into memory. If the data is a list of + parts to be sent, each part will be evaluated and stored. + extra_headers: dict (optional) HTTP headers included in the request. + url_params: dict (optional) Key value pairs which should be added to + the URL as URL parameters in the request. For example uri='/', + url_parameters={'foo':'1','bar':'2'} could become '/?foo=1&bar=2'. + escape_params: boolean (optional) Perform URL escaping on the keys and + values specified in url_params. Defaults to True. + content_type: str (optional) Provides the MIME type of the data being + sent. + """ + self.operation = operation + self.uri = _ConstructFullUrlBase(uri, host=host, ssl=ssl) + self.data = data + self.extra_headers = extra_headers + self.url_params = url_params or {} + self.escape_params = escape_params + self.content_type = content_type + + def ConcealSecrets(self, conceal_func): + """Conceal secret data in this request.""" + if self.extra_headers.has_key('Authorization'): + self.extra_headers['Authorization'] = conceal_func( + self.extra_headers['Authorization']) + + def IsMatch(self, other_request): + """Check to see if the other_request is equivalent to this request. + + Used to determine if a recording matches an incoming request so that a + recorded response should be sent to the client. + + The matching is not exact, only the operation and URL are examined + currently. + + Args: + other_request: MockRequest The request which we want to check this + (self) MockRequest against to see if they are equivalent. + """ + # More accurate matching logic will likely be required. + return (self.operation == other_request.operation and self.uri == + other_request.uri) + + +def _ConstructFullUrlBase(uri, host=None, ssl=False): + """Puts URL components into the form http(s)://full.host.strinf/uri/path + + Used to construct a roughly canonical URL so that URLs which begin with + 'http://example.com/' can be compared to a uri of '/' when the host is + set to 'example.com' + + If the uri contains 'http://host' already, the host and ssl parameters + are ignored. + + Args: + uri: str The path component of the URL, examples include '/' + host: str (optional) The host name which should prepend the URL. Example: + 'example.com' + ssl: boolean (optional) If true, the returned URL will begin with https + instead of http. + + Returns: + String which has the form http(s)://example.com/uri/string/contents + """ + if uri.startswith('http'): + return uri + if ssl: + return 'https://%s%s' % (host, uri) + else: + return 'http://%s%s' % (host, uri) + + +class MockHttpResponse(object): + """Returned from MockService crud methods as the server's response.""" + + def __init__(self, body=None, status=None, reason=None, headers=None): + """Construct a mock HTTPResponse and set members. + + Args: + body: str (optional) The HTTP body of the server's response. + status: int (optional) + reason: str (optional) + headers: dict (optional) + """ + self.body = body + self.status = status + self.reason = reason + self.headers = headers or {} + + def read(self): + return self.body + + def getheader(self, header_name): + return self.headers[header_name] + diff --git a/atom/service.py b/atom/service.py new file mode 100755 index 0000000..6310c1c --- /dev/null +++ b/atom/service.py @@ -0,0 +1,740 @@ +#!/usr/bin/python +# +# Copyright (C) 2006, 2007, 2008 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""AtomService provides CRUD ops. in line with the Atom Publishing Protocol. + + AtomService: Encapsulates the ability to perform insert, update and delete + operations with the Atom Publishing Protocol on which GData is + based. An instance can perform query, insertion, deletion, and + update. + + HttpRequest: Function that performs a GET, POST, PUT, or DELETE HTTP request + to the specified end point. An AtomService object or a subclass can be + used to specify information about the request. +""" + +__author__ = 'api.jscudder (Jeff Scudder)' + + +import atom.http_interface +import atom.url +import atom.http +import atom.token_store + +import os +import httplib +import urllib +import re +import base64 +import socket +import warnings +try: + from xml.etree import cElementTree as ElementTree +except ImportError: + try: + import cElementTree as ElementTree + except ImportError: + try: + from xml.etree import ElementTree + except ImportError: + from elementtree import ElementTree +import atom + + +class AtomService(object): + """Performs Atom Publishing Protocol CRUD operations. + + The AtomService contains methods to perform HTTP CRUD operations. + """ + + # Default values for members + port = 80 + ssl = False + # Set the current_token to force the AtomService to use this token + # instead of searching for an appropriate token in the token_store. + current_token = None + auto_store_tokens = True + auto_set_current_token = True + + def _get_override_token(self): + return self.current_token + + def _set_override_token(self, token): + self.current_token = token + + override_token = property(_get_override_token, _set_override_token) + + #@atom.v1_deprecated('Please use atom.client.AtomPubClient instead.') + def __init__(self, server=None, additional_headers=None, + application_name='', http_client=None, token_store=None): + """Creates a new AtomService client. + + Args: + server: string (optional) The start of a URL for the server + to which all operations should be directed. Example: + 'www.google.com' + additional_headers: dict (optional) Any additional HTTP headers which + should be included with CRUD operations. + http_client: An object responsible for making HTTP requests using a + request method. If none is provided, a new instance of + atom.http.ProxiedHttpClient will be used. + token_store: Keeps a collection of authorization tokens which can be + applied to requests for a specific URLs. Critical methods are + find_token based on a URL (atom.url.Url or a string), add_token, + and remove_token. + """ + self.http_client = http_client or atom.http.ProxiedHttpClient() + self.token_store = token_store or atom.token_store.TokenStore() + self.server = server + self.additional_headers = additional_headers or {} + self.additional_headers['User-Agent'] = atom.http_interface.USER_AGENT % ( + application_name,) + # If debug is True, the HTTPConnection will display debug information + self._set_debug(False) + + __init__ = atom.v1_deprecated( + 'Please use atom.client.AtomPubClient instead.')( + __init__) + + def _get_debug(self): + return self.http_client.debug + + def _set_debug(self, value): + self.http_client.debug = value + + debug = property(_get_debug, _set_debug, + doc='If True, HTTP debug information is printed.') + + def use_basic_auth(self, username, password, scopes=None): + if username is not None and password is not None: + if scopes is None: + scopes = [atom.token_store.SCOPE_ALL] + base_64_string = base64.encodestring('%s:%s' % (username, password)) + token = BasicAuthToken('Basic %s' % base_64_string.strip(), + scopes=[atom.token_store.SCOPE_ALL]) + if self.auto_set_current_token: + self.current_token = token + if self.auto_store_tokens: + return self.token_store.add_token(token) + return True + return False + + def UseBasicAuth(self, username, password, for_proxy=False): + """Sets an Authenticaiton: Basic HTTP header containing plaintext. + + Deprecated, use use_basic_auth instead. + + The username and password are base64 encoded and added to an HTTP header + which will be included in each request. Note that your username and + password are sent in plaintext. + + Args: + username: str + password: str + """ + self.use_basic_auth(username, password) + + #@atom.v1_deprecated('Please use atom.client.AtomPubClient for requests.') + def request(self, operation, url, data=None, headers=None, + url_params=None): + if isinstance(url, (str, unicode)): + if url.startswith('http:') and self.ssl: + # Force all requests to be https if self.ssl is True. + url = atom.url.parse_url('https:' + url[5:]) + elif not url.startswith('http') and self.ssl: + url = atom.url.parse_url('https://%s%s' % (self.server, url)) + elif not url.startswith('http'): + url = atom.url.parse_url('http://%s%s' % (self.server, url)) + else: + url = atom.url.parse_url(url) + + if url_params: + for name, value in url_params.iteritems(): + url.params[name] = value + + all_headers = self.additional_headers.copy() + if headers: + all_headers.update(headers) + + # 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: + content_length = CalculateDataLength(data) + if content_length: + all_headers['Content-Length'] = str(content_length) + + # Find an Authorization token for this URL if one is available. + if self.override_token: + auth_token = self.override_token + else: + auth_token = self.token_store.find_token(url) + return auth_token.perform_request(self.http_client, operation, url, + data=data, headers=all_headers) + + request = atom.v1_deprecated( + 'Please use atom.client.AtomPubClient for requests.')( + request) + + # CRUD operations + def Get(self, uri, extra_headers=None, url_params=None, escape_params=True): + """Query the APP server with the given URI + + The uri is the portion of the URI after the server value + (server example: 'www.google.com'). + + Example use: + To perform a query against Google Base, set the server to + 'base.google.com' and set the uri to '/base/feeds/...', where ... is + your query. For example, to find snippets for all digital cameras uri + should be set to: '/base/feeds/snippets?bq=digital+camera' + + Args: + uri: string The query in the form of a URI. Example: + '/base/feeds/snippets?bq=digital+camera'. + extra_headers: dicty (optional) Extra HTTP headers to be included + in the GET request. These headers are in addition to + those stored in the client's additional_headers property. + The client automatically sets the Content-Type and + Authorization headers. + url_params: dict (optional) Additional URL parameters to be included + in the query. These are translated into query arguments + in the form '&dict_key=value&...'. + Example: {'max-results': '250'} becomes &max-results=250 + escape_params: boolean (optional) If false, the calling code has already + ensured that the query will form a valid URL (all + reserved characters have been escaped). If true, this + method will escape the query and any URL parameters + provided. + + Returns: + httplib.HTTPResponse The server's response to the GET request. + """ + return self.request('GET', uri, data=None, headers=extra_headers, + url_params=url_params) + + def Post(self, data, uri, extra_headers=None, url_params=None, + escape_params=True, content_type='application/atom+xml'): + """Insert data into an APP server at the given URI. + + Args: + data: string, ElementTree._Element, or something with a __str__ method + The XML to be sent to the uri. + uri: string The location (feed) to which the data should be inserted. + Example: '/base/feeds/items'. + extra_headers: dict (optional) HTTP headers which are to be included. + The client automatically sets the Content-Type, + Authorization, and Content-Length headers. + url_params: dict (optional) Additional URL parameters to be included + in the URI. These are translated into query arguments + in the form '&dict_key=value&...'. + Example: {'max-results': '250'} becomes &max-results=250 + escape_params: boolean (optional) If false, the calling code has already + ensured that the query will form a valid URL (all + reserved characters have been escaped). If true, this + method will escape the query and any URL parameters + provided. + + Returns: + httplib.HTTPResponse Server's response to the POST request. + """ + if extra_headers is None: + extra_headers = {} + if content_type: + extra_headers['Content-Type'] = content_type + return self.request('POST', uri, data=data, headers=extra_headers, + url_params=url_params) + + def Put(self, data, uri, extra_headers=None, url_params=None, + escape_params=True, content_type='application/atom+xml'): + """Updates an entry at the given URI. + + Args: + data: string, ElementTree._Element, or xml_wrapper.ElementWrapper The + XML containing the updated data. + uri: string A URI indicating entry to which the update will be applied. + Example: '/base/feeds/items/ITEM-ID' + extra_headers: dict (optional) HTTP headers which are to be included. + The client automatically sets the Content-Type, + Authorization, and Content-Length headers. + url_params: dict (optional) Additional URL parameters to be included + in the URI. These are translated into query arguments + in the form '&dict_key=value&...'. + Example: {'max-results': '250'} becomes &max-results=250 + escape_params: boolean (optional) If false, the calling code has already + ensured that the query will form a valid URL (all + reserved characters have been escaped). If true, this + method will escape the query and any URL parameters + provided. + + Returns: + httplib.HTTPResponse Server's response to the PUT request. + """ + if extra_headers is None: + extra_headers = {} + if content_type: + extra_headers['Content-Type'] = content_type + return self.request('PUT', uri, data=data, headers=extra_headers, + url_params=url_params) + + def Delete(self, uri, extra_headers=None, url_params=None, + escape_params=True): + """Deletes the entry at the given URI. + + Args: + uri: string The URI of the entry to be deleted. Example: + '/base/feeds/items/ITEM-ID' + extra_headers: dict (optional) HTTP headers which are to be included. + The client automatically sets the Content-Type and + Authorization headers. + url_params: dict (optional) Additional URL parameters to be included + in the URI. These are translated into query arguments + in the form '&dict_key=value&...'. + Example: {'max-results': '250'} becomes &max-results=250 + escape_params: boolean (optional) If false, the calling code has already + ensured that the query will form a valid URL (all + reserved characters have been escaped). If true, this + method will escape the query and any URL parameters + provided. + + Returns: + httplib.HTTPResponse Server's response to the DELETE request. + """ + return self.request('DELETE', uri, data=None, headers=extra_headers, + url_params=url_params) + + +class BasicAuthToken(atom.http_interface.GenericToken): + def __init__(self, auth_header, scopes=None): + """Creates a token used to add Basic Auth headers to HTTP requests. + + Args: + auth_header: str The value for the Authorization header. + scopes: list of str or atom.url.Url specifying the beginnings of URLs + for which this token can be used. For example, if scopes contains + 'http://example.com/foo', then this token can be used for a request to + 'http://example.com/foo/bar' but it cannot be used for a request to + 'http://example.com/baz' + """ + self.auth_header = auth_header + self.scopes = scopes or [] + + def perform_request(self, http_client, operation, url, data=None, + headers=None): + """Sets the Authorization header to the basic auth string.""" + if headers is None: + headers = {'Authorization':self.auth_header} + else: + headers['Authorization'] = self.auth_header + return http_client.request(operation, url, data=data, headers=headers) + + def __str__(self): + return self.auth_header + + def valid_for_scope(self, url): + """Tells the caller if the token authorizes access to the desired URL. + """ + if isinstance(url, (str, unicode)): + url = atom.url.parse_url(url) + for scope in self.scopes: + if scope == atom.token_store.SCOPE_ALL: + return True + if isinstance(scope, (str, unicode)): + scope = atom.url.parse_url(scope) + if scope == url: + return True + # Check the host and the path, but ignore the port and protocol. + elif scope.host == url.host and not scope.path: + return True + elif scope.host == url.host and scope.path and not url.path: + continue + elif scope.host == url.host and url.path.startswith(scope.path): + return True + return False + + +def PrepareConnection(service, full_uri): + """Opens a connection to the server based on the full URI. + + This method is deprecated, instead use atom.http.HttpClient.request. + + Examines the target URI and the proxy settings, which are set as + environment variables, to open a connection with the server. This + connection is used to make an HTTP request. + + Args: + service: atom.AtomService or a subclass. It must have a server string which + represents the server host to which the request should be made. It may also + have a dictionary of additional_headers to send in the HTTP request. + full_uri: str Which is the target relative (lacks protocol and host) or + absolute URL to be opened. Example: + 'https://www.google.com/accounts/ClientLogin' or + 'base/feeds/snippets' where the server is set to www.google.com. + + Returns: + A tuple containing the httplib.HTTPConnection and the full_uri for the + request. + """ + deprecation('calling deprecated function PrepareConnection') + (server, port, ssl, partial_uri) = ProcessUrl(service, full_uri) + if ssl: + # destination is https + proxy = os.environ.get('https_proxy') + if proxy: + (p_server, p_port, p_ssl, p_uri) = ProcessUrl(service, proxy, True) + proxy_username = os.environ.get('proxy-username') + if not proxy_username: + proxy_username = os.environ.get('proxy_username') + proxy_password = os.environ.get('proxy-password') + if not proxy_password: + proxy_password = os.environ.get('proxy_password') + if proxy_username: + user_auth = base64.encodestring('%s:%s' % (proxy_username, + proxy_password)) + proxy_authorization = ('Proxy-authorization: Basic %s\r\n' % ( + user_auth.strip())) + else: + proxy_authorization = '' + proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % (server, port) + user_agent = 'User-Agent: %s\r\n' % ( + service.additional_headers['User-Agent']) + proxy_pieces = (proxy_connect + proxy_authorization + user_agent + + '\r\n') + + #now connect, very simple recv and error checking + p_sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + p_sock.connect((p_server,p_port)) + p_sock.sendall(proxy_pieces) + response = '' + + # Wait for the full response. + while response.find("\r\n\r\n") == -1: + response += p_sock.recv(8192) + + p_status=response.split()[1] + if p_status!=str(200): + raise 'Error status=',str(p_status) + + # Trivial setup for ssl socket. + ssl = socket.ssl(p_sock, None, None) + fake_sock = httplib.FakeSocket(p_sock, ssl) + + # Initalize httplib and replace with the proxy socket. + connection = httplib.HTTPConnection(server) + connection.sock=fake_sock + full_uri = partial_uri + + else: + connection = httplib.HTTPSConnection(server, port) + full_uri = partial_uri + + else: + # destination is http + proxy = os.environ.get('http_proxy') + if proxy: + (p_server, p_port, p_ssl, p_uri) = ProcessUrl(service.server, proxy, True) + proxy_username = os.environ.get('proxy-username') + if not proxy_username: + proxy_username = os.environ.get('proxy_username') + proxy_password = os.environ.get('proxy-password') + if not proxy_password: + proxy_password = os.environ.get('proxy_password') + if proxy_username: + UseBasicAuth(service, proxy_username, proxy_password, True) + connection = httplib.HTTPConnection(p_server, p_port) + if not full_uri.startswith("http://"): + if full_uri.startswith("/"): + full_uri = "http://%s%s" % (service.server, full_uri) + else: + full_uri = "http://%s/%s" % (service.server, full_uri) + else: + connection = httplib.HTTPConnection(server, port) + full_uri = partial_uri + + return (connection, full_uri) + + +def UseBasicAuth(service, username, password, for_proxy=False): + """Sets an Authenticaiton: Basic HTTP header containing plaintext. + + Deprecated, use AtomService.use_basic_auth insread. + + The username and password are base64 encoded and added to an HTTP header + which will be included in each request. Note that your username and + password are sent in plaintext. The auth header is added to the + additional_headers dictionary in the service object. + + Args: + service: atom.AtomService or a subclass which has an + additional_headers dict as a member. + username: str + password: str + """ + deprecation('calling deprecated function UseBasicAuth') + base_64_string = base64.encodestring('%s:%s' % (username, password)) + base_64_string = base_64_string.strip() + if for_proxy: + header_name = 'Proxy-Authorization' + else: + header_name = 'Authorization' + service.additional_headers[header_name] = 'Basic %s' % (base_64_string,) + + +def ProcessUrl(service, url, for_proxy=False): + """Processes a passed URL. If the URL does not begin with https?, then + the default value for server is used + + This method is deprecated, use atom.url.parse_url instead. + """ + if not isinstance(url, atom.url.Url): + url = atom.url.parse_url(url) + + server = url.host + ssl = False + port = 80 + + if not server: + if hasattr(service, 'server'): + server = service.server + else: + server = service + if not url.protocol and hasattr(service, 'ssl'): + ssl = service.ssl + if hasattr(service, 'port'): + port = service.port + else: + if url.protocol == 'https': + ssl = True + elif url.protocol == 'http': + ssl = False + if url.port: + port = int(url.port) + elif port == 80 and ssl: + port = 443 + + return (server, port, ssl, url.get_request_uri()) + +def DictionaryToParamList(url_parameters, escape_params=True): + """Convert a dictionary of URL arguments into a URL parameter string. + + This function is deprcated, use atom.url.Url instead. + + Args: + url_parameters: The dictionaty of key-value pairs which will be converted + into URL parameters. For example, + {'dry-run': 'true', 'foo': 'bar'} + will become ['dry-run=true', 'foo=bar']. + + Returns: + A list which contains a string for each key-value pair. The strings are + ready to be incorporated into a URL by using '&'.join([] + parameter_list) + """ + # Choose which function to use when modifying the query and parameters. + # Use quote_plus when escape_params is true. + transform_op = [str, urllib.quote_plus][bool(escape_params)] + # Create a list of tuples containing the escaped version of the + # parameter-value pairs. + parameter_tuples = [(transform_op(param), transform_op(value)) + for param, value in (url_parameters or {}).items()] + # Turn parameter-value tuples into a list of strings in the form + # 'PARAMETER=VALUE'. + return ['='.join(x) for x in parameter_tuples] + + +def BuildUri(uri, url_params=None, escape_params=True): + """Converts a uri string and a collection of parameters into a URI. + + This function is deprcated, use atom.url.Url instead. + + Args: + uri: string + url_params: dict (optional) + escape_params: boolean (optional) + uri: string The start of the desired URI. This string can alrady contain + URL parameters. Examples: '/base/feeds/snippets', + '/base/feeds/snippets?bq=digital+camera' + url_parameters: dict (optional) Additional URL parameters to be included + in the query. These are translated into query arguments + in the form '&dict_key=value&...'. + Example: {'max-results': '250'} becomes &max-results=250 + escape_params: boolean (optional) If false, the calling code has already + ensured that the query will form a valid URL (all + reserved characters have been escaped). If true, this + method will escape the query and any URL parameters + provided. + + Returns: + string The URI consisting of the escaped URL parameters appended to the + initial uri string. + """ + # Prepare URL parameters for inclusion into the GET request. + parameter_list = DictionaryToParamList(url_params, escape_params) + + # Append the URL parameters to the URL. + if parameter_list: + if uri.find('?') != -1: + # If there are already URL parameters in the uri string, add the + # parameters after a new & character. + full_uri = '&'.join([uri] + parameter_list) + else: + # The uri string did not have any URL parameters (no ? character) + # so put a ? between the uri and URL parameters. + full_uri = '%s%s' % (uri, '?%s' % ('&'.join([] + parameter_list))) + else: + full_uri = uri + + return full_uri + + +def HttpRequest(service, operation, data, uri, extra_headers=None, + url_params=None, escape_params=True, content_type='application/atom+xml'): + """Performs an HTTP call to the server, supports GET, POST, PUT, and DELETE. + + This method is deprecated, use atom.http.HttpClient.request instead. + + Usage example, perform and HTTP GET on http://www.google.com/: + import atom.service + client = atom.service.AtomService() + http_response = client.Get('http://www.google.com/') + or you could set the client.server to 'www.google.com' and use the + following: + client.server = 'www.google.com' + http_response = client.Get('/') + + Args: + service: atom.AtomService object which contains some of the parameters + needed to make the request. The following members are used to + construct the HTTP call: server (str), additional_headers (dict), + port (int), and ssl (bool). + operation: str The HTTP operation to be performed. This is usually one of + 'GET', 'POST', 'PUT', or 'DELETE' + data: ElementTree, filestream, list of parts, or other object which can be + converted to a string. + Should be set to None when performing a GET or PUT. + If data is a file-like object which can be read, this method will read + a chunk of 100K bytes at a time and send them. + If the data is a list of parts to be sent, each part will be evaluated + and sent. + uri: The beginning of the URL to which the request should be sent. + Examples: '/', '/base/feeds/snippets', + '/m8/feeds/contacts/default/base' + extra_headers: dict of strings. HTTP headers which should be sent + in the request. These headers are in addition to those stored in + service.additional_headers. + url_params: dict of strings. Key value pairs to be added to the URL as + URL parameters. For example {'foo':'bar', 'test':'param'} will + become ?foo=bar&test=param. + escape_params: bool default True. If true, the keys and values in + url_params will be URL escaped when the form is constructed + (Special characters converted to %XX form.) + content_type: str The MIME type for the data being sent. Defaults to + 'application/atom+xml', this is only used if data is set. + """ + deprecation('call to deprecated function HttpRequest') + full_uri = BuildUri(uri, url_params, escape_params) + (connection, full_uri) = PrepareConnection(service, full_uri) + + if extra_headers is None: + extra_headers = {} + + # Turn on debug mode if the debug member is set. + if service.debug: + connection.debuglevel = 1 + + connection.putrequest(operation, full_uri) + + # If the list of headers does not include a Content-Length, attempt to + # calculate it based on the data object. + if (data and not service.additional_headers.has_key('Content-Length') and + not extra_headers.has_key('Content-Length')): + content_length = CalculateDataLength(data) + if content_length: + extra_headers['Content-Length'] = str(content_length) + + if content_type: + extra_headers['Content-Type'] = content_type + + # Send the HTTP headers. + if isinstance(service.additional_headers, dict): + for header in service.additional_headers: + connection.putheader(header, service.additional_headers[header]) + if isinstance(extra_headers, dict): + for header in extra_headers: + connection.putheader(header, extra_headers[header]) + connection.endheaders() + + # If there is data, send it in the request. + if data: + if isinstance(data, list): + for data_part in data: + __SendDataPart(data_part, connection) + else: + __SendDataPart(data, connection) + + # Return the HTTP Response from the server. + return connection.getresponse() + + +def __SendDataPart(data, connection): + """This method is deprecated, use atom.http._send_data_part""" + deprecated('call to deprecated function __SendDataPart') + if isinstance(data, str): + #TODO add handling for unicode. + connection.send(data) + return + elif ElementTree.iselement(data): + connection.send(ElementTree.tostring(data)) + return + # Check to see if data is a file-like object that has a read method. + elif hasattr(data, 'read'): + # Read the file and send it a chunk at a time. + while 1: + binarydata = data.read(100000) + if binarydata == '': break + connection.send(binarydata) + return + else: + # The data object was not a file. + # Try to convert to a string and send the data. + connection.send(str(data)) + return + + +def CalculateDataLength(data): + """Attempts to determine the length of the data to send. + + This method will respond with a length only if the data is a string or + and ElementTree element. + + Args: + data: object If this is not a string or ElementTree element this funtion + will return None. + """ + if isinstance(data, str): + return len(data) + elif isinstance(data, list): + return None + elif ElementTree.iselement(data): + return len(ElementTree.tostring(data)) + elif hasattr(data, 'read'): + # If this is a file-like object, don't try to guess the length. + return None + else: + return len(str(data)) + + +def deprecation(message): + warnings.warn(message, DeprecationWarning, stacklevel=2) diff --git a/atom/service.pyc b/atom/service.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c40ce90ebd42d85e36667411ab753765bdd65cde GIT binary patch literal 26804 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 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() + diff --git a/atom/url.pyc b/atom/url.pyc new file mode 100644 index 0000000000000000000000000000000000000000..296fae9b1ded7b6aab300cec0c96b3a65871f5d8 GIT binary patch literal 4206 zcmb_fPmde76(>j1NLp`dCwA;?+7t`|1X_5pn?nK^fzu|n(?j6O$;gJ0MS{uf3?*?R z4QGd(O$_WF0{fC%PCfP9FVPRsb5H#q{Q~XpJ&vR`n&gnIc1b;wk9?2s&+k$H`p-*S zAKy8Dkc;-y!}o`1?l0&HAr8??QAFZvDGosdIueJGC_3V>BZ{s#?24i%o`bn3idg7a zOu^QtnP)+ZN!EEJ®E`?8QNc@~K@5~U0ik=U_M)}+A&I($S& zoPm~DrJaGKon7APd$bHuxXE!>taxx3@xfSn=mjxmOKfIw5zO8_`8QZE z?v8~0y^zJ==Qd!|6TS?2ZAb?H8(F^Anb%Hb>T$hL_UKsWUVSq+`CR3;uC=RdSvcjb z@^h^oCl8eKwJB%&TT0>o(cCz-)P8OYm1m{O@?2M5Raxy#wjh4Dr;Zx03{EwRg<9zB zRI8FvL;}Kr;skLZ#QP?qF+~xN-{$^<@o1Z7C=Q!? z0m<0i!2tK+$*Q*A=5_&t;m*~<_z;NJdFET_k|!!#hm|Vxb5Bu;;}+(~)A3Dt(paz4 zYSB1egPd&Xc;ncXdPI@zx5B10n1}_EB^=AaY0NN(;51BM0Y4vvv8lspL@^%`fo}vh z!JT^-)WKs1iybU8mz#tAQ?NG~E^+D(s;AW~b;fI^4l-|-2N1hoJ@w>ph;ll`#2Xm6 zJ#-?zDu?old>QRJe_xlcuE%{5%d7HwbQ^1L%Y^Kg(r>Hp6Ci;2q$rnA^@|pP-a;z^pYoY zyTrwev!zz&?{BJSk$Gr?5QIN}n|5lnm+jl%sG*WQTk} zC?beXv5!>v-&BeN4s^8B;ce%PqSu0M#0v`VGrp+NxBMnH6pOxWsNAf z)#%PUyUI-22Uc!alF*?EWoAODkx;xRzd<*m1x_BP7t^0+1J|z&e3G7~rZhfHIbcZ>5e*Ud z^p}&ibZ^p#@BU8^R1jbMpT$&#YR=>ao<@2#STh5q6=Nu&aIO9&x;fE~LdXwkYmxZz1$ywvaFUc$=~1CT(s^V4}&0 zj&SdzOSEt55|0c5FBD(A6wrkcs%$_=?eeT5WCZ#LJLzJdUjTm`^HHpHupQydh4`aO zFxIvaa|ssmS8*n#L5;GZQP7*(e1$1Ta3i8^*_XGXoE%QKB$u(V{(%HUI>JWK{8=Qb z*YS{Jctym(egqO6H&~mH4e9BNlm!&Lg?xuiLG7&fFfL#U++Zx}0UVg$TrqFkn-{nd zWG(C7ignwn6>rN*fz5(iR(fG`8XR|o?eSo;Qo7Hqz#vxO|Jg{L!CH4v8{P3%g_OZc zp;mXWw7L((McoI4A}{Jr46W|i6|M=U7f$^--M5%^>nC+@{c?rbt`OUVV`|LL((ve{ z{4{vUI-ixf<`)ttWWKBtg*DDtE^myR_dovgM~@#oN4^ME+mMEqP}JiEMGpkk%z_jC)&Kq1FmnwCJ6H039D znl5e8ENB?6U)VfN6UyymhdM>A>kL9-9?hmgH z`@_NTP5y1&Dw;L;jfGorspaC3+{TuKUh&B-bX-8fdzItxvQ%WN-3@m7!z&QPH6iXm M(M~Mm&hU-@0Zckx=Kufz literal 0 HcmV?d00001 diff --git a/gdata/__init__.py b/gdata/__init__.py new file mode 100755 index 0000000..634889b --- /dev/null +++ b/gdata/__init__.py @@ -0,0 +1,835 @@ +#!/usr/bin/python +# +# Copyright (C) 2006 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Contains classes representing Google Data elements. + + Extends Atom classes to add Google Data specific elements. +""" + + +__author__ = 'j.s@google.com (Jeffrey Scudder)' + +import os +import atom +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 + + +# XML namespaces which are often used in GData entities. +GDATA_NAMESPACE = 'http://schemas.google.com/g/2005' +GDATA_TEMPLATE = '{http://schemas.google.com/g/2005}%s' +OPENSEARCH_NAMESPACE = 'http://a9.com/-/spec/opensearchrss/1.0/' +OPENSEARCH_TEMPLATE = '{http://a9.com/-/spec/opensearchrss/1.0/}%s' +BATCH_NAMESPACE = 'http://schemas.google.com/gdata/batch' +GACL_NAMESPACE = 'http://schemas.google.com/acl/2007' +GACL_TEMPLATE = '{http://schemas.google.com/acl/2007}%s' + + +# Labels used in batch request entries to specify the desired CRUD operation. +BATCH_INSERT = 'insert' +BATCH_UPDATE = 'update' +BATCH_DELETE = 'delete' +BATCH_QUERY = 'query' + +class Error(Exception): + pass + + +class MissingRequiredParameters(Error): + pass + + +class MediaSource(object): + """GData Entries can refer to media sources, so this class provides a + place to store references to these objects along with some metadata. + """ + + def __init__(self, file_handle=None, content_type=None, content_length=None, + file_path=None, file_name=None): + """Creates an object of type MediaSource. + + Args: + file_handle: A file handle pointing to the file to be encapsulated in the + MediaSource + content_type: string The MIME type of the file. Required if a file_handle + is given. + content_length: int The size of the file. Required if a file_handle is + given. + file_path: string (optional) A full path name to the file. Used in + place of a file_handle. + file_name: string The name of the file without any path information. + Required if a file_handle is given. + """ + self.file_handle = file_handle + self.content_type = content_type + self.content_length = content_length + self.file_name = file_name + + if (file_handle is None and content_type is not None and + file_path is not None): + self.setFile(file_path, content_type) + + def setFile(self, file_name, content_type): + """A helper function which can create a file handle from a given filename + and set the content type and length all at once. + + Args: + file_name: string The path and file name to the file containing the media + content_type: string A MIME type representing the type of the media + """ + + self.file_handle = open(file_name, 'rb') + self.content_type = content_type + self.content_length = os.path.getsize(file_name) + self.file_name = os.path.basename(file_name) + + +class LinkFinder(atom.LinkFinder): + """An "interface" providing methods to find link elements + + GData Entry elements often contain multiple links which differ in the rel + attribute or content type. Often, developers are interested in a specific + type of link so this class provides methods to find specific classes of + links. + + This class is used as a mixin in GData entries. + """ + + def GetSelfLink(self): + """Find the first link with rel set to 'self' + + Returns: + An atom.Link or none if none of the links had rel equal to 'self' + """ + + for a_link in self.link: + if a_link.rel == 'self': + return a_link + return None + + def GetEditLink(self): + for a_link in self.link: + if a_link.rel == 'edit': + return a_link + return None + + def GetEditMediaLink(self): + """The Picasa API mistakenly returns media-edit rather than edit-media, but + this may change soon. + """ + for a_link in self.link: + if a_link.rel == 'edit-media': + return a_link + if a_link.rel == 'media-edit': + return a_link + return None + + def GetHtmlLink(self): + """Find the first link with rel of alternate and type of text/html + + Returns: + An atom.Link or None if no links matched + """ + for a_link in self.link: + if a_link.rel == 'alternate' and a_link.type == 'text/html': + return a_link + return None + + def GetPostLink(self): + """Get a link containing the POST target URL. + + The POST target URL is used to insert new entries. + + Returns: + A link object with a rel matching the POST type. + """ + for a_link in self.link: + if a_link.rel == 'http://schemas.google.com/g/2005#post': + return a_link + return None + + def GetAclLink(self): + for a_link in self.link: + if a_link.rel == 'http://schemas.google.com/acl/2007#accessControlList': + return a_link + return None + + def GetFeedLink(self): + for a_link in self.link: + if a_link.rel == 'http://schemas.google.com/g/2005#feed': + return a_link + return None + + def GetNextLink(self): + for a_link in self.link: + if a_link.rel == 'next': + return a_link + return None + + def GetPrevLink(self): + for a_link in self.link: + if a_link.rel == 'previous': + return a_link + return None + + +class TotalResults(atom.AtomBase): + """opensearch:TotalResults for a GData feed""" + + _tag = 'totalResults' + _namespace = OPENSEARCH_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + + def __init__(self, extension_elements=None, + extension_attributes=None, text=None): + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def TotalResultsFromString(xml_string): + return atom.CreateClassFromXMLString(TotalResults, xml_string) + + +class StartIndex(atom.AtomBase): + """The opensearch:startIndex element in GData feed""" + + _tag = 'startIndex' + _namespace = OPENSEARCH_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + + def __init__(self, extension_elements=None, + extension_attributes=None, text=None): + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def StartIndexFromString(xml_string): + return atom.CreateClassFromXMLString(StartIndex, xml_string) + + +class ItemsPerPage(atom.AtomBase): + """The opensearch:itemsPerPage element in GData feed""" + + _tag = 'itemsPerPage' + _namespace = OPENSEARCH_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + + def __init__(self, extension_elements=None, + extension_attributes=None, text=None): + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def ItemsPerPageFromString(xml_string): + return atom.CreateClassFromXMLString(ItemsPerPage, xml_string) + + +class ExtendedProperty(atom.AtomBase): + """The Google Data extendedProperty element. + + Used to store arbitrary key-value information specific to your + application. The value can either be a text string stored as an XML + attribute (.value), or an XML node (XmlBlob) as a child element. + + This element is used in the Google Calendar data API and the Google + Contacts data API. + """ + + _tag = 'extendedProperty' + _namespace = GDATA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['name'] = 'name' + _attributes['value'] = 'value' + + def __init__(self, name=None, value=None, extension_elements=None, + extension_attributes=None, text=None): + self.name = name + self.value = value + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + def GetXmlBlobExtensionElement(self): + """Returns the XML blob as an atom.ExtensionElement. + + Returns: + An atom.ExtensionElement representing the blob's XML, or None if no + blob was set. + """ + if len(self.extension_elements) < 1: + return None + else: + return self.extension_elements[0] + + def GetXmlBlobString(self): + """Returns the XML blob as a string. + + Returns: + A string containing the blob's XML, or None if no blob was set. + """ + blob = self.GetXmlBlobExtensionElement() + if blob: + return blob.ToString() + return None + + def SetXmlBlob(self, blob): + """Sets the contents of the extendedProperty to XML as a child node. + + Since the extendedProperty is only allowed one child element as an XML + blob, setting the XML blob will erase any preexisting extension elements + in this object. + + Args: + blob: str, ElementTree Element or atom.ExtensionElement representing + the XML blob stored in the extendedProperty. + """ + # Erase any existing extension_elements, clears the child nodes from the + # extendedProperty. + self.extension_elements = [] + if isinstance(blob, atom.ExtensionElement): + self.extension_elements.append(blob) + elif ElementTree.iselement(blob): + self.extension_elements.append(atom._ExtensionElementFromElementTree( + blob)) + else: + self.extension_elements.append(atom.ExtensionElementFromString(blob)) + + +def ExtendedPropertyFromString(xml_string): + return atom.CreateClassFromXMLString(ExtendedProperty, xml_string) + + +class GDataEntry(atom.Entry, LinkFinder): + """Extends Atom Entry to provide data processing""" + + _tag = atom.Entry._tag + _namespace = atom.Entry._namespace + _children = atom.Entry._children.copy() + _attributes = atom.Entry._attributes.copy() + + def __GetId(self): + return self.__id + + # This method was created to strip the unwanted whitespace from the id's + # text node. + def __SetId(self, id): + self.__id = id + if id is not None and id.text is not None: + self.__id.text = id.text.strip() + + id = property(__GetId, __SetId) + + def IsMedia(self): + """Determines whether or not an entry is a GData Media entry. + """ + if (self.GetEditMediaLink()): + return True + else: + return False + + def GetMediaURL(self): + """Returns the URL to the media content, if the entry is a media entry. + Otherwise returns None. + """ + if not self.IsMedia(): + return None + else: + return self.content.src + + +def GDataEntryFromString(xml_string): + """Creates a new GDataEntry instance given a string of XML.""" + return atom.CreateClassFromXMLString(GDataEntry, xml_string) + + +class GDataFeed(atom.Feed, LinkFinder): + """A Feed from a GData service""" + + _tag = 'feed' + _namespace = atom.ATOM_NAMESPACE + _children = atom.Feed._children.copy() + _attributes = atom.Feed._attributes.copy() + _children['{%s}totalResults' % OPENSEARCH_NAMESPACE] = ('total_results', + TotalResults) + _children['{%s}startIndex' % OPENSEARCH_NAMESPACE] = ('start_index', + StartIndex) + _children['{%s}itemsPerPage' % OPENSEARCH_NAMESPACE] = ('items_per_page', + ItemsPerPage) + # Add a conversion rule for atom:entry to make it into a GData + # Entry. + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [GDataEntry]) + + def __GetId(self): + return self.__id + + def __SetId(self, id): + self.__id = id + if id is not None and id.text is not None: + self.__id.text = id.text.strip() + + id = property(__GetId, __SetId) + + def __GetGenerator(self): + return self.__generator + + def __SetGenerator(self, generator): + self.__generator = generator + if generator is not None: + self.__generator.text = generator.text.strip() + + generator = property(__GetGenerator, __SetGenerator) + + def __init__(self, author=None, category=None, contributor=None, + generator=None, icon=None, atom_id=None, link=None, logo=None, + rights=None, subtitle=None, title=None, updated=None, entry=None, + total_results=None, start_index=None, items_per_page=None, + extension_elements=None, extension_attributes=None, text=None): + """Constructor for Source + + Args: + author: list (optional) A list of Author instances which belong to this + class. + category: list (optional) A list of Category instances + contributor: list (optional) A list on Contributor instances + generator: Generator (optional) + icon: Icon (optional) + id: Id (optional) The entry's Id element + link: list (optional) A list of Link instances + logo: Logo (optional) + rights: Rights (optional) The entry's Rights element + subtitle: Subtitle (optional) The entry's subtitle element + title: Title (optional) the entry's title element + updated: Updated (optional) the entry's updated element + entry: list (optional) A list of the Entry instances contained in the + feed. + text: String (optional) The text contents of the element. This is the + contents of the Entry's XML text node. + (Example: This is the text) + extension_elements: list (optional) A list of ExtensionElement instances + which are children of this element. + extension_attributes: dict (optional) A dictionary of strings which are + the values for additional XML attributes of this element. + """ + + self.author = author or [] + self.category = category or [] + self.contributor = contributor or [] + self.generator = generator + self.icon = icon + self.id = atom_id + self.link = link or [] + self.logo = logo + self.rights = rights + self.subtitle = subtitle + self.title = title + self.updated = updated + self.entry = entry or [] + self.total_results = total_results + self.start_index = start_index + self.items_per_page = items_per_page + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def GDataFeedFromString(xml_string): + return atom.CreateClassFromXMLString(GDataFeed, xml_string) + + +class BatchId(atom.AtomBase): + _tag = 'id' + _namespace = BATCH_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + + +def BatchIdFromString(xml_string): + return atom.CreateClassFromXMLString(BatchId, xml_string) + + +class BatchOperation(atom.AtomBase): + _tag = 'operation' + _namespace = BATCH_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['type'] = 'type' + + def __init__(self, op_type=None, extension_elements=None, + extension_attributes=None, + text=None): + self.type = op_type + atom.AtomBase.__init__(self, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + +def BatchOperationFromString(xml_string): + return atom.CreateClassFromXMLString(BatchOperation, xml_string) + + +class BatchStatus(atom.AtomBase): + """The batch:status element present in a batch response entry. + + A status element contains the code (HTTP response code) and + reason as elements. In a single request these fields would + be part of the HTTP response, but in a batch request each + Entry operation has a corresponding Entry in the response + feed which includes status information. + + See http://code.google.com/apis/gdata/batch.html#Handling_Errors + """ + + _tag = 'status' + _namespace = BATCH_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['code'] = 'code' + _attributes['reason'] = 'reason' + _attributes['content-type'] = 'content_type' + + def __init__(self, code=None, reason=None, content_type=None, + extension_elements=None, extension_attributes=None, text=None): + self.code = code + self.reason = reason + self.content_type = content_type + atom.AtomBase.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + +def BatchStatusFromString(xml_string): + return atom.CreateClassFromXMLString(BatchStatus, xml_string) + + +class BatchEntry(GDataEntry): + """An atom:entry for use in batch requests. + + The BatchEntry contains additional members to specify the operation to be + performed on this entry and a batch ID so that the server can reference + individual operations in the response feed. For more information, see: + http://code.google.com/apis/gdata/batch.html + """ + + _tag = GDataEntry._tag + _namespace = GDataEntry._namespace + _children = GDataEntry._children.copy() + _children['{%s}operation' % BATCH_NAMESPACE] = ('batch_operation', BatchOperation) + _children['{%s}id' % BATCH_NAMESPACE] = ('batch_id', BatchId) + _children['{%s}status' % BATCH_NAMESPACE] = ('batch_status', BatchStatus) + _attributes = GDataEntry._attributes.copy() + + def __init__(self, author=None, category=None, content=None, + contributor=None, atom_id=None, link=None, published=None, rights=None, + source=None, summary=None, control=None, title=None, updated=None, + batch_operation=None, batch_id=None, batch_status=None, + extension_elements=None, extension_attributes=None, text=None): + self.batch_operation = batch_operation + self.batch_id = batch_id + self.batch_status = batch_status + GDataEntry.__init__(self, author=author, category=category, + content=content, contributor=contributor, atom_id=atom_id, link=link, + published=published, rights=rights, source=source, summary=summary, + control=control, title=title, updated=updated, + extension_elements=extension_elements, + extension_attributes=extension_attributes, text=text) + + +def BatchEntryFromString(xml_string): + return atom.CreateClassFromXMLString(BatchEntry, xml_string) + + +class BatchInterrupted(atom.AtomBase): + """The batch:interrupted element sent if batch request was interrupted. + + Only appears in a feed if some of the batch entries could not be processed. + See: http://code.google.com/apis/gdata/batch.html#Handling_Errors + """ + + _tag = 'interrupted' + _namespace = BATCH_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _attributes['reason'] = 'reason' + _attributes['success'] = 'success' + _attributes['failures'] = 'failures' + _attributes['parsed'] = 'parsed' + + def __init__(self, reason=None, success=None, failures=None, parsed=None, + extension_elements=None, extension_attributes=None, text=None): + self.reason = reason + self.success = success + self.failures = failures + self.parsed = parsed + atom.AtomBase.__init__(self, extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + +def BatchInterruptedFromString(xml_string): + return atom.CreateClassFromXMLString(BatchInterrupted, xml_string) + + +class BatchFeed(GDataFeed): + """A feed containing a list of batch request entries.""" + + _tag = GDataFeed._tag + _namespace = GDataFeed._namespace + _children = GDataFeed._children.copy() + _attributes = GDataFeed._attributes.copy() + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [BatchEntry]) + _children['{%s}interrupted' % BATCH_NAMESPACE] = ('interrupted', BatchInterrupted) + + def __init__(self, author=None, category=None, contributor=None, + generator=None, icon=None, atom_id=None, link=None, logo=None, + rights=None, subtitle=None, title=None, updated=None, entry=None, + total_results=None, start_index=None, items_per_page=None, + interrupted=None, + extension_elements=None, extension_attributes=None, text=None): + self.interrupted = interrupted + GDataFeed.__init__(self, author=author, category=category, + contributor=contributor, generator=generator, + icon=icon, atom_id=atom_id, link=link, + logo=logo, rights=rights, subtitle=subtitle, + title=title, updated=updated, entry=entry, + total_results=total_results, start_index=start_index, + items_per_page=items_per_page, + extension_elements=extension_elements, + extension_attributes=extension_attributes, + text=text) + + def AddBatchEntry(self, entry=None, id_url_string=None, + batch_id_string=None, operation_string=None): + """Logic for populating members of a BatchEntry and adding to the feed. + + + If the entry is not a BatchEntry, it is converted to a BatchEntry so + that the batch specific members will be present. + + The id_url_string can be used in place of an entry if the batch operation + applies to a URL. For example query and delete operations require just + the URL of an entry, no body is sent in the HTTP request. If an + id_url_string is sent instead of an entry, a BatchEntry is created and + added to the feed. + + This method also assigns the desired batch id to the entry so that it + can be referenced in the server's response. If the batch_id_string is + None, this method will assign a batch_id to be the index at which this + entry will be in the feed's entry list. + + Args: + entry: BatchEntry, atom.Entry, or another Entry flavor (optional) The + entry which will be sent to the server as part of the batch request. + The item must have a valid atom id so that the server knows which + entry this request references. + id_url_string: str (optional) The URL of the entry to be acted on. You + can find this URL in the text member of the atom id for an entry. + If an entry is not sent, this id will be used to construct a new + BatchEntry which will be added to the request feed. + batch_id_string: str (optional) The batch ID to be used to reference + this batch operation in the results feed. If this parameter is None, + the current length of the feed's entry array will be used as a + count. Note that batch_ids should either always be specified or + never, mixing could potentially result in duplicate batch ids. + operation_string: str (optional) The desired batch operation which will + set the batch_operation.type member of the entry. Options are + 'insert', 'update', 'delete', and 'query' + + Raises: + MissingRequiredParameters: Raised if neither an id_ url_string nor an + entry are provided in the request. + + Returns: + The added entry. + """ + if entry is None and id_url_string is None: + raise MissingRequiredParameters('supply either an entry or URL string') + if entry is None and id_url_string is not None: + entry = BatchEntry(atom_id=atom.Id(text=id_url_string)) + # TODO: handle cases in which the entry lacks batch_... members. + #if not isinstance(entry, BatchEntry): + # Convert the entry to a batch entry. + if batch_id_string is not None: + entry.batch_id = BatchId(text=batch_id_string) + elif entry.batch_id is None or entry.batch_id.text is None: + entry.batch_id = BatchId(text=str(len(self.entry))) + if operation_string is not None: + entry.batch_operation = BatchOperation(op_type=operation_string) + self.entry.append(entry) + return entry + + def AddInsert(self, entry, batch_id_string=None): + """Add an insert request to the operations in this batch request feed. + + If the entry doesn't yet have an operation or a batch id, these will + be set to the insert operation and a batch_id specified as a parameter. + + Args: + entry: BatchEntry The entry which will be sent in the batch feed as an + insert request. + batch_id_string: str (optional) The batch ID to be used to reference + this batch operation in the results feed. If this parameter is None, + the current length of the feed's entry array will be used as a + count. Note that batch_ids should either always be specified or + never, mixing could potentially result in duplicate batch ids. + """ + entry = self.AddBatchEntry(entry=entry, batch_id_string=batch_id_string, + operation_string=BATCH_INSERT) + + def AddUpdate(self, entry, batch_id_string=None): + """Add an update request to the list of batch operations in this feed. + + Sets the operation type of the entry to insert if it is not already set + and assigns the desired batch id to the entry so that it can be + referenced in the server's response. + + Args: + entry: BatchEntry The entry which will be sent to the server as an + update (HTTP PUT) request. The item must have a valid atom id + so that the server knows which entry to replace. + batch_id_string: str (optional) The batch ID to be used to reference + this batch operation in the results feed. If this parameter is None, + the current length of the feed's entry array will be used as a + count. See also comments for AddInsert. + """ + entry = self.AddBatchEntry(entry=entry, batch_id_string=batch_id_string, + operation_string=BATCH_UPDATE) + + def AddDelete(self, url_string=None, entry=None, batch_id_string=None): + """Adds a delete request to the batch request feed. + + This method takes either the url_string which is the atom id of the item + to be deleted, or the entry itself. The atom id of the entry must be + present so that the server knows which entry should be deleted. + + Args: + url_string: str (optional) The URL of the entry to be deleted. You can + find this URL in the text member of the atom id for an entry. + entry: BatchEntry (optional) The entry to be deleted. + batch_id_string: str (optional) + + Raises: + MissingRequiredParameters: Raised if neither a url_string nor an entry + are provided in the request. + """ + entry = self.AddBatchEntry(entry=entry, id_url_string=url_string, + batch_id_string=batch_id_string, + operation_string=BATCH_DELETE) + + def AddQuery(self, url_string=None, entry=None, batch_id_string=None): + """Adds a query request to the batch request feed. + + This method takes either the url_string which is the query URL + whose results will be added to the result feed. The query URL will + be encapsulated in a BatchEntry, and you may pass in the BatchEntry + with a query URL instead of sending a url_string. + + Args: + url_string: str (optional) + entry: BatchEntry (optional) + batch_id_string: str (optional) + + Raises: + MissingRequiredParameters + """ + entry = self.AddBatchEntry(entry=entry, id_url_string=url_string, + batch_id_string=batch_id_string, + operation_string=BATCH_QUERY) + + def GetBatchLink(self): + for link in self.link: + if link.rel == 'http://schemas.google.com/g/2005#batch': + return link + return None + + +def BatchFeedFromString(xml_string): + return atom.CreateClassFromXMLString(BatchFeed, xml_string) + + +class EntryLink(atom.AtomBase): + """The gd:entryLink element""" + + _tag = 'entryLink' + _namespace = GDATA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + # The entry used to be an atom.Entry, now it is a GDataEntry. + _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', GDataEntry) + _attributes['rel'] = 'rel' + _attributes['readOnly'] = 'read_only' + _attributes['href'] = 'href' + + def __init__(self, href=None, read_only=None, rel=None, + entry=None, extension_elements=None, + extension_attributes=None, text=None): + self.href = href + self.read_only = read_only + self.rel = rel + self.entry = entry + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def EntryLinkFromString(xml_string): + return atom.CreateClassFromXMLString(EntryLink, xml_string) + + +class FeedLink(atom.AtomBase): + """The gd:feedLink element""" + + _tag = 'feedLink' + _namespace = GDATA_NAMESPACE + _children = atom.AtomBase._children.copy() + _attributes = atom.AtomBase._attributes.copy() + _children['{%s}feed' % atom.ATOM_NAMESPACE] = ('feed', GDataFeed) + _attributes['rel'] = 'rel' + _attributes['readOnly'] = 'read_only' + _attributes['countHint'] = 'count_hint' + _attributes['href'] = 'href' + + def __init__(self, count_hint=None, href=None, read_only=None, rel=None, + feed=None, extension_elements=None, extension_attributes=None, + text=None): + self.count_hint = count_hint + self.href = href + self.read_only = read_only + self.rel = rel + self.feed = feed + self.text = text + self.extension_elements = extension_elements or [] + self.extension_attributes = extension_attributes or {} + + +def FeedLinkFromString(xml_string): + return atom.CreateClassFromXMLString(FeedLink, xml_string) diff --git a/gdata/__init__.pyc b/gdata/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1933e40e57ad0ab598d611c6e694c21207419071 GIT binary patch literal 33483 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 HT=)MU9#IGz literal 0 HcmV?d00001 diff --git a/gdata/auth.py b/gdata/auth.py new file mode 100644 index 0000000..139c6cd --- /dev/null +++ b/gdata/auth.py @@ -0,0 +1,952 @@ +#!/usr/bin/python +# +# Copyright (C) 2007 - 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. + + +import cgi +import math +import random +import re +import time +import types +import urllib +import atom.http_interface +import atom.token_store +import atom.url +import gdata.oauth as oauth +import gdata.oauth.rsa as oauth_rsa +import gdata.tlslite.utils.keyfactory as keyfactory +import gdata.tlslite.utils.cryptomath as cryptomath + +import gdata.gauth + +__author__ = 'api.jscudder (Jeff Scudder)' + + +PROGRAMMATIC_AUTH_LABEL = 'GoogleLogin auth=' +AUTHSUB_AUTH_LABEL = 'AuthSub token=' + + +"""This module provides functions and objects used with Google authentication. + +Details on Google authorization mechanisms used with the Google Data APIs can +be found here: +http://code.google.com/apis/gdata/auth.html +http://code.google.com/apis/accounts/ + +The essential functions are the following. +Related to ClientLogin: + generate_client_login_request_body: Constructs the body of an HTTP request to + obtain a ClientLogin token for a specific + service. + extract_client_login_token: Creates a ClientLoginToken with the token from a + success response to a ClientLogin request. + get_captcha_challenge: If the server responded to the ClientLogin request + with a CAPTCHA challenge, this method extracts the + CAPTCHA URL and identifying CAPTCHA token. + +Related to AuthSub: + generate_auth_sub_url: Constructs a full URL for a AuthSub request. The + user's browser must be sent to this Google Accounts + URL and redirected back to the app to obtain the + AuthSub token. + extract_auth_sub_token_from_url: Once the user's browser has been + redirected back to the web app, use this + function to create an AuthSubToken with + the correct authorization token and scope. + token_from_http_body: Extracts the AuthSubToken value string from the + server's response to an AuthSub session token upgrade + request. +""" + +def generate_client_login_request_body(email, password, service, source, + account_type='HOSTED_OR_GOOGLE', captcha_token=None, + captcha_response=None): + """Creates the body of the autentication request + + See http://code.google.com/apis/accounts/AuthForInstalledApps.html#Request + for more details. + + Args: + email: str + password: str + service: str + source: str + account_type: str (optional) Defaul is 'HOSTED_OR_GOOGLE', other valid + values are 'GOOGLE' and 'HOSTED' + captcha_token: str (optional) + captcha_response: str (optional) + + Returns: + The HTTP body to send in a request for a client login token. + """ + return gdata.gauth.generate_client_login_request_body(email, password, + service, source, account_type, captcha_token, captcha_response) + + +GenerateClientLoginRequestBody = generate_client_login_request_body + + +def GenerateClientLoginAuthToken(http_body): + """Returns the token value to use in Authorization headers. + + Reads the token from the server's response to a Client Login request and + creates header value to use in requests. + + Args: + http_body: str The body of the server's HTTP response to a Client Login + request + + Returns: + The value half of an Authorization header. + """ + token = get_client_login_token(http_body) + if token: + return 'GoogleLogin auth=%s' % token + return None + + +def get_client_login_token(http_body): + """Returns the token value for a ClientLoginToken. + + Reads the token from the server's response to a Client Login request and + creates the token value string to use in requests. + + Args: + http_body: str The body of the server's HTTP response to a Client Login + request + + Returns: + The token value string for a ClientLoginToken. + """ + return gdata.gauth.get_client_login_token_string(http_body) + + +def extract_client_login_token(http_body, scopes): + """Parses the server's response and returns a ClientLoginToken. + + Args: + http_body: str The body of the server's HTTP response to a Client Login + request. It is assumed that the login request was successful. + scopes: list containing atom.url.Urls or strs. The scopes list contains + all of the partial URLs under which the client login token is + valid. For example, if scopes contains ['http://example.com/foo'] + then the client login token would be valid for + http://example.com/foo/bar/baz + + Returns: + A ClientLoginToken which is valid for the specified scopes. + """ + token_string = get_client_login_token(http_body) + token = ClientLoginToken(scopes=scopes) + token.set_token_string(token_string) + return token + + +def get_captcha_challenge(http_body, + captcha_base_url='http://www.google.com/accounts/'): + """Returns the URL and token for a CAPTCHA challenge issued by the server. + + Args: + http_body: str The body of the HTTP response from the server which + contains the CAPTCHA challenge. + captcha_base_url: str This function returns a full URL for viewing the + challenge image which is built from the server's response. This + base_url is used as the beginning of the URL because the server + only provides the end of the URL. For example the server provides + 'Captcha?ctoken=Hi...N' and the URL for the image is + 'http://www.google.com/accounts/Captcha?ctoken=Hi...N' + + Returns: + A dictionary containing the information needed to repond to the CAPTCHA + challenge, the image URL and the ID token of the challenge. The + dictionary is in the form: + {'token': string identifying the CAPTCHA image, + 'url': string containing the URL of the image} + Returns None if there was no CAPTCHA challenge in the response. + """ + return gdata.gauth.get_captcha_challenge(http_body, captcha_base_url) + + +GetCaptchaChallenge = get_captcha_challenge + + +def GenerateOAuthRequestTokenUrl( + oauth_input_params, scopes, + request_token_url='https://www.google.com/accounts/OAuthGetRequestToken', + extra_parameters=None): + """Generate a URL at which a request for OAuth request token is to be sent. + + Args: + oauth_input_params: OAuthInputParams OAuth input parameters. + scopes: list of strings The URLs of the services to be accessed. + request_token_url: string The beginning of the request token URL. This is + normally 'https://www.google.com/accounts/OAuthGetRequestToken' or + '/accounts/OAuthGetRequestToken' + extra_parameters: dict (optional) key-value pairs as any additional + parameters to be included in the URL and signature while making a + request for fetching an OAuth request token. All the OAuth parameters + are added by default. But if provided through this argument, any + default parameters will be overwritten. For e.g. a default parameter + oauth_version 1.0 can be overwritten if + extra_parameters = {'oauth_version': '2.0'} + + Returns: + atom.url.Url OAuth request token URL. + """ + scopes_string = ' '.join([str(scope) for scope in scopes]) + parameters = {'scope': scopes_string} + if extra_parameters: + parameters.update(extra_parameters) + oauth_request = oauth.OAuthRequest.from_consumer_and_token( + oauth_input_params.GetConsumer(), http_url=request_token_url, + parameters=parameters) + oauth_request.sign_request(oauth_input_params.GetSignatureMethod(), + oauth_input_params.GetConsumer(), None) + return atom.url.parse_url(oauth_request.to_url()) + + +def GenerateOAuthAuthorizationUrl( + request_token, + authorization_url='https://www.google.com/accounts/OAuthAuthorizeToken', + callback_url=None, extra_params=None, + include_scopes_in_callback=False, scopes_param_prefix='oauth_token_scope'): + """Generates URL at which user will login to authorize the request token. + + Args: + request_token: gdata.auth.OAuthToken OAuth request token. + authorization_url: string The beginning of the authorization URL. This is + normally 'https://www.google.com/accounts/OAuthAuthorizeToken' or + '/accounts/OAuthAuthorizeToken' + callback_url: string (optional) The URL user will be sent to after + logging in and granting access. + extra_params: dict (optional) Additional parameters to be sent. + include_scopes_in_callback: Boolean (default=False) if set to True, and + if 'callback_url' is present, the 'callback_url' will be modified to + include the scope(s) from the request token as a URL parameter. The + key for the 'callback' URL's scope parameter will be + OAUTH_SCOPE_URL_PARAM_NAME. The benefit of including the scope URL as + a parameter to the 'callback' URL, is that the page which receives + the OAuth token will be able to tell which URLs the token grants + access to. + scopes_param_prefix: string (default='oauth_token_scope') The URL + parameter key which maps to the list of valid scopes for the token. + This URL parameter will be included in the callback URL along with + the scopes of the token as value if include_scopes_in_callback=True. + + Returns: + atom.url.Url OAuth authorization URL. + """ + scopes = request_token.scopes + if isinstance(scopes, list): + scopes = ' '.join(scopes) + if include_scopes_in_callback and callback_url: + if callback_url.find('?') > -1: + callback_url += '&' + else: + callback_url += '?' + callback_url += urllib.urlencode({scopes_param_prefix:scopes}) + oauth_token = oauth.OAuthToken(request_token.key, request_token.secret) + oauth_request = oauth.OAuthRequest.from_token_and_callback( + token=oauth_token, callback=callback_url, + http_url=authorization_url, parameters=extra_params) + return atom.url.parse_url(oauth_request.to_url()) + + +def GenerateOAuthAccessTokenUrl( + authorized_request_token, + oauth_input_params, + access_token_url='https://www.google.com/accounts/OAuthGetAccessToken', + oauth_version='1.0', + oauth_verifier=None): + """Generates URL at which user will login to authorize the request token. + + Args: + authorized_request_token: gdata.auth.OAuthToken OAuth authorized request + token. + oauth_input_params: OAuthInputParams OAuth input parameters. + access_token_url: string The beginning of the authorization URL. This is + normally 'https://www.google.com/accounts/OAuthGetAccessToken' or + '/accounts/OAuthGetAccessToken' + oauth_version: str (default='1.0') oauth_version parameter. + oauth_verifier: str (optional) If present, it is assumed that the client + will use the OAuth v1.0a protocol which includes passing the + oauth_verifier (as returned by the SP) in the access token step. + + Returns: + atom.url.Url OAuth access token URL. + """ + oauth_token = oauth.OAuthToken(authorized_request_token.key, + authorized_request_token.secret) + parameters = {'oauth_version': oauth_version} + if oauth_verifier is not None: + parameters['oauth_verifier'] = oauth_verifier + oauth_request = oauth.OAuthRequest.from_consumer_and_token( + oauth_input_params.GetConsumer(), token=oauth_token, + http_url=access_token_url, parameters=parameters) + oauth_request.sign_request(oauth_input_params.GetSignatureMethod(), + oauth_input_params.GetConsumer(), oauth_token) + return atom.url.parse_url(oauth_request.to_url()) + + +def GenerateAuthSubUrl(next, scope, secure=False, session=True, + request_url='https://www.google.com/accounts/AuthSubRequest', + domain='default'): + """Generate a URL at which the user will login and be redirected back. + + Users enter their credentials on a Google login page and a token is sent + to the URL specified in next. See documentation for AuthSub login at: + http://code.google.com/apis/accounts/AuthForWebApps.html + + Args: + request_url: str The beginning of the request URL. This is normally + 'http://www.google.com/accounts/AuthSubRequest' or + '/accounts/AuthSubRequest' + next: string The URL user will be sent to after logging in. + scope: string The URL of the service to be accessed. + secure: boolean (optional) Determines whether or not the issued token + is a secure token. + session: boolean (optional) Determines whether or not the issued token + can be upgraded to a session token. + domain: str (optional) The Google Apps domain for this account. If this + is not a Google Apps account, use 'default' which is the default + value. + """ + # Translate True/False values for parameters into numeric values acceoted + # by the AuthSub service. + if secure: + secure = 1 + else: + secure = 0 + + if session: + session = 1 + else: + session = 0 + + request_params = urllib.urlencode({'next': next, 'scope': scope, + 'secure': secure, 'session': session, + 'hd': domain}) + if request_url.find('?') == -1: + return '%s?%s' % (request_url, request_params) + else: + # The request URL already contained url parameters so we should add + # the parameters using the & seperator + return '%s&%s' % (request_url, request_params) + + +def generate_auth_sub_url(next, scopes, secure=False, session=True, + request_url='https://www.google.com/accounts/AuthSubRequest', + domain='default', scopes_param_prefix='auth_sub_scopes'): + """Constructs a URL string for requesting a multiscope AuthSub token. + + The generated token will contain a URL parameter to pass along the + requested scopes to the next URL. When the Google Accounts page + redirects the broswser to the 'next' URL, it appends the single use + AuthSub token value to the URL as a URL parameter with the key 'token'. + However, the information about which scopes were requested is not + included by Google Accounts. This method adds the scopes to the next + URL before making the request so that the redirect will be sent to + a page, and both the token value and the list of scopes can be + extracted from the request URL. + + Args: + next: atom.url.URL or string The URL user will be sent to after + authorizing this web application to access their data. + scopes: list containint strings The URLs of the services to be accessed. + secure: boolean (optional) Determines whether or not the issued token + is a secure token. + session: boolean (optional) Determines whether or not the issued token + can be upgraded to a session token. + request_url: atom.url.Url or str The beginning of the request URL. This + is normally 'http://www.google.com/accounts/AuthSubRequest' or + '/accounts/AuthSubRequest' + domain: The domain which the account is part of. This is used for Google + Apps accounts, the default value is 'default' which means that the + requested account is a Google Account (@gmail.com for example) + scopes_param_prefix: str (optional) The requested scopes are added as a + URL parameter to the next URL so that the page at the 'next' URL can + extract the token value and the valid scopes from the URL. The key + for the URL parameter defaults to 'auth_sub_scopes' + + Returns: + An atom.url.Url which the user's browser should be directed to in order + to authorize this application to access their information. + """ + if isinstance(next, (str, unicode)): + next = atom.url.parse_url(next) + scopes_string = ' '.join([str(scope) for scope in scopes]) + next.params[scopes_param_prefix] = scopes_string + + if isinstance(request_url, (str, unicode)): + request_url = atom.url.parse_url(request_url) + request_url.params['next'] = str(next) + request_url.params['scope'] = scopes_string + if session: + request_url.params['session'] = 1 + else: + request_url.params['session'] = 0 + if secure: + request_url.params['secure'] = 1 + else: + request_url.params['secure'] = 0 + request_url.params['hd'] = domain + return request_url + + +def AuthSubTokenFromUrl(url): + """Extracts the AuthSub token from the URL. + + Used after the AuthSub redirect has sent the user to the 'next' page and + appended the token to the URL. This function returns the value to be used + in the Authorization header. + + Args: + url: str The URL of the current page which contains the AuthSub token as + a URL parameter. + """ + token = TokenFromUrl(url) + if token: + return 'AuthSub token=%s' % token + return None + + +def TokenFromUrl(url): + """Extracts the AuthSub token from the URL. + + Returns the raw token value. + + Args: + url: str The URL or the query portion of the URL string (after the ?) of + the current page which contains the AuthSub token as a URL parameter. + """ + if url.find('?') > -1: + query_params = url.split('?')[1] + else: + query_params = url + for pair in query_params.split('&'): + if pair.startswith('token='): + return pair[6:] + return None + + +def extract_auth_sub_token_from_url(url, + scopes_param_prefix='auth_sub_scopes', rsa_key=None): + """Creates an AuthSubToken and sets the token value and scopes from the URL. + + After the Google Accounts AuthSub pages redirect the user's broswer back to + the web application (using the 'next' URL from the request) the web app must + extract the token from the current page's URL. The token is provided as a + URL parameter named 'token' and if generate_auth_sub_url was used to create + the request, the token's valid scopes are included in a URL parameter whose + name is specified in scopes_param_prefix. + + Args: + url: atom.url.Url or str representing the current URL. The token value + and valid scopes should be included as URL parameters. + scopes_param_prefix: str (optional) The URL parameter key which maps to + the list of valid scopes for the token. + + Returns: + An AuthSubToken with the token value from the URL and set to be valid for + the scopes passed in on the URL. If no scopes were included in the URL, + the AuthSubToken defaults to being valid for no scopes. If there was no + 'token' parameter in the URL, this function returns None. + """ + if isinstance(url, (str, unicode)): + url = atom.url.parse_url(url) + if 'token' not in url.params: + return None + scopes = [] + if scopes_param_prefix in url.params: + scopes = url.params[scopes_param_prefix].split(' ') + token_value = url.params['token'] + if rsa_key: + token = SecureAuthSubToken(rsa_key, scopes=scopes) + else: + token = AuthSubToken(scopes=scopes) + token.set_token_string(token_value) + return token + + +def AuthSubTokenFromHttpBody(http_body): + """Extracts the AuthSub token from an HTTP body string. + + Used to find the new session token after making a request to upgrade a + single use AuthSub token. + + Args: + http_body: str The repsonse from the server which contains the AuthSub + key. For example, this function would find the new session token + from the server's response to an upgrade token request. + + Returns: + The header value to use for Authorization which contains the AuthSub + token. + """ + token_value = token_from_http_body(http_body) + if token_value: + return '%s%s' % (AUTHSUB_AUTH_LABEL, token_value) + return None + + +def token_from_http_body(http_body): + """Extracts the AuthSub token from an HTTP body string. + + Used to find the new session token after making a request to upgrade a + single use AuthSub token. + + Args: + http_body: str The repsonse from the server which contains the AuthSub + key. For example, this function would find the new session token + from the server's response to an upgrade token request. + + Returns: + The raw token value to use in an AuthSubToken object. + """ + for response_line in http_body.splitlines(): + if response_line.startswith('Token='): + # Strip off Token= and return the token value string. + return response_line[6:] + return None + + +TokenFromHttpBody = token_from_http_body + + +def OAuthTokenFromUrl(url, scopes_param_prefix='oauth_token_scope'): + """Creates an OAuthToken and sets token key and scopes (if present) from URL. + + After the Google Accounts OAuth pages redirect the user's broswer back to + the web application (using the 'callback' URL from the request) the web app + can extract the token from the current page's URL. The token is same as the + request token, but it is either authorized (if user grants access) or + unauthorized (if user denies access). The token is provided as a + URL parameter named 'oauth_token' and if it was chosen to use + GenerateOAuthAuthorizationUrl with include_scopes_in_param=True, the token's + valid scopes are included in a URL parameter whose name is specified in + scopes_param_prefix. + + Args: + url: atom.url.Url or str representing the current URL. The token value + and valid scopes should be included as URL parameters. + scopes_param_prefix: str (optional) The URL parameter key which maps to + the list of valid scopes for the token. + + Returns: + An OAuthToken with the token key from the URL and set to be valid for + the scopes passed in on the URL. If no scopes were included in the URL, + the OAuthToken defaults to being valid for no scopes. If there was no + 'oauth_token' parameter in the URL, this function returns None. + """ + if isinstance(url, (str, unicode)): + url = atom.url.parse_url(url) + if 'oauth_token' not in url.params: + return None + scopes = [] + if scopes_param_prefix in url.params: + scopes = url.params[scopes_param_prefix].split(' ') + token_key = url.params['oauth_token'] + token = OAuthToken(key=token_key, scopes=scopes) + return token + + +def OAuthTokenFromHttpBody(http_body): + """Parses the HTTP response body and returns an OAuth token. + + The returned OAuth token will just have key and secret parameters set. + It won't have any knowledge about the scopes or oauth_input_params. It is + your responsibility to make it aware of the remaining parameters. + + Returns: + OAuthToken OAuth token. + """ + token = oauth.OAuthToken.from_string(http_body) + oauth_token = OAuthToken(key=token.key, secret=token.secret) + return oauth_token + + +class OAuthSignatureMethod(object): + """Holds valid OAuth signature methods. + + RSA_SHA1: Class to build signature according to RSA-SHA1 algorithm. + HMAC_SHA1: Class to build signature according to HMAC-SHA1 algorithm. + """ + + HMAC_SHA1 = oauth.OAuthSignatureMethod_HMAC_SHA1 + + class RSA_SHA1(oauth_rsa.OAuthSignatureMethod_RSA_SHA1): + """Provides implementation for abstract methods to return RSA certs.""" + + def __init__(self, private_key, public_cert): + self.private_key = private_key + self.public_cert = public_cert + + def _fetch_public_cert(self, unused_oauth_request): + return self.public_cert + + def _fetch_private_cert(self, unused_oauth_request): + return self.private_key + + +class OAuthInputParams(object): + """Stores OAuth input parameters. + + This class is a store for OAuth input parameters viz. consumer key and secret, + signature method and RSA key. + """ + + def __init__(self, signature_method, consumer_key, consumer_secret=None, + rsa_key=None, requestor_id=None): + """Initializes object with parameters required for using OAuth mechanism. + + NOTE: Though consumer_secret and rsa_key are optional, either of the two + is required depending on the value of the signature_method. + + Args: + signature_method: class which provides implementation for strategy class + oauth.oauth.OAuthSignatureMethod. Signature method to be used for + signing each request. Valid implementations are provided as the + constants defined by gdata.auth.OAuthSignatureMethod. Currently + they are gdata.auth.OAuthSignatureMethod.RSA_SHA1 and + gdata.auth.OAuthSignatureMethod.HMAC_SHA1. Instead of passing in + the strategy class, you may pass in a string for 'RSA_SHA1' or + 'HMAC_SHA1'. If you plan to use OAuth on App Engine (or another + WSGI environment) I recommend specifying signature method using a + string (the only options are 'RSA_SHA1' and 'HMAC_SHA1'). In these + environments there are sometimes issues with pickling an object in + which a member references a class or function. Storing a string to + refer to the signature method mitigates complications when + pickling. + consumer_key: string Domain identifying third_party web application. + consumer_secret: string (optional) Secret generated during registration. + Required only for HMAC_SHA1 signature method. + rsa_key: string (optional) Private key required for RSA_SHA1 signature + method. + requestor_id: string (optional) User email adress to make requests on + their behalf. This parameter should only be set when performing + 2 legged OAuth requests. + """ + if (signature_method == OAuthSignatureMethod.RSA_SHA1 + or signature_method == 'RSA_SHA1'): + self.__signature_strategy = 'RSA_SHA1' + elif (signature_method == OAuthSignatureMethod.HMAC_SHA1 + or signature_method == 'HMAC_SHA1'): + self.__signature_strategy = 'HMAC_SHA1' + else: + self.__signature_strategy = signature_method + self.rsa_key = rsa_key + self._consumer = oauth.OAuthConsumer(consumer_key, consumer_secret) + self.requestor_id = requestor_id + + def __get_signature_method(self): + if self.__signature_strategy == 'RSA_SHA1': + return OAuthSignatureMethod.RSA_SHA1(self.rsa_key, None) + elif self.__signature_strategy == 'HMAC_SHA1': + return OAuthSignatureMethod.HMAC_SHA1() + else: + return self.__signature_strategy() + + def __set_signature_method(self, signature_method): + if (signature_method == OAuthSignatureMethod.RSA_SHA1 + or signature_method == 'RSA_SHA1'): + self.__signature_strategy = 'RSA_SHA1' + elif (signature_method == OAuthSignatureMethod.HMAC_SHA1 + or signature_method == 'HMAC_SHA1'): + self.__signature_strategy = 'HMAC_SHA1' + else: + self.__signature_strategy = signature_method + + _signature_method = property(__get_signature_method, __set_signature_method, + doc="""Returns object capable of signing the request using RSA of HMAC. + + Replaces the _signature_method member to avoid pickle errors.""") + + def GetSignatureMethod(self): + """Gets the OAuth signature method. + + Returns: + object of supertype + """ + return self._signature_method + + def GetConsumer(self): + """Gets the OAuth consumer. + + Returns: + object of type + """ + return self._consumer + + +class ClientLoginToken(atom.http_interface.GenericToken): + """Stores the Authorization header in auth_header and adds to requests. + + This token will add it's Authorization header to an HTTP request + as it is made. Ths token class is simple but + some Token classes must calculate portions of the Authorization header + based on the request being made, which is why the token is responsible + for making requests via an http_client parameter. + + Args: + auth_header: str The value for the Authorization header. + scopes: list of str or atom.url.Url specifying the beginnings of URLs + for which this token can be used. For example, if scopes contains + 'http://example.com/foo', then this token can be used for a request to + 'http://example.com/foo/bar' but it cannot be used for a request to + 'http://example.com/baz' + """ + def __init__(self, auth_header=None, scopes=None): + self.auth_header = auth_header + self.scopes = scopes or [] + + def __str__(self): + return self.auth_header + + def perform_request(self, http_client, operation, url, data=None, + headers=None): + """Sets the Authorization header and makes the HTTP request.""" + if headers is None: + headers = {'Authorization':self.auth_header} + else: + headers['Authorization'] = self.auth_header + return http_client.request(operation, url, data=data, headers=headers) + + def get_token_string(self): + """Removes PROGRAMMATIC_AUTH_LABEL to give just the token value.""" + return self.auth_header[len(PROGRAMMATIC_AUTH_LABEL):] + + def set_token_string(self, token_string): + self.auth_header = '%s%s' % (PROGRAMMATIC_AUTH_LABEL, token_string) + + def valid_for_scope(self, url): + """Tells the caller if the token authorizes access to the desired URL. + """ + if isinstance(url, (str, unicode)): + url = atom.url.parse_url(url) + for scope in self.scopes: + if scope == atom.token_store.SCOPE_ALL: + return True + if isinstance(scope, (str, unicode)): + scope = atom.url.parse_url(scope) + if scope == url: + return True + # Check the host and the path, but ignore the port and protocol. + elif scope.host == url.host and not scope.path: + return True + elif scope.host == url.host and scope.path and not url.path: + continue + elif scope.host == url.host and url.path.startswith(scope.path): + return True + return False + + +class AuthSubToken(ClientLoginToken): + def get_token_string(self): + """Removes AUTHSUB_AUTH_LABEL to give just the token value.""" + return self.auth_header[len(AUTHSUB_AUTH_LABEL):] + + def set_token_string(self, token_string): + self.auth_header = '%s%s' % (AUTHSUB_AUTH_LABEL, token_string) + + +class OAuthToken(atom.http_interface.GenericToken): + """Stores the token key, token secret and scopes for which token is valid. + + This token adds the authorization header to each request made. It + re-calculates authorization header for every request since the OAuth + signature to be added to the authorization header is dependent on the + request parameters. + + Attributes: + key: str The value for the OAuth token i.e. token key. + secret: str The value for the OAuth token secret. + scopes: list of str or atom.url.Url specifying the beginnings of URLs + for which this token can be used. For example, if scopes contains + 'http://example.com/foo', then this token can be used for a request to + 'http://example.com/foo/bar' but it cannot be used for a request to + 'http://example.com/baz' + oauth_input_params: OAuthInputParams OAuth input parameters. + """ + + def __init__(self, key=None, secret=None, scopes=None, + oauth_input_params=None): + self.key = key + self.secret = secret + self.scopes = scopes or [] + self.oauth_input_params = oauth_input_params + + def __str__(self): + return self.get_token_string() + + def get_token_string(self): + """Returns the token string. + + The token string returned is of format + oauth_token=[0]&oauth_token_secret=[1], where [0] and [1] are some strings. + + Returns: + A token string of format oauth_token=[0]&oauth_token_secret=[1], + where [0] and [1] are some strings. If self.secret is absent, it just + returns oauth_token=[0]. If self.key is absent, it just returns + oauth_token_secret=[1]. If both are absent, it returns None. + """ + if self.key and self.secret: + return urllib.urlencode({'oauth_token': self.key, + 'oauth_token_secret': self.secret}) + elif self.key: + return 'oauth_token=%s' % self.key + elif self.secret: + return 'oauth_token_secret=%s' % self.secret + else: + return None + + def set_token_string(self, token_string): + """Sets the token key and secret from the token string. + + Args: + token_string: str Token string of form + oauth_token=[0]&oauth_token_secret=[1]. If oauth_token is not present, + self.key will be None. If oauth_token_secret is not present, + self.secret will be None. + """ + token_params = cgi.parse_qs(token_string, keep_blank_values=False) + if 'oauth_token' in token_params: + self.key = token_params['oauth_token'][0] + if 'oauth_token_secret' in token_params: + self.secret = token_params['oauth_token_secret'][0] + + def GetAuthHeader(self, http_method, http_url, realm=''): + """Get the authentication header. + + Args: + http_method: string HTTP method i.e. operation e.g. GET, POST, PUT, etc. + http_url: string or atom.url.Url HTTP URL to which request is made. + realm: string (default='') realm parameter to be included in the + authorization header. + + Returns: + dict Header to be sent with every subsequent request after + authentication. + """ + if isinstance(http_url, types.StringTypes): + http_url = atom.url.parse_url(http_url) + header = None + token = None + if self.key or self.secret: + token = oauth.OAuthToken(self.key, self.secret) + oauth_request = oauth.OAuthRequest.from_consumer_and_token( + self.oauth_input_params.GetConsumer(), token=token, + http_url=str(http_url), http_method=http_method, + parameters=http_url.params) + oauth_request.sign_request(self.oauth_input_params.GetSignatureMethod(), + self.oauth_input_params.GetConsumer(), token) + header = oauth_request.to_header(realm=realm) + header['Authorization'] = header['Authorization'].replace('+', '%2B') + return header + + def perform_request(self, http_client, operation, url, data=None, + headers=None): + """Sets the Authorization header and makes the HTTP request.""" + if not headers: + headers = {} + if self.oauth_input_params.requestor_id: + url.params['xoauth_requestor_id'] = self.oauth_input_params.requestor_id + headers.update(self.GetAuthHeader(operation, url)) + return http_client.request(operation, url, data=data, headers=headers) + + def valid_for_scope(self, url): + if isinstance(url, (str, unicode)): + url = atom.url.parse_url(url) + for scope in self.scopes: + if scope == atom.token_store.SCOPE_ALL: + return True + if isinstance(scope, (str, unicode)): + scope = atom.url.parse_url(scope) + if scope == url: + return True + # Check the host and the path, but ignore the port and protocol. + elif scope.host == url.host and not scope.path: + return True + elif scope.host == url.host and scope.path and not url.path: + continue + elif scope.host == url.host and url.path.startswith(scope.path): + return True + return False + + +class SecureAuthSubToken(AuthSubToken): + """Stores the rsa private key, token, and scopes for the secure AuthSub token. + + This token adds the authorization header to each request made. It + re-calculates authorization header for every request since the secure AuthSub + signature to be added to the authorization header is dependent on the + request parameters. + + Attributes: + rsa_key: string The RSA private key in PEM format that the token will + use to sign requests + token_string: string (optional) The value for the AuthSub token. + scopes: list of str or atom.url.Url specifying the beginnings of URLs + for which this token can be used. For example, if scopes contains + 'http://example.com/foo', then this token can be used for a request to + 'http://example.com/foo/bar' but it cannot be used for a request to + 'http://example.com/baz' + """ + + def __init__(self, rsa_key, token_string=None, scopes=None): + self.rsa_key = keyfactory.parsePEMKey(rsa_key) + self.token_string = token_string or '' + self.scopes = scopes or [] + + def __str__(self): + return self.get_token_string() + + def get_token_string(self): + return str(self.token_string) + + def set_token_string(self, token_string): + self.token_string = token_string + + def GetAuthHeader(self, http_method, http_url): + """Generates the Authorization header. + + The form of the secure AuthSub Authorization header is + Authorization: AuthSub token="token" sigalg="sigalg" data="data" sig="sig" + and data represents a string in the form + data = http_method http_url timestamp nonce + + Args: + http_method: string HTTP method i.e. operation e.g. GET, POST, PUT, etc. + http_url: string or atom.url.Url HTTP URL to which request is made. + + Returns: + dict Header to be sent with every subsequent request after authentication. + """ + timestamp = int(math.floor(time.time())) + nonce = '%lu' % random.randrange(1, 2**64) + data = '%s %s %d %s' % (http_method, str(http_url), timestamp, nonce) + sig = cryptomath.bytesToBase64(self.rsa_key.hashAndSign(data)) + header = {'Authorization': '%s"%s" data="%s" sig="%s" sigalg="rsa-sha1"' % + (AUTHSUB_AUTH_LABEL, self.token_string, data, sig)} + return header + + def perform_request(self, http_client, operation, url, data=None, + headers=None): + """Sets the Authorization header and makes the HTTP request.""" + if not headers: + headers = {} + headers.update(self.GetAuthHeader(operation, url)) + return http_client.request(operation, url, data=data, headers=headers) diff --git a/gdata/auth.pyc b/gdata/auth.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c2fa063bdb104aedaec36039c564b2df6529cb8 GIT binary patch literal 38540 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 l