305 lines
12 KiB
Python
305 lines
12 KiB
Python
"""
|
|
Copyright 2013 Rackspace
|
|
|
|
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.
|
|
"""
|
|
|
|
import requests
|
|
from time import time
|
|
|
|
from cafe.common.reporting import cclogging
|
|
from cafe.engine.clients.base import BaseClient
|
|
|
|
|
|
def _log_transaction(log, level=cclogging.logging.DEBUG):
|
|
""" Paramaterized decorator
|
|
Takes a python Logger object and an optional logging level.
|
|
"""
|
|
def _decorator(func):
|
|
"""Accepts a function and returns wrapped version of that function."""
|
|
def _wrapper(*args, **kwargs):
|
|
"""Logging wrapper for any method that returns a requests response.
|
|
Logs requestslib response objects, and the args and kwargs
|
|
sent to the request() method, to the provided log at the provided
|
|
log level.
|
|
"""
|
|
logline = '{0} {1}'.format(args, kwargs)
|
|
|
|
try:
|
|
log.debug(logline.decode('utf-8', 'replace'))
|
|
except Exception as exception:
|
|
#Ignore all exceptions that happen in logging, then log them
|
|
log.info(
|
|
'Exception occured while logging signature of calling'
|
|
'method in rest connector')
|
|
log.exception(exception)
|
|
|
|
#Make the request and time it's execution
|
|
response = None
|
|
elapsed = None
|
|
try:
|
|
start = time()
|
|
response = func(*args, **kwargs)
|
|
elapsed = time() - start
|
|
except Exception as exception:
|
|
log.critical('Call to Requests failed due to exception')
|
|
log.exception(exception)
|
|
raise exception
|
|
|
|
#requests lib 1.0.0 renamed body to data in the request object
|
|
request_body = ''
|
|
if 'body' in dir(response.request):
|
|
request_body = response.request.body
|
|
elif 'data' in dir(response.request):
|
|
request_body = response.request.data
|
|
else:
|
|
log.info(
|
|
"Unable to log request body, neither a 'data' nor a "
|
|
"'body' object could be found")
|
|
|
|
#requests lib 1.0.4 removed params from response.request
|
|
request_params = ''
|
|
request_url = response.request.url
|
|
if 'params' in dir(response.request):
|
|
request_params = response.request.params
|
|
elif '?' in request_url:
|
|
request_url, request_params = request_url.split('?')
|
|
|
|
logline = ''.join([
|
|
'\n{0}\nREQUEST SENT\n{0}\n'.format('-' * 12),
|
|
'request method..: {0}\n'.format(response.request.method),
|
|
'request url.....: {0}\n'.format(request_url),
|
|
'request params..: {0}\n'.format(request_params),
|
|
'request headers.: {0}\n'.format(response.request.headers),
|
|
'request body....: {0}\n'.format(request_body)])
|
|
try:
|
|
log.log(level, logline.decode('utf-8', 'replace'))
|
|
except Exception as exception:
|
|
#Ignore all exceptions that happen in logging, then log them
|
|
log.log(level, '\n{0}\nREQUEST INFO\n{0}\n'.format('-' * 12))
|
|
log.exception(exception)
|
|
|
|
logline = ''.join([
|
|
'\n{0}\nRESPONSE RECIEVED\n{0}\n'.format('-' * 17),
|
|
'response status..: {0}\n'.format(response),
|
|
'response time....: {0}\n'.format(elapsed),
|
|
'response headers.: {0}\n'.format(response.headers),
|
|
'response body....: {0}\n'.format(response.content),
|
|
'-' * 79])
|
|
try:
|
|
log.log(level, logline.decode('utf-8', 'replace'))
|
|
except Exception as exception:
|
|
#Ignore all exceptions that happen in logging, then log them
|
|
log.log(level, '\n{0}\nRESPONSE INFO\n{0}\n'.format('-' * 13))
|
|
log.exception(exception)
|
|
return response
|
|
return _wrapper
|
|
return _decorator
|
|
|
|
|
|
def _inject_exception(exception_handlers):
|
|
"""Paramaterized decorator takes a list of exception_handler objects"""
|
|
def _decorator(func):
|
|
"""Accepts a function and returns wrapped version of that function."""
|
|
def _wrapper(*args, **kwargs):
|
|
"""Wrapper for any function that returns a Requests response.
|
|
Allows exception handlers to raise custom exceptions based on
|
|
response object attributes such as status_code.
|
|
"""
|
|
response = func(*args, **kwargs)
|
|
if exception_handlers:
|
|
for handler in exception_handlers:
|
|
handler.check_for_errors(response)
|
|
return response
|
|
return _wrapper
|
|
return _decorator
|
|
|
|
|
|
class BaseRestClient(BaseClient):
|
|
"""Re-implementation of Requests' api.py that removes many assumptions.
|
|
Adds verbose logging.
|
|
Adds support for response-code based exception injection.
|
|
(Raising exceptions based on response code)
|
|
|
|
@see: http://docs.python-requests.org/en/latest/api/#configurations
|
|
"""
|
|
_exception_handlers = []
|
|
_log = cclogging.getLogger(__name__)
|
|
|
|
def __init__(self):
|
|
super(BaseRestClient, self).__init__()
|
|
|
|
@_inject_exception(_exception_handlers)
|
|
@_log_transaction(log=_log)
|
|
def request(self, method, url, **kwargs):
|
|
""" Performs <method> HTTP request to <url> using the requests lib"""
|
|
return requests.request(method, url, **kwargs)
|
|
|
|
def put(self, url, **kwargs):
|
|
""" HTTP PUT request """
|
|
return self.request('PUT', url, **kwargs)
|
|
|
|
def copy(self, url, **kwargs):
|
|
""" HTTP COPY request """
|
|
return self.request('COPY', url, **kwargs)
|
|
|
|
def post(self, url, data=None, **kwargs):
|
|
""" HTTP POST request """
|
|
return self.request('POST', url, data=data, **kwargs)
|
|
|
|
def get(self, url, **kwargs):
|
|
""" HTTP GET request """
|
|
return self.request('GET', url, **kwargs)
|
|
|
|
def head(self, url, **kwargs):
|
|
""" HTTP HEAD request """
|
|
return self.request('HEAD', url, **kwargs)
|
|
|
|
def delete(self, url, **kwargs):
|
|
""" HTTP DELETE request """
|
|
return self.request('DELETE', url, **kwargs)
|
|
|
|
def options(self, url, **kwargs):
|
|
""" HTTP OPTIONS request """
|
|
return self.request('OPTIONS', url, **kwargs)
|
|
|
|
def patch(self, url, **kwargs):
|
|
""" HTTP PATCH request """
|
|
return self.request('PATCH', url, **kwargs)
|
|
|
|
@classmethod
|
|
def add_exception_handler(cls, handler):
|
|
"""Adds a specific L{ExceptionHandler} to the rest connector
|
|
@warning: SHOULD ONLY BE CALLED FROM A PROVIDER THROUGH A TEST
|
|
FIXTURE
|
|
"""
|
|
cls._exception_handlers.append(handler)
|
|
|
|
@classmethod
|
|
def delete_exception_handler(cls, handler):
|
|
"""Removes a L{ExceptionHandler} from the rest connector
|
|
@warning: SHOULD ONLY BE CALLED FROM A PROVIDER THROUGH A TEST
|
|
FIXTURE
|
|
"""
|
|
if handler in cls._exception_handlers:
|
|
cls._exception_handlers.remove(handler)
|
|
|
|
|
|
class RestClient(BaseRestClient):
|
|
"""
|
|
@summary: Allows clients to inherit all requests-defined RESTfull
|
|
verbs. Redefines request() so that keyword args are passed
|
|
through a named dictionary instead of kwargs.
|
|
Client methods can then take paramaters that may overload
|
|
request paramaters, which allows client method calls to
|
|
override parts of the request with paramters sent directly
|
|
to requests, overiding the client method logic either in
|
|
part or whole on the fly.
|
|
|
|
@see: http://docs.python-requests.org/en/latest/api/#configurations
|
|
"""
|
|
|
|
def __init__(self):
|
|
super(RestClient, self).__init__()
|
|
self.default_headers = {}
|
|
|
|
def request(
|
|
self, method, url, headers=None, params=None, data=None,
|
|
requestslib_kwargs=None):
|
|
|
|
#set requestslib_kwargs to an empty dict if None
|
|
requestslib_kwargs = requestslib_kwargs if (
|
|
requestslib_kwargs is not None) else {}
|
|
|
|
#Set defaults
|
|
params = params if params is not None else {}
|
|
verify = False
|
|
|
|
#If headers are provided by both, headers "wins" over default_headers
|
|
headers = dict(self.default_headers, **(headers or {}))
|
|
|
|
#Override url if present in requestslib_kwargs
|
|
if 'url' in requestslib_kwargs.keys():
|
|
url = requestslib_kwargs.get('url', None) or url
|
|
del requestslib_kwargs['url']
|
|
|
|
#Override method if present in requestslib_kwargs
|
|
if 'method' in requestslib_kwargs.keys():
|
|
method = requestslib_kwargs.get('method', None) or method
|
|
del requestslib_kwargs['method']
|
|
|
|
#The requests lib already removes None key/value pairs, but we force it
|
|
#here in case that behavior ever changes
|
|
for key in requestslib_kwargs.keys():
|
|
if requestslib_kwargs[key] is None:
|
|
del requestslib_kwargs[key]
|
|
|
|
#Create the final paramaters for the call to the base request()
|
|
#Wherever a paramater is provided both by the calling method AND
|
|
#the requests_lib kwargs dictionary, requestslib_kwargs "wins"
|
|
requestslib_kwargs = dict({'headers': headers,
|
|
'params': params,
|
|
'verify': verify,
|
|
'data': data},
|
|
**requestslib_kwargs)
|
|
|
|
#Make the request
|
|
return super(RestClient, self).request(method, url,
|
|
**requestslib_kwargs)
|
|
|
|
|
|
class AutoMarshallingRestClient(RestClient):
|
|
"""@TODO: Turn serialization and deserialization into decorators so
|
|
that we can support serialization and deserialization on a per-method
|
|
basis"""
|
|
def __init__(self, serialize_format=None, deserialize_format=None):
|
|
super(AutoMarshallingRestClient, self).__init__()
|
|
self.serialize_format = serialize_format
|
|
self.deserialize_format = deserialize_format or self.serialize_format
|
|
|
|
def request(self, method, url, headers=None, params=None, data=None,
|
|
response_entity_type=None, request_entity=None,
|
|
requestslib_kwargs=None):
|
|
|
|
#defaults requestslib_kwargs to a dictionary if it is None
|
|
requestslib_kwargs = requestslib_kwargs if (requestslib_kwargs is not
|
|
None) else {}
|
|
|
|
#set the 'data' paramater of the request to either what's already in
|
|
#requestslib_kwargs, or the deserialized output of the request_entity
|
|
if request_entity is not None:
|
|
requestslib_kwargs = dict(
|
|
{'data': request_entity.serialize(self.serialize_format)},
|
|
**requestslib_kwargs)
|
|
|
|
#Make the request
|
|
response = super(AutoMarshallingRestClient, self).request(
|
|
method, url, headers=headers, params=params, data=data,
|
|
requestslib_kwargs=requestslib_kwargs)
|
|
|
|
#Append the deserialized data object to the response
|
|
response.request.__dict__['entity'] = None
|
|
response.__dict__['entity'] = None
|
|
|
|
#If present, append the serialized request data object to
|
|
#response.request
|
|
if response.request is not None:
|
|
response.request.__dict__['entity'] = request_entity
|
|
|
|
if response_entity_type is not None:
|
|
response.__dict__['entity'] = response_entity_type.deserialize(
|
|
response.content,
|
|
self.deserialize_format)
|
|
|
|
return response
|