Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion docs/source/adapter_class.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <serializers>`.

.. 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
-------

Expand Down Expand Up @@ -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):

Expand Down
10 changes: 7 additions & 3 deletions docs/source/buildingawrapper.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,18 @@ Please refer to the :doc:`serializers <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):
...
5 changes: 4 additions & 1 deletion docs/source/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down
2 changes: 1 addition & 1 deletion tapioca/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
22 changes: 15 additions & 7 deletions tapioca/tapioca.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -47,13 +50,15 @@ 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):
request_kwargs = kwargs.pop('request_kwargs', self._request_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):
Expand Down Expand Up @@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
65 changes: 18 additions & 47 deletions tests/test_tapioca.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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')
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -481,37 +450,39 @@ 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,
callback=request_callback,
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')