Move AWS query-specifics to class AWSQueryRequest

Hey, the name is actually quite appropriate.
This commit is contained in:
Garrett Holmstrom
2013-02-02 23:53:15 -08:00
parent 6fc17e3bb2
commit 6bbcaf5b24
2 changed files with 119 additions and 138 deletions

View File

@@ -50,9 +50,6 @@ class BaseRequest(BaseCommand):
Important members of this class include:
- SERVICE_CLASS: a class corresponding to the web service in use
- API_VERSION: the API version to send along with the request. This is
only necessary to override the service class's API
version for a specific request.
- NAME: a string containing the Action query parameter. This
defaults to the class's name.
- DESCRIPTION: a string describing the tool. This becomes part of the
@@ -69,7 +66,6 @@ class BaseRequest(BaseCommand):
'''
SERVICE_CLASS = BaseService
API_VERSION = None
NAME = None
FILTERS = []
@@ -79,12 +75,10 @@ class BaseRequest(BaseCommand):
def __init__(self, service=None, **kwargs):
self.service = service
# Parts of the HTTP request to be sent to the server.
# Note that self.serialize_params will update self.params for each
# entry in self.args that routes to PARAMS.
self.method = 'GET'
self.headers = {}
self.params = {}
self.post_data = None
self.method = 'GET'
self.body = None
# HTTP response obtained from the server
self.response = None
@@ -111,22 +105,6 @@ class BaseRequest(BaseCommand):
def preprocess_arg_objs(self, arg_objs):
self.service.preprocess_arg_objs(arg_objs)
def populate_parser(self, parser, arg_objs):
BaseCommand.populate_parser(self, parser, arg_objs)
if self.FILTERS:
parser.add_argument('--filter', metavar='NAME=VALUE',
action='append', dest='filters',
help='restrict results to those that meet criteria',
type=partial(_parse_filter, filter_objs=self.FILTERS))
parser.epilog = self.__build_filter_help()
self._arg_routes['filters'] = None
def process_cli_args(self):
BaseCommand.process_cli_args(self)
if 'filters' in self.args:
self.args['Filter'] = _process_filters(self.args.pop('filters'))
self._arg_routes['Filter'] = self.params
def configure(self):
self.service.configure()
@@ -158,7 +136,119 @@ class BaseRequest(BaseCommand):
else:
return None
def serialize_params(self, args, prefix=None):
def send(self):
headers = dict(self.headers or {})
headers.setdefault('User-Agent', self.user_agent)
params = self.prepare_params()
self.response = self.service.send_request(method=self.method,
headers=headers, params=params, data=self.body)
try:
if 200 <= self.response.status_code < 300:
parsed = self.parse_response(self.response)
self.log.info('result: success')
return parsed
else:
self.log.debug('-- response content --\n',
extra={'append': True})
self.log.debug(self.response.text, extra={'append': True})
self.log.debug('-- end of response content --')
self.log.info('result: failure')
raise ServerError(self.response.status_code,
self.response.content)
finally:
# Empty the socket buffer so it can be reused
try:
self.response.content
except RuntimeError:
# The content was already consumed
pass
def prepare_params(self):
return self.params or {}
def parse_response(self, response):
return response
def main(self):
'''
The main processing method for this type of request. In this method,
inheriting classes generally populate self.headers, self.params, and
self.body with information gathered from self.args or elsewhere,
call self.send, and return the response. BaseRequest's default
behavior is to simply return the result of a request with everything
that routes to PARAMS.
'''
self.preprocess()
response = self.send()
self.postprocess(response)
return response
def preprocess(self):
pass
def postprocess(self, response):
pass
def handle_cli_exception(self, err):
if isinstance(err, ServerError):
if err.code:
print >> sys.stderr, 'error ({code}) {msg}'.format(
code=err.code, msg=err.message or '')
else:
print >> sys.stderr, 'error {msg}'.format(
msg=err.message or '')
if self.debug:
raise
sys.exit(1)
else:
BaseCommand.handle_cli_exception(self, err)
class AWSQueryRequest(BaseRequest):
API_VERSION = None
def populate_parser(self, parser, arg_objs):
BaseRequest.populate_parser(self, parser, arg_objs)
if self.FILTERS:
parser.add_argument('--filter', metavar='NAME=VALUE',
action='append', dest='filters',
help='restrict results to those that meet criteria',
type=partial(_parse_filter, filter_objs=self.FILTERS))
parser.epilog = self.__build_filter_help()
self._arg_routes['filters'] = None
def process_cli_args(self):
BaseRequest.process_cli_args(self)
if 'filters' in self.args:
self.args['Filter'] = _process_filters(self.args.pop('filters'))
self._arg_routes['Filter'] = self.params
def prepare_params(self):
params = self.flatten_params(self.params)
params['Action'] = self.name
params['Version'] = self.API_VERSION or self.service.API_VERSION
self.log.info('parameters: %s', params)
return params
def parse_response(self, response):
# Parser for list-delimited responses like EC2's
# We do some extra handling here to log stuff as it comes in rather
# than reading it all into memory at once.
self.log.debug('-- response content --\n', extra={'append': True})
# Using Response.iter_content gives us automatic decoding, but we then
# have to make the generator look like a file so etree can use it.
with _IteratorFileObjAdapter(self.response.iter_content(16384)) \
as content_fileobj:
logged_fileobj = _ReadLoggingFileWrapper(content_fileobj, self.log,
logging.DEBUG)
response_dict = parse_listdelimited_aws_xml(logged_fileobj,
self.LIST_MARKERS)
self.log.debug('-- end of response content --')
# Strip off the root element
return response_dict[list(response_dict.keys())[0]]
def flatten_params(self, args, prefix=None):
'''
Given a possibly-nested dict of args and an arg routing destination,
transform each element in the dict that matches the corresponding
@@ -204,8 +294,7 @@ class BaseRequest(BaseCommand):
prefixed_key = str(key)
if isinstance(val, dict) or isinstance(val, list):
flattened.update(self.serialize_params(val,
prefixed_key))
flattened.update(self.flatten_params(val, prefixed_key))
elif isinstance(val, file):
flattened[prefixed_key] = val.read()
elif val or val is 0:
@@ -221,7 +310,7 @@ class BaseRequest(BaseCommand):
prefixed_key = str(i_item)
if isinstance(item, dict) or isinstance(item, list):
flattened.update(self.serialize_params(item, prefixed_key))
flattened.update(self.flatten_params(item, prefixed_key))
elif isinstance(item, file):
flattened[prefixed_key] = item.read()
elif item or item == 0:
@@ -232,102 +321,6 @@ class BaseRequest(BaseCommand):
raise TypeError('non-flattenable type: ' + args.__class__.__name__)
return flattened
def send(self):
'''
Send a request to the server and return its response. More precisely:
1. Build a dict of params suitable for submission as HTTP request
parameters, based first upon the content of self.params, and
second upon everything in self.args that routes to PARAMS.
2. Send an HTTP request via self.service with the HTTP method given
in self.method using query parameters from the aforementioned
serialized dict, headers based on self.headers, and POST data based
on self.post_data.
3. If the response's status code indicates success, parse the
response with self.parse_response and return the result.
4. If the response's status code does not indicate success, log an
error and raise a ServerError.
'''
params = self.serialize_params(self.params)
headers = dict(self.headers or {})
headers.setdefault('User-Agent', self.user_agent)
self.log.info('parameters: %s', params)
self.response = self.service.send_request(self.name,
method=self.method, headers=headers, params=params,
data=self.post_data, api_version=self.API_VERSION)
try:
if 200 <= self.response.status_code < 300:
parsed = self.parse_response(self.response)
self.log.info('result: success')
return parsed
else:
self.log.debug('-- response content --\n',
extra={'append': True})
self.log.debug(self.response.text, extra={'append': True})
self.log.debug('-- end of response content --')
self.log.info('result: failure')
raise ServerError(self.response.status_code,
self.response.content)
finally:
# Empty the socket buffer so it can be reused
try:
self.response.content
except RuntimeError:
# The content was already consumed
pass
def parse_response(self, response):
# Parser for list-delimited responses like EC2's
# We do some extra handling here to log stuff as it comes in rather
# than reading it all into memory at once.
self.log.debug('-- response content --\n', extra={'append': True})
# Using Response.iter_content gives us automatic decoding, but we then
# have to make the generator look like a file so etree can use it.
with _IteratorFileObjAdapter(self.response.iter_content(16384)) \
as content_fileobj:
logged_fileobj = _ReadLoggingFileWrapper(content_fileobj, self.log,
logging.DEBUG)
response_dict = parse_listdelimited_aws_xml(logged_fileobj,
self.LIST_MARKERS)
self.log.debug('-- end of response content --')
# Strip off the root element
return response_dict[list(response_dict.keys())[0]]
def main(self):
'''
The main processing method for this type of request. In this method,
inheriting classes generally populate self.headers, self.params, and
self.post_data with information gathered from self.args or elsewhere,
call self.send, and return the response. BaseRequest's default
behavior is to simply return the result of a request with everything
that routes to PARAMS.
'''
self.preprocess()
response = self.send()
self.postprocess(response)
return response
def preprocess(self):
pass
def postprocess(self, response):
pass
def handle_cli_exception(self, err):
if isinstance(err, ServerError):
if err.code:
print >> sys.stderr, 'error ({code}) {msg}'.format(
code=err.code, msg=err.message or '')
else:
print >> sys.stderr, 'error {msg}'.format(
msg=err.message or '')
if self.debug:
raise
sys.exit(1)
else:
BaseCommand.handle_cli_exception(self, err)
def __build_filter_help(self, force=False):
'''
Return a pre-formatted help string for all of the filters defined in

View File

@@ -19,7 +19,6 @@ import requests.exceptions
import time
import urlparse
from .auth import QuerySigV2Auth
from .exceptions import ClientError, ServiceInitError
from .util import aggregate_subclass_fields
@@ -124,19 +123,8 @@ class BaseService(object):
if user and self.config.current_user is None:
self.config.current_user = user
## TODO: nuke Action; the request should make it a param instead
## TODO: the same should probably happen with API versions, but the
## request would have to deal with service.API_VERSION, too
def send_request(self, action, method='GET', path=None, params=None,
headers=None, data=None, api_version=None):
params = params or {}
if action:
params['Action'] = action
if api_version:
params['Version'] = api_version
elif self.API_VERSION:
params['Version'] = self.API_VERSION
def send_request(self, method='GET', path=None, params=None, headers=None,
data=None):
## TODO: test url-encoding
if path:
# We can't simply use urljoin because a path might start with '/'