Jose Idar d8a0dcb5e6 Modernize setup.py and add wheel support
* Incremented version to 0.2.2 for pypi release.
  * Removed all post-install hooks.  All initialization and configuration
    should be done through the cafe-config cli tool.
  * Added setup.cfg with support for universal bdist wheel.
  * Updated MANIFEST.in file.
  * Updated README.rst to reflect new install procedures.
  * Modified cafe-config so that initialization command is now just
      "cafe-config init".
  * Since the ".opencafe" directory is no longer initialized at install
    while access to the source code is guaranteed, the plugins are now
    distributed as package_data, and installed as such to site-packages
    under the new "plugins" directory within the "cafe" namespace.
  * The plugins directory is moved to the cafe package directory as
     package_data.
  * The plugin cache is no longer created at initialization, and all
    code relating to it in the cli.py and managers.py file has been
    removed.
  * Removed pip-requires file in favor of including the only requirement,
    'six', in setup.py.  The plan is to refactor so as to remove the
    dependency on six eventually.
  * Renamed test-requirements.txt to test-requires.
  * Added Authors.rst

Change-Id: I28a605f926ae5f2d972a6a36171d0e4eb92cac09
2015-10-11 22:15:08 -05:00

323 lines
13 KiB
Python

# Copyright 2015 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
import six
from time import time
from cafe.common.reporting import cclogging
from cafe.engine.clients.base import BaseClient
from requests.packages import urllib3
urllib3.disable_warnings()
def _log_transaction(log, level=cclogging.logging.DEBUG):
def _safe_decode(text, incoming='utf-8', errors='replace'):
"""Decodes incoming text/bytes string using `incoming`
if they're not already unicode.
:param incoming: Text's current encoding
:param errors: Errors handling policy. See here for valid
values http://docs.python.org/2/library/codecs.html
:returns: text or a unicode `incoming` encoded
representation of it.
"""
if isinstance(text, six.text_type):
return text
return text.decode(incoming, errors)
""" 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(_safe_decode(logline))
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 http client')
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, _safe_decode(logline))
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 RECEIVED\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, _safe_decode(logline))
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 BaseHTTPClient(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(BaseHTTPClient, 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 HTTP client
@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 HTTP client
@warning: SHOULD ONLY BE CALLED FROM A PROVIDER THROUGH A TEST
FIXTURE
"""
if handler in cls._exception_handlers:
cls._exception_handlers.remove(handler)
class HTTPClient(BaseHTTPClient):
"""
@summary: Allows clients to inherit all requests-defined RESTful
verbs. Redefines request() so that keyword args are passed
through a named dictionary instead of kwargs.
Client methods can then take parameters that may overload
request parameters, which allows client method calls to
override parts of the request with parameters sent directly
to requests, overriding 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(HTTPClient, 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 list(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 list(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 list(requestslib_kwargs.keys()):
if requestslib_kwargs[key] is None:
del requestslib_kwargs[key]
# Create the final parameters for the call to the base request()
# Wherever a parameter 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(HTTPClient, self).request(
method, url, **requestslib_kwargs)
class AutoMarshallingHTTPClient(HTTPClient):
"""@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(AutoMarshallingHTTPClient, self).__init__()
self.serialize_format = serialize_format
self.deserialize_format = deserialize_format or self.serialize_format
self.default_headers = {'Content-Type': 'application/{format}'.format(
format=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' parameter 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(AutoMarshallingHTTPClient, 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