Better EMR coverage and boto3 request/response handling

This revision includes:

- A handler for requests for which content-type is JSON (from boto3).

- A decorator (generate_boto3_response) to convert XML responses to
  JSON (for boto3). This way, existing response templates for boto can
  be shared for generating boto3 response.

- Utility class/functions to use botocore's service specification data
  (accessible under botocore.data) for type casting, from query
  parameters to Python objects and XML to JSON.

- Updates to response handlers/models to cover more EMR end points and
  mockable parameters
This commit is contained in:
Taro Sato 2016-09-21 20:59:19 -07:00
commit 7cd404808b
10 changed files with 2399 additions and 841 deletions

View file

@ -8,7 +8,10 @@ from jinja2 import Environment, DictLoader, TemplateNotFound
import six
from six.moves.urllib.parse import parse_qs, urlparse
import xmltodict
from pkg_resources import resource_filename
from werkzeug.exceptions import HTTPException
from moto.compat import OrderedDict
from moto.core.utils import camelcase_to_underscores, method_names_from_class
@ -90,6 +93,7 @@ class BaseResponse(_TemplateEnvironmentMixin):
default_region = 'us-east-1'
region_regex = r'\.(.+?)\.amazonaws\.com'
aws_service_spec = None
@classmethod
def dispatch(cls, *args, **kwargs):
@ -115,7 +119,20 @@ class BaseResponse(_TemplateEnvironmentMixin):
if not querystring:
querystring.update(parse_qs(urlparse(full_url).query, keep_blank_values=True))
if not querystring:
querystring.update(parse_qs(self.body, keep_blank_values=True))
if 'json' in request.headers.get('content-type', []) and self.aws_service_spec:
if isinstance(self.body, six.binary_type):
decoded = json.loads(self.body.decode('utf-8'))
else:
decoded = json.loads(self.body)
target = request.headers.get('x-amz-target') or request.headers.get('X-Amz-Target')
service, method = target.split('.')
input_spec = self.aws_service_spec.input_spec(method)
flat = flatten_json_request_body('', decoded, input_spec)
for key, value in flat.items():
querystring[key] = [value]
else:
querystring.update(parse_qs(self.body, keep_blank_values=True))
if not querystring:
querystring.update(headers)
@ -125,15 +142,19 @@ class BaseResponse(_TemplateEnvironmentMixin):
self.path = urlparse(full_url).path
self.querystring = querystring
self.method = request.method
region = re.search(self.region_regex, full_url)
if region:
self.region = region.group(1)
else:
self.region = self.default_region
self.region = self.get_region_from_url(full_url)
self.headers = request.headers
self.response_headers = headers
def get_region_from_url(self, full_url):
match = re.search(self.region_regex, full_url)
if match:
region = match.group(1)
else:
region = self.default_region
return region
def _dispatch(self, request, full_url, headers):
self.setup_class(request, full_url, headers)
return self.call_action()
@ -164,21 +185,26 @@ class BaseResponse(_TemplateEnvironmentMixin):
return status, headers, body
raise NotImplementedError("The {0} action has not been implemented".format(action))
def _get_param(self, param_name):
return self.querystring.get(param_name, [None])[0]
def _get_param(self, param_name, if_none=None):
val = self.querystring.get(param_name)
if val is not None:
return val[0]
return if_none
def _get_int_param(self, param_name):
def _get_int_param(self, param_name, if_none=None):
val = self._get_param(param_name)
if val is not None:
return int(val)
return if_none
def _get_bool_param(self, param_name):
def _get_bool_param(self, param_name, if_none=None):
val = self._get_param(param_name)
if val is not None:
if val.lower() == 'true':
return True
elif val.lower() == 'false':
return False
return if_none
def _get_multi_param(self, param_prefix):
"""
@ -257,6 +283,28 @@ class BaseResponse(_TemplateEnvironmentMixin):
param_index += 1
return results
def _get_map_prefix(self, param_prefix):
results = {}
param_index = 1
while 1:
index_prefix = '{0}.{1}.'.format(param_prefix, param_index)
k, v = None, None
for key, value in self.querystring.items():
if key.startswith(index_prefix):
if key.endswith('.key'):
k = value[0]
elif key.endswith('.value'):
v = value[0]
if not (k and v):
break
results[k] = v
param_index += 1
return results
@property
def request_json(self):
return 'JSON' in self.querystring.get('ContentType', [])
@ -299,3 +347,227 @@ def metadata_response(request, full_url, headers):
else:
raise NotImplementedError("The {0} metadata path has not been implemented".format(path))
return 200, headers, result
class _RecursiveDictRef(object):
"""Store a recursive reference to dict."""
def __init__(self):
self.key = None
self.dic = {}
def __repr__(self):
return '{!r}'.format(self.dic)
def __getattr__(self, key):
return self.dic.__getattr__(key)
def set_reference(self, key, dic):
"""Set the RecursiveDictRef object to keep reference to dict object
(dic) at the key.
"""
self.key = key
self.dic = dic
class AWSServiceSpec(object):
"""Parse data model from botocore. This is used to recover type info
for fields in AWS API XML response.
"""
def __init__(self, path):
self.path = resource_filename('botocore', path)
with open(self.path) as f:
spec = json.load(f)
self.metadata = spec['metadata']
self.operations = spec['operations']
self.shapes = spec['shapes']
def input_spec(self, operation):
try:
op = self.operations[operation]
except KeyError:
raise ValueError('Invalid operation: {}'.format(operation))
if 'input' not in op:
return {}
shape = self.shapes[op['input']['shape']]
return self._expand(shape)
def output_spec(self, operation):
"""Produce a JSON with a valid API response syntax for operation, but
with type information. Each node represented by a key has the
value containing field type, e.g.,
output_spec["SomeBooleanNode"] => {"type": "boolean"}
"""
try:
op = self.operations[operation]
except KeyError:
raise ValueError('Invalid operation: {}'.format(operation))
if 'output' not in op:
return {}
shape = self.shapes[op['output']['shape']]
return self._expand(shape)
def _expand(self, shape):
def expand(dic, seen=None):
seen = seen or {}
if dic['type'] == 'structure':
nodes = {}
for k, v in dic['members'].items():
seen_till_here = dict(seen)
if k in seen_till_here:
nodes[k] = seen_till_here[k]
continue
seen_till_here[k] = _RecursiveDictRef()
nodes[k] = expand(self.shapes[v['shape']], seen_till_here)
seen_till_here[k].set_reference(k, nodes[k])
nodes['type'] = 'structure'
return nodes
elif dic['type'] == 'list':
seen_till_here = dict(seen)
shape = dic['member']['shape']
if shape in seen_till_here:
return seen_till_here[shape]
seen_till_here[shape] = _RecursiveDictRef()
expanded = expand(self.shapes[shape], seen_till_here)
seen_till_here[shape].set_reference(shape, expanded)
return {'type': 'list', 'member': expanded}
elif dic['type'] == 'map':
seen_till_here = dict(seen)
node = {'type': 'map'}
if 'shape' in dic['key']:
shape = dic['key']['shape']
seen_till_here[shape] = _RecursiveDictRef()
node['key'] = expand(self.shapes[shape], seen_till_here)
seen_till_here[shape].set_reference(shape, node['key'])
else:
node['key'] = dic['key']['type']
if 'shape' in dic['value']:
shape = dic['value']['shape']
seen_till_here[shape] = _RecursiveDictRef()
node['value'] = expand(self.shapes[shape], seen_till_here)
seen_till_here[shape].set_reference(shape, node['value'])
else:
node['value'] = dic['value']['type']
return node
else:
return {'type': dic['type']}
return expand(shape)
def to_str(value, spec):
vtype = spec['type']
if vtype == 'boolean':
return 'true' if value else 'false'
elif vtype == 'integer':
return str(value)
elif vtype == 'string':
return str(value)
elif value is None:
return 'null'
else:
raise TypeError('Unknown type {}'.format(vtype))
def from_str(value, spec):
vtype = spec['type']
if vtype == 'boolean':
return True if value == 'true' else False
elif vtype == 'integer':
return int(value)
elif vtype == 'float':
return float(value)
elif vtype == 'timestamp':
return value
elif vtype == 'string':
return value
raise TypeError('Unknown type {}'.format(vtype))
def flatten_json_request_body(prefix, dict_body, spec):
"""Convert a JSON request body into query params."""
if len(spec) == 1 and 'type' in spec:
return {prefix: to_str(dict_body, spec)}
flat = {}
for key, value in dict_body.items():
node_type = spec[key]['type']
if node_type == 'list':
for idx, v in enumerate(value, 1):
pref = key + '.member.' + str(idx)
flat.update(flatten_json_request_body(pref, v, spec[key]['member']))
elif node_type == 'map':
for idx, (k, v) in enumerate(value.items(), 1):
pref = key + '.entry.' + str(idx)
flat.update(flatten_json_request_body(pref + '.key', k, spec[key]['key']))
flat.update(flatten_json_request_body(pref + '.value', v, spec[key]['value']))
else:
flat.update(flatten_json_request_body(key, value, spec[key]))
if prefix:
prefix = prefix + '.'
return dict((prefix + k, v) for k, v in flat.items())
def xml_to_json_response(service_spec, operation, xml, result_node=None):
"""Convert rendered XML response to JSON for use with boto3."""
def transform(value, spec):
"""Apply transformations to make the output JSON comply with the
expected form. This function applies:
(1) Type cast to nodes with "type" property (e.g., 'true' to
True). XML field values are all in text so this step is
necessary to convert it to valid JSON objects.
(2) Squashes "member" nodes to lists.
"""
if len(spec) == 1:
return from_str(value, spec)
od = OrderedDict()
for k, v in value.items():
if k.startswith('@') or v is None:
continue
if spec[k]['type'] == 'list':
if len(spec[k]['member']) == 1:
if isinstance(v['member'], list):
od[k] = transform(v['member'], spec[k]['member'])
else:
od[k] = [transform(v['member'], spec[k]['member'])]
elif isinstance(v['member'], list):
od[k] = [transform(o, spec[k]['member']) for o in v['member']]
elif isinstance(v['member'], OrderedDict):
od[k] = [transform(v['member'], spec[k]['member'])]
else:
raise ValueError('Malformatted input')
elif spec[k]['type'] == 'map':
key = from_str(v['entry']['key'], spec[k]['key'])
val = from_str(v['entry']['value'], spec[k]['value'])
od[k] = {key: val}
else:
od[k] = transform(v, spec[k])
return od
dic = xmltodict.parse(xml)
output_spec = service_spec.output_spec(operation)
try:
for k in (result_node or (operation + 'Response', operation + 'Result')):
dic = dic[k]
except KeyError:
return None
else:
return transform(dic, output_spec)
return None