419 lines
15 KiB
Python
419 lines
15 KiB
Python
# Copyright (c) 2012-2013 Mitch Garnaat http://garnaat.org/
|
|
# Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
# copy of this software and associated documentation files (the
|
|
# "Software"), to deal in the Software without restriction, including
|
|
# without limitation the rights to use, copy, modify, merge, publish, dis-
|
|
# tribute, sublicense, and/or sell copies of the Software, and to permit
|
|
# persons to whom the Software is furnished to do so, subject to the fol-
|
|
# lowing conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included
|
|
# in all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
|
|
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
|
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
# IN THE SOFTWARE.
|
|
#
|
|
import sys
|
|
import xml.etree.cElementTree
|
|
from botocore import ScalarTypes
|
|
from .hooks import first_non_none_response
|
|
from botocore.compat import json
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Response(object):
|
|
|
|
def __init__(self, session, operation):
|
|
self.session = session
|
|
self.operation = operation
|
|
self.value = {}
|
|
|
|
def parse(self, s, encoding):
|
|
pass
|
|
|
|
def get_value(self):
|
|
value = ''
|
|
if self.value:
|
|
value = self.value
|
|
return value
|
|
|
|
def merge_header_values(self, headers):
|
|
pass
|
|
|
|
|
|
class XmlResponse(Response):
|
|
|
|
def __init__(self, session, operation):
|
|
Response.__init__(self, session, operation)
|
|
self.tree = None
|
|
self.element_map = {}
|
|
self.value = {}
|
|
self._parent = None
|
|
|
|
def clark_notation(self, tag):
|
|
return '{%s}%s' % (self.operation.service.xmlnamespace, tag)
|
|
|
|
def get_element_base_tag(self, elem):
|
|
if '}' in elem.tag:
|
|
elem_tag = elem.tag.split('}')[1]
|
|
else:
|
|
elem_tag = elem.tag
|
|
return elem_tag
|
|
|
|
def parse(self, s, encoding):
|
|
parser = xml.etree.cElementTree.XMLParser(
|
|
target=xml.etree.cElementTree.TreeBuilder(),
|
|
encoding=encoding)
|
|
parser.feed(s)
|
|
self.tree = parser.close()
|
|
if self.operation.output:
|
|
self.build_element_map(self.operation.output, 'root')
|
|
self.start(self.tree)
|
|
|
|
def get_response_metadata(self):
|
|
rmd = {}
|
|
self.value['ResponseMetadata'] = rmd
|
|
rmd_elem = self.tree.find(self.clark_notation('ResponseMetadata'))
|
|
if rmd_elem is not None:
|
|
rmd_elem.tail = True
|
|
request_id = rmd_elem.find(self.clark_notation('RequestId'))
|
|
else:
|
|
request_id = self.tree.find(self.clark_notation('requestId'))
|
|
if request_id is None:
|
|
request_id = self.tree.find(self.clark_notation('RequestId'))
|
|
if request_id is None:
|
|
request_id = self.tree.find('RequestID')
|
|
if request_id is not None:
|
|
request_id.tail = True
|
|
rmd['RequestId'] = request_id.text.strip()
|
|
|
|
def _get_error_data(self, error_elem):
|
|
data = {}
|
|
for elem in error_elem:
|
|
elem.tail = True
|
|
data[self.get_element_base_tag(elem)] = elem.text
|
|
return data
|
|
|
|
def get_response_errors(self):
|
|
errors = None
|
|
error_elems = self.tree.find('Errors')
|
|
if error_elems is not None:
|
|
error_elems.tail = True
|
|
errors = [self._get_error_data(e) for e in error_elems]
|
|
else:
|
|
error_elems = self.tree.find(self.clark_notation('Error'))
|
|
if error_elems is not None:
|
|
error_elems.tail = True
|
|
errors = [self._get_error_data(error_elems)]
|
|
elif self.tree.tag == 'Error':
|
|
errors = [self._get_error_data(self.tree)]
|
|
if errors:
|
|
self.value['Errors'] = errors
|
|
|
|
def build_element_map(self, defn, keyname):
|
|
xmlname = defn.get('xmlname', keyname)
|
|
if not xmlname:
|
|
xmlname = defn.get('shape_name')
|
|
self.element_map[xmlname] = defn
|
|
if defn['type'] == 'structure':
|
|
for member_name in defn['members']:
|
|
self.build_element_map(defn['members'][member_name],
|
|
member_name)
|
|
elif defn['type'] == 'list':
|
|
self.build_element_map(defn['members'], None)
|
|
elif defn['type'] == 'map':
|
|
self.build_element_map(defn['keys'], 'key')
|
|
self.build_element_map(defn['members'], 'value')
|
|
|
|
def find(self, parent, tag):
|
|
tag = tag.split(':')[-1]
|
|
cn = self.clark_notation(tag)
|
|
child = parent.find(cn)
|
|
if child is None:
|
|
child = parent.find('*/%s' % cn)
|
|
return child
|
|
|
|
def findall(self, parent, tag):
|
|
cn = self.clark_notation(tag)
|
|
children = parent.findall(cn)
|
|
if not children:
|
|
try:
|
|
children = parent.findall('*/%s' % cn)
|
|
except:
|
|
pass
|
|
return children
|
|
|
|
def parent_slow(self, elem, target):
|
|
for child in elem:
|
|
if child == target:
|
|
self._parent = elem
|
|
break
|
|
self.parent_slow(child, target)
|
|
|
|
def parent(self, elem):
|
|
# We need the '..' operator in XPath but only that is only
|
|
# available in Python versions >= 2.7
|
|
if sys.version_info[0] == 2 and sys.version_info[1] == 6:
|
|
self.parent_slow(self.tree, elem)
|
|
parent = self._parent
|
|
self._parent = None
|
|
else:
|
|
parent = self.tree.find('.//%s/..' % elem.tag)
|
|
return parent
|
|
|
|
def get_elem_text(self, elem):
|
|
data = elem.text
|
|
if data is not None:
|
|
data = elem.text.strip()
|
|
return data
|
|
|
|
def _handle_string(self, elem, shape):
|
|
data = self.get_elem_text(elem)
|
|
if not data:
|
|
children = list(elem)
|
|
if len(children) == 1:
|
|
data = self.get_elem_text(children[0])
|
|
return data
|
|
|
|
_handle_timestamp = _handle_string
|
|
_handle_blob = _handle_string
|
|
|
|
def _handle_integer(self, elem, shape):
|
|
data = self.get_elem_text(elem)
|
|
if data:
|
|
data = int(data)
|
|
return data
|
|
|
|
_handle_long = _handle_integer
|
|
|
|
def _handle_float(self, elem, shape):
|
|
data = self.get_elem_text(elem)
|
|
if data:
|
|
data = float(data)
|
|
return data
|
|
|
|
_handle_double = _handle_float
|
|
|
|
def _handle_boolean(self, elem, shape):
|
|
return True if elem.text.lower() == 'true' else False
|
|
|
|
def _handle_structure(self, elem, shape):
|
|
new_data = {}
|
|
xmlname = shape.get('xmlname')
|
|
if xmlname:
|
|
tagname = self.get_element_base_tag(elem)
|
|
if xmlname != tagname:
|
|
return new_data
|
|
for member_name in shape['members']:
|
|
member_shape = shape['members'][member_name]
|
|
xmlname = member_shape.get('xmlname', member_name)
|
|
child = self.find(elem, xmlname)
|
|
if child is not None:
|
|
new_data[member_name] = self.handle_elem(
|
|
member_name, child, member_shape)
|
|
return new_data
|
|
|
|
def _handle_list(self, elem, shape):
|
|
xmlname = shape['members'].get('xmlname', 'member')
|
|
children = self.findall(elem, xmlname)
|
|
if not children and shape.get('flattened'):
|
|
parent = self.parent(elem)
|
|
if parent is not None:
|
|
tagname = self.get_element_base_tag(elem)
|
|
children = self.findall(parent, tagname)
|
|
if not children:
|
|
children = []
|
|
return [self.handle_elem(None, child, shape['members'])
|
|
for child in children]
|
|
|
|
def _handle_map(self, elem, shape):
|
|
data = {}
|
|
# First collect all map entries
|
|
xmlname = shape.get('xmlname', 'entry')
|
|
keyshape = shape['keys']
|
|
valueshape = shape['members']
|
|
key_xmlname = keyshape.get('xmlname', 'key')
|
|
value_xmlname = valueshape.get('xmlname', 'value')
|
|
members = self.findall(elem, xmlname)
|
|
if not members:
|
|
parent = self.parent(elem)
|
|
if parent is not None:
|
|
members = self.findall(parent, xmlname)
|
|
for member in members:
|
|
key = self.find(member, key_xmlname)
|
|
value = self.find(member, value_xmlname)
|
|
cn = self.clark_notation(value_xmlname)
|
|
value = member.find(cn)
|
|
key_name = self.handle_elem(None, key, keyshape)
|
|
data[key_name] = self.handle_elem(key_name, value, valueshape)
|
|
return data
|
|
|
|
def emit_event(self, tag, shape, value):
|
|
if 'shape_name' in shape:
|
|
event = self.session.create_event(
|
|
'after-parsed', self.operation.service.endpoint_prefix,
|
|
self.operation.name, shape['shape_name'], tag)
|
|
rv = first_non_none_response(self.session.emit(event,
|
|
shape=shape,
|
|
value=value),
|
|
None)
|
|
if rv:
|
|
value = rv
|
|
return value
|
|
|
|
def handle_elem(self, key, elem, shape):
|
|
handler_name = '_handle_%s' % shape['type']
|
|
elem.tail = True
|
|
if hasattr(self, handler_name):
|
|
value = getattr(self, handler_name)(elem, shape)
|
|
value = self.emit_event(key, shape, value)
|
|
return value
|
|
else:
|
|
logger.debug('Unhandled type: %s', shape['type'])
|
|
|
|
def fake_shape(self, elem):
|
|
shape = {}
|
|
tags = set()
|
|
nchildren = 0
|
|
for child in elem:
|
|
tags.add(child)
|
|
nchildren += 1
|
|
if nchildren == 0:
|
|
shape['type'] = 'string'
|
|
elif nchildren > 1 and len(tags) == 1:
|
|
shape['type'] = 'list'
|
|
shape['members'] = {'type': 'string'}
|
|
else:
|
|
shape['type'] = 'structure'
|
|
shape['members'] = {}
|
|
for tag in tags:
|
|
base_tag = self.get_element_base_tag(tag)
|
|
shape['members'][base_tag] = {'type': 'string'}
|
|
return shape
|
|
|
|
def start(self, elem):
|
|
self.value = {}
|
|
if self.operation.output:
|
|
for member_name in self.operation.output['members']:
|
|
member = self.operation.output['members'][member_name]
|
|
xmlname = member.get('xmlname', member_name)
|
|
child = self.find(elem, xmlname)
|
|
if child is None and member['type'] not in ScalarTypes:
|
|
child = elem
|
|
if child is not None:
|
|
self.value[member_name] = self.handle_elem(member_name,
|
|
child, member)
|
|
self.get_response_metadata()
|
|
self.get_response_errors()
|
|
for child in self.tree:
|
|
if child.tail is not True:
|
|
child_tag = self.get_element_base_tag(child)
|
|
if child_tag not in self.element_map:
|
|
if not child_tag.startswith(self.operation.name):
|
|
shape = self.fake_shape(child)
|
|
self.value[child_tag] = self.handle_elem(child_tag,
|
|
child, shape)
|
|
|
|
def merge_header_values(self, headers):
|
|
if self.operation.output:
|
|
for member_name in self.operation.output['members']:
|
|
member = self.operation.output['members'][member_name]
|
|
location = member.get('location')
|
|
if location == 'header':
|
|
location_name = member.get('location_name')
|
|
if location_name in headers:
|
|
self.value[member_name] = headers[location_name]
|
|
|
|
|
|
class JSONResponse(Response):
|
|
|
|
def parse(self, s, encoding):
|
|
try:
|
|
decoded = s.decode(encoding)
|
|
self.value = json.loads(decoded)
|
|
self.get_response_errors()
|
|
except Exception as err:
|
|
logger.debug('Error loading JSON response body, %r', err)
|
|
|
|
def get_response_errors(self):
|
|
# Most JSON services return a __type in error response bodies.
|
|
# Unfortunately, ElasticTranscoder does not. It simply returns
|
|
# a JSON body with a single key, "message".
|
|
error = None
|
|
if '__type' in self.value:
|
|
error_type = self.value['__type']
|
|
error = {'Type': error_type}
|
|
del self.value['__type']
|
|
if 'message' in self.value:
|
|
error['Message'] = self.value['message']
|
|
del self.value['message']
|
|
code = self._parse_code_from_type(error_type)
|
|
error['Code'] = code
|
|
elif 'message' in self.value and len(self.value.keys()) == 1:
|
|
error = {'Type': 'Unspecified', 'Code': 'Unspecified',
|
|
'Message': self.value['message']}
|
|
del self.value['message']
|
|
if error:
|
|
self.value['Errors'] = [error]
|
|
|
|
def _parse_code_from_type(self, error_type):
|
|
return error_type.rsplit('#', 1)[-1]
|
|
|
|
|
|
class StreamingResponse(Response):
|
|
|
|
def __init__(self, session, operation):
|
|
Response.__init__(self, session, operation)
|
|
self.value = {}
|
|
|
|
def parse(self, headers, stream):
|
|
for member_name in self.operation.output['members']:
|
|
member_dict = self.operation.output['members'][member_name]
|
|
if member_dict.get('location') == 'header':
|
|
header_name = member_dict.get('location_name')
|
|
if header_name and header_name in headers:
|
|
self.value[member_name] = headers[header_name]
|
|
elif member_dict.get('type') == 'blob':
|
|
if member_dict.get('payload'):
|
|
if member_dict.get('streaming'):
|
|
self.value[member_name] = stream
|
|
|
|
|
|
def get_response(session, operation, http_response):
|
|
encoding = 'utf-8'
|
|
if http_response.encoding:
|
|
encoding = http_response.encoding
|
|
content_type = http_response.headers.get('content-type')
|
|
if content_type and ';' in content_type:
|
|
content_type = content_type.split(';')[0]
|
|
logger.debug('Content type from response: %s', content_type)
|
|
if operation.is_streaming():
|
|
streaming_response = StreamingResponse(session, operation)
|
|
streaming_response.parse(http_response.headers, http_response.raw)
|
|
return (http_response, streaming_response.get_value())
|
|
body = http_response.content
|
|
logger.debug("Response Body:\n%s", body)
|
|
if operation.service.type == 'json':
|
|
json_response = JSONResponse(session, operation)
|
|
if body:
|
|
json_response.parse(body, encoding)
|
|
json_response.merge_header_values(http_response.headers)
|
|
return (http_response, json_response.get_value())
|
|
# We are defaulting to an XML response handler because many query
|
|
# services send XML error responses but do not include a Content-Type
|
|
# header.
|
|
xml_response = XmlResponse(session, operation)
|
|
if body:
|
|
xml_response.parse(body, encoding)
|
|
xml_response.merge_header_values(http_response.headers)
|
|
return (http_response, xml_response.get_value())
|