From f7dc1c3313424e4cd23338f6fcf4bf2943540bb2 Mon Sep 17 00:00:00 2001 From: Brad Pitcher Date: Thu, 21 Apr 2016 17:00:37 -0700 Subject: [PATCH 1/8] safely refresh tokens in parallel environment --- fitapp/migrations/0006_auto_20160421_1204.py | 26 ++++++ fitapp/models.py | 3 + fitapp/tasks.py | 98 ++++++++++++++++---- fitapp/tests/base.py | 26 ++++-- fitapp/tests/test_integration.py | 55 +++++++++-- fitapp/tests/test_retrieval.py | 80 ++++++++++++++-- fitapp/utils.py | 42 +++++++-- fitapp/views.py | 22 +---- requirements/base.txt | 1 + 9 files changed, 285 insertions(+), 68 deletions(-) create mode 100644 fitapp/migrations/0006_auto_20160421_1204.py diff --git a/fitapp/migrations/0006_auto_20160421_1204.py b/fitapp/migrations/0006_auto_20160421_1204.py new file mode 100644 index 0000000..4083496 --- /dev/null +++ b/fitapp/migrations/0006_auto_20160421_1204.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('fitapp', '0005_upgrade_oauth1_tokens_to_oauth2'), + ] + + operations = [ + migrations.AddField( + model_name='userfitbit', + name='expires_at', + field=models.FloatField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='userfitbit', + name='timezone', + field=models.CharField(default='UTC', max_length=128), + preserve_default=False, + ), + ] diff --git a/fitapp/models.py b/fitapp/models.py index cff0110..3af8010 100644 --- a/fitapp/models.py +++ b/fitapp/models.py @@ -13,6 +13,9 @@ class UserFitbit(models.Model): access_token = models.TextField() auth_secret = models.TextField() refresh_token = models.TextField() + # We will store the timestamp float number as it comes from Fitbit here + expires_at = models.FloatField() + timezone = models.CharField(max_length=128) def __str__(self): return self.user.__str__() diff --git a/fitapp/tasks.py b/fitapp/tasks.py index 46ad9d3..00bd895 100644 --- a/fitapp/tasks.py +++ b/fitapp/tasks.py @@ -12,7 +12,20 @@ logger = logging.getLogger(__name__) -LOCK_EXPIRE = 60 * 5 # Lock expires in 5 minutes +LOCK_EXPIRE = 60 * 5 # Lock expires in 5 minutes + + +def _hit_rate_limit(exc, task): + # We have hit the rate limit for the user, retry when it's reset, + # according to the reply from the failing API call + logger.debug('Rate limit reached, will try again in %s seconds' % + exc.retry_after_secs) + raise task.retry(exc=exc, countdown=exc.retry_after_secs) + + +def _generic_task_exception(exc, task_name): + logger.exception("Exception running task %s: %s" % (task_name, exc)) + raise Reject(exc, requeue=False) @shared_task @@ -24,10 +37,10 @@ def subscribe(fitbit_user, subscriber_id): fb = utils.create_fitbit(**fbuser.get_user_data()) try: fb.subscription(fbuser.user.id, subscriber_id) - except: - exc = sys.exc_info()[1] - logger.exception("Error subscribing user: %s" % exc) - raise Reject(exc, requeue=False) + except HTTPTooManyRequests: + _hit_rate_limit(sys.exc_info()[1], subscribe) + except Exception: + _generic_task_exception(sys.exc_info()[1], 'subscribe') @shared_task @@ -40,11 +53,10 @@ def unsubscribe(*args, **kwargs): if sub['ownerId'] == kwargs['user_id']: fb.subscription(sub['subscriptionId'], sub['subscriberId'], method="DELETE") - except: - exc = sys.exc_info()[1] - logger.exception("Error unsubscribing user: %s" % exc) - raise Reject(exc, requeue=False) - + except HTTPTooManyRequests: + _hit_rate_limit(sys.exc_info()[1], unsubscribe) + except Exception: + _generic_task_exception(sys.exc_info()[1], 'unsubscribe') @shared_task @@ -83,12 +95,7 @@ def get_time_series_data(fitbit_user, cat, resource, date=None): # Release the lock cache.delete(lock_id) except HTTPTooManyRequests: - # We have hit the rate limit for the user, retry when it's reset, - # according to the reply from the failing API call - e = sys.exc_info()[1] - logger.debug('Rate limit reached, will try again in %s seconds' % - e.retry_after_secs) - raise get_time_series_data.retry(exc=e, countdown=e.retry_after_secs) + _hit_rate_limit(sys.exc_info()[1], get_time_series_data) except HTTPBadRequest: # If the resource is elevation or floors, we are just getting this # error because the data doesn't exist for this user, so we can ignore @@ -98,6 +105,59 @@ def get_time_series_data(fitbit_user, cat, resource, date=None): logger.exception("Exception updating data: %s" % exc) raise Reject(exc, requeue=False) except Exception: - exc = sys.exc_info()[1] - logger.exception("Exception updating data: %s" % exc) - raise Reject(exc, requeue=False) + _generic_task_exception(sys.exc_info()[1], 'get_time_series_data') + + +@shared_task +def update_user_timezone(fitbit_user): + """ Get the user's profile and update the timezone we have on file """ + + fbusers = UserFitbit.objects.filter(fitbit_user=fitbit_user) + try: + for fbuser in fbusers: + fb = utils.create_fitbit(**fbuser.get_user_data()) + profile = fb.user_profile_get() + fbuser.timezone = profile['user']['timezone'] + fbuser.save() + utils.check_for_new_token(fbuser, fb.client.token) + except HTTPTooManyRequests: + _hit_rate_limit(sys.exc_info()[1], update_user_timezone) + except Exception: + _generic_task_exception(sys.exc_info()[1], 'update_user_timezone') + + +@shared_task +def create_fitbit_user(user_id, token): + """ Create the fitbit user and retrieve data for it """ + try: + fb = utils.create_fitbit(access_token=token['access_token'], + refresh_token=token['refresh_token']) + profile = fb.user_profile_get() + if UserFitbit.objects.filter(user_id=user_id).exists(): + fbuser = UserFitbit.objects.get(user_id=user_id) + fbuser.expires_at = token['expires_at'] + else: + fbuser = UserFitbit.objects.create( + user_id=user_id, expires_at=token['expires_at']) + fbuser.access_token = token['access_token'] + fbuser.fitbit_user = token['user_id'] + fbuser.refresh_token = token['refresh_token'] + fbuser.timezone = profile['user']['timezone'] + fbuser.save() + + if utils.get_setting('FITAPP_SUBSCRIBE'): + SUBSCRIBER_ID = utils.get_setting('FITAPP_SUBSCRIBER_ID') + subscribe.apply_async( + (token['user_id'], SUBSCRIBER_ID), countdown=5) + # Create tasks for all data in all data types + for i, _type in enumerate(TimeSeriesDataType.objects.all()): + # Delay execution for a few seconds to speed up response + # Offset each call by 5 seconds so they don't bog down the + # server + get_time_series_data.apply_async( + (token['user_id'], _type.category, _type.resource,), + countdown=10 + (i * 5)) + except HTTPTooManyRequests: + _hit_rate_limit(sys.exc_info()[1], create_fitbit_user) + except Exception: + _generic_task_exception(sys.exc_info()[1], 'create_fitbit_user') diff --git a/fitapp/tests/base.py b/fitapp/tests/base.py index 981cc03..c95be9f 100644 --- a/fitapp/tests/base.py +++ b/fitapp/tests/base.py @@ -1,6 +1,15 @@ -from mock import patch, Mock import django +import json import random + + +from datetime import datetime +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.test import TestCase +from fitbit.api import Fitbit +from mock import patch, Mock + try: from urllib.parse import urlencode from string import ascii_letters @@ -9,12 +18,6 @@ from urllib import urlencode from string import letters as ascii_letters -from django.contrib.auth.models import User -from django.core.urlresolvers import reverse -from django.test import TestCase - -from fitbit.api import Fitbit - from fitapp.models import UserFitbit @@ -25,6 +28,7 @@ def __init__(self, **kwargs): self.client_secret = kwargs.get('client_secret', 'S12345Secret') self.access_token = kwargs.get('access_token', None) self.refresh_token = kwargs.get('refresh_token', None) + self.make_request_resp = kwargs.get('make_request_resp', {}) self.error = kwargs.get('error', None) def authorize_token_url(self, *args, **kwargs): @@ -37,6 +41,7 @@ def fetch_access_token(self, *args, **kwargs): token = { 'user_id': self.user_id, 'refresh_token': self.refresh_token, + 'expires_at': 1461103848.405841, 'token_type': 'Bearer', 'scope': ['weight', 'sleep', 'heartrate', 'activity'] } @@ -47,7 +52,7 @@ def fetch_access_token(self, *args, **kwargs): def make_request(self, *args, **kwargs): response = Mock() response.status_code = 204 - response.content = "{}".encode('utf8') + response.content = json.dumps(self.make_request_resp).encode('utf8') return response @@ -83,7 +88,10 @@ def create_userfitbit(self, **kwargs): 'fitbit_user': kwargs.pop('fitbit_user', self.random_string(25)), 'access_token': self.random_string(25), 'auth_secret': self.random_string(25), - 'refresh_token': self.random_string(25) + 'refresh_token': self.random_string(25), + # Set the token to expire on 2016-4-18 11:24:08.405841 + 'expires_at': 1461003848.405841, + 'timezone': 'America/Los_Angeles' } defaults.update(kwargs) return UserFitbit.objects.create(**defaults) diff --git a/fitapp/tests/test_integration.py b/fitapp/tests/test_integration.py index e88e7c7..5e33fff 100644 --- a/fitapp/tests/test_integration.py +++ b/fitapp/tests/test_integration.py @@ -9,7 +9,7 @@ from fitapp import utils from fitapp.decorators import fitbit_integration_warning from fitapp.models import UserFitbit, TimeSeriesDataType -from fitapp.tasks import subscribe, unsubscribe +from fitapp.tasks import create_fitbit_user, subscribe, unsubscribe from .base import FitappTestBase @@ -136,23 +136,42 @@ class TestCompleteView(FitappTestBase): token = { 'access_token': 'AccessToken123', 'refresh_token': 'RefreshToken123', + 'expires_at': 1461103848.405841, 'user_id': user_id } + fetch_token = { + 'user_id': 'userid', + 'access_token': 'AccessToken123', + 'refresh_token': 'RefreshToken123', + 'expires_at': 1461103848.405841, + 'token_type': 'Bearer', + 'scope': ['weight', 'sleep', 'heartrate', 'activity'] + } code = 'Code123' def setUp(self): super(TestCompleteView, self).setUp() self.fbuser.delete() + @patch('fitapp.tasks.create_fitbit_user.apply_async') @patch('fitapp.tasks.subscribe.apply_async') @patch('fitapp.tasks.get_time_series_data.apply_async') - def test_complete(self, tsd_apply_async, sub_apply_async): + def test_complete(self, tsd_apply_async, sub_apply_async, cfu_apply_async): """Complete view should fetch & store user's access credentials.""" + def side_effect(args_tuple, countdown=1): + create_fitbit_user(*args_tuple) + cfu_apply_async.side_effect = side_effect + profile = {'user': {'timezone': 'America/Los_Angeles'}} + client_kwargs = dict(self.token.items() + [ + ('make_request_resp', profile,) + ]) response = self._mock_client( - client_kwargs=self.token, get_kwargs={'code': self.code}) + client_kwargs=client_kwargs, get_kwargs={'code': self.code}) self.assertRedirectsNoFollow( response, utils.get_setting('FITAPP_LOGIN_REDIRECT')) fbuser = UserFitbit.objects.get() + cfu_apply_async.assert_called_once_with( + (fbuser.user.id, self.fetch_token,), countdown=1) sub_apply_async.assert_called_once_with( (fbuser.fitbit_user, settings.FITAPP_SUBSCRIBER_ID), countdown=5) tsdts = TimeSeriesDataType.objects.all() @@ -168,7 +187,8 @@ def test_complete(self, tsd_apply_async, sub_apply_async): @patch('fitapp.tasks.subscribe.apply_async') @patch('fitapp.tasks.get_time_series_data.apply_async') - def test_complete_already_integrated(self, tsd_apply_async, sub_apply_async): + def test_complete_already_integrated(self, tsd_apply_async, + sub_apply_async): """ Complete view redirect to the error view if a user attempts to connect an already integrated fitbit user to a second user. @@ -192,15 +212,24 @@ def test_unauthenticated(self): self.assertEqual(response.status_code, 302) self.assertEqual(UserFitbit.objects.count(), 0) + @patch('fitapp.tasks.create_fitbit_user.apply_async') @patch('fitapp.tasks.subscribe.apply_async') @patch('fitapp.tasks.get_time_series_data.apply_async') - def test_next(self, tsd_apply_async, sub_apply_async): + def test_next(self, tsd_apply_async, sub_apply_async, cfu_apply_async): """ Complete view should redirect to session['fitbit_next'] if available. """ self._set_session_vars(fitbit_next='/test') + + def side_effect(args_tuple, countdown=1): + create_fitbit_user(*args_tuple) + cfu_apply_async.side_effect = side_effect + profile = {'user': {'timezone': 'America/Los_Angeles'}} + client_kwargs = dict(self.token.items() + [ + ('make_request_resp', profile,) + ]) response = self._mock_client( - client_kwargs=self.token, get_kwargs={'code': self.code}) + client_kwargs=client_kwargs, get_kwargs={'code': self.code}) self.assertRedirectsNoFollow(response, '/test') fbuser = UserFitbit.objects.get() sub_apply_async.assert_called_once_with( @@ -241,14 +270,24 @@ def test_no_access_token(self): self.assertRedirectsNoFollow(response, reverse('fitbit-error')) self.assertEqual(UserFitbit.objects.count(), 0) + @patch('fitapp.tasks.create_fitbit_user.apply_async') @patch('fitapp.tasks.subscribe.apply_async') @patch('fitapp.tasks.get_time_series_data.apply_async') - def test_integrated(self, tsd_apply_async, sub_apply_async): + def test_integrated(self, tsd_apply_async, sub_apply_async, + cfu_apply_async): """Complete view should overwrite existing credentials for this user. """ self.fbuser = self.create_userfitbit(user=self.user) + + def side_effect(args_tuple, countdown=1): + create_fitbit_user(*args_tuple) + cfu_apply_async.side_effect = side_effect + profile = {'user': {'timezone': 'America/Los_Angeles'}} + client_kwargs = dict(self.token.items() + [ + ('make_request_resp', profile,) + ]) response = self._mock_client( - client_kwargs=self.token, get_kwargs={'code': self.code}) + client_kwargs=client_kwargs, get_kwargs={'code': self.code}) fbuser = UserFitbit.objects.get() sub_apply_async.assert_called_with( (fbuser.fitbit_user, settings.FITAPP_SUBSCRIBER_ID), countdown=5) diff --git a/fitapp/tests/test_retrieval.py b/fitapp/tests/test_retrieval.py index 2cc0a20..1ddc17a 100644 --- a/fitapp/tests/test_retrieval.py +++ b/fitapp/tests/test_retrieval.py @@ -4,6 +4,7 @@ import json import sys +from datetime import datetime from dateutil import parser from django.core.cache import cache from django.core.files.uploadedfile import InMemoryUploadedFile @@ -13,7 +14,6 @@ from mock import MagicMock, patch from fitbit import exceptions as fitbit_exceptions -from fitbit.api import Fitbit from fitapp import utils from fitapp.models import UserFitbit, TimeSeriesData, TimeSeriesDataType @@ -36,16 +36,30 @@ def setUp(self): self.base_date = '2012-06-01' self.end_date = None - @patch.object(Fitbit, 'time_series') - def _mock_time_series(self, time_series=None, error=None, response=None, - error_attrs={}): + @patch('fitapp.utils.create_fitbit') + def _mock_time_series(self, create_fitbit, error=None, response=None, + error_attrs={}, access_token=None, + refresh_token=None, expires_at=None): + fitbit = MagicMock() + fitbit.time_series = MagicMock() if error: exc = error(self._error_response()) for k, v in error_attrs.items(): setattr(exc, k, v) - time_series.side_effect = exc + fitbit.time_series.side_effect = exc elif response: - time_series.return_value = response + fitbit.time_series.return_value = response + client = MagicMock() + client.token = self.fbuser.get_user_data() + if access_token and refresh_token: + client.token.update({ + 'access_token': access_token, + 'refresh_token': refresh_token + }) + if expires_at: + client.token['expires_at'] = expires_at + fitbit.client = client + create_fitbit.return_value = fitbit resource_type = TimeSeriesDataType.objects.get( category=TimeSeriesDataType.activities, resource='steps') return utils.get_fitbit_data( @@ -104,6 +118,59 @@ def test_retrieval(self): steps = self._mock_time_series(response=response) self.assertEqual(steps, response['activities-steps']) + def test_expired_token(self): + """ + get_fitbit_data should save updated token if it has changed. + To prevent an old refresh_token from being re-saved on the UserFitbit + model due to a race condition in celery events, we check that the + expiration date in the returned token is greater than the model's + expiration date and greater than now before saving. This tests that + as well. + """ + userfitbit = UserFitbit.objects.all()[0] + self.assertEqual(userfitbit.access_token, self.fbuser.access_token) + self.assertEqual(userfitbit.refresh_token, self.fbuser.refresh_token) + response = {'activities-steps': [1, 2, 3]} + kwargs = { + 'response': response, + 'access_token': 'new_at', + 'refresh_token': 'new_rt' + } + + # Check that when the new expiration date is less than the old one, we + # don't update the model. Current expires_at is + # 2016-4-18 11:24:08.405841 + self.assertEqual(userfitbit.expires_at, 1461003848.405841) + kwargs['expires_at'] = 1460899999.405841 # 2016-4-17 6:33:19.405841 + steps = self._mock_time_series(**kwargs) + self.assertEqual(steps, response['activities-steps']) + userfitbit = UserFitbit.objects.all()[0] + self.assertEqual(userfitbit.access_token, self.fbuser.access_token) + self.assertEqual(userfitbit.refresh_token, self.fbuser.refresh_token) + + # Check that when expires_at is less than now, we don't update the + # model + with freeze_time('2016-04-22'): + # 2016-4-20 18:57:38.405841 + kwargs['expires_at'] = 1461203858.405841 + steps = self._mock_time_series(**kwargs) + self.assertEqual(steps, response['activities-steps']) + userfitbit = UserFitbit.objects.all()[0] + self.assertEqual(userfitbit.access_token, self.fbuser.access_token) + self.assertEqual(userfitbit.refresh_token, + self.fbuser.refresh_token) + + # Now that `now` is sufficiently in the past, we update the model with + # the new tokens + with freeze_time('2016-04-19'): + # 2016-4-20 18:57:38.405841 + kwargs['expires_at'] = 1461203858.405841 + steps = self._mock_time_series(**kwargs) + self.assertEqual(steps, response['activities-steps']) + userfitbit = UserFitbit.objects.all()[0] + self.assertEqual(userfitbit.access_token, 'new_at') + self.assertEqual(userfitbit.refresh_token, 'new_rt') + class TestRetrievalTask(FitappTestBase): def setUp(self): @@ -187,6 +254,7 @@ def test_subscription_update_too_many(self, get_fitbit_data): self.fbuser.fitbit_user, _type, self.date) exc = fitbit_exceptions.HTTPTooManyRequests(self._error_response()) exc.retry_after_secs = 21 + def side_effect(*args, **kwargs): # Delete the cache lock after the first try and adjust the # get_fitbit_data mock to be successful diff --git a/fitapp/utils.py b/fitapp/utils.py index 6f56393..929f916 100644 --- a/fitapp/utils.py +++ b/fitapp/utils.py @@ -1,10 +1,13 @@ +import pytz + +from datetime import datetime from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from django.utils.timezone import localtime, make_aware from fitbit import Fitbit -from . import defaults -from .models import UserFitbit +from . import defaults, models def create_fitbit(consumer_key=None, consumer_secret=None, **kwargs): @@ -36,7 +39,7 @@ def is_integrated(user): :param user: A Django User. """ if user.is_authenticated() and user.is_active: - return UserFitbit.objects.filter(user=user).exists() + return models.UserFitbit.objects.filter(user=user).exists() return False @@ -68,15 +71,36 @@ def get_fitbit_data(fbuser, resource_type, base_date=None, period=None, period=period, base_date=base_date, end_date=end_date) - # Update the token if necessary. We are making sure we have a valid - # access_token and refresh_token next time we request Fitbit data - if fb.client.token['access_token'] != fbuser.access_token: - fbuser.access_token = fb.client.token['access_token'] - fbuser.refresh_token = fb.client.token['refresh_token'] - fbuser.save() + check_for_new_token(fbuser, fb.client.token) return data[resource_path.replace('/', '-')] +def check_for_new_token(fbuser, token): + """ + Update the token if necessary. We are making sure we have a valid + access_token and refresh_token next time we request Fitbit data + """ + fb = create_fitbit(**fbuser.get_user_data()) + if fb.client.token['access_token'] != fbuser.access_token: + expires_at = token.get('expires_at', None) + if expires_at and expires_at > fbuser.expires_at: + # We've compared the expires_at float values sent by fitbit, now + # let's check that the timezone aware expires_at datetime is + # greater than now in the fitbit user's timezone + timezone = pytz.timezone(fbuser.timezone) + expires_at_dt = make_aware(datetime.fromtimestamp(expires_at), + timezone) + utc_now = make_aware(datetime.utcnow(), pytz.timezone('UTC')) + if expires_at_dt > localtime(utc_now, timezone): + fbuser.access_token = fb.client.token['access_token'] + fbuser.refresh_token = fb.client.token['refresh_token'] + fbuser.expires_at = expires_at + fbuser.save() + from .tasks import update_user_timezone + update_user_timezone.apply_async( + (fbuser.fitbit_user,), countdown=1) + + def get_setting(name, use_defaults=True): """Retrieves the specified setting from the settings file. diff --git a/fitapp/views.py b/fitapp/views.py index f3873aa..44c82c9 100644 --- a/fitapp/views.py +++ b/fitapp/views.py @@ -1,5 +1,6 @@ import simplejson as json +from datetime import datetime from dateutil import parser from dateutil.relativedelta import relativedelta from django.contrib.auth.decorators import login_required @@ -20,7 +21,7 @@ from . import forms from . import utils from .models import UserFitbit, TimeSeriesData, TimeSeriesDataType -from .tasks import get_time_series_data, subscribe, unsubscribe +from .tasks import create_fitbit_user, get_time_series_data, unsubscribe @login_required @@ -83,33 +84,20 @@ def complete(request): token = fb.client.fetch_access_token(code, callback_uri) access_token = token['access_token'] fitbit_user = token['user_id'] + expires_at = token['expires_at'] except KeyError: return redirect(reverse('fitbit-error')) if UserFitbit.objects.filter(fitbit_user=fitbit_user).exists(): return redirect(reverse('fitbit-error')) - fbuser, _ = UserFitbit.objects.get_or_create(user=request.user) - fbuser.access_token = access_token - fbuser.fitbit_user = fitbit_user - fbuser.refresh_token = token['refresh_token'] - fbuser.save() - - # Add the Fitbit user info to the session - request.session['fitbit_profile'] = fb.user_profile_get() if utils.get_setting('FITAPP_SUBSCRIBE'): try: SUBSCRIBER_ID = utils.get_setting('FITAPP_SUBSCRIBER_ID') except ImproperlyConfigured: return redirect(reverse('fitbit-error')) - subscribe.apply_async((fbuser.fitbit_user, SUBSCRIBER_ID), countdown=5) - # Create tasks for all data in all data types - for i, _type in enumerate(TimeSeriesDataType.objects.all()): - # Delay execution for a few seconds to speed up response - # Offset each call by 2 seconds so they don't bog down the server - get_time_series_data.apply_async( - (fbuser.fitbit_user, _type.category, _type.resource,), - countdown=10 + (i * 5)) + + create_fitbit_user.apply_async((request.user.id, token), countdown=1) next_url = request.session.pop('fitbit_next', None) or utils.get_setting( 'FITAPP_LOGIN_REDIRECT') diff --git a/requirements/base.txt b/requirements/base.txt index 549baa7..368a575 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,4 +1,5 @@ fitbit==0.2.2 celery>=3.1.13 +pytz>=2016.3 simplejson six From 7e9f9ba633178cc87e1e10a934324ce0a4991326 Mon Sep 17 00:00:00 2001 From: Brad Pitcher Date: Fri, 22 Apr 2016 08:12:44 -0700 Subject: [PATCH 2/8] fix problem queueing tasks test --- fitapp/tests/test_retrieval.py | 6 +++--- fitapp/views.py | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/fitapp/tests/test_retrieval.py b/fitapp/tests/test_retrieval.py index 1ddc17a..feb8a25 100644 --- a/fitapp/tests/test_retrieval.py +++ b/fitapp/tests/test_retrieval.py @@ -279,10 +279,10 @@ def side_effect(*args, **kwargs): self.assertEqual(TimeSeriesData.objects.count(), 1) self.assertEqual(TimeSeriesData.objects.get().value, '34') - def test_problem_queueing_task(self): - get_time_series_data = MagicMock() + @patch('fitapp.tasks.get_time_series_data.apply_async') + def test_problem_queueing_task(self, gtsd_apply_async): # If queueing the task raises an exception, it doesn't propagate - get_time_series_data.apply_async.side_effect = Exception + gtsd_apply_async.side_effect = Exception try: self._receive_fitbit_updates() except: diff --git a/fitapp/views.py b/fitapp/views.py index 44c82c9..a265e05 100644 --- a/fitapp/views.py +++ b/fitapp/views.py @@ -221,6 +221,10 @@ def update(request): countdown=(2 * i)) except (KeyError, ValueError, OverflowError): raise Http404 + except Exception: + # Unexpected error, ignore and return 204 so fitbit doesn't disable + # our subscriber + pass return HttpResponse(status=204) elif request.method == 'GET': From efe782a7d96951ff95ba1f12ca78b59ab3cff44d Mon Sep 17 00:00:00 2001 From: Brad Pitcher Date: Fri, 22 Apr 2016 09:54:26 -0700 Subject: [PATCH 3/8] fix token expiration test --- fitapp/tests/test_retrieval.py | 28 +++++++++++++++++----------- fitapp/utils.py | 2 ++ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/fitapp/tests/test_retrieval.py b/fitapp/tests/test_retrieval.py index feb8a25..2c7e1ce 100644 --- a/fitapp/tests/test_retrieval.py +++ b/fitapp/tests/test_retrieval.py @@ -9,6 +9,7 @@ from django.core.cache import cache from django.core.files.uploadedfile import InMemoryUploadedFile from django.core.urlresolvers import reverse +from django.test import TransactionTestCase from django.test.utils import override_settings from freezegun import freeze_time from mock import MagicMock, patch @@ -27,7 +28,7 @@ from .base import FitappTestBase -class TestRetrievalUtility(FitappTestBase): +class TestRetrievalUtility(FitappTestBase, TransactionTestCase): """Tests for the get_fitbit_data utility function.""" def setUp(self): @@ -38,10 +39,11 @@ def setUp(self): @patch('fitapp.utils.create_fitbit') def _mock_time_series(self, create_fitbit, error=None, response=None, - error_attrs={}, access_token=None, + error_attrs={}, access_token=None, profile_mock={}, refresh_token=None, expires_at=None): fitbit = MagicMock() fitbit.time_series = MagicMock() + fitbit.user_profile_get = MagicMock() if error: exc = error(self._error_response()) for k, v in error_attrs.items(): @@ -49,6 +51,7 @@ def _mock_time_series(self, create_fitbit, error=None, response=None, fitbit.time_series.side_effect = exc elif response: fitbit.time_series.return_value = response + fitbit.user_profile_get.return_value = profile_mock client = MagicMock() client.token = self.fbuser.get_user_data() if access_token and refresh_token: @@ -134,7 +137,8 @@ def test_expired_token(self): kwargs = { 'response': response, 'access_token': 'new_at', - 'refresh_token': 'new_rt' + 'refresh_token': 'new_rt', + 'profile_mock': {'user': {'timezone': 'America/Denver'}} } # Check that when the new expiration date is less than the old one, we @@ -147,6 +151,7 @@ def test_expired_token(self): userfitbit = UserFitbit.objects.all()[0] self.assertEqual(userfitbit.access_token, self.fbuser.access_token) self.assertEqual(userfitbit.refresh_token, self.fbuser.refresh_token) + self.assertEqual(userfitbit.timezone, 'America/Los_Angeles') # Check that when expires_at is less than now, we don't update the # model @@ -155,10 +160,10 @@ def test_expired_token(self): kwargs['expires_at'] = 1461203858.405841 steps = self._mock_time_series(**kwargs) self.assertEqual(steps, response['activities-steps']) - userfitbit = UserFitbit.objects.all()[0] - self.assertEqual(userfitbit.access_token, self.fbuser.access_token) - self.assertEqual(userfitbit.refresh_token, - self.fbuser.refresh_token) + userfitbit = UserFitbit.objects.all()[0] + self.assertEqual(userfitbit.access_token, self.fbuser.access_token) + self.assertEqual(userfitbit.refresh_token, self.fbuser.refresh_token) + self.assertEqual(userfitbit.timezone, 'America/Los_Angeles') # Now that `now` is sufficiently in the past, we update the model with # the new tokens @@ -166,10 +171,11 @@ def test_expired_token(self): # 2016-4-20 18:57:38.405841 kwargs['expires_at'] = 1461203858.405841 steps = self._mock_time_series(**kwargs) - self.assertEqual(steps, response['activities-steps']) - userfitbit = UserFitbit.objects.all()[0] - self.assertEqual(userfitbit.access_token, 'new_at') - self.assertEqual(userfitbit.refresh_token, 'new_rt') + self.assertEqual(steps, response['activities-steps']) + userfitbit = UserFitbit.objects.all()[0] + self.assertEqual(userfitbit.access_token, 'new_at') + self.assertEqual(userfitbit.refresh_token, 'new_rt') + self.assertEqual(userfitbit.timezone, 'America/Denver') class TestRetrievalTask(FitappTestBase): diff --git a/fitapp/utils.py b/fitapp/utils.py index 929f916..f293134 100644 --- a/fitapp/utils.py +++ b/fitapp/utils.py @@ -3,6 +3,7 @@ from datetime import datetime from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from django.db import transaction from django.utils.timezone import localtime, make_aware from fitbit import Fitbit @@ -48,6 +49,7 @@ def get_valid_periods(): return ['1d', '7d', '30d', '1w', '1m', '3m', '6m', '1y', 'max'] +@transaction.atomic() def get_fitbit_data(fbuser, resource_type, base_date=None, period=None, end_date=None): """Creates a Fitbit API instance and retrieves step data for the period. From 3e23eb271c46c26aa8da93ca4c86430bbb8faf1d Mon Sep 17 00:00:00 2001 From: Brad Pitcher Date: Fri, 22 Apr 2016 14:51:27 -0700 Subject: [PATCH 4/8] iron out issues from the refactor --- fitapp/tasks.py | 52 ++++++---------------------- fitapp/tests/test_integration.py | 59 ++++++++++---------------------- fitapp/utils.py | 37 ++++++++++---------- fitapp/views.py | 37 +++++++++----------- 4 files changed, 63 insertions(+), 122 deletions(-) diff --git a/fitapp/tasks.py b/fitapp/tasks.py index 00bd895..dd7d7fe 100644 --- a/fitapp/tasks.py +++ b/fitapp/tasks.py @@ -30,10 +30,9 @@ def _generic_task_exception(exc, task_name): @shared_task def subscribe(fitbit_user, subscriber_id): - """ Subscribe to the user's fitbit data """ - - fbusers = UserFitbit.objects.filter(fitbit_user=fitbit_user) - for fbuser in fbusers: + """ Subscribe the user and retrieve historical data for it """ + update_user_timezone.apply_async((fitbit_user,), countdown=1) + for fbuser in UserFitbit.objects.filter(fitbit_user=fitbit_user): fb = utils.create_fitbit(**fbuser.get_user_data()) try: fb.subscription(fbuser.user.id, subscriber_id) @@ -42,6 +41,14 @@ def subscribe(fitbit_user, subscriber_id): except Exception: _generic_task_exception(sys.exc_info()[1], 'subscribe') + # Create tasks for all data in all data types + for i, _type in enumerate(TimeSeriesDataType.objects.all()): + # Delay execution for a few seconds to speed up response Offset each + # call by 5 seconds so they don't bog down the server + get_time_series_data.apply_async( + (fitbit_user, _type.category, _type.resource,), + countdown=10 + (i * 5)) + @shared_task def unsubscribe(*args, **kwargs): @@ -124,40 +131,3 @@ def update_user_timezone(fitbit_user): _hit_rate_limit(sys.exc_info()[1], update_user_timezone) except Exception: _generic_task_exception(sys.exc_info()[1], 'update_user_timezone') - - -@shared_task -def create_fitbit_user(user_id, token): - """ Create the fitbit user and retrieve data for it """ - try: - fb = utils.create_fitbit(access_token=token['access_token'], - refresh_token=token['refresh_token']) - profile = fb.user_profile_get() - if UserFitbit.objects.filter(user_id=user_id).exists(): - fbuser = UserFitbit.objects.get(user_id=user_id) - fbuser.expires_at = token['expires_at'] - else: - fbuser = UserFitbit.objects.create( - user_id=user_id, expires_at=token['expires_at']) - fbuser.access_token = token['access_token'] - fbuser.fitbit_user = token['user_id'] - fbuser.refresh_token = token['refresh_token'] - fbuser.timezone = profile['user']['timezone'] - fbuser.save() - - if utils.get_setting('FITAPP_SUBSCRIBE'): - SUBSCRIBER_ID = utils.get_setting('FITAPP_SUBSCRIBER_ID') - subscribe.apply_async( - (token['user_id'], SUBSCRIBER_ID), countdown=5) - # Create tasks for all data in all data types - for i, _type in enumerate(TimeSeriesDataType.objects.all()): - # Delay execution for a few seconds to speed up response - # Offset each call by 5 seconds so they don't bog down the - # server - get_time_series_data.apply_async( - (token['user_id'], _type.category, _type.resource,), - countdown=10 + (i * 5)) - except HTTPTooManyRequests: - _hit_rate_limit(sys.exc_info()[1], create_fitbit_user) - except Exception: - _generic_task_exception(sys.exc_info()[1], 'create_fitbit_user') diff --git a/fitapp/tests/test_integration.py b/fitapp/tests/test_integration.py index 5e33fff..20f1b2f 100644 --- a/fitapp/tests/test_integration.py +++ b/fitapp/tests/test_integration.py @@ -9,7 +9,7 @@ from fitapp import utils from fitapp.decorators import fitbit_integration_warning from fitapp.models import UserFitbit, TimeSeriesDataType -from fitapp.tasks import create_fitbit_user, subscribe, unsubscribe +from fitapp.tasks import subscribe, unsubscribe from .base import FitappTestBase @@ -153,27 +153,17 @@ def setUp(self): super(TestCompleteView, self).setUp() self.fbuser.delete() - @patch('fitapp.tasks.create_fitbit_user.apply_async') - @patch('fitapp.tasks.subscribe.apply_async') + @patch('fitapp.tasks.update_user_timezone.apply_async') @patch('fitapp.tasks.get_time_series_data.apply_async') - def test_complete(self, tsd_apply_async, sub_apply_async, cfu_apply_async): + def test_complete(self, tsd_apply_async, uut_apply_async): """Complete view should fetch & store user's access credentials.""" - def side_effect(args_tuple, countdown=1): - create_fitbit_user(*args_tuple) - cfu_apply_async.side_effect = side_effect - profile = {'user': {'timezone': 'America/Los_Angeles'}} - client_kwargs = dict(self.token.items() + [ - ('make_request_resp', profile,) - ]) response = self._mock_client( - client_kwargs=client_kwargs, get_kwargs={'code': self.code}) + client_kwargs=self.token, get_kwargs={'code': self.code}) self.assertRedirectsNoFollow( response, utils.get_setting('FITAPP_LOGIN_REDIRECT')) fbuser = UserFitbit.objects.get() - cfu_apply_async.assert_called_once_with( - (fbuser.user.id, self.fetch_token,), countdown=1) - sub_apply_async.assert_called_once_with( - (fbuser.fitbit_user, settings.FITAPP_SUBSCRIBER_ID), countdown=5) + uut_apply_async.assert_called_once_with( + (fbuser.fitbit_user,), countdown=1) tsdts = TimeSeriesDataType.objects.all() self.assertEqual(tsd_apply_async.call_count, tsdts.count()) for i, _type in enumerate(tsdts): @@ -185,10 +175,10 @@ def side_effect(args_tuple, countdown=1): self.assertEqual(fbuser.refresh_token, self.token['refresh_token']) self.assertEqual(fbuser.fitbit_user, self.user_id) - @patch('fitapp.tasks.subscribe.apply_async') + @patch('fitapp.tasks.update_user_timezone.apply_async') @patch('fitapp.tasks.get_time_series_data.apply_async') def test_complete_already_integrated(self, tsd_apply_async, - sub_apply_async): + uut_apply_async): """ Complete view redirect to the error view if a user attempts to connect an already integrated fitbit user to a second user. @@ -202,7 +192,7 @@ def test_complete_already_integrated(self, tsd_apply_async, client_kwargs=self.token, get_kwargs={'code': self.code}) self.assertRedirectsNoFollow(response, reverse('fitbit-error')) self.assertEqual(UserFitbit.objects.all().count(), 1) - self.assertEqual(sub_apply_async.call_count, 0) + self.assertEqual(uut_apply_async.call_count, 0) self.assertEqual(tsd_apply_async.call_count, 0) def test_unauthenticated(self): @@ -212,18 +202,14 @@ def test_unauthenticated(self): self.assertEqual(response.status_code, 302) self.assertEqual(UserFitbit.objects.count(), 0) - @patch('fitapp.tasks.create_fitbit_user.apply_async') - @patch('fitapp.tasks.subscribe.apply_async') + @patch('fitapp.tasks.update_user_timezone.apply_async') @patch('fitapp.tasks.get_time_series_data.apply_async') - def test_next(self, tsd_apply_async, sub_apply_async, cfu_apply_async): + def test_next(self, tsd_apply_async, uut_apply_async): """ Complete view should redirect to session['fitbit_next'] if available. """ self._set_session_vars(fitbit_next='/test') - def side_effect(args_tuple, countdown=1): - create_fitbit_user(*args_tuple) - cfu_apply_async.side_effect = side_effect profile = {'user': {'timezone': 'America/Los_Angeles'}} client_kwargs = dict(self.token.items() + [ ('make_request_resp', profile,) @@ -232,8 +218,8 @@ def side_effect(args_tuple, countdown=1): client_kwargs=client_kwargs, get_kwargs={'code': self.code}) self.assertRedirectsNoFollow(response, '/test') fbuser = UserFitbit.objects.get() - sub_apply_async.assert_called_once_with( - (fbuser.fitbit_user, settings.FITAPP_SUBSCRIBER_ID), countdown=5) + uut_apply_async.assert_called_once_with( + (fbuser.fitbit_user,), countdown=1) self.assertEqual( tsd_apply_async.call_count, TimeSeriesDataType.objects.count()) self.assertEqual(fbuser.user, self.user) @@ -270,27 +256,18 @@ def test_no_access_token(self): self.assertRedirectsNoFollow(response, reverse('fitbit-error')) self.assertEqual(UserFitbit.objects.count(), 0) - @patch('fitapp.tasks.create_fitbit_user.apply_async') - @patch('fitapp.tasks.subscribe.apply_async') + @patch('fitapp.tasks.update_user_timezone.apply_async') @patch('fitapp.tasks.get_time_series_data.apply_async') - def test_integrated(self, tsd_apply_async, sub_apply_async, - cfu_apply_async): + def test_integrated(self, tsd_apply_async, uut_apply_async): """Complete view should overwrite existing credentials for this user. """ self.fbuser = self.create_userfitbit(user=self.user) - def side_effect(args_tuple, countdown=1): - create_fitbit_user(*args_tuple) - cfu_apply_async.side_effect = side_effect - profile = {'user': {'timezone': 'America/Los_Angeles'}} - client_kwargs = dict(self.token.items() + [ - ('make_request_resp', profile,) - ]) response = self._mock_client( - client_kwargs=client_kwargs, get_kwargs={'code': self.code}) + client_kwargs=self.token, get_kwargs={'code': self.code}) fbuser = UserFitbit.objects.get() - sub_apply_async.assert_called_with( - (fbuser.fitbit_user, settings.FITAPP_SUBSCRIBER_ID), countdown=5) + uut_apply_async.assert_called_with( + (fbuser.fitbit_user,), countdown=1) self.assertEqual(tsd_apply_async.call_count, TimeSeriesDataType.objects.count()) self.assertEqual(fbuser.user, self.user) diff --git a/fitapp/utils.py b/fitapp/utils.py index f293134..b4dfe7a 100644 --- a/fitapp/utils.py +++ b/fitapp/utils.py @@ -82,25 +82,24 @@ def check_for_new_token(fbuser, token): Update the token if necessary. We are making sure we have a valid access_token and refresh_token next time we request Fitbit data """ - fb = create_fitbit(**fbuser.get_user_data()) - if fb.client.token['access_token'] != fbuser.access_token: - expires_at = token.get('expires_at', None) - if expires_at and expires_at > fbuser.expires_at: - # We've compared the expires_at float values sent by fitbit, now - # let's check that the timezone aware expires_at datetime is - # greater than now in the fitbit user's timezone - timezone = pytz.timezone(fbuser.timezone) - expires_at_dt = make_aware(datetime.fromtimestamp(expires_at), - timezone) - utc_now = make_aware(datetime.utcnow(), pytz.timezone('UTC')) - if expires_at_dt > localtime(utc_now, timezone): - fbuser.access_token = fb.client.token['access_token'] - fbuser.refresh_token = fb.client.token['refresh_token'] - fbuser.expires_at = expires_at - fbuser.save() - from .tasks import update_user_timezone - update_user_timezone.apply_async( - (fbuser.fitbit_user,), countdown=1) + expires_at = token.get('expires_at', None) + if expires_at and expires_at > fbuser.expires_at: + # We've compared the expires_at float values sent by fitbit, now let's + # check that the timezone aware expires_at datetime is greater than now + # in the fitbit user's timezone + timezone = pytz.timezone(fbuser.timezone) + expires_at_local = make_aware(datetime.fromtimestamp(expires_at), + timezone) + utc_now = make_aware(datetime.utcnow(), pytz.timezone('UTC')) + if expires_at_local > localtime(utc_now, timezone): + print('w00t! Updating with refreshed token!') + fbuser.access_token = token['access_token'] + fbuser.refresh_token = token['refresh_token'] + fbuser.expires_at = expires_at + fbuser.save() + from .tasks import update_user_timezone + update_user_timezone.apply_async( + (fbuser.fitbit_user,), countdown=1) def get_setting(name, use_defaults=True): diff --git a/fitapp/views.py b/fitapp/views.py index a265e05..f373530 100644 --- a/fitapp/views.py +++ b/fitapp/views.py @@ -1,3 +1,4 @@ +import pytz import simplejson as json from datetime import datetime @@ -21,7 +22,7 @@ from . import forms from . import utils from .models import UserFitbit, TimeSeriesData, TimeSeriesDataType -from .tasks import create_fitbit_user, get_time_series_data, unsubscribe +from .tasks import subscribe, get_time_series_data, unsubscribe @login_required @@ -91,34 +92,26 @@ def complete(request): if UserFitbit.objects.filter(fitbit_user=fitbit_user).exists(): return redirect(reverse('fitbit-error')) + UserFitbit.objects.update_or_create(user_id=request.user.id, defaults={ + 'fitbit_user': fitbit_user, + 'access_token': access_token, + 'refresh_token': token['refresh_token'], + 'expires_at': expires_at, + 'timezone': 'UTC' + }) + if utils.get_setting('FITAPP_SUBSCRIBE'): try: SUBSCRIBER_ID = utils.get_setting('FITAPP_SUBSCRIBER_ID') + subscribe.apply_async((fitbit_user, SUBSCRIBER_ID,), countdown=1) except ImproperlyConfigured: return redirect(reverse('fitbit-error')) - create_fitbit_user.apply_async((request.user.id, token), countdown=1) - next_url = request.session.pop('fitbit_next', None) or utils.get_setting( 'FITAPP_LOGIN_REDIRECT') return redirect(next_url) -@receiver(user_logged_in) -def create_fitbit_session(sender, request, user, **kwargs): - """ If the user is a fitbit user, update the profile in the session. """ - - if user.is_authenticated() and utils.is_integrated(user) and \ - user.is_active: - fbuser = UserFitbit.objects.filter(user=user) - if fbuser.exists(): - fb = utils.create_fitbit(**fbuser[0].get_user_data()) - try: - request.session['fitbit_profile'] = fb.user_profile_get() - except: - pass - - @login_required def error(request): """ @@ -255,9 +248,11 @@ def normalize_date_range(request, fitbit_data): base_date = fitbit_data['base_date'] if base_date == 'today': now = timezone.now() - if 'fitbit_profile' in request.session.keys(): - tz = request.session['fitbit_profile']['user']['timezone'] - now = timezone.pytz.timezone(tz).normalize(timezone.now()) + fbuser = UserFitbit.objects.filter(user_id=request.user.id) + if fbuser.exists(): + tz = pytz.timezone(fbuser[0].timezone) + utc_now = timezone.make_aware(datetime.utcnow(), pytz.utc) + now = timezone.localtime(utc_now, tz) base_date = now.date().strftime('%Y-%m-%d') result['date__gte'] = base_date From 4c5e220c42e24552ff51cb4d94a552e5a0325294 Mon Sep 17 00:00:00 2001 From: Brad Pitcher Date: Fri, 22 Apr 2016 15:00:48 -0700 Subject: [PATCH 5/8] drop django 1.4 and python 2.6 support --- .travis.yml | 3 --- setup.py | 1 - tox.ini | 5 ++--- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4863a0d..57dd03a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,6 @@ env: #- TOX_ENV=pypy-1.9.X #- TOX_ENV=pypy-1.8.X #- TOX_ENV=pypy-1.7.X - #- TOX_ENV=pypy-1.4.X - TOX_ENV=py35-trunk - TOX_ENV=py35-1.9.X - TOX_ENV=py34-trunk @@ -18,8 +17,6 @@ env: - TOX_ENV=py27-1.9.X - TOX_ENV=py27-1.8.X - TOX_ENV=py27-1.7.X - - TOX_ENV=py27-1.4.X - - TOX_ENV=py26-1.4.X install: - pip install coveralls - pip install tox diff --git a/setup.py b/setup.py index 33afdb1..348e18f 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,6 @@ "Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", diff --git a/tox.ini b/tox.ini index 55b55bd..e1d92f9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,9 @@ [tox] -envlist = pypy-1.9.X,pypy-1.8.X,pypy-1.7.X,pypy-1.4.X, +envlist = pypy-1.9.X,pypy-1.8.X,pypy-1.7.X py35-trunk,py35-1.9.X, py34-trunk,py34-1.9.X,py34-1.8.X,py34-1.7.X, py33-1.8.X,py33-1.7.X, - py27-1.9.X,py27-1.8.X,py27-1.7.X,py27-1.4.X, - py26-1.4.X + py27-1.9.X,py27-1.8.X,py27-1.7.X [testenv] commands = {envpython} run_tests.py From 97f5b8e81e80fc9feea8b58deeef2324cab843aa Mon Sep 17 00:00:00 2001 From: Brad Pitcher Date: Fri, 22 Apr 2016 15:01:02 -0700 Subject: [PATCH 6/8] remove debugging statement --- fitapp/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fitapp/utils.py b/fitapp/utils.py index b4dfe7a..0ce3f14 100644 --- a/fitapp/utils.py +++ b/fitapp/utils.py @@ -92,7 +92,6 @@ def check_for_new_token(fbuser, token): timezone) utc_now = make_aware(datetime.utcnow(), pytz.timezone('UTC')) if expires_at_local > localtime(utc_now, timezone): - print('w00t! Updating with refreshed token!') fbuser.access_token = token['access_token'] fbuser.refresh_token = token['refresh_token'] fbuser.expires_at = expires_at From 802d7a026a97b1333ee236525fc12b15d0bab7ad Mon Sep 17 00:00:00 2001 From: Brad Pitcher Date: Fri, 22 Apr 2016 15:01:13 -0700 Subject: [PATCH 7/8] fix tests on python 3 --- fitapp/tests/test_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fitapp/tests/test_integration.py b/fitapp/tests/test_integration.py index 20f1b2f..1f21949 100644 --- a/fitapp/tests/test_integration.py +++ b/fitapp/tests/test_integration.py @@ -211,7 +211,7 @@ def test_next(self, tsd_apply_async, uut_apply_async): self._set_session_vars(fitbit_next='/test') profile = {'user': {'timezone': 'America/Los_Angeles'}} - client_kwargs = dict(self.token.items() + [ + client_kwargs = dict(list(self.token.items()) + [ ('make_request_resp', profile,) ]) response = self._mock_client( From 89df0b5a5a8ea1578ed238601a5c68bc83b93531 Mon Sep 17 00:00:00 2001 From: Brad Pitcher Date: Fri, 22 Apr 2016 15:36:49 -0700 Subject: [PATCH 8/8] give the migration a human readable name --- ..._auto_20160421_1204.py => 0006_add_expires_timezone_fields.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename fitapp/migrations/{0006_auto_20160421_1204.py => 0006_add_expires_timezone_fields.py} (100%) diff --git a/fitapp/migrations/0006_auto_20160421_1204.py b/fitapp/migrations/0006_add_expires_timezone_fields.py similarity index 100% rename from fitapp/migrations/0006_auto_20160421_1204.py rename to fitapp/migrations/0006_add_expires_timezone_fields.py