Skip to content
Browse files

adding atom and gdata directories from http://code.google.com/p/gdata…

  • Loading branch information...
1 parent 9a2515b commit 7ec3665c10166e62512dbda660896ec47417ecd9 @bradfitz committed
Sorry, we could not display the entire diff because it was too big.
View
1,395 atom/__init__.py
@@ -0,0 +1,1395 @@
+#!/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
+
+
+# XML namespaces which are often used in Atom entities.
+ATOM_NAMESPACE = 'http://www.w3.org/2005/Atom'
+ELEMENT_TEMPLATE = '{http://www.w3.org/2005/Atom}%s'
+APP_NAMESPACE = 'http://purl.org/atom/app#'
+APP_TEMPLATE = '{http://purl.org/atom/app#}%s'
+
+# This encoding is used for converting strings before translating the XML
+# into an object.
+XML_STRING_ENCODING = 'utf-8'
+# The desired string encoding for object members.
+MEMBER_STRING_ENCODING = 'utf-8'
+
+
+def CreateClassFromXMLString(target_class, xml_string, string_encoding=None):
+ """Creates an instance of the target class from the string contents.
+
+ Args:
+ target_class: class The class which will be instantiated and populated
+ with the contents of the XML. This class must have a _tag and a
+ _namespace class variable.
+ xml_string: str A string which contains valid XML. The root element
+ of the XML string should match the tag and namespace of the desired
+ class.
+ string_encoding: str The character encoding which the xml_string should
+ be converted to before it is interpreted and translated into
+ objects. The default is None in which case the string encoding
+ is not changed.
+
+ Returns:
+ An instance of the target class with members assigned according to the
+ contents of the XML - or None if the root XML tag and namespace did not
+ match those of the target class.
+ """
+ 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)
+
+
+def _CreateClassFromElementTree(target_class, tree, namespace=None, tag=None):
+ """Instantiates the class and populates members according to the tree.
+
+ Note: Only use this function with classes that have _namespace and _tag
+ class members.
+
+ Args:
+ target_class: class The class which will be instantiated and populated
+ with the contents of the XML.
+ tree: ElementTree An element tree whose contents will be converted into
+ members of the new target_class instance.
+ namespace: str (optional) The namespace which the XML tree's root node must
+ match. If omitted, the namespace defaults to the _namespace of the
+ target class.
+ tag: str (optional) The tag which the XML tree's root node must match. If
+ omitted, the tag defaults to the _tag class member of the target
+ class.
+
+ Returns:
+ An instance of the target class - or None if the tag and namespace of
+ the XML tree's root node did not match the desired namespace and tag.
+ """
+ if namespace is None:
+ namespace = target_class._namespace
+ if tag is None:
+ tag = target_class._tag
+ if tree.tag == '{%s}%s' % (namespace, tag):
+ target = target_class()
+ target._HarvestElementTree(tree)
+ return target
+ else:
+ return None
+
+
+class ExtensionContainer(object):
+
+ def __init__(self, extension_elements=None, extension_attributes=None,
+ text=None):
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+ self.text = text
+
+ # Three methods to create an object from an ElementTree
+ def _HarvestElementTree(self, tree):
+ # Fill in the instance members from the contents of the XML tree.
+ for child in tree:
+ self._ConvertElementTreeToMember(child)
+ for attribute, value in tree.attrib.iteritems():
+ self._ConvertElementAttributeToMember(attribute, value)
+ # Encode the text string according to the desired encoding type. (UTF-8)
+ if tree.text:
+ self.text = tree.text.encode(MEMBER_STRING_ENCODING)
+
+ def _ConvertElementTreeToMember(self, child_tree, current_class=None):
+ self.extension_elements.append(_ExtensionElementFromElementTree(
+ child_tree))
+
+ def _ConvertElementAttributeToMember(self, attribute, value):
+ # Encode the attribute value's string with the desired type Default UTF-8
+ if value:
+ self.extension_attributes[attribute] = value.encode(
+ MEMBER_STRING_ENCODING)
+
+ # One method to create an ElementTree from an object
+ def _AddMembersToElementTree(self, tree):
+ for child in self.extension_elements:
+ child._BecomeChildElement(tree)
+ for attribute, value in self.extension_attributes.iteritems():
+ if value:
+ # Decode the value from the desired encoding (default UTF-8).
+ if not isinstance(value, unicode):
+ tree.attrib[attribute] = value.decode(MEMBER_STRING_ENCODING)
+ else:
+ tree.attrib[attribute] = value
+ if self.text and not isinstance(self.text, unicode):
+ tree.text = self.text.decode(MEMBER_STRING_ENCODING)
+ else:
+ tree.text = self.text
+
+ def FindExtensions(self, tag=None, namespace=None):
+ """Searches extension elements for child nodes with the desired name.
+
+ Returns a list of extension elements within this object whose tag
+ and/or namespace match those passed in. To find all extensions in
+ a particular namespace, specify the namespace but not the tag name.
+ If you specify only the tag, the result list may contain extension
+ elements in multiple namespaces.
+
+ Args:
+ tag: str (optional) The desired tag
+ namespace: str (optional) The desired namespace
+
+ Returns:
+ A list of elements whose tag and/or namespace match the parameters
+ values
+ """
+
+ results = []
+
+ if tag and namespace:
+ for element in self.extension_elements:
+ if element.tag == tag and element.namespace == namespace:
+ results.append(element)
+ elif tag and not namespace:
+ for element in self.extension_elements:
+ if element.tag == tag:
+ results.append(element)
+ elif namespace and not tag:
+ for element in self.extension_elements:
+ if element.namespace == namespace:
+ results.append(element)
+ else:
+ for element in self.extension_elements:
+ results.append(element)
+
+ return results
+
+
+class AtomBase(ExtensionContainer):
+
+ _children = {}
+ _attributes = {}
+
+ def __init__(self, extension_elements=None, extension_attributes=None,
+ text=None):
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+ self.text = text
+
+ def _ConvertElementTreeToMember(self, child_tree):
+ # Find the element's tag in this class's list of child members
+ if self.__class__._children.has_key(child_tree.tag):
+ member_name = self.__class__._children[child_tree.tag][0]
+ member_class = self.__class__._children[child_tree.tag][1]
+ # If the class member is supposed to contain a list, make sure the
+ # matching member is set to a list, then append the new member
+ # instance to the list.
+ if isinstance(member_class, list):
+ if getattr(self, member_name) is None:
+ setattr(self, member_name, [])
+ getattr(self, member_name).append(_CreateClassFromElementTree(
+ member_class[0], child_tree))
+ else:
+ setattr(self, member_name,
+ _CreateClassFromElementTree(member_class, child_tree))
+ else:
+ ExtensionContainer._ConvertElementTreeToMember(self, child_tree)
+
+ def _ConvertElementAttributeToMember(self, attribute, value):
+ # Find the attribute in this class's list of attributes.
+ if self.__class__._attributes.has_key(attribute):
+ # Find the member of this class which corresponds to the XML attribute
+ # (lookup in current_class._attributes) and set this member to the
+ # desired value (using self.__dict__).
+ if value:
+ # Encode the string to capture non-ascii characters (default UTF-8)
+ setattr(self, self.__class__._attributes[attribute],
+ value.encode(MEMBER_STRING_ENCODING))
+ else:
+ ExtensionContainer._ConvertElementAttributeToMember(self, attribute,
+ value)
+
+ # Three methods to create an ElementTree from an object
+ def _AddMembersToElementTree(self, tree):
+ # Convert the members of this class which are XML child nodes.
+ # This uses the class's _children dictionary to find the members which
+ # should become XML child nodes.
+ member_node_names = [values[0] for tag, values in
+ self.__class__._children.iteritems()]
+ for member_name in member_node_names:
+ member = getattr(self, member_name)
+ if member is None:
+ pass
+ elif isinstance(member, list):
+ for instance in member:
+ instance._BecomeChildElement(tree)
+ else:
+ member._BecomeChildElement(tree)
+ # Convert the members of this class which are XML attributes.
+ for xml_attribute, member_name in self.__class__._attributes.iteritems():
+ member = getattr(self, member_name)
+ if member is not None:
+ if not isinstance(member, unicode):
+ tree.attrib[xml_attribute] = member.decode(MEMBER_STRING_ENCODING)
+ else:
+ tree.attrib[xml_attribute] = member
+ # Lastly, call the ExtensionContainers's _AddMembersToElementTree to
+ # convert any extension attributes.
+ ExtensionContainer._AddMembersToElementTree(self, tree)
+
+
+ def _BecomeChildElement(self, tree):
+ """
+
+ Note: Only for use with classes that have a _tag and _namespace class
+ member. It is in AtomBase so that it can be inherited but it should
+ not be called on instances of AtomBase.
+
+ """
+ new_child = ElementTree.Element('')
+ tree.append(new_child)
+ new_child.tag = '{%s}%s' % (self.__class__._namespace,
+ self.__class__._tag)
+ self._AddMembersToElementTree(new_child)
+
+ def _ToElementTree(self):
+ """
+
+ Note, this method is designed to be used only with classes that have a
+ _tag and _namespace. It is placed in AtomBase for inheritance but should
+ not be called on this class.
+
+ """
+ new_tree = ElementTree.Element('{%s}%s' % (self.__class__._namespace,
+ self.__class__._tag))
+ self._AddMembersToElementTree(new_tree)
+ return new_tree
+
+ def ToString(self, string_encoding='UTF-8'):
+ """Converts the Atom object to a string containing XML."""
+ return ElementTree.tostring(self._ToElementTree(), encoding=string_encoding)
+
+ def __str__(self):
+ return self.ToString()
+
+
+class Name(AtomBase):
+ """The atom:name element"""
+
+ _tag = 'name'
+ _namespace = ATOM_NAMESPACE
+ _children = AtomBase._children.copy()
+ _attributes = AtomBase._attributes.copy()
+
+ def __init__(self, text=None, extension_elements=None,
+ extension_attributes=None):
+ """Constructor for Name
+
+ Args:
+ text: str The text data in the this element
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ """
+
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+def NameFromString(xml_string):
+ return CreateClassFromXMLString(Name, xml_string)
+
+
+class Email(AtomBase):
+ """The atom:email element"""
+
+ _tag = 'email'
+ _namespace = ATOM_NAMESPACE
+ _children = AtomBase._children.copy()
+ _attributes = AtomBase._attributes.copy()
+
+ def __init__(self, text=None, extension_elements=None,
+ extension_attributes=None):
+ """Constructor for Email
+
+ Args:
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ text: str The text data in the this element
+ """
+
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+def EmailFromString(xml_string):
+ return CreateClassFromXMLString(Email, xml_string)
+
+
+class Uri(AtomBase):
+ """The atom:uri element"""
+
+ _tag = 'uri'
+ _namespace = ATOM_NAMESPACE
+ _children = AtomBase._children.copy()
+ _attributes = AtomBase._attributes.copy()
+
+ def __init__(self, text=None, extension_elements=None,
+ extension_attributes=None):
+ """Constructor for Uri
+
+ Args:
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ text: str The text data in the this element
+ """
+
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+def UriFromString(xml_string):
+ return CreateClassFromXMLString(Uri, xml_string)
+
+
+class Person(AtomBase):
+ """A foundation class from which atom:author and atom:contributor extend.
+
+ A person contains information like name, email address, and web page URI for
+ an author or contributor to an Atom feed.
+ """
+
+ _children = AtomBase._children.copy()
+ _attributes = AtomBase._attributes.copy()
+ _children['{%s}name' % (ATOM_NAMESPACE)] = ('name', Name)
+ _children['{%s}email' % (ATOM_NAMESPACE)] = ('email', Email)
+ _children['{%s}uri' % (ATOM_NAMESPACE)] = ('uri', Uri)
+
+ def __init__(self, name=None, email=None, uri=None,
+ extension_elements=None, extension_attributes=None, text=None):
+ """Foundation from which author and contributor are derived.
+
+ The constructor is provided for illustrative purposes, you should not
+ need to instantiate a Person.
+
+ Args:
+ name: Name The person's name
+ email: Email The person's email address
+ uri: Uri The URI of the person's webpage
+ extension_elements: list A list of ExtensionElement instances which are
+ children of this element.
+ extension_attributes: dict A dictionary of strings which are the values
+ for additional XML attributes of this element.
+ text: String The text contents of the element. This is the contents
+ of the Entry's XML text node. (Example: <foo>This is the text</foo>)
+ """
+
+ self.name = name
+ self.email = email
+ self.uri = uri
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+ self.text = text
+
+
+class Author(Person):
+ """The atom:author element
+
+ An author is a required element in Feed.
+ """
+
+ _tag = 'author'
+ _namespace = ATOM_NAMESPACE
+ _children = Person._children.copy()
+ _attributes = Person._attributes.copy()
+ #_children = {}
+ #_attributes = {}
+
+ def __init__(self, name=None, email=None, uri=None,
+ extension_elements=None, extension_attributes=None, text=None):
+ """Constructor for Author
+
+ Args:
+ name: Name
+ email: Email
+ uri: Uri
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ text: str The text data in the this element
+ """
+
+ self.name = name
+ self.email = email
+ self.uri = uri
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+ self.text = text
+
+
+def AuthorFromString(xml_string):
+ return CreateClassFromXMLString(Author, xml_string)
+
+
+class Contributor(Person):
+ """The atom:contributor element"""
+
+ _tag = 'contributor'
+ _namespace = ATOM_NAMESPACE
+ _children = Person._children.copy()
+ _attributes = Person._attributes.copy()
+
+ def __init__(self, name=None, email=None, uri=None,
+ extension_elements=None, extension_attributes=None, text=None):
+ """Constructor for Contributor
+
+ Args:
+ name: Name
+ email: Email
+ uri: Uri
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ text: str The text data in the this element
+ """
+
+ self.name = name
+ self.email = email
+ self.uri = uri
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+ self.text = text
+
+
+def ContributorFromString(xml_string):
+ return CreateClassFromXMLString(Contributor, xml_string)
+
+
+class Link(AtomBase):
+ """The atom:link element"""
+
+ _tag = 'link'
+ _namespace = ATOM_NAMESPACE
+ _children = AtomBase._children.copy()
+ _attributes = AtomBase._attributes.copy()
+ _attributes['rel'] = 'rel'
+ _attributes['href'] = 'href'
+ _attributes['type'] = 'type'
+ _attributes['title'] = 'title'
+ _attributes['length'] = 'length'
+ _attributes['hreflang'] = 'hreflang'
+
+ def __init__(self, href=None, rel=None, link_type=None, hreflang=None,
+ title=None, length=None, text=None, extension_elements=None,
+ extension_attributes=None):
+ """Constructor for Link
+
+ Args:
+ href: string The href attribute of the link
+ rel: string
+ type: string
+ hreflang: string The language for the href
+ title: string
+ length: string The length of the href's destination
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ text: str The text data in the this element
+ """
+
+ self.href = href
+ self.rel = rel
+ self.type = link_type
+ self.hreflang = hreflang
+ self.title = title
+ self.length = length
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+def LinkFromString(xml_string):
+ return CreateClassFromXMLString(Link, xml_string)
+
+
+class Generator(AtomBase):
+ """The atom:generator element"""
+
+ _tag = 'generator'
+ _namespace = ATOM_NAMESPACE
+ _children = AtomBase._children.copy()
+ _attributes = AtomBase._attributes.copy()
+ _attributes['uri'] = 'uri'
+ _attributes['version'] = 'version'
+
+ def __init__(self, uri=None, version=None, text=None,
+ extension_elements=None, extension_attributes=None):
+ """Constructor for Generator
+
+ Args:
+ uri: string
+ version: string
+ text: str The text data in the this element
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ """
+
+ self.uri = uri
+ self.version = version
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+def GeneratorFromString(xml_string):
+ return CreateClassFromXMLString(Generator, xml_string)
+
+
+class Text(AtomBase):
+ """A foundation class from which atom:title, summary, etc. extend.
+
+ This class should never be instantiated.
+ """
+
+ _children = AtomBase._children.copy()
+ _attributes = AtomBase._attributes.copy()
+ _attributes['type'] = 'type'
+
+ def __init__(self, text_type=None, text=None, extension_elements=None,
+ extension_attributes=None):
+ """Constructor for Text
+
+ Args:
+ text_type: string
+ text: str The text data in the this element
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ """
+
+ self.type = text_type
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+class Title(Text):
+ """The atom:title element"""
+
+ _tag = 'title'
+ _namespace = ATOM_NAMESPACE
+ _children = Text._children.copy()
+ _attributes = Text._attributes.copy()
+
+ def __init__(self, title_type=None, text=None, extension_elements=None,
+ extension_attributes=None):
+ """Constructor for Title
+
+ Args:
+ title_type: string
+ text: str The text data in the this element
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ """
+
+ self.type = title_type
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+def TitleFromString(xml_string):
+ return CreateClassFromXMLString(Title, xml_string)
+
+
+class Subtitle(Text):
+ """The atom:subtitle element"""
+
+ _tag = 'subtitle'
+ _namespace = ATOM_NAMESPACE
+ _children = Text._children.copy()
+ _attributes = Text._attributes.copy()
+
+ def __init__(self, subtitle_type=None, text=None, extension_elements=None,
+ extension_attributes=None):
+ """Constructor for Subtitle
+
+ Args:
+ subtitle_type: string
+ text: str The text data in the this element
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ """
+
+ self.type = subtitle_type
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+def SubtitleFromString(xml_string):
+ return CreateClassFromXMLString(Subtitle, xml_string)
+
+
+class Rights(Text):
+ """The atom:rights element"""
+
+ _tag = 'rights'
+ _namespace = ATOM_NAMESPACE
+ _children = Text._children.copy()
+ _attributes = Text._attributes.copy()
+
+ def __init__(self, rights_type=None, text=None, extension_elements=None,
+ extension_attributes=None):
+ """Constructor for Rights
+
+ Args:
+ rights_type: string
+ text: str The text data in the this element
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ """
+
+ self.type = rights_type
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+def RightsFromString(xml_string):
+ return CreateClassFromXMLString(Rights, xml_string)
+
+
+class Summary(Text):
+ """The atom:summary element"""
+
+ _tag = 'summary'
+ _namespace = ATOM_NAMESPACE
+ _children = Text._children.copy()
+ _attributes = Text._attributes.copy()
+
+ def __init__(self, summary_type=None, text=None, extension_elements=None,
+ extension_attributes=None):
+ """Constructor for Summary
+
+ Args:
+ summary_type: string
+ text: str The text data in the this element
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ """
+
+ self.type = summary_type
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+def SummaryFromString(xml_string):
+ return CreateClassFromXMLString(Summary, xml_string)
+
+
+class Content(Text):
+ """The atom:content element"""
+
+ _tag = 'content'
+ _namespace = ATOM_NAMESPACE
+ _children = Text._children.copy()
+ _attributes = Text._attributes.copy()
+ _attributes['src'] = 'src'
+
+ def __init__(self, content_type=None, src=None, text=None,
+ extension_elements=None, extension_attributes=None):
+ """Constructor for Content
+
+ Args:
+ content_type: string
+ src: string
+ text: str The text data in the this element
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ """
+
+ self.type = content_type
+ self.src = src
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+def ContentFromString(xml_string):
+ return CreateClassFromXMLString(Content, xml_string)
+
+
+class Category(AtomBase):
+ """The atom:category element"""
+
+ _tag = 'category'
+ _namespace = ATOM_NAMESPACE
+ _children = AtomBase._children.copy()
+ _attributes = AtomBase._attributes.copy()
+ _attributes['term'] = 'term'
+ _attributes['scheme'] = 'scheme'
+ _attributes['label'] = 'label'
+
+ def __init__(self, term=None, scheme=None, label=None, text=None,
+ extension_elements=None, extension_attributes=None):
+ """Constructor for Category
+
+ Args:
+ term: str
+ scheme: str
+ label: str
+ text: str The text data in the this element
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ """
+
+ self.term = term
+ self.scheme = scheme
+ self.label = label
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+def CategoryFromString(xml_string):
+ return CreateClassFromXMLString(Category, xml_string)
+
+
+class Id(AtomBase):
+ """The atom:id element."""
+
+ _tag = 'id'
+ _namespace = ATOM_NAMESPACE
+ _children = AtomBase._children.copy()
+ _attributes = AtomBase._attributes.copy()
+
+ def __init__(self, text=None, extension_elements=None,
+ extension_attributes=None):
+ """Constructor for Id
+
+ Args:
+ text: str The text data in the this element
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ """
+
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+def IdFromString(xml_string):
+ return CreateClassFromXMLString(Id, xml_string)
+
+
+class Icon(AtomBase):
+ """The atom:icon element."""
+
+ _tag = 'icon'
+ _namespace = ATOM_NAMESPACE
+ _children = AtomBase._children.copy()
+ _attributes = AtomBase._attributes.copy()
+
+ def __init__(self, text=None, extension_elements=None,
+ extension_attributes=None):
+ """Constructor for Icon
+
+ Args:
+ text: str The text data in the this element
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ """
+
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+def IconFromString(xml_string):
+ return CreateClassFromXMLString(Icon, xml_string)
+
+
+class Logo(AtomBase):
+ """The atom:logo element."""
+
+ _tag = 'logo'
+ _namespace = ATOM_NAMESPACE
+ _children = AtomBase._children.copy()
+ _attributes = AtomBase._attributes.copy()
+
+ def __init__(self, text=None, extension_elements=None,
+ extension_attributes=None):
+ """Constructor for Logo
+
+ Args:
+ text: str The text data in the this element
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ """
+
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+def LogoFromString(xml_string):
+ return CreateClassFromXMLString(Logo, xml_string)
+
+
+class Draft(AtomBase):
+ """The app:draft element which indicates if this entry should be public."""
+
+ _tag = 'draft'
+ _namespace = APP_NAMESPACE
+ _children = AtomBase._children.copy()
+ _attributes = AtomBase._attributes.copy()
+
+ def __init__(self, text=None, extension_elements=None,
+ extension_attributes=None):
+ """Constructor for app:draft
+
+ Args:
+ text: str The text data in the this element
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ """
+
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+def DraftFromString(xml_string):
+ return CreateClassFromXMLString(Draft, xml_string)
+
+
+class Control(AtomBase):
+ """The app:control element indicating restrictions on publication.
+
+ The APP control element may contain a draft element indicating whether or
+ not this entry should be publicly available.
+ """
+
+ _tag = 'control'
+ _namespace = APP_NAMESPACE
+ _children = AtomBase._children.copy()
+ _attributes = AtomBase._attributes.copy()
+ _children['{%s}draft' % APP_NAMESPACE] = ('draft', Draft)
+
+ def __init__(self, draft=None, text=None, extension_elements=None,
+ extension_attributes=None):
+ """Constructor for app:control"""
+
+ self.draft = draft
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+def ControlFromString(xml_string):
+ return CreateClassFromXMLString(Control, xml_string)
+
+
+class Date(AtomBase):
+ """A parent class for atom:updated, published, etc."""
+
+ #TODO Add text to and from time conversion methods to allow users to set
+ # the contents of a Date to a python DateTime object.
+
+ _children = AtomBase._children.copy()
+ _attributes = AtomBase._attributes.copy()
+
+ def __init__(self, text=None, extension_elements=None,
+ extension_attributes=None):
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+class Updated(Date):
+ """The atom:updated element."""
+
+ _tag = 'updated'
+ _namespace = ATOM_NAMESPACE
+ _children = Date._children.copy()
+ _attributes = Date._attributes.copy()
+
+ def __init__(self, text=None, extension_elements=None,
+ extension_attributes=None):
+ """Constructor for Updated
+
+ Args:
+ text: str The text data in the this element
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ """
+
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+def UpdatedFromString(xml_string):
+ return CreateClassFromXMLString(Updated, xml_string)
+
+
+class Published(Date):
+ """The atom:published element."""
+
+ _tag = 'published'
+ _namespace = ATOM_NAMESPACE
+ _children = Date._children.copy()
+ _attributes = Date._attributes.copy()
+
+ def __init__(self, text=None, extension_elements=None,
+ extension_attributes=None):
+ """Constructor for Published
+
+ Args:
+ text: str The text data in the this element
+ extension_elements: list A list of ExtensionElement instances
+ extension_attributes: dict A dictionary of attribute value string pairs
+ """
+
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+def PublishedFromString(xml_string):
+ return CreateClassFromXMLString(Published, xml_string)
+
+
+class LinkFinder(object):
+ """An "interface" providing methods to find link elements
+
+ Entry elements often contain multiple links which differ in the rel
+ attribute or content type. Often, developers are interested in a specific
+ type of link so this class provides methods to find specific classes of
+ links.
+
+ This class is used as a mixin in Atom entries and feeds.
+ """
+
+ def GetSelfLink(self):
+ """Find the first link with rel set to 'self'
+
+ Returns:
+ An atom.Link or none if none of the links had rel equal to 'self'
+ """
+
+ for a_link in self.link:
+ if a_link.rel == 'self':
+ return a_link
+ return None
+
+ def GetEditLink(self):
+ for a_link in self.link:
+ if a_link.rel == 'edit':
+ return a_link
+ return None
+
+ def GetNextLink(self):
+ for a_link in self.link:
+ if a_link.rel == 'next':
+ return a_link
+ return None
+
+ def GetLicenseLink(self):
+ for a_link in self.link:
+ if a_link.rel == 'license':
+ return a_link
+ return None
+
+ def GetAlternateLink(self):
+ for a_link in self.link:
+ if a_link.rel == 'alternate':
+ return a_link
+ return None
+
+
+class FeedEntryParent(AtomBase, LinkFinder):
+ """A super class for atom:feed and entry, contains shared attributes"""
+
+ _children = AtomBase._children.copy()
+ _attributes = AtomBase._attributes.copy()
+ _children['{%s}author' % ATOM_NAMESPACE] = ('author', [Author])
+ _children['{%s}category' % ATOM_NAMESPACE] = ('category', [Category])
+ _children['{%s}contributor' % ATOM_NAMESPACE] = ('contributor', [Contributor])
+ _children['{%s}id' % ATOM_NAMESPACE] = ('id', Id)
+ _children['{%s}link' % ATOM_NAMESPACE] = ('link', [Link])
+ _children['{%s}rights' % ATOM_NAMESPACE] = ('rights', Rights)
+ _children['{%s}title' % ATOM_NAMESPACE] = ('title', Title)
+ _children['{%s}updated' % ATOM_NAMESPACE] = ('updated', Updated)
+
+ def __init__(self, author=None, category=None, contributor=None,
+ atom_id=None, link=None, rights=None, title=None, updated=None,
+ text=None, extension_elements=None, extension_attributes=None):
+ self.author = author or []
+ self.category = category or []
+ self.contributor = contributor or []
+ self.id = atom_id
+ self.link = link or []
+ self.rights = rights
+ self.title = title
+ self.updated = updated
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+class Source(FeedEntryParent):
+ """The atom:source element"""
+
+ _tag = 'source'
+ _namespace = ATOM_NAMESPACE
+ _children = FeedEntryParent._children.copy()
+ _attributes = FeedEntryParent._attributes.copy()
+ _children['{%s}generator' % ATOM_NAMESPACE] = ('generator', Generator)
+ _children['{%s}icon' % ATOM_NAMESPACE] = ('icon', Icon)
+ _children['{%s}logo' % ATOM_NAMESPACE] = ('logo', Logo)
+ _children['{%s}subtitle' % ATOM_NAMESPACE] = ('subtitle', Subtitle)
+
+ def __init__(self, author=None, category=None, contributor=None,
+ generator=None, icon=None, atom_id=None, link=None, logo=None,
+ rights=None, subtitle=None, title=None, updated=None, text=None,
+ extension_elements=None, extension_attributes=None):
+ """Constructor for Source
+
+ Args:
+ author: list (optional) A list of Author instances which belong to this
+ class.
+ category: list (optional) A list of Category instances
+ contributor: list (optional) A list on Contributor instances
+ generator: Generator (optional)
+ icon: Icon (optional)
+ id: Id (optional) The entry's Id element
+ link: list (optional) A list of Link instances
+ logo: Logo (optional)
+ rights: Rights (optional) The entry's Rights element
+ subtitle: Subtitle (optional) The entry's subtitle element
+ title: Title (optional) the entry's title element
+ updated: Updated (optional) the entry's updated element
+ text: String (optional) The text contents of the element. This is the
+ contents of the Entry's XML text node.
+ (Example: <foo>This is the text</foo>)
+ extension_elements: list (optional) A list of ExtensionElement instances
+ which are children of this element.
+ extension_attributes: dict (optional) A dictionary of strings which are
+ the values for additional XML attributes of this element.
+ """
+
+ self.author = author or []
+ self.category = category or []
+ self.contributor = contributor or []
+ self.generator = generator
+ self.icon = icon
+ self.id = atom_id
+ self.link = link or []
+ self.logo = logo
+ self.rights = rights
+ self.subtitle = subtitle
+ self.title = title
+ self.updated = updated
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+def SourceFromString(xml_string):
+ return CreateClassFromXMLString(Source, xml_string)
+
+
+class Entry(FeedEntryParent):
+ """The atom:entry element"""
+
+ _tag = 'entry'
+ _namespace = ATOM_NAMESPACE
+ _children = FeedEntryParent._children.copy()
+ _attributes = FeedEntryParent._attributes.copy()
+ _children['{%s}content' % ATOM_NAMESPACE] = ('content', Content)
+ _children['{%s}published' % ATOM_NAMESPACE] = ('published', Published)
+ _children['{%s}source' % ATOM_NAMESPACE] = ('source', Source)
+ _children['{%s}summary' % ATOM_NAMESPACE] = ('summary', Summary)
+ _children['{%s}control' % APP_NAMESPACE] = ('control', Control)
+
+ def __init__(self, author=None, category=None, content=None,
+ contributor=None, atom_id=None, link=None, published=None, rights=None,
+ source=None, summary=None, control=None, title=None, updated=None,
+ extension_elements=None, extension_attributes=None, text=None):
+ """Constructor for atom:entry
+
+ Args:
+ author: list A list of Author instances which belong to this class.
+ category: list A list of Category instances
+ content: Content The entry's Content
+ contributor: list A list on Contributor instances
+ id: Id The entry's Id element
+ link: list A list of Link instances
+ published: Published The entry's Published element
+ rights: Rights The entry's Rights element
+ source: Source the entry's source element
+ summary: Summary the entry's summary element
+ title: Title the entry's title element
+ updated: Updated the entry's updated element
+ control: The entry's app:control element which can be used to mark an
+ entry as a draft which should not be publicly viewable.
+ text: String The text contents of the element. This is the contents
+ of the Entry's XML text node. (Example: <foo>This is the text</foo>)
+ extension_elements: list A list of ExtensionElement instances which are
+ children of this element.
+ extension_attributes: dict A dictionary of strings which are the values
+ for additional XML attributes of this element.
+ """
+
+ self.author = author or []
+ self.category = category or []
+ self.content = content
+ self.contributor = contributor or []
+ self.id = atom_id
+ self.link = link or []
+ self.published = published
+ self.rights = rights
+ self.source = source
+ self.summary = summary
+ self.title = title
+ self.updated = updated
+ self.control = control
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+def EntryFromString(xml_string):
+ return CreateClassFromXMLString(Entry, xml_string)
+
+
+class Feed(Source):
+ """The atom:feed element"""
+
+ _tag = 'feed'
+ _namespace = ATOM_NAMESPACE
+ _children = Source._children.copy()
+ _attributes = Source._attributes.copy()
+ _children['{%s}entry' % ATOM_NAMESPACE] = ('entry', [Entry])
+
+ def __init__(self, author=None, category=None, contributor=None,
+ generator=None, icon=None, atom_id=None, link=None, logo=None,
+ rights=None, subtitle=None, title=None, updated=None, entry=None,
+ text=None, extension_elements=None, extension_attributes=None):
+ """Constructor for Source
+
+ Args:
+ author: list (optional) A list of Author instances which belong to this
+ class.
+ category: list (optional) A list of Category instances
+ contributor: list (optional) A list on Contributor instances
+ generator: Generator (optional)
+ icon: Icon (optional)
+ id: Id (optional) The entry's Id element
+ link: list (optional) A list of Link instances
+ logo: Logo (optional)
+ rights: Rights (optional) The entry's Rights element
+ subtitle: Subtitle (optional) The entry's subtitle element
+ title: Title (optional) the entry's title element
+ updated: Updated (optional) the entry's updated element
+ entry: list (optional) A list of the Entry instances contained in the
+ feed.
+ text: String (optional) The text contents of the element. This is the
+ contents of the Entry's XML text node.
+ (Example: <foo>This is the text</foo>)
+ extension_elements: list (optional) A list of ExtensionElement instances
+ which are children of this element.
+ extension_attributes: dict (optional) A dictionary of strings which are
+ the values for additional XML attributes of this element.
+ """
+
+ self.author = author or []
+ self.category = category or []
+ self.contributor = contributor or []
+ self.generator = generator
+ self.icon = icon
+ self.id = atom_id
+ self.link = link or []
+ self.logo = logo
+ self.rights = rights
+ self.subtitle = subtitle
+ self.title = title
+ self.updated = updated
+ self.entry = entry or []
+ self.text = text
+ self.extension_elements = extension_elements or []
+ self.extension_attributes = extension_attributes or {}
+
+
+def FeedFromString(xml_string):
+ return CreateClassFromXMLString(Feed, xml_string)
+
+
+class ExtensionElement(object):
+ """Represents extra XML elements contained in Atom classes."""
+
+ def __init__(self, tag, namespace=None, attributes=None,
+ children=None, text=None):
+ """Constructor for EtensionElement
+
+ Args:
+ namespace: string (optional) The XML namespace for this element.
+ tag: string (optional) The tag (without the namespace qualifier) for
+ this element. To reconstruct the full qualified name of the element,
+ combine this tag with the namespace.
+ attributes: dict (optinal) The attribute value string pairs for the XML
+ attributes of this element.
+ children: list (optional) A list of ExtensionElements which represent
+ the XML child nodes of this element.
+ """
+
+ self.namespace = namespace
+ self.tag = tag
+ self.attributes = attributes or {}
+ self.children = children or []
+ self.text = text
+
+ def ToString(self):
+ element_tree = self._TransferToElementTree(ElementTree.Element(''))
+ return ElementTree.tostring(element_tree, encoding="UTF-8")
+
+ def _TransferToElementTree(self, element_tree):
+ if self.tag is None:
+ return None
+
+ if self.namespace is not None:
+ element_tree.tag = '{%s}%s' % (self.namespace, self.tag)
+ else:
+ element_tree.tag = self.tag
+
+ for key, value in self.attributes.iteritems():
+ element_tree.attrib[key] = value
+
+ for child in self.children:
+ child._BecomeChildElement(element_tree)
+
+ element_tree.text = self.text
+
+ return element_tree
+
+ def _BecomeChildElement(self, element_tree):
+ """Converts this object into an etree element and adds it as a child node.
+
+ Adds self to the ElementTree. This method is required to avoid verbose XML
+ which constantly redefines the namespace.
+
+ Args:
+ element_tree: ElementTree._Element The element to which this object's XML
+ will be added.
+ """
+ new_element = ElementTree.Element('')
+ element_tree.append(new_element)
+ self._TransferToElementTree(new_element)
+
+ def FindChildren(self, tag=None, namespace=None):
+ """Searches child nodes for objects with the desired tag/namespace.
+
+ Returns a list of extension elements within this object whose tag
+ and/or namespace match those passed in. To find all children in
+ a particular namespace, specify the namespace but not the tag name.
+ If you specify only the tag, the result list may contain extension
+ elements in multiple namespaces.
+
+ Args:
+ tag: str (optional) The desired tag
+ namespace: str (optional) The desired namespace
+
+ Returns:
+ A list of elements whose tag and/or namespace match the parameters
+ values
+ """
+
+ results = []
+
+ if tag and namespace:
+ for element in self.children:
+ if element.tag == tag and element.namespace == namespace:
+ results.append(element)
+ elif tag and not namespace:
+ for element in self.children:
+ if element.tag == tag:
+ results.append(element)
+ elif namespace and not tag:
+ for element in self.children:
+ if element.namespace == namespace:
+ results.append(element)
+ else:
+ for element in self.children:
+ results.append(element)
+
+ return results
+
+
+def ExtensionElementFromString(xml_string):
+ element_tree = ElementTree.fromstring(xml_string)
+ return _ExtensionElementFromElementTree(element_tree)
+
+
+def _ExtensionElementFromElementTree(element_tree):
+ element_tag = element_tree.tag
+ if '}' in element_tag:
+ namespace = element_tag[1:element_tag.index('}')]
+ tag = element_tag[element_tag.index('}')+1:]
+ else:
+ namespace = None
+ tag = element_tag
+ extension = ExtensionElement(namespace=namespace, tag=tag)
+ for key, value in element_tree.attrib.iteritems():
+ extension.attributes[key] = value
+ for child in element_tree:
+ extension.children.append(_ExtensionElementFromElementTree(child))
+ extension.text = element_tree.text
+ return extension
View
286 atom/http.py
@@ -0,0 +1,286 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""HttpClients in this module use httplib to make HTTP requests.
+
+This module make HTTP requests based on httplib, but there are environments
+in which an httplib based approach will not work (if running in Google App
+Engine for example). In those cases, higher level classes (like AtomService
+and GDataService) can swap out the HttpClient to transparently use a
+different mechanism for making HTTP requests.
+
+ HttpClient: Contains a request method which performs an HTTP call to the
+ server.
+
+ ProxiedHttpClient: Contains a request method which connects to a proxy using
+ settings stored in operating system environment variables then
+ performs an HTTP call to the endpoint server.
+"""
+
+
+__author__ = 'api.jscudder (Jeff Scudder)'
+
+
+import types
+import os
+import httplib
+import atom.url
+import atom.http_interface
+import socket
+import base64
+
+
+class ProxyError(atom.http_interface.Error):
+ pass
+
+
+DEFAULT_CONTENT_TYPE = 'application/atom+xml'
+
+
+class HttpClient(atom.http_interface.GenericHttpClient):
+ def __init__(self, headers=None):
+ self.debug = False
+ self.headers = headers or {}
+
+ def request(self, operation, url, data=None, headers=None):
+ """Performs an HTTP call to the server, supports GET, POST, PUT, and
+ DELETE.
+
+ Usage example, perform and HTTP GET on http://www.google.com/:
+ import atom.http
+ client = atom.http.HttpClient()
+ http_response = client.request('GET', 'http://www.google.com/')
+
+ Args:
+ operation: str The HTTP operation to be performed. This is usually one
+ of 'GET', 'POST', 'PUT', or 'DELETE'
+ data: filestream, list of parts, or other object which can be converted
+ to a string. Should be set to None when performing a GET or DELETE.
+ If data is a file-like object which can be read, this method will
+ read a chunk of 100K bytes at a time and send them.
+ If the data is a list of parts to be sent, each part will be
+ evaluated and sent.
+ url: The full URL to which the request should be sent. Can be a string
+ or atom.url.Url.
+ headers: dict of strings. HTTP headers which should be sent
+ in the request.
+ """
+ if not isinstance(url, atom.url.Url):
+ if isinstance(url, types.StringType):
+ url = atom.url.parse_url(url)
+ else:
+ raise atom.http_interface.UnparsableUrlObject('Unable to parse url '
+ 'parameter because it was not a string or atom.url.Url')
+
+ all_headers = self.headers.copy()
+ if headers:
+ all_headers.update(headers)
+
+ connection = self._prepare_connection(url, all_headers)
+
+ if self.debug:
+ connection.debuglevel = 1
+
+ connection.putrequest(operation, self._get_access_url(url),
+ skip_host=True)
+ connection.putheader('Host', url.host)
+
+ # Overcome a bug in Python 2.4 and 2.5
+ # httplib.HTTPConnection.putrequest adding
+ # HTTP request header 'Host: www.google.com:443' instead of
+ # 'Host: www.google.com', and thus resulting the error message
+ # 'Token invalid - AuthSub token has wrong scope' in the HTTP response.
+ if (url.protocol == 'https' and int(url.port or 443) == 443 and
+ hasattr(connection, '_buffer') and
+ isinstance(connection._buffer, list)):
+ header_line = 'Host: %s:443' % url.host
+ replacement_header_line = 'Host: %s' % url.host
+ try:
+ connection._buffer[connection._buffer.index(header_line)] = (
+ replacement_header_line)
+ except ValueError: # header_line missing from connection._buffer
+ pass
+
+ # If the list of headers does not include a Content-Length, attempt to
+ # calculate it based on the data object.
+ if data and 'Content-Length' not in all_headers:
+ if isinstance(data, types.StringType):
+ all_headers['Content-Length'] = len(data)
+ else:
+ raise atom.http_interface.ContentLengthRequired('Unable to calculate '
+ 'the length of the data parameter. Specify a value for '
+ 'Content-Length')
+
+ # Set the content type to the default value if none was set.
+ if 'Content-Type' not in all_headers:
+ all_headers['Content-Type'] = DEFAULT_CONTENT_TYPE
+
+ # Send the HTTP headers.
+ for header_name in all_headers:
+ connection.putheader(header_name, all_headers[header_name])
+ connection.endheaders()
+
+ # If there is data, send it in the request.
+ if data:
+ if isinstance(data, list):
+ for data_part in data:
+ _send_data_part(data_part, connection)
+ else:
+ _send_data_part(data, connection)
+
+ # Return the HTTP Response from the server.
+ return connection.getresponse()
+
+ def _prepare_connection(self, url, headers):
+ if not isinstance(url, atom.url.Url):
+ if isinstance(url, types.StringType):
+ url = atom.url.parse_url(url)
+ else:
+ raise atom.http_interface.UnparsableUrlObject('Unable to parse url '
+ 'parameter because it was not a string or atom.url.Url')
+ if url.protocol == 'https':
+ if not url.port:
+ return httplib.HTTPSConnection(url.host)
+ return httplib.HTTPSConnection(url.host, int(url.port))
+ else:
+ if not url.port:
+ return httplib.HTTPConnection(url.host)
+ return httplib.HTTPConnection(url.host, int(url.port))
+
+ def _get_access_url(self, url):
+ return url.to_string()
+
+
+class ProxiedHttpClient(HttpClient):
+ """Performs an HTTP request through a proxy.
+
+ The proxy settings are obtained from enviroment variables. The URL of the
+ proxy server is assumed to be stored in the environment variables
+ 'https_proxy' and 'http_proxy' respectively. If the proxy server requires
+ a Basic Auth authorization header, the username and password are expected to
+ be in the 'proxy-username' or 'proxy_username' variable and the
+ 'proxy-password' or 'proxy_password' variable.
+
+ After connecting to the proxy server, the request is completed as in
+ HttpClient.request.
+ """
+ def _prepare_connection(self, url, headers):
+ proxy_auth = _get_proxy_auth()
+ if url.protocol == 'https':
+ # destination is https
+ proxy = os.environ.get('https_proxy')
+ if proxy:
+ # Set any proxy auth headers
+ if proxy_auth:
+ proxy_auth = 'Proxy-authorization: %s' % proxy_auth
+
+ # Construct the proxy connect command.
+ port = url.port
+ if not port:
+ port = '443'
+ proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % (url.host, port)
+
+ # Set the user agent to send to the proxy
+ if headers and 'User-Agent' in headers:
+ user_agent = 'User-Agent: %s\r\n' % (headers['User-Agent'])
+ else:
+ user_agent = ''
+
+ proxy_pieces = '%s%s%s\r\n' % (proxy_connect, proxy_auth, user_agent)
+
+ # Find the proxy host and port.
+ proxy_url = atom.url.parse_url(proxy)
+ if not proxy_url.port:
+ proxy_url.port = '80'
+
+ # Connect to the proxy server, very simple recv and error checking
+ p_sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
+ p_sock.connect((proxy_url.host, int(proxy_url.port)))
+ p_sock.sendall(proxy_pieces)
+ response = ''
+
+ # Wait for the full response.
+ while response.find("\r\n\r\n") == -1:
+ response += p_sock.recv(8192)
+
+ p_status = response.split()[1]
+ if p_status != str(200):
+ raise ProxyError('Error status=%s' % str(p_status))
+
+ # Trivial setup for ssl socket.
+ ssl = socket.ssl(p_sock, None, None)
+ fake_sock = httplib.FakeSocket(p_sock, ssl)
+
+ # Initalize httplib and replace with the proxy socket.
+ connection = httplib.HTTPConnection(proxy_url.host)
+ connection.sock=fake_sock
+ return connection
+ else:
+ # The request was HTTPS, but there was no https_proxy set.
+ return HttpClient._prepare_connection(self, url, headers)
+ else:
+ proxy = os.environ.get('http_proxy')
+ if proxy:
+ # Find the proxy host and port.
+ proxy_url = atom.url.parse_url(proxy)
+ if not proxy_url.port:
+ proxy_url.port = '80'
+
+ if proxy_auth:
+ headers['Proxy-Authorization'] = proxy_auth.strip()
+
+ return httplib.HTTPConnection(proxy_url.host, int(proxy_url.port))
+ else:
+ # The request was HTTP, but there was no http_proxy set.
+ return HttpClient._prepare_connection(self, url, headers)
+
+ def _get_access_url(self, url):
+ return url.to_string()
+
+
+def _get_proxy_auth():
+ proxy_username = os.environ.get('proxy-username')
+ if not proxy_username:
+ proxy_username = os.environ.get('proxy_username')
+ proxy_password = os.environ.get('proxy-password')
+ if not proxy_password:
+ proxy_password = os.environ.get('proxy_password')
+ if proxy_username:
+ user_auth = base64.encodestring('%s:%s' % (proxy_username,
+ proxy_password))
+ return 'Basic %s\r\n' % (user_auth.strip())
+ else:
+ return ''
+
+
+def _send_data_part(data, connection):
+ if isinstance(data, types.StringType):
+ 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
View
158 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/1.2.2'
+
+
+class Error(Exception):
+ pass
+
+
+class UnparsableUrlObject(Error):
+ pass
+
+
+class ContentLengthRequired(Error):
+ pass
+
+
+class HttpResponse(object):
+ def __init__(self, body=None, status=None, reason=None, headers=None):
+ """Constructor for an HttpResponse object.
+
+ HttpResponse represents the server's response to an HTTP request from
+ the client. The HttpClient.request method returns a httplib.HTTPResponse
+ object and this HttpResponse class is designed to mirror the interface
+ exposed by httplib.HTTPResponse.
+
+ Args:
+ body: A file like object, with a read() method. The body could also
+ be a string, and the constructor will wrap it so that
+ HttpResponse.read(self) will return the full string.
+ status: The HTTP status code as an int. Example: 200, 201, 404.
+ reason: The HTTP status message which follows the code. Example:
+ OK, Created, Not Found
+ headers: A dictionary containing the HTTP headers in the server's
+ response. A common header in the response is Content-Length.
+ """
+ if body:
+ if hasattr(body, 'read'):
+ self._body = body
+ else:
+ self._body = StringIO.StringIO(body)
+ else:
+ self._body = None
+ if status is not None:
+ self.status = int(status)
+ else:
+ self.status = None
+ self.reason = reason
+ self._headers = headers or {}
+
+ def getheader(self, name, default=None):
+ if name in self._headers:
+ return self._headers[name]
+ else:
+ return default
+
+ def read(self, amt=None):
+ if not amt:
+ return self._body.read()
+ else:
+ return self._body.read(amt)
+
+
+class GenericHttpClient(object):
+ debug = False
+
+ def __init__(self, http_client, headers=None):
+ """
+
+ Args:
+ http_client: An object which provides a request method to make an HTTP
+ request. The request method in GenericHttpClient performs a
+ call-through to the contained HTTP client object.
+ headers: A dictionary containing HTTP headers which should be included
+ in every HTTP request. Common persistent headers include
+ 'User-Agent'.
+ """
+ self.http_client = http_client
+ self.headers = headers or {}
+
+ def request(self, operation, url, data=None, headers=None):
+ all_headers = self.headers.copy()
+ if headers:
+ all_headers.update(headers)
+ return self.http_client.request(operation, url, data=data,
+ headers=all_headers)
+
+ def get(self, url, headers=None):
+ return self.request('GET', url, headers=headers)
+
+ def post(self, url, data, headers=None):
+ return self.request('POST', url, data=data, headers=headers)
+
+ def put(self, url, data, headers=None):
+ return self.request('PUT', url, data=data, headers=headers)
+
+ def delete(self, url, headers=None):
+ return self.request('DELETE', url, headers=headers)
+
+
+class GenericToken(object):
+ """Represents an Authorization token to be added to HTTP requests.
+
+ Some Authorization headers included calculated fields (digital
+ signatures for example) which are based on the parameters of the HTTP
+ request. Therefore the token is responsible for signing the request
+ and adding the Authorization header.
+ """
+ def perform_request(self, http_client, operation, url, data=None,
+ headers=None):
+ """For the GenericToken, no Authorization token is set."""
+ return http_client.request(operation, url, data=data, headers=headers)
+
+ def valid_for_scope(self, url):
+ """Tells the caller if the token authorizes access to the desired URL.
+
+ Since the generic token doesn't add an auth header, it is not valid for
+ any scope.
+ """
+ return False
+
+
View
132 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
View
243 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]
+
View
726 atom/service.py
@@ -0,0 +1,726 @@
+#!/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
+
+
+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)
+
+ 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)
+
+ 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)
+
+ def request(self, operation, url, data=None, headers=None,
+ url_params=None):
+ if isinstance(url, str):
+ if 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()