diff --git a/bravado/http_future.py b/bravado/http_future.py index d926bad7..505a5e10 100644 --- a/bravado/http_future.py +++ b/bravado/http_future.py @@ -33,6 +33,9 @@ ) +SENTINEL = object() + + class FutureAdapter(object): """ Mimics a :class:`concurrent.futures.Future` regardless of which client is @@ -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 @@ -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 ' @@ -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()) diff --git a/bravado/response.py b/bravado/response.py index 1963086d..0e5cd54d 100644 --- a/bravado/response.py +++ b/bravado/response.py @@ -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 """ @@ -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 diff --git a/bravado/testing/response_mocks.py b/bravado/testing/response_mocks.py index 2548f98d..264f8f5f 100644 --- a/bravado/testing/response_mocks.py +++ b/bravado/testing/response_mocks.py @@ -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 @@ -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 @@ -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 diff --git a/docs/source/advanced.rst b/docs/source/advanced.rst index 11cd6745..4807e97b 100644 --- a/docs/source/advanced.rst +++ b/docs/source/advanced.rst @@ -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() @@ -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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -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 diff --git a/docs/source/testing.rst b/docs/source/testing.rst index 5fc10d9a..6fa32669 100644 --- a/docs/source/testing.rst +++ b/docs/source/testing.rst @@ -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, diff --git a/tests/http_future/HttpFuture/response_test.py b/tests/http_future/HttpFuture/response_test.py index bb2b516e..355e0265 100644 --- a/tests/http_future/HttpFuture/response_test.py +++ b/tests/http_future/HttpFuture/response_test.py @@ -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 diff --git a/tests/testing/response_mocks_test.py b/tests/testing/response_mocks_test.py index 93b60f6e..4df9f96c 100644 --- a/tests/testing/response_mocks_test.py +++ b/tests/testing/response_mocks_test.py @@ -13,7 +13,7 @@ @pytest.fixture def mock_result(): - return mock.Mock(name='mock result') + return mock.NonCallableMock(name='mock result') @pytest.fixture @@ -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): @@ -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()