Skip to content

Commit

Permalink
Merge pull request #376 from sjaensch/non-callable-fallback-results
Browse files Browse the repository at this point in the history
Support non-callable fallback results, stabilize API
  • Loading branch information
sjaensch authored Jun 25, 2018
2 parents b4fa537 + a560ea8 commit 714f9d4
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 44 deletions.
21 changes: 11 additions & 10 deletions bravado/http_future.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
)


SENTINEL = object()


class FutureAdapter(object):
"""
Mimics a :class:`concurrent.futures.Future` regardless of which client is
Expand Down Expand Up @@ -123,23 +126,20 @@ def __init__(self, future, response_adapter, operation=None,
also_return_response_default=False,
)

def response(self, timeout=None, fallback_result=None, exceptions_to_catch=FALLBACK_EXCEPTIONS):
def response(self, timeout=None, fallback_result=SENTINEL, exceptions_to_catch=FALLBACK_EXCEPTIONS):
"""Blocking call to wait for the HTTP response.
:param timeout: Number of seconds to wait for a response. Defaults to
None which means wait indefinitely.
:type timeout: float
:param fallback_result: callable that accepts an exception as argument and returns the
swagger result to use in case of errors
:type fallback_result: callable that takes an exception and returns a fallback swagger result
:param fallback_result: either the swagger result or a callable that accepts an exception as argument
and returns the swagger result to use in case of errors
:type fallback_result: Optional[Union[Any, Callable[[Exception], Any]]]
:param exceptions_to_catch: Exception classes to catch and call `fallback_result`
with. Has no effect if `fallback_result` is not provided. By default, `fallback_result`
will be called for read timeout and server errors (HTTP 5XX).
:type exceptions_to_catch: List/Tuple of Exception classes.
:return: A BravadoResponse instance containing the swagger result and response metadata.
WARNING: This interface is considered UNSTABLE. Backwards-incompatible API changes may occur;
use at your own risk.
"""
incoming_response = None
exc_info = None
Expand All @@ -157,7 +157,7 @@ def response(self, timeout=None, fallback_result=None, exceptions_to_catch=FALLB
raise make_http_exception(response=incoming_response)

# Trigger fallback_result if the option is set
if fallback_result and self.request_config.force_fallback_result:
if fallback_result is not SENTINEL and self.request_config.force_fallback_result:
if self.operation.swagger_spec.config['bravado'].disable_fallback_results:
log.warning(
'force_fallback_result set in request options and disable_fallback_results '
Expand All @@ -176,10 +176,11 @@ def response(self, timeout=None, fallback_result=None, exceptions_to_catch=FALLB
exc_info = list(sys.exc_info()[:2])
exc_info.append(traceback.format_exc())
if (
fallback_result and self.operation
fallback_result is not SENTINEL
and self.operation
and not self.operation.swagger_spec.config['bravado'].disable_fallback_results
):
swagger_result = fallback_result(e)
swagger_result = fallback_result(e) if callable(fallback_result) else fallback_result
else:
six.reraise(*sys.exc_info())

Expand Down
6 changes: 0 additions & 6 deletions bravado/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
class BravadoResponse(object):
"""Bravado response object containing the swagger result as well as response metadata.
WARNING: This interface is considered UNSTABLE. Backwards-incompatible API changes may occur;
use at your own risk.
:ivar result: Swagger result from the server
:ivar BravadoResponseMetadata metadata: metadata for this response including HTTP response
"""
Expand All @@ -29,9 +26,6 @@ class BravadoResponseMetadata(object):
Nevertheless, it should be accurate enough for logging and debugging, i.e. determining what went
on and how much time was spent waiting for the response.
WARNING: This interface is considered UNSTABLE. Backwards-incompatible API changes may occur;
use at your own risk.
:ivar float start_time: monotonic timestamp at which the future was created
:ivar float request_end_time: monotonic timestamp at which we received the HTTP response
:ivar float processing_end_time: monotonic timestamp at which processing the response ended
Expand Down
9 changes: 5 additions & 4 deletions bravado/testing/response_mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from bravado.exception import BravadoTimeoutError
from bravado.http_future import FALLBACK_EXCEPTIONS
from bravado.http_future import SENTINEL
from bravado.response import BravadoResponseMetadata


Expand Down Expand Up @@ -33,7 +34,7 @@ def __init__(self, result, metadata=None):
request_config=None,
)

def __call__(self, timeout=None, fallback_result=None, exceptions_to_catch=FALLBACK_EXCEPTIONS):
def __call__(self, timeout=None, fallback_result=SENTINEL, exceptions_to_catch=FALLBACK_EXCEPTIONS):
return self

@property
Expand Down Expand Up @@ -65,10 +66,10 @@ def __init__(self, exception=BravadoTimeoutError(), metadata=None):
request_config=None,
)

def __call__(self, timeout=None, fallback_result=None, exceptions_to_catch=FALLBACK_EXCEPTIONS):
assert callable(fallback_result), 'You\'re using FallbackResultBravadoResponseMock without a callable ' + \
def __call__(self, timeout=None, fallback_result=SENTINEL, exceptions_to_catch=FALLBACK_EXCEPTIONS):
assert fallback_result is not SENTINEL, 'You\'re using FallbackResultBravadoResponseMock without a callable ' \
'fallback_result. Either provide a callable or use BravadoResponseMock.'
self._fallback_result = fallback_result(self._exception)
self._fallback_result = fallback_result(self._exception) if callable(fallback_result) else fallback_result
self._metadata._swagger_result = self._fallback_result
return self

Expand Down
52 changes: 40 additions & 12 deletions docs/source/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ attribute to access the incoming response:
.. code-block:: python
petstore = SwaggerClient.from_url(
'http://petstore.swagger.io/swagger.json',
'http://petstore.swagger.io/v2/swagger.json',
config={'also_return_response': True},
)
pet_response = petstore.pet.getPetById(petId=42).response()
Expand All @@ -169,25 +169,53 @@ By default, if the server returns an error or doesn't respond in time, you have
the resulting exception accordingly. A simpler way would be to use the support for fallback results
provided by :meth:`.HttpFuture.response`.

:meth:`.HttpFuture.response` takes an optional argument ``fallback_result`` which is a callable
that returns a Swagger result. The callable takes one mandatory argument: the exception that would
have been raised normally. This allows you to return different results based on the type of error
(e.g. a :class:`.BravadoTimeoutError`) or, if a server response was received, on any data pertaining
to that response, like the HTTP status code.

In the simplest case, you can just specify what you're going to return:
:meth:`.HttpFuture.response` takes an optional argument ``fallback_result`` which is the fallback
Swagger result to return in case of errors:

.. code-block:: python
petstore = SwaggerClient.from_url('http://petstore.swagger.io/swagger.json')
petstore = SwaggerClient.from_url('http://petstore.swagger.io/v2/swagger.json')
response = petstore.pet.findPetsByStatus(status=['available']).response(
timeout=0.5,
fallback_result=lambda e: [],
fallback_result=[],
)
This code will return an empty list in case the server doesn't respond quickly enough (or it
responded quickly enough, but returned an error).

Handling error types differently
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Sometimes, you might want to treat timeout errors differently from server errors. To do this you may
pass in a callable as ``fallback_result`` argument. The callable takes one mandatory argument: the exception
that would have been raised normally. This allows you to return different results based on the type of error
(e.g. a :class:`.BravadoTimeoutError`) or, if a server response was received, on any data pertaining
to that response, like the HTTP status code. Subclasses of :class:`.HTTPError` have a ``response`` attribute
that provides access to that data.

.. code-block:: python
def pet_status_fallback(exc):
if isinstance(exc, BravadoTimeoutError):
# Backend is slow, return last cached response
return pet_status_cache
# Some server issue, let's not show any pets
return []
petstore = SwaggerClient.from_url(
'http://petstore.swagger.io/v2/swagger.json',
# The petstore result for this call is not spec compliant...
config={'validate_responses': False},
)
response = petstore.pet.findPetsByStatus(status=['available']).response(
timeout=0.5,
fallback_result=pet_status_fallback,
)
if not response.metadata.is_fallback_result:
pet_status_cache = response.result
Customizing which error types to handle
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand All @@ -206,10 +234,10 @@ to return one as well from your fallback_result function to stay compatible with

.. code-block:: python
petstore = SwaggerClient.from_url('http://petstore.swagger.io/swagger.json')
petstore = SwaggerClient.from_url('http://petstore.swagger.io/v2/swagger.json')
response = petstore.pet.getPetById(petId=101).response(
timeout=0.5,
fallback_result=lambda e: petstore.get_model('Pet')(name='No Pet found', photoUrls=[]),
fallback_result=petstore.get_model('Pet')(name='No Pet found', photoUrls=[]),
)
Two things to note here: first, use :meth:`.SwaggerClient.get_model` to get the model class for a
Expand Down
2 changes: 1 addition & 1 deletion docs/source/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ First of all, let's define the code we'd like to test:
def get_available_pet_photos():
petstore = SwaggerClient.from_url(
'http://petstore.swagger.io/swagger.json',
'http://petstore.swagger.io/v2/swagger.json',
)
pets = petstore.pet.findPetsByStatus(status=['available']).response(
timeout=0.5,
Expand Down
16 changes: 16 additions & 0 deletions tests/http_future/HttpFuture/response_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,28 @@ def fallback_result():
return mock.Mock(name='fallback result')


@pytest.mark.parametrize(
'fallback_result',
(None, False, [], (), object()),
)
def test_fallback_result(fallback_result, mock_future_adapter, mock_operation, http_future):
mock_future_adapter.result.side_effect = BravadoTimeoutError()
mock_operation.swagger_spec.config = {
'bravado': BravadoConfig.from_config_dict({'disable_fallback_results': False})
}

response = http_future.response(fallback_result=fallback_result)

assert response.result is fallback_result
assert response.metadata.is_fallback_result is True


def test_fallback_result_callable(fallback_result, mock_future_adapter, mock_operation, http_future):
mock_future_adapter.result.side_effect = BravadoTimeoutError()
mock_operation.swagger_spec.config = {
'bravado': BravadoConfig.from_config_dict({'disable_fallback_results': False})
}

response = http_future.response(fallback_result=lambda e: fallback_result)

assert response.result == fallback_result
Expand Down
25 changes: 14 additions & 11 deletions tests/testing/response_mocks_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

@pytest.fixture
def mock_result():
return mock.Mock(name='mock result')
return mock.NonCallableMock(name='mock result')


@pytest.fixture
Expand Down Expand Up @@ -53,6 +53,15 @@ def test_bravado_response_custom_metadata(mock_result, mock_metadata):


def test_fallback_result_bravado_response(mock_result):
response_mock = FallbackResultBravadoResponseMock()
response = response_mock(fallback_result=mock_result)

assert response.result is mock_result
assert isinstance(response.metadata, BravadoResponseMetadata)
assert response.metadata._swagger_result is mock_result


def test_fallback_result_bravado_response_callable(mock_result):
exception = HTTPServerError(mock.Mock('incoming response', status_code=500))

def handle_fallback_result(exc):
Expand All @@ -68,20 +77,14 @@ def handle_fallback_result(exc):


def test_fallback_result_bravado_response_custom_metadata(mock_result, mock_metadata):
exception = HTTPServerError(mock.Mock('incoming response', status_code=500))

def handle_fallback_result(exc):
assert exc is exception
return mock_result

response_mock = FallbackResultBravadoResponseMock(exception, metadata=mock_metadata)
response = response_mock(fallback_result=handle_fallback_result)
response_mock = FallbackResultBravadoResponseMock(metadata=mock_metadata)
response = response_mock(fallback_result=mock_result)

assert response.metadata is mock_metadata
assert response.metadata._swagger_result is mock_result


def test_fallback_result_without_callable():
def test_fallback_result_response_without_fallback_result():
response_mock = FallbackResultBravadoResponseMock()
with pytest.raises(AssertionError):
response_mock(fallback_result={})
response_mock()

0 comments on commit 714f9d4

Please sign in to comment.