[med-svn] [python-fitbit] 01/02: Imported Upstream version 0.3.0
Dylan Aïssi
bob.dybian-guest at moszumanska.debian.org
Tue Feb 14 21:50:11 UTC 2017
This is an automated email from the git hooks/post-receive script.
bob.dybian-guest pushed a commit to branch master
in repository python-fitbit.
commit 0644d06e8e8c8413cef6eb7bacbcb2dc7b7db7bf
Author: Dylan Aïssi <bob.dybian at gmail.com>
Date: Tue Feb 14 22:50:00 2017 +0100
Imported Upstream version 0.3.0
---
.gitignore | 2 +
LICENSE | 2 +-
fitbit/__init__.py | 8 +-
fitbit/api.py | 321 ++++++++++++++++++++--------------------
fitbit/compliance.py | 26 ++++
fitbit/exceptions.py | 26 ++++
fitbit_tests/test_api.py | 62 ++++++--
fitbit_tests/test_auth.py | 192 +++++++++++++++---------
fitbit_tests/test_exceptions.py | 15 +-
gather_keys_oauth2.py | 26 ++--
requirements/base.txt | 4 +-
requirements/test.txt | 4 +-
12 files changed, 429 insertions(+), 259 deletions(-)
diff --git a/.gitignore b/.gitignore
index b629af3..8dff601 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,9 +6,11 @@
docs/_build
*.egg-info
*.egg
+.eggs
dist
build
env
+htmlcov
# Editors
.idea
diff --git a/LICENSE b/LICENSE
index eb83cdf..c9269bf 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright 2012-2015 ORCAS
+Copyright 2012-2017 ORCAS
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/fitbit/__init__.py b/fitbit/__init__.py
index be97389..a19bb4a 100644
--- a/fitbit/__init__.py
+++ b/fitbit/__init__.py
@@ -3,7 +3,7 @@
Fitbit API Library
------------------
-:copyright: 2012-2015 ORCAS.
+:copyright: 2012-2017 ORCAS.
:license: BSD, see LICENSE for more details.
"""
@@ -14,11 +14,11 @@ from .api import Fitbit, FitbitOauth2Client
__title__ = 'fitbit'
__author__ = 'Issac Kelly and ORCAS'
__author_email__ = 'bpitcher at orcasinc.com'
-__copyright__ = 'Copyright 2012-2015 ORCAS'
+__copyright__ = 'Copyright 2012-2017 ORCAS'
__license__ = 'Apache 2.0'
-__version__ = '0.2.4'
-__release__ = '0.2.4'
+__version__ = '0.3.0'
+__release__ = '0.3.0'
# Module namespace.
diff --git a/fitbit/api.py b/fitbit/api.py
index 1984135..b7e92e5 100644
--- a/fitbit/api.py
+++ b/fitbit/api.py
@@ -9,13 +9,12 @@ except ImportError:
# Python 2.x
from urllib import urlencode
-from requests_oauthlib import OAuth2, OAuth2Session
-from oauthlib.oauth2.rfc6749.errors import TokenExpiredError
-from fitbit.exceptions import (BadResponse, DeleteError, HTTPBadRequest,
- HTTPUnauthorized, HTTPForbidden,
- HTTPServerError, HTTPConflict, HTTPNotFound,
- HTTPTooManyRequests)
-from fitbit.utils import curry
+from requests.auth import HTTPBasicAuth
+from requests_oauthlib import OAuth2Session
+
+from . import exceptions
+from .compliance import fitbit_compliance_fix
+from .utils import curry
class FitbitOauth2Client(object):
@@ -28,9 +27,9 @@ class FitbitOauth2Client(object):
access_token_url = request_token_url
refresh_token_url = request_token_url
- def __init__(self, client_id, client_secret,
- access_token=None, refresh_token=None, refresh_cb=None,
- *args, **kwargs):
+ def __init__(self, client_id, client_secret, access_token=None,
+ refresh_token=None, expires_at=None, refresh_cb=None,
+ redirect_uri=None, *args, **kwargs):
"""
Create a FitbitOauth2Client object. Specify the first 7 parameters if
you have them to access user data. Specify just the first 2 parameters
@@ -40,70 +39,65 @@ class FitbitOauth2Client(object):
- access_token, refresh_token are obtained after the user grants permission
"""
- self.session = requests.Session()
- self.client_id = client_id
- self.client_secret = client_secret
- self.token = {
- 'access_token': access_token,
- 'refresh_token': refresh_token
- }
- self.refresh_cb = refresh_cb
- self.oauth = OAuth2Session(client_id)
+ self.client_id, self.client_secret = client_id, client_secret
+ token = {}
+ if access_token and refresh_token:
+ token.update({
+ 'access_token': access_token,
+ 'refresh_token': refresh_token
+ })
+ if expires_at:
+ token['expires_at'] = expires_at
+ self.session = fitbit_compliance_fix(OAuth2Session(
+ client_id,
+ auto_refresh_url=self.refresh_token_url,
+ token_updater=refresh_cb,
+ token=token,
+ redirect_uri=redirect_uri,
+ ))
+ self.timeout = kwargs.get("timeout", None)
def _request(self, method, url, **kwargs):
"""
A simple wrapper around requests.
"""
- return self.session.request(method, url, **kwargs)
+ if self.timeout is not None and 'timeout' not in kwargs:
+ kwargs['timeout'] = self.timeout
+
+ try:
+ response = self.session.request(method, url, **kwargs)
+
+ # If our current token has no expires_at, or something manages to slip
+ # through that check
+ if response.status_code == 401:
+ d = json.loads(response.content.decode('utf8'))
+ if d['errors'][0]['errorType'] == 'expired_token':
+ self.refresh_token()
+ response = self.session.request(method, url, **kwargs)
- def make_request(self, url, data={}, method=None, **kwargs):
+ return response
+ except requests.Timeout as e:
+ raise exceptions.Timeout(*e.args)
+
+ def make_request(self, url, data=None, method=None, **kwargs):
"""
Builds and makes the OAuth2 Request, catches errors
- https://wiki.fitbit.com/display/API/API+Response+Format+And+Errors
- """
- if not method:
- method = 'POST' if data else 'GET'
+ https://dev.fitbit.com/docs/oauth2/#authorization-errors
+ """
+ data = data or {}
+ method = method or ('POST' if data else 'GET')
+ response = self._request(
+ method,
+ url,
+ data=data,
+ client_id=self.client_id,
+ client_secret=self.client_secret,
+ **kwargs
+ )
+
+ exceptions.detect_and_raise_error(response)
- try:
- auth = OAuth2(client_id=self.client_id, token=self.token)
- response = self._request(method, url, data=data, auth=auth, **kwargs)
- except (HTTPUnauthorized, TokenExpiredError) as e:
- self.refresh_token()
- auth = OAuth2(client_id=self.client_id, token=self.token)
- response = self._request(method, url, data=data, auth=auth, **kwargs)
-
- # yet another token expiration check
- # (the above try/except only applies if the expired token was obtained
- # using the current instance of the class this is a a general case)
- if response.status_code == 401:
- d = json.loads(response.content.decode('utf8'))
- try:
- if(d['errors'][0]['errorType'] == 'expired_token' and
- d['errors'][0]['message'].find('Access token expired:') == 0):
- self.refresh_token()
- auth = OAuth2(client_id=self.client_id, token=self.token)
- response = self._request(method, url, data=data, auth=auth, **kwargs)
- except:
- pass
-
- if response.status_code == 401:
- raise HTTPUnauthorized(response)
- elif response.status_code == 403:
- raise HTTPForbidden(response)
- elif response.status_code == 404:
- raise HTTPNotFound(response)
- elif response.status_code == 409:
- raise HTTPConflict(response)
- elif response.status_code == 429:
- exc = HTTPTooManyRequests(response)
- exc.retry_after_secs = int(response.headers['Retry-After'])
- raise exc
-
- elif response.status_code >= 500:
- raise HTTPServerError(response)
- elif response.status_code >= 400:
- raise HTTPBadRequest(response)
return response
def authorize_token_url(self, scope=None, redirect_uri=None, **kwargs):
@@ -112,62 +106,59 @@ class FitbitOauth2Client(object):
URL, open their browser to it, or tell them to copy the URL into their
browser.
- scope: pemissions that that are being requested [default ask all]
- - redirect_uri: url to which the reponse will posted
- required only if your app does not have one
- for more info see https://wiki.fitbit.com/display/API/OAuth+2.0
- """
-
- # the scope parameter is caussing some issues when refreshing tokens
- # so not saving it
- old_scope = self.oauth.scope
- old_redirect = self.oauth.redirect_uri
- if scope:
- self.oauth.scope = scope
- else:
- self.oauth.scope = [
- "activity", "nutrition", "heartrate", "location", "nutrition",
- "profile", "settings", "sleep", "social", "weight"
- ]
+ - redirect_uri: url to which the reponse will posted. required here
+ unless you specify only one Callback URL on the fitbit app or
+ you already passed it to the constructor
+ for more info see https://dev.fitbit.com/docs/oauth2/
+ """
+
+ self.session.scope = scope or [
+ "activity",
+ "nutrition",
+ "heartrate",
+ "location",
+ "nutrition",
+ "profile",
+ "settings",
+ "sleep",
+ "social",
+ "weight",
+ ]
if redirect_uri:
- self.oauth.redirect_uri = redirect_uri
+ self.session.redirect_uri = redirect_uri
- out = self.oauth.authorization_url(self.authorization_url, **kwargs)
- self.oauth.scope = old_scope
- self.oauth.redirect_uri = old_redirect
- return(out)
+ return self.session.authorization_url(self.authorization_url, **kwargs)
- def fetch_access_token(self, code, redirect_uri):
+ def fetch_access_token(self, code, redirect_uri=None):
"""Step 2: Given the code from fitbit from step 1, call
fitbit again and returns an access token object. Extract the needed
information from that and save it to use in future API calls.
the token is internally saved
"""
- auth = OAuth2Session(self.client_id, redirect_uri=redirect_uri)
- self.token = auth.fetch_token(
+ if redirect_uri:
+ self.session.redirect_uri = redirect_uri
+ return self.session.fetch_token(
self.access_token_url,
username=self.client_id,
password=self.client_secret,
code=code)
- return self.token
-
def refresh_token(self):
"""Step 3: obtains a new access_token from the the refresh token
- obtained in step 2.
- the token is internally saved
+ obtained in step 2. Only do the refresh if there is `token_updater(),`
+ which saves the token.
"""
- self.token = self.oauth.refresh_token(
- self.refresh_token_url,
- refresh_token=self.token['refresh_token'],
- auth=requests.auth.HTTPBasicAuth(self.client_id, self.client_secret)
- )
-
- if self.refresh_cb:
- self.refresh_cb(self.token)
+ token = {}
+ if self.session.token_updater:
+ token = self.session.refresh_token(
+ self.refresh_token_url,
+ auth=HTTPBasicAuth(self.client_id, self.client_secret)
+ )
+ self.session.token_updater(token)
- return self.token
+ return token
class Fitbit(object):
@@ -237,11 +228,11 @@ class Fitbit(object):
if response.status_code == 204:
return True
else:
- raise DeleteError(response)
+ raise exceptions.DeleteError(response)
try:
rep = json.loads(response.content.decode('utf8'))
except ValueError:
- raise BadResponse
+ raise exceptions.BadResponse
return rep
@@ -255,7 +246,7 @@ class Fitbit(object):
This is not the same format that the GET comes back in, GET requests
are wrapped in {'user': <dict of user data>}
- https://wiki.fitbit.com/display/API/API-Get-User-Info
+ https://dev.fitbit.com/docs/user/
"""
url = "{0}/{1}/user/{2}/profile.json".format(*self._get_common_args(user_id))
return self.make_request(url)
@@ -269,7 +260,7 @@ class Fitbit(object):
This is not the same format that the GET comes back in, GET requests
are wrapped in {'user': <dict of user data>}
- https://wiki.fitbit.com/display/API/API-Update-User-Info
+ https://dev.fitbit.com/docs/user/#update-profile
"""
url = "{0}/{1}/user/-/profile.json".format(*self._get_common_args())
return self.make_request(url, data)
@@ -307,7 +298,7 @@ class Fitbit(object):
heart(date=None, user_id=None, data=None)
bp(date=None, user_id=None, data=None)
- * https://wiki.fitbit.com/display/API/Fitbit+Resource+Access+API
+ * https://dev.fitbit.com/docs/
"""
if not date:
@@ -368,8 +359,8 @@ class Fitbit(object):
"""
Implements the following APIs
- * https://wiki.fitbit.com/display/API/API-Get-Body-Fat
- * https://wiki.fitbit.com/display/API/API-Update-Fat-Goal
+ * https://dev.fitbit.com/docs/body/#get-body-goals
+ * https://dev.fitbit.com/docs/body/#update-body-fat-goal
Pass no arguments to get the body fat goal. Pass a ``fat`` argument
to update the body fat goal.
@@ -383,8 +374,8 @@ class Fitbit(object):
"""
Implements the following APIs
- * https://wiki.fitbit.com/display/API/API-Get-Body-Weight-Goal
- * https://wiki.fitbit.com/display/API/API-Update-Weight-Goal
+ * https://dev.fitbit.com/docs/body/#get-body-goals
+ * https://dev.fitbit.com/docs/body/#update-weight-goal
Pass no arguments to get the body weight goal. Pass ``start_date``,
``start_weight`` and optionally ``weight`` to set the weight goal.
@@ -407,10 +398,10 @@ class Fitbit(object):
def activities_daily_goal(self, calories_out=None, active_minutes=None,
floors=None, distance=None, steps=None):
"""
- Implements the following APIs
+ Implements the following APIs for period equal to daily
- https://wiki.fitbit.com/display/API/API-Get-Activity-Daily-Goals
- https://wiki.fitbit.com/display/API/API-Update-Activity-Daily-Goals
+ https://dev.fitbit.com/docs/activity/#get-activity-goals
+ https://dev.fitbit.com/docs/activity/#update-activity-goals
Pass no arguments to get the daily activities goal. Pass any one of
the optional arguments to set that component of the daily activities
@@ -434,10 +425,10 @@ class Fitbit(object):
def activities_weekly_goal(self, distance=None, floors=None, steps=None):
"""
- Implements the following APIs
+ Implements the following APIs for period equal to weekly
- https://wiki.fitbit.com/display/API/API-Get-Activity-Weekly-Goals
- https://wiki.fitbit.com/display/API/API-Update-Activity-Weekly-Goals
+ https://dev.fitbit.com/docs/activity/#get-activity-goals
+ https://dev.fitbit.com/docs/activity/#update-activity-goals
Pass no arguments to get the weekly activities goal. Pass any one of
the optional arguments to set that component of the weekly activities
@@ -456,8 +447,8 @@ class Fitbit(object):
"""
Implements the following APIs
- https://wiki.fitbit.com/display/API/API-Get-Food-Goals
- https://wiki.fitbit.com/display/API/API-Update-Food-Goals
+ https://dev.fitbit.com/docs/food-logging/#get-food-goals
+ https://dev.fitbit.com/docs/food-logging/#update-food-goal
Pass no arguments to get the food goal. Pass at least ``calories`` or
``intensity`` and optionally ``personalized`` to update the food goal.
@@ -477,8 +468,8 @@ class Fitbit(object):
"""
Implements the following APIs
- https://wiki.fitbit.com/display/API/API-Get-Water-Goal
- https://wiki.fitbit.com/display/API/API-Update-Water-Goal
+ https://dev.fitbit.com/docs/food-logging/#get-water-goal
+ https://dev.fitbit.com/docs/food-logging/#update-water-goal
Pass no arguments to get the water goal. Pass ``target`` to update it.
@@ -491,14 +482,18 @@ class Fitbit(object):
def time_series(self, resource, user_id=None, base_date='today',
period=None, end_date=None):
"""
- The time series is a LOT of methods, (documented at url below) so they
+ The time series is a LOT of methods, (documented at urls below) so they
don't get their own method. They all follow the same patterns, and
return similar formats.
Taking liberty, this assumes a base_date of today, the current user,
and a 1d period.
- https://wiki.fitbit.com/display/API/API-Get-Time-Series
+ https://dev.fitbit.com/docs/activity/#activity-time-series
+ https://dev.fitbit.com/docs/body/#body-time-series
+ https://dev.fitbit.com/docs/food-logging/#food-or-water-time-series
+ https://dev.fitbit.com/docs/heart-rate/#heart-rate-time-series
+ https://dev.fitbit.com/docs/sleep/#sleep-time-series
"""
if period and end_date:
raise TypeError("Either end_date or period can be specified, not both")
@@ -523,10 +518,10 @@ class Fitbit(object):
"""
The intraday time series extends the functionality of the regular time series, but returning data at a
more granular level for a single day, defaulting to 1 minute intervals. To access this feature, one must
- send an email to api at fitbit.com and request to have access to the Partner API
- (see https://wiki.fitbit.com/display/API/Fitbit+Partner+API). For details on the resources available, see:
+ fill out the Private Support form here (see https://dev.fitbit.com/docs/help/).
+ For details on the resources available and more information on how to get access, see:
- https://wiki.fitbit.com/display/API/API-Get-Intraday-Time-Series
+ https://dev.fitbit.com/docs/activity/#get-activity-intraday-time-series
"""
# Check that the time range is valid
@@ -537,7 +532,7 @@ class Fitbit(object):
"""
Per
- https://wiki.fitbit.com/display/API/API-Get-Intraday-Time-Series
+ https://dev.fitbit.com/docs/activity/#get-activity-intraday-time-series
the detail-level is now (OAuth 2.0 ):
either "1min" or "15min" (optional). "1sec" for heart rate.
"""
@@ -565,10 +560,10 @@ class Fitbit(object):
def activity_stats(self, user_id=None, qualifier=''):
"""
- * https://wiki.fitbit.com/display/API/API-Get-Activity-Stats
- * https://wiki.fitbit.com/display/API/API-Get-Favorite-Activities
- * https://wiki.fitbit.com/display/API/API-Get-Recent-Activities
- * https://wiki.fitbit.com/display/API/API-Get-Frequent-Activities
+ * https://dev.fitbit.com/docs/activity/#activity-types
+ * https://dev.fitbit.com/docs/activity/#get-favorite-activities
+ * https://dev.fitbit.com/docs/activity/#get-recent-activity-types
+ * https://dev.fitbit.com/docs/activity/#get-frequent-activities
This implements the following methods::
@@ -599,9 +594,9 @@ class Fitbit(object):
favorite_foods(user_id=None, qualifier='')
frequent_foods(user_id=None, qualifier='')
- * https://wiki.fitbit.com/display/API/API-Get-Recent-Foods
- * https://wiki.fitbit.com/display/API/API-Get-Frequent-Foods
- * https://wiki.fitbit.com/display/API/API-Get-Favorite-Foods
+ * https://dev.fitbit.com/docs/food-logging/#get-favorite-foods
+ * https://dev.fitbit.com/docs/food-logging/#get-frequent-foods
+ * https://dev.fitbit.com/docs/food-logging/#get-recent-foods
"""
url = "{0}/{1}/user/{2}/foods/log/{qualifier}.json".format(
*self._get_common_args(user_id),
@@ -611,7 +606,7 @@ class Fitbit(object):
def add_favorite_activity(self, activity_id):
"""
- https://wiki.fitbit.com/display/API/API-Add-Favorite-Activity
+ https://dev.fitbit.com/docs/activity/#add-favorite-activity
"""
url = "{0}/{1}/user/-/activities/favorite/{activity_id}.json".format(
*self._get_common_args(),
@@ -621,14 +616,14 @@ class Fitbit(object):
def log_activity(self, data):
"""
- https://wiki.fitbit.com/display/API/API-Log-Activity
+ https://dev.fitbit.com/docs/activity/#log-activity
"""
url = "{0}/{1}/user/-/activities.json".format(*self._get_common_args())
return self.make_request(url, data=data)
def delete_favorite_activity(self, activity_id):
"""
- https://wiki.fitbit.com/display/API/API-Delete-Favorite-Activity
+ https://dev.fitbit.com/docs/activity/#delete-favorite-activity
"""
url = "{0}/{1}/user/-/activities/favorite/{activity_id}.json".format(
*self._get_common_args(),
@@ -638,7 +633,7 @@ class Fitbit(object):
def add_favorite_food(self, food_id):
"""
- https://wiki.fitbit.com/display/API/API-Add-Favorite-Food
+ https://dev.fitbit.com/docs/food-logging/#add-favorite-food
"""
url = "{0}/{1}/user/-/foods/log/favorite/{food_id}.json".format(
*self._get_common_args(),
@@ -648,7 +643,7 @@ class Fitbit(object):
def delete_favorite_food(self, food_id):
"""
- https://wiki.fitbit.com/display/API/API-Delete-Favorite-Food
+ https://dev.fitbit.com/docs/food-logging/#delete-favorite-food
"""
url = "{0}/{1}/user/-/foods/log/favorite/{food_id}.json".format(
*self._get_common_args(),
@@ -658,28 +653,28 @@ class Fitbit(object):
def create_food(self, data):
"""
- https://wiki.fitbit.com/display/API/API-Create-Food
+ https://dev.fitbit.com/docs/food-logging/#create-food
"""
url = "{0}/{1}/user/-/foods.json".format(*self._get_common_args())
return self.make_request(url, data=data)
def get_meals(self):
"""
- https://wiki.fitbit.com/display/API/API-Get-Meals
+ https://dev.fitbit.com/docs/food-logging/#get-meals
"""
url = "{0}/{1}/user/-/meals.json".format(*self._get_common_args())
return self.make_request(url)
def get_devices(self):
"""
- https://wiki.fitbit.com/display/API/API-Get-Devices
+ https://dev.fitbit.com/docs/devices/#get-devices
"""
url = "{0}/{1}/user/-/devices.json".format(*self._get_common_args())
return self.make_request(url)
def get_alarms(self, device_id):
"""
- https://wiki.fitbit.com/display/API/API-Devices-Get-Alarms
+ https://dev.fitbit.com/docs/devices/#get-alarms
"""
url = "{0}/{1}/user/-/devices/tracker/{device_id}/alarms.json".format(
*self._get_common_args(),
@@ -691,7 +686,7 @@ class Fitbit(object):
enabled=True, label=None, snooze_length=None,
snooze_count=None, vibe='DEFAULT'):
"""
- https://wiki.fitbit.com/display/API/API-Devices-Add-Alarm
+ https://dev.fitbit.com/docs/devices/#add-alarm
alarm_time should be a timezone aware datetime object.
"""
url = "{0}/{1}/user/-/devices/tracker/{device_id}/alarms.json".format(
@@ -724,7 +719,7 @@ class Fitbit(object):
def update_alarm(self, device_id, alarm_id, alarm_time, week_days, recurring=False, enabled=True, label=None,
snooze_length=None, snooze_count=None, vibe='DEFAULT'):
"""
- https://wiki.fitbit.com/display/API/API-Devices-Update-Alarm
+ https://dev.fitbit.com/docs/devices/#update-alarm
alarm_time should be a timezone aware datetime object.
"""
# TODO Refactor with create_alarm. Tons of overlap.
@@ -759,7 +754,7 @@ class Fitbit(object):
def delete_alarm(self, device_id, alarm_id):
"""
- https://wiki.fitbit.com/display/API/API-Devices-Delete-Alarm
+ https://dev.fitbit.com/docs/devices/#delete-alarm
"""
url = "{0}/{1}/user/-/devices/tracker/{device_id}/alarms/{alarm_id}.json".format(
*self._get_common_args(),
@@ -770,7 +765,7 @@ class Fitbit(object):
def get_sleep(self, date):
"""
- https://wiki.fitbit.com/display/API/API-Get-Sleep
+ https://dev.fitbit.com/docs/sleep/#get-sleep-logs
date should be a datetime.date object.
"""
url = "{0}/{1}/user/-/sleep/date/{year}-{month}-{day}.json".format(
@@ -783,7 +778,7 @@ class Fitbit(object):
def log_sleep(self, start_time, duration):
"""
- https://wiki.fitbit.com/display/API/API-Log-Sleep
+ https://dev.fitbit.com/docs/sleep/#log-sleep
start time should be a datetime object. We will be using the year, month, day, hour, and minute.
"""
data = {
@@ -796,14 +791,14 @@ class Fitbit(object):
def activities_list(self):
"""
- https://wiki.fitbit.com/display/API/API-Browse-Activities
+ https://dev.fitbit.com/docs/activity/#browse-activity-types
"""
url = "{0}/{1}/activities.json".format(*self._get_common_args())
return self.make_request(url)
def activity_detail(self, activity_id):
"""
- https://wiki.fitbit.com/display/API/API-Get-Activity
+ https://dev.fitbit.com/docs/activity/#get-activity-type
"""
url = "{0}/{1}/activities/{activity_id}.json".format(
*self._get_common_args(),
@@ -813,7 +808,7 @@ class Fitbit(object):
def search_foods(self, query):
"""
- https://wiki.fitbit.com/display/API/API-Search-Foods
+ https://dev.fitbit.com/docs/food-logging/#search-foods
"""
url = "{0}/{1}/foods/search.json?{encoded_query}".format(
*self._get_common_args(),
@@ -823,7 +818,7 @@ class Fitbit(object):
def food_detail(self, food_id):
"""
- https://wiki.fitbit.com/display/API/API-Get-Food
+ https://dev.fitbit.com/docs/food-logging/#get-food
"""
url = "{0}/{1}/foods/{food_id}.json".format(
*self._get_common_args(),
@@ -833,14 +828,14 @@ class Fitbit(object):
def food_units(self):
"""
- https://wiki.fitbit.com/display/API/API-Get-Food-Units
+ https://dev.fitbit.com/docs/food-logging/#get-food-units
"""
url = "{0}/{1}/foods/units.json".format(*self._get_common_args())
return self.make_request(url)
def get_bodyweight(self, base_date=None, user_id=None, period=None, end_date=None):
"""
- https://wiki.fitbit.com/display/API/API-Get-Body-Weight
+ https://dev.fitbit.com/docs/body/#get-weight-logs
base_date should be a datetime.date object (defaults to today),
period can be '1d', '7d', '30d', '1w', '1m', '3m', '6m', '1y', 'max' or None
end_date should be a datetime.date object, or None.
@@ -851,7 +846,7 @@ class Fitbit(object):
def get_bodyfat(self, base_date=None, user_id=None, period=None, end_date=None):
"""
- https://wiki.fitbit.com/display/API/API-Get-Body-fat
+ https://dev.fitbit.com/docs/body/#get-body-fat-logs
base_date should be a datetime.date object (defaults to today),
period can be '1d', '7d', '30d', '1w', '1m', '3m', '6m', '1y', 'max' or None
end_date should be a datetime.date object, or None.
@@ -888,14 +883,14 @@ class Fitbit(object):
def get_friends(self, user_id=None):
"""
- https://wiki.fitbit.com/display/API/API-Get-Friends
+ https://dev.fitbit.com/docs/friends/#get-friends
"""
url = "{0}/{1}/user/{2}/friends.json".format(*self._get_common_args(user_id))
return self.make_request(url)
def get_friends_leaderboard(self, period):
"""
- https://wiki.fitbit.com/display/API/API-Get-Friends-Leaderboard
+ https://dev.fitbit.com/docs/friends/#get-friends-leaderboard
"""
if not period in ['7d', '30d']:
raise ValueError("Period must be one of '7d', '30d'")
@@ -907,7 +902,7 @@ class Fitbit(object):
def invite_friend(self, data):
"""
- https://wiki.fitbit.com/display/API/API-Create-Invite
+ https://dev.fitbit.com/docs/friends/#invite-friend
"""
url = "{0}/{1}/user/-/friends/invitations.json".format(*self._get_common_args())
return self.make_request(url, data=data)
@@ -915,20 +910,20 @@ class Fitbit(object):
def invite_friend_by_email(self, email):
"""
Convenience Method for
- https://wiki.fitbit.com/display/API/API-Create-Invite
+ https://dev.fitbit.com/docs/friends/#invite-friend
"""
return self.invite_friend({'invitedUserEmail': email})
def invite_friend_by_userid(self, user_id):
"""
Convenience Method for
- https://wiki.fitbit.com/display/API/API-Create-Invite
+ https://dev.fitbit.com/docs/friends/#invite-friend
"""
return self.invite_friend({'invitedUserId': user_id})
def respond_to_invite(self, other_user_id, accept=True):
"""
- https://wiki.fitbit.com/display/API/API-Accept-Invite
+ https://dev.fitbit.com/docs/friends/#respond-to-friend-invitation
"""
url = "{0}/{1}/user/-/friends/invitations/{user_id}.json".format(
*self._get_common_args(),
@@ -951,7 +946,7 @@ class Fitbit(object):
def get_badges(self, user_id=None):
"""
- https://wiki.fitbit.com/display/API/API-Get-Badges
+ https://dev.fitbit.com/docs/friends/#badges
"""
url = "{0}/{1}/user/{2}/badges.json".format(*self._get_common_args(user_id))
return self.make_request(url)
@@ -959,7 +954,7 @@ class Fitbit(object):
def subscription(self, subscription_id, subscriber_id, collection=None,
method='POST'):
"""
- https://wiki.fitbit.com/display/API/Fitbit+Subscriptions+API
+ https://dev.fitbit.com/docs/subscriptions/
"""
base_url = "{0}/{1}/user/-{collection}/apiSubscriptions/{end_string}.json"
kwargs = {'collection': '', 'end_string': subscription_id}
@@ -976,7 +971,7 @@ class Fitbit(object):
def list_subscriptions(self, collection=''):
"""
- https://wiki.fitbit.com/display/API/Fitbit+Subscriptions+API
+ https://dev.fitbit.com/docs/subscriptions/#getting-a-list-of-subscriptions
"""
url = "{0}/{1}/user/-{collection}/apiSubscriptions.json".format(
*self._get_common_args(),
diff --git a/fitbit/compliance.py b/fitbit/compliance.py
new file mode 100644
index 0000000..cec533b
--- /dev/null
+++ b/fitbit/compliance.py
@@ -0,0 +1,26 @@
+"""
+The Fitbit API breaks from the OAuth2 RFC standard by returning an "errors"
+object list, rather than a single "error" string. This puts hooks in place so
+that oauthlib can process an error in the results from access token and refresh
+token responses. This is necessary to prevent getting the generic red herring
+MissingTokenError.
+"""
+
+from json import loads, dumps
+
+from oauthlib.common import to_unicode
+
+
+def fitbit_compliance_fix(session):
+
+ def _missing_error(r):
+ token = loads(r.text)
+ if 'errors' in token:
+ # Set the error to the first one we have
+ token['error'] = token['errors'][0]['errorType']
+ r._content = to_unicode(dumps(token)).encode('UTF-8')
+ return r
+
+ session.register_compliance_hook('access_token_response', _missing_error)
+ session.register_compliance_hook('refresh_token_response', _missing_error)
+ return session
diff --git a/fitbit/exceptions.py b/fitbit/exceptions.py
index d6249ea..677958a 100644
--- a/fitbit/exceptions.py
+++ b/fitbit/exceptions.py
@@ -15,6 +15,13 @@ class DeleteError(Exception):
pass
+class Timeout(Exception):
+ """
+ Used when a timeout occurs.
+ """
+ pass
+
+
class HTTPException(Exception):
def __init__(self, response, *args, **kwargs):
try:
@@ -68,3 +75,22 @@ class HTTPServerError(HTTPException):
"""Generic >= 500 error
"""
pass
+
+
+def detect_and_raise_error(response):
+ if response.status_code == 401:
+ raise HTTPUnauthorized(response)
+ elif response.status_code == 403:
+ raise HTTPForbidden(response)
+ elif response.status_code == 404:
+ raise HTTPNotFound(response)
+ elif response.status_code == 409:
+ raise HTTPConflict(response)
+ elif response.status_code == 429:
+ exc = HTTPTooManyRequests(response)
+ exc.retry_after_secs = int(response.headers['Retry-After'])
+ raise exc
+ elif response.status_code >= 500:
+ raise HTTPServerError(response)
+ elif response.status_code >= 400:
+ raise HTTPBadRequest(response)
diff --git a/fitbit_tests/test_api.py b/fitbit_tests/test_api.py
index 651a189..f019d72 100644
--- a/fitbit_tests/test_api.py
+++ b/fitbit_tests/test_api.py
@@ -1,8 +1,9 @@
from unittest import TestCase
import datetime
import mock
+import requests
from fitbit import Fitbit
-from fitbit.exceptions import DeleteError
+from fitbit.exceptions import DeleteError, Timeout
URLBASE = "%s/%s/user" % (Fitbit.API_ENDPOINT, Fitbit.API_VERSION)
@@ -24,6 +25,49 @@ class TestBase(TestCase):
self.assertRaises(exc, getattr(self.fb, funcname), *args, **kwargs)
+class TimeoutTest(TestCase):
+
+ def setUp(self):
+ self.fb = Fitbit('x', 'y')
+ self.fb_timeout = Fitbit('x', 'y', timeout=10)
+
+ self.test_url = 'invalid://do.not.connect'
+
+ def test_fb_without_timeout(self):
+ with mock.patch.object(self.fb.client.session, 'request') as request:
+ mock_response = mock.Mock()
+ mock_response.status_code = 200
+ mock_response.content = b'{}'
+ request.return_value = mock_response
+ result = self.fb.make_request(self.test_url)
+
+ request.assert_called_once()
+ self.assertNotIn('timeout', request.call_args[1])
+ self.assertEqual({}, result)
+
+ def test_fb_with_timeout__timing_out(self):
+ with mock.patch.object(self.fb_timeout.client.session, 'request') as request:
+ request.side_effect = requests.Timeout('Timed out')
+ with self.assertRaisesRegexp(Timeout, 'Timed out'):
+ self.fb_timeout.make_request(self.test_url)
+
+ request.assert_called_once()
+ self.assertEqual(10, request.call_args[1]['timeout'])
+
+ def test_fb_with_timeout__not_timing_out(self):
+ with mock.patch.object(self.fb_timeout.client.session, 'request') as request:
+ mock_response = mock.Mock()
+ mock_response.status_code = 200
+ mock_response.content = b'{}'
+ request.return_value = mock_response
+
+ result = self.fb_timeout.make_request(self.test_url)
+
+ request.assert_called_once()
+ self.assertEqual(10, request.call_args[1]['timeout'])
+ self.assertEqual({}, result)
+
+
class APITest(TestBase):
"""
Tests for python-fitbit API, not directly involved in getting
@@ -210,12 +254,12 @@ class DeleteCollectionResourceTest(TestBase):
class ResourceAccessTest(TestBase):
"""
Class for testing the Fitbit Resource Access API:
- https://wiki.fitbit.com/display/API/Fitbit+Resource+Access+API
+ https://dev.fitbit.com/docs/
"""
def test_user_profile_get(self):
"""
Test getting a user profile.
- https://wiki.fitbit.com/display/API/API-Get-User-Info
+ https://dev.fitbit.com/docs/user/
Tests the following HTTP method/URLs:
GET https://api.fitbit.com/1/user/FOO/profile.json
@@ -230,7 +274,7 @@ class ResourceAccessTest(TestBase):
def test_user_profile_update(self):
"""
Test updating a user profile.
- https://wiki.fitbit.com/display/API/API-Update-User-Info
+ https://dev.fitbit.com/docs/user/#update-profile
Tests the following HTTP method/URLs:
POST https://api.fitbit.com/1/user/-/profile.json
@@ -441,7 +485,7 @@ class ResourceAccessTest(TestBase):
def test_bodyweight(self):
"""
Tests for retrieving body weight measurements.
- https://wiki.fitbit.com/display/API/API-Get-Body-Weight
+ https://dev.fitbit.com/docs/body/#get-weight-logs
Tests the following methods/URLs:
GET https://api.fitbit.com/1/user/-/body/log/weight/date/1992-05-12.json
GET https://api.fitbit.com/1/user/BAR/body/log/weight/date/1992-05-12/1998-12-31.json
@@ -483,7 +527,7 @@ class ResourceAccessTest(TestBase):
def test_bodyfat(self):
"""
Tests for retrieving bodyfat measurements.
- https://wiki.fitbit.com/display/API/API-Get-Body-Fat
+ https://dev.fitbit.com/docs/body/#get-body-fat-logs
Tests the following methods/URLs:
GET https://api.fitbit.com/1/user/-/body/log/fat/date/1992-05-12.json
GET https://api.fitbit.com/1/user/BAR/body/log/fat/date/1992-05-12/1998-12-31.json
@@ -608,7 +652,7 @@ class ResourceAccessTest(TestBase):
class SubscriptionsTest(TestBase):
"""
Class for testing the Fitbit Subscriptions API:
- https://wiki.fitbit.com/display/API/Fitbit+Subscriptions+API
+ https://dev.fitbit.com/docs/subscriptions/
"""
def test_subscriptions(self):
@@ -637,7 +681,7 @@ class SubscriptionsTest(TestBase):
class PartnerAPITest(TestBase):
"""
Class for testing the Fitbit Partner API:
- https://wiki.fitbit.com/display/API/Fitbit+Partner+API
+ https://dev.fitbit.com/docs/
"""
def _test_intraday_timeseries(self, resource, base_date, detail_level,
@@ -652,7 +696,7 @@ class PartnerAPITest(TestBase):
def test_intraday_timeseries(self):
"""
Intraday Time Series tests:
- https://wiki.fitbit.com/display/API/API-Get-Intraday-Time-Series
+ https://dev.fitbit.com/docs/activity/#get-activity-intraday-time-series
Tests the following methods/URLs:
GET https://api.fitbit.com/1/user/-/FOO/date/1918-05-11/1d/1min.json
diff --git a/fitbit_tests/test_auth.py b/fitbit_tests/test_auth.py
index c7395d2..6bf7ab7 100644
--- a/fitbit_tests/test_auth.py
+++ b/fitbit_tests/test_auth.py
@@ -1,8 +1,15 @@
-from unittest import TestCase
-from fitbit import Fitbit, FitbitOauth2Client
-from fitbit.exceptions import HTTPUnauthorized
+import copy
+import json
import mock
-from requests_oauthlib import OAuth2Session
+import requests_mock
+
+from datetime import datetime
+from freezegun import freeze_time
+from oauthlib.oauth2.rfc6749.errors import InvalidGrantError
+from requests.auth import _basic_auth_str
+from unittest import TestCase
+
+from fitbit import Fitbit
class Auth2Test(TestCase):
@@ -14,7 +21,7 @@ class Auth2Test(TestCase):
client_kwargs = {
'client_id': 'fake_id',
'client_secret': 'fake_secret',
- 'callback_uri': 'fake_callback_url',
+ 'redirect_uri': 'http://127.0.0.1:8080',
'scope': ['fake_scope1']
}
@@ -22,107 +29,154 @@ class Auth2Test(TestCase):
# authorize_token_url calls oauth and returns a URL
fb = Fitbit(**self.client_kwargs)
retval = fb.client.authorize_token_url()
- self.assertEqual(retval[0], 'https://www.fitbit.com/oauth2/authorize?response_type=code&client_id=fake_id&scope=activity+nutrition+heartrate+location+nutrition+profile+settings+sleep+social+weight&state='+retval[1])
+ self.assertEqual(retval[0], 'https://www.fitbit.com/oauth2/authorize?response_type=code&client_id=fake_id&redirect_uri=http%3A%2F%2F127.0.0.1%3A8080&scope=activity+nutrition+heartrate+location+nutrition+profile+settings+sleep+social+weight&state='+retval[1])
- def test_authorize_token_url_with_parameters(self):
+ def test_authorize_token_url_with_scope(self):
# authorize_token_url calls oauth and returns a URL
fb = Fitbit(**self.client_kwargs)
- retval = fb.client.authorize_token_url(
- scope=self.client_kwargs['scope'],
- callback_uri=self.client_kwargs['callback_uri'])
- self.assertEqual(retval[0], 'https://www.fitbit.com/oauth2/authorize?response_type=code&client_id=fake_id&scope='+ str(self.client_kwargs['scope'][0])+ '&state='+retval[1]+'&callback_uri='+self.client_kwargs['callback_uri'])
+ retval = fb.client.authorize_token_url(scope=self.client_kwargs['scope'])
+ self.assertEqual(retval[0], 'https://www.fitbit.com/oauth2/authorize?response_type=code&client_id=fake_id&redirect_uri=http%3A%2F%2F127.0.0.1%3A8080&scope='+ str(self.client_kwargs['scope'][0])+ '&state='+retval[1])
def test_fetch_access_token(self):
# tests the fetching of access token using code and redirect_URL
fb = Fitbit(**self.client_kwargs)
fake_code = "fake_code"
- with mock.patch.object(OAuth2Session, 'fetch_token') as fat:
- fat.return_value = {
+ with requests_mock.mock() as m:
+ m.post(fb.client.access_token_url, text=json.dumps({
'access_token': 'fake_return_access_token',
'refresh_token': 'fake_return_refresh_token'
- }
- retval = fb.client.fetch_access_token(fake_code, self.client_kwargs['callback_uri'])
+ }))
+ retval = fb.client.fetch_access_token(fake_code)
self.assertEqual("fake_return_access_token", retval['access_token'])
self.assertEqual("fake_return_refresh_token", retval['refresh_token'])
def test_refresh_token(self):
# test of refresh function
- kwargs = self.client_kwargs
+ kwargs = copy.copy(self.client_kwargs)
kwargs['access_token'] = 'fake_access_token'
kwargs['refresh_token'] = 'fake_refresh_token'
+ kwargs['refresh_cb'] = lambda x: None
fb = Fitbit(**kwargs)
- with mock.patch.object(OAuth2Session, 'refresh_token') as rt:
- rt.return_value = {
+ with requests_mock.mock() as m:
+ m.post(fb.client.refresh_token_url, text=json.dumps({
'access_token': 'fake_return_access_token',
'refresh_token': 'fake_return_refresh_token'
- }
+ }))
retval = fb.client.refresh_token()
self.assertEqual("fake_return_access_token", retval['access_token'])
self.assertEqual("fake_return_refresh_token", retval['refresh_token'])
- def test_auto_refresh_token_exception(self):
- """Test of auto_refresh with Unauthorized exception"""
+ @freeze_time(datetime.fromtimestamp(1483563319))
+ def test_auto_refresh_expires_at(self):
+ """Test of auto_refresh with expired token"""
# 1. first call to _request causes a HTTPUnauthorized
# 2. the token_refresh call is faked
# 3. the second call to _request returns a valid value
refresh_cb = mock.MagicMock()
- kwargs = self.client_kwargs
- kwargs['access_token'] = 'fake_access_token'
- kwargs['refresh_token'] = 'fake_refresh_token'
- kwargs['refresh_cb'] = refresh_cb
+ kwargs = copy.copy(self.client_kwargs)
+ kwargs.update({
+ 'access_token': 'fake_access_token',
+ 'refresh_token': 'fake_refresh_token',
+ 'expires_at': 1483530000,
+ 'refresh_cb': refresh_cb,
+ })
fb = Fitbit(**kwargs)
- with mock.patch.object(FitbitOauth2Client, '_request') as r:
- r.side_effect = [
- HTTPUnauthorized(fake_response(401, b'correct_response')),
- fake_response(200, 'correct_response')
- ]
- with mock.patch.object(OAuth2Session, 'refresh_token') as rt:
- rt.return_value = {
- 'access_token': 'fake_return_access_token',
- 'refresh_token': 'fake_return_refresh_token'
- }
- retval = fb.client.make_request(Fitbit.API_ENDPOINT + '/1/user/-/profile.json')
- self.assertEqual("correct_response", retval.text)
- self.assertEqual(
- "fake_return_access_token", fb.client.token['access_token'])
+ profile_url = Fitbit.API_ENDPOINT + '/1/user/-/profile.json'
+ with requests_mock.mock() as m:
+ m.get(
+ profile_url,
+ text='{"user":{"aboutMe": "python-fitbit developer"}}',
+ status_code=200
+ )
+ token = {
+ 'access_token': 'fake_return_access_token',
+ 'refresh_token': 'fake_return_refresh_token',
+ 'expires_at': 1483570000,
+ }
+ m.post(fb.client.refresh_token_url, text=json.dumps(token))
+ retval = fb.make_request(profile_url)
+
+ self.assertEqual(m.request_history[0].path, '/oauth2/token')
self.assertEqual(
- "fake_return_refresh_token", fb.client.token['refresh_token'])
- self.assertEqual(1, rt.call_count)
- self.assertEqual(2, r.call_count)
- refresh_cb.assert_called_once_with(rt.return_value)
-
- def test_auto_refresh_token_non_exception(self):
- """Test of auto_refersh when the exception doesn't fire"""
- # 1. first call to _request causes a 401 expired token response
+ m.request_history[0].headers['Authorization'],
+ _basic_auth_str(
+ self.client_kwargs['client_id'],
+ self.client_kwargs['client_secret']
+ )
+ )
+ self.assertEqual(retval['user']['aboutMe'], "python-fitbit developer")
+ self.assertEqual("fake_return_access_token", token['access_token'])
+ self.assertEqual("fake_return_refresh_token", token['refresh_token'])
+ refresh_cb.assert_called_once_with(token)
+
+ def test_auto_refresh_token_exception(self):
+ """Test of auto_refresh with Unauthorized exception"""
+ # 1. first call to _request causes a HTTPUnauthorized
# 2. the token_refresh call is faked
# 3. the second call to _request returns a valid value
refresh_cb = mock.MagicMock()
- kwargs = self.client_kwargs
- kwargs['access_token'] = 'fake_access_token'
- kwargs['refresh_token'] = 'fake_refresh_token'
- kwargs['refresh_cb'] = refresh_cb
+ kwargs = copy.copy(self.client_kwargs)
+ kwargs.update({
+ 'access_token': 'fake_access_token',
+ 'refresh_token': 'fake_refresh_token',
+ 'refresh_cb': refresh_cb,
+ })
fb = Fitbit(**kwargs)
- with mock.patch.object(FitbitOauth2Client, '_request') as r:
- r.side_effect = [
- fake_response(401, b'{"errors": [{"message": "Access token expired: some_token_goes_here", "errorType": "expired_token", "fieldName": "access_token"}]}'),
- fake_response(200, 'correct_response')
- ]
- with mock.patch.object(OAuth2Session, 'refresh_token') as rt:
- rt.return_value = {
- 'access_token': 'fake_return_access_token',
- 'refresh_token': 'fake_return_refresh_token'
- }
- retval = fb.client.make_request(Fitbit.API_ENDPOINT + '/1/user/-/profile.json')
- self.assertEqual("correct_response", retval.text)
- self.assertEqual(
- "fake_return_access_token", fb.client.token['access_token'])
+ profile_url = Fitbit.API_ENDPOINT + '/1/user/-/profile.json'
+ with requests_mock.mock() as m:
+ m.get(profile_url, [{
+ 'text': json.dumps({
+ "errors": [{
+ "errorType": "expired_token",
+ "message": "Access token expired:"
+ }]
+ }),
+ 'status_code': 401
+ }, {
+ 'text': '{"user":{"aboutMe": "python-fitbit developer"}}',
+ 'status_code': 200
+ }])
+ token = {
+ 'access_token': 'fake_return_access_token',
+ 'refresh_token': 'fake_return_refresh_token'
+ }
+ m.post(fb.client.refresh_token_url, text=json.dumps(token))
+ retval = fb.make_request(profile_url)
+
+ self.assertEqual(m.request_history[1].path, '/oauth2/token')
self.assertEqual(
- "fake_return_refresh_token", fb.client.token['refresh_token'])
- self.assertEqual(1, rt.call_count)
- self.assertEqual(2, r.call_count)
- refresh_cb.assert_called_once_with(rt.return_value)
+ m.request_history[1].headers['Authorization'],
+ _basic_auth_str(
+ self.client_kwargs['client_id'],
+ self.client_kwargs['client_secret']
+ )
+ )
+ self.assertEqual(retval['user']['aboutMe'], "python-fitbit developer")
+ self.assertEqual("fake_return_access_token", token['access_token'])
+ self.assertEqual("fake_return_refresh_token", token['refresh_token'])
+ refresh_cb.assert_called_once_with(token)
+
+ def test_auto_refresh_error(self):
+ """Test of auto_refresh with expired refresh token"""
+
+ refresh_cb = mock.MagicMock()
+ kwargs = copy.copy(self.client_kwargs)
+ kwargs.update({
+ 'access_token': 'fake_access_token',
+ 'refresh_token': 'fake_refresh_token',
+ 'refresh_cb': refresh_cb,
+ })
+
+ fb = Fitbit(**kwargs)
+ with requests_mock.mock() as m:
+ response = {
+ "errors": [{"errorType": "invalid_grant"}],
+ "success": False
+ }
+ m.post(fb.client.refresh_token_url, text=json.dumps(response))
+ self.assertRaises(InvalidGrantError, fb.client.refresh_token)
class fake_response(object):
diff --git a/fitbit_tests/test_exceptions.py b/fitbit_tests/test_exceptions.py
index f656445..d43b656 100644
--- a/fitbit_tests/test_exceptions.py
+++ b/fitbit_tests/test_exceptions.py
@@ -1,4 +1,5 @@
import unittest
+import json
import mock
import requests
import sys
@@ -44,7 +45,14 @@ class ExceptionTest(unittest.TestCase):
"""
r = mock.Mock(spec=requests.Response)
r.status_code = 401
- r.content = b'{"normal": "resource"}'
+ json_response = {
+ "errors": [{
+ "errorType": "unauthorized",
+ "message": "Unknown auth error"}
+ ],
+ "normal": "resource"
+ }
+ r.content = json.dumps(json_response).encode('utf8')
f = Fitbit(**self.client_kwargs)
f.client._request = lambda *args, **kwargs: r
@@ -52,6 +60,11 @@ class ExceptionTest(unittest.TestCase):
self.assertRaises(exceptions.HTTPUnauthorized, f.user_profile_get)
r.status_code = 403
+ json_response['errors'][0].update({
+ "errorType": "forbidden",
+ "message": "Forbidden"
+ })
+ r.content = json.dumps(json_response).encode('utf8')
self.assertRaises(exceptions.HTTPForbidden, f.user_profile_get)
def test_response_error(self):
diff --git a/gather_keys_oauth2.py b/gather_keys_oauth2.py
index 7188644..aade911 100755
--- a/gather_keys_oauth2.py
+++ b/gather_keys_oauth2.py
@@ -7,29 +7,33 @@ import traceback
import webbrowser
from base64 import b64encode
-from fitbit.api import FitbitOauth2Client
+from fitbit.api import Fitbit
from oauthlib.oauth2.rfc6749.errors import MismatchingStateError, MissingTokenError
-from requests_oauthlib import OAuth2Session
class OAuth2Server:
def __init__(self, client_id, client_secret,
redirect_uri='http://127.0.0.1:8080/'):
""" Initialize the FitbitOauth2Client """
- self.redirect_uri = redirect_uri
self.success_html = """
<h1>You are now authorized to access the Fitbit API!</h1>
<br/><h3>You can close this window</h3>"""
self.failure_html = """
<h1>ERROR: %s</h1><br/><h3>You can close this window</h3>%s"""
- self.oauth = FitbitOauth2Client(client_id, client_secret)
+
+ self.fitbit = Fitbit(
+ client_id,
+ client_secret,
+ redirect_uri=redirect_uri,
+ timeout=10,
+ )
def browser_authorize(self):
"""
Open a browser to the authorization url and spool up a CherryPy
server to accept the response
"""
- url, _ = self.oauth.authorize_token_url(redirect_uri=self.redirect_uri)
+ url, _ = self.fitbit.client.authorize_token_url()
# Open the web browser in a new thread for command-line browser support
threading.Timer(1, webbrowser.open, args=(url,)).start()
cherrypy.quickstart(self)
@@ -43,7 +47,7 @@ class OAuth2Server:
error = None
if code:
try:
- self.oauth.fetch_access_token(code, self.redirect_uri)
+ self.fitbit.client.fetch_access_token(code)
except MissingTokenError:
error = self._fmt_failure(
'Missing access token parameter.</br>Please check that '
@@ -76,6 +80,10 @@ if __name__ == '__main__':
server = OAuth2Server(*sys.argv[1:])
server.browser_authorize()
- print('FULL RESULTS = %s' % server.oauth.token)
- print('ACCESS_TOKEN = %s' % server.oauth.token['access_token'])
- print('REFRESH_TOKEN = %s' % server.oauth.token['refresh_token'])
+ profile = server.fitbit.user_profile_get()
+ print('You are authorized to access data for the user: {}'.format(
+ profile['user']['fullName']))
+
+ print('TOKEN\n=====\n')
+ for key, value in server.fitbit.client.session.token.items():
+ print('{} = {}'.format(key, value))
diff --git a/requirements/base.txt b/requirements/base.txt
index 90630aa..1331f7b 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -1,2 +1,2 @@
-python-dateutil>=1.5,<2.5
-requests-oauthlib>=0.6.1,<1.1
+python-dateutil>=1.5
+requests-oauthlib>=0.7
diff --git a/requirements/test.txt b/requirements/test.txt
index d5c6230..711c52b 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -1,3 +1,5 @@
-mock>=1.0,<1.4
coverage>=3.7,<4.0
+freezegun>=0.3.8
+mock>=1.0
+requests-mock>=1.2.0
Sphinx>=1.2,<1.4
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-med/python-fitbit.git
More information about the debian-med-commit
mailing list