""" 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 HTTP request to 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