summaryrefslogtreecommitdiff
path: root/troveclient/xml.py
diff options
context:
space:
mode:
authorMichael Basnight <mbasnight@gmail.com>2013-06-17 23:34:27 -0700
committerMichael Basnight <mbasnight@gmail.com>2013-06-21 20:15:23 +0000
commit9916c8f2733b683d859770d05dacd2c9c82912d7 (patch)
tree084a0d53580cbbd34ed8f28de9302d6c78f7050d /troveclient/xml.py
parentbc90b3e088d3d4b83b5b3de0f9f83d9b6956947d (diff)
downloadpython-troveclient-0.1.3.tar.gz
Rename from reddwarf to trove.0.1.3
Implements Blueprint reddwarf-trove-rename Change-Id: Ib2d694c7466887ca297bea4250eca17cdc06b7bf
Diffstat (limited to 'troveclient/xml.py')
-rw-r--r--troveclient/xml.py293
1 files changed, 293 insertions, 0 deletions
diff --git a/troveclient/xml.py b/troveclient/xml.py
new file mode 100644
index 0000000..d4a7451
--- /dev/null
+++ b/troveclient/xml.py
@@ -0,0 +1,293 @@
+from lxml import etree
+import json
+from numbers import Number
+
+from troveclient import exceptions
+from troveclient.client import TroveHTTPClient
+
+XML_NS = {None: "http://docs.openstack.org/database/api/v1.0"}
+
+# If XML element is listed here then this searches through the ancestors.
+LISTIFY = {
+ "accounts": [[]],
+ "databases": [[]],
+ "flavors": [[]],
+ "instances": [[]],
+ "links": [[]],
+ "hosts": [[]],
+ "devices": [[]],
+ "users": [[]],
+ "versions": [[]],
+ "attachments": [[]],
+ "limits": [[]],
+ "security_groups": [[]],
+ "backups": [[]]
+}
+
+
+class IntDict(object):
+ pass
+
+
+TYPE_MAP = {
+ "instance": {
+ "volume": {
+ "used": float,
+ "size": int,
+ },
+ "deleted": bool,
+ "server": {
+ "local_id": int,
+ "deleted": bool,
+ },
+ },
+ "instances": {
+ "deleted": bool,
+ },
+ "deleted": bool,
+ "flavor": {
+ "ram": int,
+ },
+ "diagnostics": {
+ "vmHwm": int,
+ "vmPeak": int,
+ "vmSize": int,
+ "threads": int,
+ "vmRss": int,
+ "fdSize": int,
+ },
+ "security_group_rule": {
+ "from_port": int,
+ "to_port": int,
+ },
+ "quotas": IntDict,
+}
+TYPE_MAP["flavors"] = TYPE_MAP["flavor"]
+
+REQUEST_AS_LIST = set(['databases', 'users'])
+
+
+def element_ancestors_match_list(element, list):
+ """
+ For element root at <foo><blah><root></blah></foo> matches against
+ list ["blah", "foo"].
+ """
+ itr_elem = element.getparent()
+ for name in list:
+ if itr_elem is None:
+ break
+ if name != normalize_tag(itr_elem):
+ return False
+ itr_elem = itr_elem.getparent()
+ return True
+
+
+def element_must_be_list(parent_element, name):
+ """Determines if an element to be created should be a dict or list."""
+ if name in LISTIFY:
+ list_of_lists = LISTIFY[name]
+ for tag_list in list_of_lists:
+ if element_ancestors_match_list(parent_element, tag_list):
+ return True
+ return False
+
+
+def element_to_json(name, element):
+ if element_must_be_list(element, name):
+ return element_to_list(element)
+ else:
+ return element_to_dict(element)
+
+
+def root_element_to_json(name, element):
+ """Returns a tuple of the root JSON value, plus the links if found."""
+ if name == "rootEnabled": # Why oh why were we inconsistent here? :'(
+ if element.text.strip() == "False":
+ return False, None
+ elif element.text.strip() == "True":
+ return True, None
+ if element_must_be_list(element, name):
+ return element_to_list(element, True)
+ else:
+ return element_to_dict(element), None
+
+
+def element_to_list(element, check_for_links=False):
+ """
+ For element "foo" in <foos><foo/><foo/></foos>
+ Returns [{}, {}]
+ """
+ links = None
+ result = []
+ for child_element in element:
+ # The "links" element gets jammed into the root element.
+ if check_for_links and normalize_tag(child_element) == "links":
+ links = element_to_list(child_element)
+ else:
+ result.append(element_to_dict(child_element))
+ if check_for_links:
+ return result, links
+ else:
+ return result
+
+
+def element_to_dict(element):
+ result = {}
+ for name, value in element.items():
+ result[name] = value
+ for child_element in element:
+ name = normalize_tag(child_element)
+ result[name] = element_to_json(name, child_element)
+ if len(result) == 0 and element.text:
+ string_value = element.text.strip()
+ if len(string_value):
+ if string_value == 'None':
+ return None
+ return string_value
+ return result
+
+
+def standardize_json_lists(json_dict):
+ """
+ In XML, we might see something like {'instances':{'instances':[...]}},
+ which we must change to just {'instances':[...]} to be compatable with
+ the true JSON format.
+
+ If any items are dictionaries with only one item which is a list,
+ simply remove the dictionary and insert its list directly.
+ """
+ found_items = []
+ for key, value in json_dict.items():
+ value = json_dict[key]
+ if isinstance(value, dict):
+ if len(value) == 1 and isinstance(value.values()[0], list):
+ found_items.append(key)
+ else:
+ standardize_json_lists(value)
+ for key in found_items:
+ json_dict[key] = json_dict[key].values()[0]
+
+
+def normalize_tag(elem):
+ """Given an element, returns the tag minus the XMLNS junk.
+
+ IOW, .tag may sometimes return the XML namespace at the start of the
+ string. This gets rids of that.
+ """
+ try:
+ prefix = "{" + elem.nsmap[None] + "}"
+ if elem.tag.startswith(prefix):
+ return elem.tag[len(prefix):]
+ except KeyError:
+ pass
+ return elem.tag
+
+
+def create_root_xml_element(name, value):
+ """Create the first element using a name and a dictionary."""
+ element = etree.Element(name, nsmap=XML_NS)
+ if name in REQUEST_AS_LIST:
+ add_subelements_from_list(element, name, value)
+ else:
+ populate_element_from_dict(element, value)
+ return element
+
+
+def create_subelement(parent_element, name, value):
+ """Attaches a new element onto the parent element."""
+ if isinstance(value, dict):
+ create_subelement_from_dict(parent_element, name, value)
+ elif isinstance(value, list):
+ create_subelement_from_list(parent_element, name, value)
+ else:
+ raise TypeError("Can't handle type %s." % type(value))
+
+
+def create_subelement_from_dict(parent_element, name, dict):
+ element = etree.SubElement(parent_element, name)
+ populate_element_from_dict(element, dict)
+
+
+def create_subelement_from_list(parent_element, name, list):
+ element = etree.SubElement(parent_element, name)
+ add_subelements_from_list(element, name, list)
+
+
+def add_subelements_from_list(element, name, list):
+ if name.endswith("s"):
+ item_name = name[:len(name) - 1]
+ else:
+ item_name = name
+ for item in list:
+ create_subelement(element, item_name, item)
+
+
+def populate_element_from_dict(element, dict):
+ for key, value in dict.items():
+ if isinstance(value, basestring):
+ element.set(key, value)
+ elif isinstance(value, Number):
+ element.set(key, str(value))
+ elif isinstance(value, None.__class__):
+ element.set(key, '')
+ else:
+ create_subelement(element, key, value)
+
+
+def modify_response_types(value, type_translator):
+ """
+ This will convert some string in response dictionary to ints or bool
+ so that our respose is compatiable with code expecting JSON style responses
+ """
+ if isinstance(value, str):
+ if value == 'True':
+ return True
+ elif value == 'False':
+ return False
+ else:
+ return type_translator(value)
+ elif isinstance(value, dict):
+ for k, v in value.iteritems():
+ if type_translator is not IntDict:
+ if v.__class__ is dict and v.__len__() == 0:
+ value[k] = None
+ elif k in type_translator:
+ value[k] = modify_response_types(value[k],
+ type_translator[k])
+ else:
+ value[k] = int(value[k])
+ return value
+ elif isinstance(value, list):
+ return [modify_response_types(element, type_translator)
+ for element in value]
+
+
+class TroveXmlClient(TroveHTTPClient):
+
+ @classmethod
+ def morph_request(self, kwargs):
+ kwargs['headers']['Accept'] = 'application/xml'
+ kwargs['headers']['Content-Type'] = 'application/xml'
+ if 'body' in kwargs:
+ body = kwargs['body']
+ root_name = body.keys()[0]
+ xml = create_root_xml_element(root_name, body[root_name])
+ xml_string = etree.tostring(xml, pretty_print=True)
+ kwargs['body'] = xml_string
+
+ @classmethod
+ def morph_response_body(self, body_string):
+ # The root XML element always becomes a dictionary with a single
+ # field, which has the same key as the elements name.
+ result = {}
+ try:
+ root_element = etree.XML(body_string)
+ except etree.XMLSyntaxError:
+ raise exceptions.ResponseFormatError()
+ root_name = normalize_tag(root_element)
+ root_value, links = root_element_to_json(root_name, root_element)
+ result = {root_name: root_value}
+ if links:
+ result['links'] = links
+ modify_response_types(result, TYPE_MAP)
+ return result