diff --git a/docs/source/adapter_class.rst b/docs/source/adapter_class.rst index f2f860c..2aeb94e 100644 --- a/docs/source/adapter_class.rst +++ b/docs/source/adapter_class.rst @@ -15,6 +15,14 @@ This should contain the base URL that will be concatenated with the resource map For more information about the ``serializer_class`` attribute, read the :doc:`serializers documentation `. +.. attribute:: refresh_token_by_default + +The default value for this attribute is ```False```. +If set to ```True```, automatically calls ```refresh_authentication``` if ```is_authentication_expired``` returns ```True```. + +For more information about token refreshing, read the doc :doc:`buildingawrapper`. + + Methods ------- @@ -100,7 +108,12 @@ In this example, the object list is enclosed in the ``data`` attribute. .. method:: is_authentication_expired(self, exception, *args, **kwargs) -Given an exception, should returne if the authentication is expired. If so, tapioca will call ``refresh_authentication``. After ``refresh_authentication`` is done, and if ``refresh_auth`` was passed to the HTTP method being called, it will retry the request with the new keys. +Given an exception, checks if the authentication has expired or not. If so and ```refresh_token_by_default=True``` or +the HTTP method was called with ```refresh_token=True```, then it will automatically call ```refresh_authentication``` +method and retry the original request. + +If not implemented, ```is_authentication_expired``` will assume ```False```, ```refresh_token_by_default``` also +defaults to ```False``` in the client initialization. .. method:: refresh_authentication(self, api_params, *args, **kwargs): diff --git a/docs/source/buildingawrapper.rst b/docs/source/buildingawrapper.rst index 399bae9..09dc245 100644 --- a/docs/source/buildingawrapper.rst +++ b/docs/source/buildingawrapper.rst @@ -116,14 +116,18 @@ Please refer to the :doc:`serializers ` for more information about Refreshing Authentication (optional) ------------------------------------ -You can implement the ```refresh_authentication``` and ```is_authentication_expired``` methods in your Tapioca Client to refresh your authentication token every time that it expires. -```is_authentication_expired``` receives an error object from the request method (it contains the server response and HTTP Status code). You can use it to decide if a request failed because of the token. This method should return true if the authentication is expired or false otherwise. If the authentication is expired, ```refresh_authentication``` is called automatically. +You can implement the ```refresh_authentication``` and ```is_authentication_expired``` methods in your Tapioca Client to refresh your authentication token every time it expires. + +```is_authentication_expired``` receives an error object from the request method (it contains the server response and HTTP Status code). You can use it to decide if a request failed because of the token. This method should return ```True``` if the authentication is expired or ```False``` otherwise (default behavior). + +Once these methods are implemented, the client can be instantiated with ```refresh_token_by_default=True``` (or pass +```refresh_token=True``` in HTTP calls) and ```refresh_authentication``` will be called automatically. .. code-block:: python def is_authentication_expired(self, exception, *args, **kwargs): .... - + def refresh_authentication(self, api_params, *args, **kwargs): ... diff --git a/docs/source/features.rst b/docs/source/features.rst index 31ecb23..811ec2e 100644 --- a/docs/source/features.rst +++ b/docs/source/features.rst @@ -53,7 +53,10 @@ Executors have access to make HTTP calls using the current data it possesses as Auth refreshing (\*) -------------------- -Make any HTTP call passing ``refresh_auth=True`` and in case you have an expired API token, it will automatically be refreshed and the call retried. +Some clients needs to update its token once they have expired. If the clients supports, you might instantiate it passing +```refresh_token_by_default=True``` or make any HTTP call passing ```refresh_auth=True``` (both defaults to +```False```). Note that if your client instance have ```refresh_token_by_default=True```, then you don't need to +explicity set it on HTTP calls. **TODO: add examples** diff --git a/tapioca/adapters.py b/tapioca/adapters.py index f03824b..4446d49 100644 --- a/tapioca/adapters.py +++ b/tapioca/adapters.py @@ -82,7 +82,7 @@ def get_iterator_next_request_kwargs(self, iterator_request_kwargs, raise NotImplementedError() def is_authentication_expired(self, exception, *args, **kwargs): - raise NotImplementedError() + return False def refresh_authentication(self, api_params, *args, **kwargs): raise NotImplementedError() diff --git a/tapioca/tapioca.py b/tapioca/tapioca.py index 55e6631..c4849a1 100755 --- a/tapioca/tapioca.py +++ b/tapioca/tapioca.py @@ -19,21 +19,24 @@ def __init__(self, adapter_class): self.adapter_class = adapter_class def __call__(self, serializer_class=None, **kwargs): + refresh_token_default = kwargs.pop('refresh_token_by_default', False) return TapiocaClient( self.adapter_class(serializer_class=serializer_class), - api_params=kwargs) + api_params=kwargs, refresh_token_by_default=refresh_token_default) class TapiocaClient(object): def __init__(self, api, data=None, response=None, request_kwargs=None, - api_params=None, resource=None, *args, **kwargs): + api_params=None, resource=None, refresh_token_by_default=False, + *args, **kwargs): self._api = api self._data = data self._response = response self._api_params = api_params or {} self._request_kwargs = request_kwargs self._resource = resource + self._refresh_token_default = refresh_token_by_default def _instatiate_api(self): serializer_class = None @@ -47,6 +50,7 @@ def _wrap_in_tapioca(self, data, *args, **kwargs): return TapiocaClient(self._instatiate_api(), data=data, api_params=self._api_params, request_kwargs=request_kwargs, + refresh_token_by_default=self._refresh_token_default, *args, **kwargs) def _wrap_in_tapioca_executor(self, data, *args, **kwargs): @@ -54,6 +58,7 @@ def _wrap_in_tapioca_executor(self, data, *args, **kwargs): return TapiocaClientExecutor(self._instatiate_api(), data=data, api_params=self._api_params, request_kwargs=request_kwargs, + refresh_token_by_default=self._refresh_token_default, *args, **kwargs) def _get_doc(self): @@ -201,7 +206,7 @@ def response(self): def status_code(self): return self.response.status_code - def _make_request(self, request_method, refresh_auth=False, *args, **kwargs): + def _make_request(self, request_method, refresh_token=None, *args, **kwargs): if 'url' not in kwargs: kwargs['url'] = self._data @@ -214,9 +219,12 @@ def _make_request(self, request_method, refresh_auth=False, *args, **kwargs): data = self._api.process_response(response) except ResponseProcessException as e: client = self._wrap_in_tapioca(e.data, response=response, - request_kwargs=request_kwargs) + request_kwargs=request_kwargs) tapioca_exception = e.tapioca_exception(client=client) - if refresh_auth and self._api.is_authentication_expired(tapioca_exception): + + should_refresh_token = (refresh_token is not False and + self._refresh_token_default) + if should_refresh_token and self._api.is_authentication_expired(tapioca_exception): self._api.refresh_authentication(self._api_params) return self._make_request(request_method, *args, **kwargs) else: @@ -260,11 +268,11 @@ def pages(self, max_pages=None, max_items=None, **kwargs): item_count = 0 while iterator_list: - if self._reached_max_limits(page_count, item_count, max_pages, + if self._reached_max_limits(page_count, item_count, max_pages, max_items): break for item in iterator_list: - if self._reached_max_limits(page_count, item_count, max_pages, + if self._reached_max_limits(page_count, item_count, max_pages, max_items): break yield self._wrap_in_tapioca(item) diff --git a/tests/test_tapioca.py b/tests/test_tapioca.py index 37eb8f2..5261fa7 100755 --- a/tests/test_tapioca.py +++ b/tests/test_tapioca.py @@ -4,15 +4,12 @@ import unittest import responses -import arrow import json -from decimal import Decimal from tapioca.tapioca import TapiocaClient -from tapioca.serializers import SimpleSerializer from tapioca.exceptions import ClientError -from tests.client import TesterClient, SerializerClient, TokenRefreshClient +from tests.client import TesterClient, TokenRefreshClient class TestTapiocaClient(unittest.TestCase): @@ -65,7 +62,6 @@ def test_transform_camelCase_in_snake_case(self): status=200, content_type='application/json') - response = self.wrapper.test().get() self.assertEqual(response.data.key_snake().data, 'value') @@ -74,8 +70,6 @@ def test_transform_camelCase_in_snake_case(self): @responses.activate def test_should_be_able_to_access_by_index(self): - next_url = 'http://api.teste.com/next_batch' - responses.add(responses.GET, self.wrapper.test().data, body='["a", "b", "c"]', status=200, @@ -89,8 +83,6 @@ def test_should_be_able_to_access_by_index(self): @responses.activate def test_accessing_index_out_of_bounds_should_raise_index_error(self): - next_url = 'http://api.teste.com/next_batch' - responses.add(responses.GET, self.wrapper.test().data, body='["a", "b", "c"]', status=200, @@ -103,8 +95,6 @@ def test_accessing_index_out_of_bounds_should_raise_index_error(self): @responses.activate def test_accessing_empty_list_should_raise_index_error(self): - next_url = 'http://api.teste.com/next_batch' - responses.add(responses.GET, self.wrapper.test().data, body='[]', status=200, @@ -444,35 +434,14 @@ def test_simple_pages_max_item_zero_iterator(self): self.assertEqual(iterations_count, 0) - @responses.activate - def test_simple_pages_max_item_zero_iterator(self): - next_url = 'http://api.teste.com/next_batch' - - responses.add(responses.GET, self.wrapper.test().data, - body='{"data": [{"key": "value"}], "paging": {"next": "%s"}}' % next_url, - status=200, - content_type='application/json') - - responses.add(responses.GET, next_url, - body='{"data": [{"key": "value"}], "paging": {"next": ""}}', - status=200, - content_type='application/json') - - response = self.wrapper.test().get() - - iterations_count = 0 - for item in response().pages(max_items=0): - self.assertIn(item.key().data, 'value') - iterations_count += 1 - class TestTokenRefreshing(unittest.TestCase): def setUp(self): - self.wrapper = TokenRefreshClient(token='token') + self.wrapper = TokenRefreshClient(token='token', refresh_token_by_default=True) @responses.activate - def test_not_token_refresh_ready_client_call_raises_not_implemented(self): + def test_not_token_refresh_client_propagates_client_error(self): no_refresh_client = TesterClient() responses.add_callback( @@ -481,29 +450,31 @@ def test_not_token_refresh_ready_client_call_raises_not_implemented(self): content_type='application/json', ) - with self.assertRaises(NotImplementedError): - no_refresh_client.test().post(refresh_auth=True) + with self.assertRaises(ClientError): + no_refresh_client.test().post() @responses.activate - def test_token_expired_and_no_refresh_flag(self): - responses.add(responses.POST, self.wrapper.test().data, - body='{"error": "Token expired"}', - status=401, - content_type='application/json') - with self.assertRaises(ClientError) as context: - response = self.wrapper.test().post() + def test_disable_token_refreshing(self): + responses.add_callback( + responses.POST, self.wrapper.test().data, + callback=lambda *a, **k: (401, {}, ''), + content_type='application/json', + ) + + with self.assertRaises(ClientError): + self.wrapper.test().post(refresh_token=False) @responses.activate - def test_token_expired_with_active_refresh_flag(self): + def test_token_expired_automatically_refresh_authentication(self): self.first_call = True def request_callback(request): if self.first_call: self.first_call = False - return (401, {'content_type':'application/json'}, json.dumps('{"error": "Token expired"}')) + return (401, {'content_type': 'application/json'}, json.dumps('{"error": "Token expired"}')) else: self.first_call = None - return (201, {'content_type':'application/json'}, '') + return (201, {'content_type': 'application/json'}, '') responses.add_callback( responses.POST, self.wrapper.test().data, @@ -511,7 +482,7 @@ def request_callback(request): content_type='application/json', ) - response = self.wrapper.test().post(refresh_auth=True) + response = self.wrapper.test().post() # refresh_authentication method should be able to update api_params self.assertEqual(response._api_params['token'], 'new_token')