This is an automated email from the git hooks/post-receive script. irl-guest pushed a commit to branch master in repository python-fitbit.
commit 43d778899c5d45decec2c304e9bee76ef7a61cdc Author: Iain R. Learmonth <[email protected]> Date: Tue Jun 3 18:32:01 2014 +0100 Imported Upstream version 0.1.0 --- .gitignore | 6 + .travis.yml | 17 +- AUTHORS | 1 + MANIFEST.in | 2 +- README.md | 15 + README.rst | 14 - docs/conf.py | 12 +- docs/index.rst | 4 +- fitbit/__init__.py | 4 +- fitbit/api.py | 438 ++++++++++++++++++------ fitbit/exceptions.py | 7 +- fitbit_tests/__init__.py | 6 +- fitbit_tests/base.py | 18 - fitbit_tests/test_api.py | 124 ++++++- fitbit_tests/test_auth.py | 102 ++---- fitbit_tests/test_exceptions.py | 15 +- fitbit/gather_keys_cli.py => gather_keys_cli.py | 80 ++--- requirements.txt | 4 - requirements/base.txt | 2 + requirements/dev.txt | 5 + requirements/test.txt | 2 + requirements_dev.txt | 4 - requirements_test.txt | 1 - setup.py | 13 +- tox.ini | 24 ++ 25 files changed, 625 insertions(+), 295 deletions(-) diff --git a/.gitignore b/.gitignore index 3649edf..b629af3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,14 @@ *.pyc *.DS_Store +.coverage +.tox *~ docs/_build *.egg-info *.egg dist build +env + +# Editors +.idea diff --git a/.travis.yml b/.travis.yml index e8cd8a0..f1c6347 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,12 @@ language: python - -python: - - 2.6 - - 2.7 - -script: python setup.py test +python: 3.3 +env: + - TOX_ENV=pypy + - TOX_ENV=py33 + - TOX_ENV=py32 + - TOX_ENV=py27 + - TOX_ENV=py26 +install: + - pip install coveralls tox +script: tox -e $TOX_ENV +after_success: coveralls diff --git a/AUTHORS b/AUTHORS index e833c69..835f236 100644 --- a/AUTHORS +++ b/AUTHORS @@ -4,3 +4,4 @@ Rebecca Lovewell (Caktus Consulting Group) Dan Poirier (Caktus Consulting Group) Brad Pitcher (ORCAS) Silvio Tomatis +Steven Skoczen \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index 44a59eb..8bd82f5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1 @@ -include LICENSE AUTHORS README.rst requirements* docs/* +include LICENSE AUTHORS README.md requirements/* docs/* diff --git a/README.md b/README.md new file mode 100644 index 0000000..d88db85 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +python-fitbit +============= + +[](https://travis-ci.org/orcasgit/python-fitbit) +[](https://coveralls.io/r/orcasgit/python-fitbit?branch=master) +[](https://requires.io/github/orcasgit/python-fitbit/requirements/?branch=master) + +Fitbit API Python Client Implementation + +For documentation: [http://python-fitbit.readthedocs.org/](http://python-fitbit.readthedocs.org/) + +Requirements +============ + +* Python 2.6+ diff --git a/README.rst b/README.rst deleted file mode 100644 index e2914b9..0000000 --- a/README.rst +++ /dev/null @@ -1,14 +0,0 @@ -============= -python-fitbit -============= - -.. image:: https://travis-ci.org/orcasgit/python-fitbit.png?branch=master :target: https://travis-ci.org/orcasgit/python-fitbit - -Fitbit API Python Client Implementation - -For documentation: `http://python-fitbit.readthedocs.org/ <http://python-fitbit.readthedocs.org/>`_ - -Requirements -============ - -* Python 2.6+ diff --git a/docs/conf.py b/docs/conf.py index 4e73820..cd95ecc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,16 +41,16 @@ master_doc = 'index' # General information about the project. project = u'Python-Fitbit' -copyright = u'Copyright 2012 ORCAS' +copyright = u'Copyright 2014 ORCAS' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.0' +version = '0.1' # The full version, including alpha/beta/rc tags. -release = '0.0.2' +release = '0.1.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -184,7 +184,7 @@ latex_elements = { # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'Python-Fitbit.tex', u'Python-Fitbit Documentation', - u'Issac Kelly, Percy Perez', 'manual'), + u'Issac Kelly, Percy Perez, Brad Pitcher', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -214,7 +214,7 @@ latex_documents = [ # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'python-fitbit', u'Python-Fitbit Documentation', - [u'Issac Kelly, Percy Perez'], 1) + [u'Issac Kelly, Percy Perez, Brad Pitcher'], 1) ] # If true, show URL addresses after external links. @@ -228,7 +228,7 @@ man_pages = [ # dir menu entry, description, category) texinfo_documents = [ ('index', 'Python-Fitbit', u'Python-Fitbit Documentation', - u'Issac Kelly, Percy Perez', 'Python-Fitbit', 'One line description of project.', + u'Issac Kelly, Percy Perez, Brad Pitcher', 'Python-Fitbit', 'Fitbit API Python Client Implementation', 'Miscellaneous'), ] diff --git a/docs/index.rst b/docs/index.rst index ff6e3ea..a237953 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,7 +8,7 @@ Overview This is a complete python implementation of the Fitbit API. -It uses oAuath for authentication, it supports both us and si +It uses oAuth for authentication, it supports both us and si measurements Quickstart @@ -22,7 +22,7 @@ Here is some example usage:: unauth_client.activities() # You'll have to gather the user keys on your own, or try ./fitbit/gather_keys_cli.py <con_key> <con_sec> for development - authd_client = fitbit.Fitbit('<consumer_key>', '<consumer_secret>', user_key='<user_key>', user_secret='<user_secret>') + authd_client = fitbit.Fitbit('<consumer_key>', '<consumer_secret>', resource_owner_key='<user_key>', resource_owner_secret='<user_secret>') authd_client.sleep() Fitbit API diff --git a/fitbit/__init__.py b/fitbit/__init__.py index 25787bf..e4957cb 100644 --- a/fitbit/__init__.py +++ b/fitbit/__init__.py @@ -7,7 +7,7 @@ Fitbit API Library :license: BSD, see LICENSE for more details. """ -from .api import Fitbit, FitbitConsumer, FitbitOauthClient +from .api import Fitbit, FitbitOauthClient # Meta. @@ -17,7 +17,7 @@ __author_email__ = '[email protected]' __copyright__ = 'Copyright 2012 ORCAS' __license__ = 'Apache 2.0' -__version__ = '0.0.3' +__version__ = '0.1.0' # Module namespace. diff --git a/fitbit/api.py b/fitbit/api.py index 7516476..4ada88a 100644 --- a/fitbit/api.py +++ b/fitbit/api.py @@ -1,43 +1,60 @@ # -*- coding: utf-8 -*- -import oauth2 as oauth import requests import json import datetime -import urllib -from requests_oauthlib import OAuth1Session +try: + from urllib.parse import urlencode +except ImportError: + # Python 2.x + from urllib import urlencode + +from requests_oauthlib import OAuth1, OAuth1Session + from fitbit.exceptions import (BadResponse, DeleteError, HTTPBadRequest, HTTPUnauthorized, HTTPForbidden, HTTPServerError, HTTPConflict, HTTPNotFound) from fitbit.utils import curry -class FitbitConsumer(oauth.Consumer): - pass - - -# example client using httplib with headers -class FitbitOauthClient(oauth.Client): +class FitbitOauthClient(object): API_ENDPOINT = "https://api.fitbit.com" AUTHORIZE_ENDPOINT = "https://www.fitbit.com" API_VERSION = 1 - _signature_method = oauth.SignatureMethod_HMAC_SHA1() request_token_url = "%s/oauth/request_token" % API_ENDPOINT access_token_url = "%s/oauth/access_token" % API_ENDPOINT authorization_url = "%s/oauth/authorize" % AUTHORIZE_ENDPOINT - def __init__(self, consumer_key, consumer_secret, user_key=None, - user_secret=None, user_id=None, *args, **kwargs): - if user_key and user_secret: - self._token = oauth.Token(user_key, user_secret) - else: - # This allows public calls to be made - self._token = None + def __init__(self, client_key, client_secret, resource_owner_key=None, + resource_owner_secret=None, user_id=None, callback_uri=None, + *args, **kwargs): + """ + Create a FitbitOauthClient object. Specify the first 5 parameters if + you have them to access user data. Specify just the first 2 parameters + to access anonymous data and start the set up for user authorization. + + Set callback_uri to a URL and when the user has granted us access at + the fitbit site, fitbit will redirect them to the URL you passed. This + is how we get back the magic verifier string from fitbit if we're a web + app. If we don't pass it, then fitbit will just display the verifier + string for the user to copy and we'll have to ask them to paste it for + us and read it that way. + """ + + self.client_key = client_key + self.client_secret = client_secret + self.resource_owner_key = resource_owner_key + self.resource_owner_secret = resource_owner_secret if user_id: self.user_id = user_id - self._consumer = FitbitConsumer(consumer_key, consumer_secret) - super(FitbitOauthClient, self).__init__(self._consumer, *args, **kwargs) + params = {'client_secret': client_secret} + if callback_uri: + params['callback_uri'] = callback_uri + if self.resource_owner_key and self.resource_owner_secret: + params['resource_owner_key'] = self.resource_owner_key + params['resource_owner_secret'] = self.resource_owner_secret + self.oauth = OAuth1Session(client_key, **params) def _request(self, method, url, **kwargs): """ @@ -47,17 +64,16 @@ class FitbitOauthClient(oauth.Client): def make_request(self, url, data={}, method=None, **kwargs): """ - Builds and makes the Oauth Request, catches errors + Builds and makes the OAuth Request, catches errors https://wiki.fitbit.com/display/API/API+Response+Format+And+Errors """ if not method: method = 'POST' if data else 'GET' - request = oauth.Request.from_consumer_and_token(self._consumer, self._token, http_method=method, http_url=url, parameters=data) - request.sign_request(self._signature_method, self._consumer, - self._token) - response = self._request(method, url, data=data, - headers=request.to_header()) + auth = OAuth1( + self.client_key, self.client_secret, self.resource_owner_key, + self.resource_owner_secret, signature_type='auth_header') + response = self._request(method, url, data=data, auth=auth, **kwargs) if response.status_code == 401: raise HTTPUnauthorized(response) @@ -73,79 +89,49 @@ class FitbitOauthClient(oauth.Client): raise HTTPBadRequest(response) return response - def fetch_request_token(self, parameters=None): + def fetch_request_token(self): """ Step 1 of getting authorized to access a user's data at fitbit: this - makes a signed request to fitbit to get a token to use in the next - step. Returns that token. - - Set parameters['oauth_callback'] to a URL and when the user has - granted us access at the fitbit site, fitbit will redirect them to the URL - you passed. This is how we get back the magic verifier string from fitbit - if we're a web app. If we don't pass it, then fitbit will just display - the verifier string for the user to copy and we'll have to ask them to - paste it for us and read it that way. - """ - - """ - via headers - -> OAuthToken - - Providing 'oauth_callback' parameter in the Authorization header of - request_token_url request, will have priority over the dev.fitbit.com - settings, ie. parameters = {'oauth_callback': 'callback_url'} - """ - - request = oauth.Request.from_consumer_and_token( - self._consumer, - http_url=self.request_token_url, - parameters=parameters - ) - request.sign_request(self._signature_method, self._consumer, None) - response = self._request(request.method, self.request_token_url, - headers=request.to_header()) - return oauth.Token.from_string(response.content) - - def authorize_token_url(self, token): - """Step 2: Given the token returned by fetch_request_token(), return - the URL the user needs to go to in order to grant us authorization - to look at their data. Then redirect the user to that URL, open their - browser to it, or tell them to copy the URL into their browser. - """ - request = oauth.Request.from_token_and_callback( - token=token, - http_url=self.authorization_url - ) - return request.to_url() - - #def authorize_token(self, token): - # # via url - # # -> typically just some okay response - # request = oauth.Request.from_token_and_callback(token=token, - # http_url=self.authorization_url) - # response = self._request(request.method, request.to_url(), - # headers=request.to_header()) - # return response.content - - def fetch_access_token(self, token, verifier): - """Step 4: Given the token from step 1, and the verifier from step 3 (see step 2), - calls fitbit again and returns an access token object. Extract .key and .secret - from that and save them, then pass them as user_key and user_secret in future - API calls to fitbit to get this user's data. - """ - client = OAuth1Session( - self._consumer.key, - client_secret=self._consumer.secret, - resource_owner_key=token.key, - resource_owner_secret=token.secret, + makes a signed request to fitbit to get a token to use in step 3. + Returns that token.} + """ + + token = self.oauth.fetch_request_token(self.request_token_url) + self.resource_owner_key = token.get('oauth_token') + self.resource_owner_secret = token.get('oauth_token_secret') + return token + + def authorize_token_url(self): + """Step 2: Return the URL the user needs to go to in order to grant us + authorization to look at their data. Then redirect the user to that + URL, open their browser to it, or tell them to copy the URL into their + browser. + """ + + return self.oauth.authorization_url(self.authorization_url) + + def fetch_access_token(self, verifier, token=None): + """Step 3: Given the verifier from fitbit, and optionally a token from + step 1 (not necessary if using the same FitbitOAuthClient object) calls + fitbit again and returns an access token object. Extract the needed + information from that and save it to use in future API calls. + """ + if token: + self.resource_owner_key = token.get('oauth_token') + self.resource_owner_secret = token.get('oauth_token_secret') + + self.oauth = OAuth1Session( + self.client_key, + client_secret=self.client_secret, + resource_owner_key=self.resource_owner_key, + resource_owner_secret=self.resource_owner_secret, verifier=verifier) - response = client.fetch_access_token(self.access_token_url) + response = self.oauth.fetch_access_token(self.access_token_url) - self.user_id = response['encoded_user_id'] - self._token = oauth.Token( - key=response['oauth_token'], - secret=response['oauth_token_secret']) - return self._token + self.user_id = response.get('encoded_user_id') + self.resource_owner_key = response.get('oauth_token') + self.resource_owner_secret = response.get('oauth_token_secret') + return response class Fitbit(object): @@ -154,6 +140,7 @@ class Fitbit(object): API_ENDPOINT = "https://api.fitbit.com" API_VERSION = 1 + WEEK_DAYS = ['SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY'] _resource_list = [ 'body', @@ -172,8 +159,8 @@ class Fitbit(object): 'frequent', ] - def __init__(self, consumer_key, consumer_secret, system=US, **kwargs): - self.client = FitbitOauthClient(consumer_key, consumer_secret, **kwargs) + def __init__(self, client_key, client_secret, system=US, **kwargs): + self.client = FitbitOauthClient(client_key, client_secret, **kwargs) self.SYSTEM = system # All of these use the same patterns, define the method for accessing @@ -210,7 +197,7 @@ class Fitbit(object): else: raise DeleteError(response) try: - rep = json.loads(response.content) + rep = json.loads(response.content.decode('utf8')) except ValueError: raise BadResponse @@ -277,7 +264,7 @@ class Fitbit(object): date = datetime.date.today() if not user_id: user_id = '-' - if not isinstance(date, basestring): + if not isinstance(date, str): date = date.strftime('%Y-%m-%d') if not data: @@ -345,7 +332,7 @@ class Fitbit(object): raise TypeError("Either end_date or period can be specified, not both") if end_date: - if not isinstance(end_date, basestring): + if not isinstance(end_date, str): end = end_date.strftime('%Y-%m-%d') else: end = end_date @@ -354,7 +341,7 @@ class Fitbit(object): raise ValueError("Period must be one of '1d', '7d', '30d', '1w', '1m', '3m', '6m', '1y', 'max'") end = period - if not isinstance(base_date, basestring): + if not isinstance(base_date, str): base_date = base_date.strftime('%Y-%m-%d') url = "%s/%s/user/%s/%s/date/%s/%s.json" % ( @@ -434,6 +421,15 @@ class Fitbit(object): ) return self.make_request(url, method='POST') + def log_activity(self, data): + """ + https://wiki.fitbit.com/display/API/API-Log-Activity + """ + url = "%s/%s/user/-/activities.json" % ( + self.API_ENDPOINT, + self.API_VERSION) + return self.make_request(url, data = data) + def delete_favorite_activity(self, activity_id): """ https://wiki.fitbit.com/display/API/API-Delete-Favorite-Activity @@ -497,6 +493,130 @@ class Fitbit(object): ) return self.make_request(url) + def get_alarms(self, device_id): + """ + https://wiki.fitbit.com/display/API/API-Devices-Get-Alarms + """ + url = "%s/%s/user/-/devices/tracker/%s/alarms.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + device_id + ) + return self.make_request(url) + + def add_alarm(self, device_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-Add-Alarm + alarm_time should be a timezone aware datetime object. + """ + url = "%s/%s/user/-/devices/tracker/%s/alarms.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + device_id + ) + alarm_time = alarm_time.strftime("%H:%M%z") + # Check week_days list + if not isinstance(week_days, list): + raise ValueError("Week days needs to be a list") + for day in week_days: + if day not in self.WEEK_DAYS: + raise ValueError("Incorrect week day %s. see WEEK_DAY_LIST." % day) + data = { + 'time': alarm_time, + 'weekDays': week_days, + 'recurring': recurring, + 'enabled': enabled, + 'vibe': vibe + } + if label: + data['label'] = label + if snooze_length: + data['snoozeLength'] = snooze_length + if snooze_count: + data['snoozeCount'] = snooze_count + return self.make_request(url, data=data, method="POST") + # return + + 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 + alarm_time should be a timezone aware datetime object. + """ + # TODO Refactor with create_alarm. Tons of overlap. + # Check week_days list + if not isinstance(week_days, list): + raise ValueError("Week days needs to be a list") + for day in week_days: + if day not in self.WEEK_DAYS: + raise ValueError("Incorrect week day %s. see WEEK_DAY_LIST." % day) + url = "%s/%s/user/-/devices/tracker/%s/alarms/%s.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + device_id, + alarm_id + ) + alarm_time = alarm_time.strftime("%H:%M%z") + + data = { + 'time': alarm_time, + 'weekDays': week_days, + 'recurring': recurring, + 'enabled': enabled, + 'vibe': vibe + } + if label: + data['label'] = label + if snooze_length: + data['snoozeLength'] = snooze_length + if snooze_count: + data['snoozeCount'] = snooze_count + return self.make_request(url, data=data, method="POST") + # return + + def delete_alarm(self, device_id, alarm_id): + """ + https://wiki.fitbit.com/display/API/API-Devices-Delete-Alarm + """ + url = "%s/%s/user/-/devices/tracker/%s/alarms/%s.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + device_id, + alarm_id + ) + return self.make_request(url, method="DELETE") + + def get_sleep(self, date): + """ + https://wiki.fitbit.com/display/API/API-Get-Sleep + date should be a datetime.date object. + """ + url = "%s/%s/user/-/sleep/date/%s-%s-%s.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + date.year, + date.month, + date.day + ) + return self.make_request(url) + + def log_sleep(self, start_time, duration): + """ + https://wiki.fitbit.com/display/API/API-Log-Sleep + start time should be a datetime object. We will be using the year, month, day, hour, and minute. + """ + data = { + 'startTime': start_time.strftime("%H:%M"), + 'duration': duration, + 'date': start_time.strftime("%Y-%m-%d"), + } + url = "%s/%s/user/-/sleep" % ( + self.API_ENDPOINT, + self.API_VERSION, + ) + return self.make_request(url, data=data, method="POST") + def activities_list(self): """ https://wiki.fitbit.com/display/API/API-Browse-Activities @@ -525,7 +645,7 @@ class Fitbit(object): url = "%s/%s/foods/search.json?%s" % ( self.API_ENDPOINT, self.API_VERSION, - urllib.urlencode({'query': query}) + urlencode({'query': query}) ) return self.make_request(url) @@ -550,6 +670,118 @@ class Fitbit(object): ) 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 + 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. + + You can specify period or end_date, or neither, but not both. + """ + if not base_date: + base_date = datetime.date.today() + + if not user_id: + user_id = '-' + + if period and end_date: + raise TypeError("Either end_date or period can be specified, not both") + + if not isinstance(base_date, str): + base_date_string = base_date.strftime('%Y-%m-%d') + else: + base_date_string = base_date + + if period: + if not period in ['1d', '7d', '30d', '1w', '1m', '3m', '6m', '1y', 'max']: + raise ValueError("Period must be one of '1d', '7d', '30d', '1w', '1m', '3m', '6m', '1y', 'max'") + + url = "%s/%s/user/%s/body/log/weight/date/%s/%s.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + user_id, + base_date_string, + period + ) + elif end_date: + if not isinstance(end_date, str): + end_string = end_date.strftime('%Y-%m-%d') + else: + end_string = end_date + + url = "%s/%s/user/%s/body/log/weight/date/%s/%s.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + user_id, + base_date_string, + end_string + ) + else: + url = "%s/%s/user/%s/body/log/weight/date/%s.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + user_id, + base_date_string, + ) + return self.make_request(url) + + 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 + 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. + + You can specify period or end_date, or neither, but not both. + """ + if not base_date: + base_date = datetime.date.today() + + if not user_id: + user_id = '-' + + if period and end_date: + raise TypeError("Either end_date or period can be specified, not both") + + if not isinstance(base_date, str): + base_date_string = base_date.strftime('%Y-%m-%d') + else: + base_date_string = base_date + + if period: + if not period in ['1d', '7d', '30d', '1w', '1m', '3m', '6m', '1y', 'max']: + raise ValueError("Period must be one of '1d', '7d', '30d', '1w', '1m', '3m', '6m', '1y', 'max'") + + url = "%s/%s/user/%s/body/log/fat/date/%s/%s.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + user_id, + base_date_string, + period + ) + elif end_date: + if not isinstance(end_date, str): + end_string = end_date.strftime('%Y-%m-%d') + else: + end_string = end_date + + url = "%s/%s/user/%s/body/log/fat/date/%s/%s.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + user_id, + base_date_string, + end_string + ) + else: + url = "%s/%s/user/%s/body/log/fat/date/%s.json" % ( + self.API_ENDPOINT, + self.API_VERSION, + user_id, + base_date_string, + ) + return self.make_request(url) + def get_friends(self, user_id=None): """ https://wiki.fitbit.com/display/API/API-Get-Friends @@ -676,8 +908,8 @@ class Fitbit(object): return self.make_request(url) @classmethod - def from_oauth_keys(self, consumer_key, consumer_secret, user_key=None, + def from_oauth_keys(self, client_key, client_secret, user_key=None, user_secret=None, user_id=None, system=US): - client = FitbitOauthClient(consumer_key, consumer_secret, user_key, + client = FitbitOauthClient(client_key, client_secret, user_key, user_secret, user_id) return self(client, system) diff --git a/fitbit/exceptions.py b/fitbit/exceptions.py index 33ccc20..b594b02 100644 --- a/fitbit/exceptions.py +++ b/fitbit/exceptions.py @@ -15,10 +15,13 @@ class DeleteError(Exception): class HTTPException(Exception): def __init__(self, response, *args, **kwargs): try: - errors = json.loads(response.content)['errors'] + errors = json.loads(response.content.decode('utf8'))['errors'] message = '\n'.join([error['message'] for error in errors]) except Exception: - message = response + if response.status_code == 401: + message = response.content.decode('utf8') + else: + message = response super(HTTPException, self).__init__(message, *args, **kwargs) class HTTPBadRequest(HTTPException): diff --git a/fitbit_tests/__init__.py b/fitbit_tests/__init__.py index 06457ff..e3e0700 100644 --- a/fitbit_tests/__init__.py +++ b/fitbit_tests/__init__.py @@ -1,7 +1,7 @@ import unittest -from test_exceptions import ExceptionTest -from test_auth import AuthTest -from fitbit_tests.test_api import APITest, CollectionResourceTest, DeleteCollectionResourceTest, MiscTest +from .test_exceptions import ExceptionTest +from .test_auth import AuthTest +from .test_api import APITest, CollectionResourceTest, DeleteCollectionResourceTest, MiscTest def all_tests(consumer_key="", consumer_secret="", user_key=None, user_secret=None): diff --git a/fitbit_tests/base.py b/fitbit_tests/base.py deleted file mode 100644 index 9bb7866..0000000 --- a/fitbit_tests/base.py +++ /dev/null @@ -1,18 +0,0 @@ -import unittest - -class APITestCase(unittest.TestCase): - - - def __init__(self, consumer_key="", consumer_secret="", client_key=None, client_secret=None, *args, **kwargs): - self.consumer_key = consumer_key - self.consumer_secret = consumer_secret - self.client_key = client_key - self.client_secret = client_secret - - self.client_kwargs = { - "consumer_key": consumer_key, - "consumer_secret": consumer_secret, - "client_key": client_key, - "client_secret": client_secret, - } - super(APITestCase, self).__init__(*args, **kwargs) diff --git a/fitbit_tests/test_api.py b/fitbit_tests/test_api.py index 0a0c696..0eb3f3e 100644 --- a/fitbit_tests/test_api.py +++ b/fitbit_tests/test_api.py @@ -9,7 +9,7 @@ URLBASE = "%s/%s/user" % (Fitbit.API_ENDPOINT, Fitbit.API_VERSION) class TestBase(TestCase): def setUp(self): - self.fb = Fitbit(consumer_key='x', consumer_secret='y') + self.fb = Fitbit('x', 'y') def common_api_test(self, funcname, args, kwargs, expected_args, expected_kwargs): # Create a fitbit object, call the named function on it with the given @@ -33,7 +33,7 @@ class APITest(TestBase): KWARGS = { 'a': 3, 'b': 4, 'headers': {'Accept-Language': self.fb.SYSTEM}} mock_response = mock.Mock() mock_response.status_code = 200 - mock_response.content = "1" + mock_response.content = b"1" with mock.patch.object(self.fb.client, 'make_request') as client_make_request: client_make_request.return_value = mock_response retval = self.fb.make_request(*ARGS, **KWARGS) @@ -155,7 +155,7 @@ class CollectionResourceTest(TestBase): # since the __init__ is going to set up references to it with mock.patch('fitbit.api.Fitbit._COLLECTION_RESOURCE') as coll_resource: coll_resource.return_value = 999 - fb = Fitbit(consumer_key='x', consumer_secret='y') + fb = Fitbit('x', 'y') retval = fb.body(date=1, user_id=2, data=3) args, kwargs = coll_resource.call_args self.assertEqual(('body',), args) @@ -181,7 +181,7 @@ class DeleteCollectionResourceTest(TestBase): # since the __init__ is going to set up references to it with mock.patch('fitbit.api.Fitbit._DELETE_COLLECTION_RESOURCE') as delete_resource: delete_resource.return_value = 999 - fb = Fitbit(consumer_key='x', consumer_secret='y') + fb = Fitbit('x', 'y') retval = fb.delete_water(log_id=log_id) args, kwargs = delete_resource.call_args self.assertEqual(('water',), args) @@ -193,7 +193,7 @@ class MiscTest(TestBase): def test_recent_activities(self): user_id = "LukeSkywalker" with mock.patch('fitbit.api.Fitbit.activity_stats') as act_stats: - fb = Fitbit(consumer_key='x', consumer_secret='y') + fb = Fitbit('x', 'y') retval = fb.recent_activities(user_id=user_id) args, kwargs = act_stats.call_args self.assertEqual((), args) @@ -287,9 +287,55 @@ class MiscTest(TestBase): def test_activities(self): url = "%s/%s/activities.json" % (Fitbit.API_ENDPOINT, Fitbit.API_VERSION) self.common_api_test('activities_list', (), {}, (url,), {}) + url = "%s/%s/user/-/activities.json" % (Fitbit.API_ENDPOINT, Fitbit.API_VERSION) + self.common_api_test('log_activity', (), {'data' : 'FOO'}, (url,), {'data' : 'FOO'} ) url = "%s/%s/activities/FOOBAR.json" % (Fitbit.API_ENDPOINT, Fitbit.API_VERSION) self.common_api_test('activity_detail', ("FOOBAR",), {}, (url,), {}) + def test_bodyweight(self): + def test_get_bodyweight(fb, base_date=None, user_id=None, period=None, end_date=None, expected_url=None): + with mock.patch.object(fb, 'make_request') as make_request: + fb.get_bodyweight(base_date, user_id=user_id, period=period, end_date=end_date) + args, kwargs = make_request.call_args + self.assertEqual((expected_url,), args) + + user_id = 'BAR' + + # No end_date or period + test_get_bodyweight(self.fb, base_date=datetime.date(1992, 5, 12), user_id=None, period=None, end_date=None, + expected_url=URLBASE + "/-/body/log/weight/date/1992-05-12.json") + # With end_date + test_get_bodyweight(self.fb, base_date=datetime.date(1992, 5, 12), user_id=user_id, period=None, end_date=datetime.date(1998, 12, 31), + expected_url=URLBASE + "/BAR/body/log/weight/date/1992-05-12/1998-12-31.json") + # With period + test_get_bodyweight(self.fb, base_date=datetime.date(1992, 5, 12), user_id=user_id, period="1d", end_date=None, + expected_url=URLBASE + "/BAR/body/log/weight/date/1992-05-12/1d.json") + # Date defaults to today + test_get_bodyweight(self.fb, base_date=None, user_id=None, period=None, end_date=None, + expected_url=URLBASE + "/-/body/log/weight/date/%s.json" % datetime.date.today().strftime('%Y-%m-%d')) + + def test_bodyfat(self): + def test_get_bodyfat(fb, base_date=None, user_id=None, period=None, end_date=None, expected_url=None): + with mock.patch.object(fb, 'make_request') as make_request: + fb.get_bodyfat(base_date, user_id=user_id, period=period, end_date=end_date) + args, kwargs = make_request.call_args + self.assertEqual((expected_url,), args) + + user_id = 'BAR' + + # No end_date or period + test_get_bodyfat(self.fb, base_date=datetime.date(1992, 5, 12), user_id=None, period=None, end_date=None, + expected_url=URLBASE + "/-/body/log/fat/date/1992-05-12.json") + # With end_date + test_get_bodyfat(self.fb, base_date=datetime.date(1992, 5, 12), user_id=user_id, period=None, end_date=datetime.date(1998, 12, 31), + expected_url=URLBASE + "/BAR/body/log/fat/date/1992-05-12/1998-12-31.json") + # With period + test_get_bodyfat(self.fb, base_date=datetime.date(1992, 5, 12), user_id=user_id, period="1d", end_date=None, + expected_url=URLBASE + "/BAR/body/log/fat/date/1992-05-12/1d.json") + # Date defaults to today + test_get_bodyfat(self.fb, base_date=None, user_id=None, period=None, end_date=None, + expected_url=URLBASE + "/-/body/log/fat/date/%s.json" % datetime.date.today().strftime('%Y-%m-%d')) + def test_friends(self): url = URLBASE + "/-/friends.json" self.common_api_test('get_friends', (), {}, (url,), {}) @@ -326,3 +372,71 @@ class MiscTest(TestBase): url = URLBASE + "/-/COLLECTION/apiSubscriptions/SUBSCRIPTION_ID-COLLECTION.json" self.common_api_test('subscription', ("SUBSCRIPTION_ID", "SUBSCRIBER_ID"), {'method': 'THROW', 'collection': "COLLECTION"}, (url,), {'method': 'THROW', 'headers': {'X-Fitbit-Subscriber-id': "SUBSCRIBER_ID"}}) + + def test_alarms(self): + url = "%s/-/devices/tracker/%s/alarms.json" % (URLBASE, 'FOO') + self.common_api_test('get_alarms', (), {'device_id': 'FOO'}, (url,), {}) + url = "%s/-/devices/tracker/%s/alarms/%s.json" % (URLBASE, 'FOO', 'BAR') + self.common_api_test('delete_alarm', (), {'device_id': 'FOO', 'alarm_id': 'BAR'}, (url,), {'method': 'DELETE'}) + url = "%s/-/devices/tracker/%s/alarms.json" % (URLBASE, 'FOO') + self.common_api_test('add_alarm', + (), + {'device_id': 'FOO', + 'alarm_time': datetime.datetime(year=2013, month=11, day=13, hour=8, minute=16), + 'week_days': ['MONDAY'] + }, + (url,), + {'data': + {'enabled': True, + 'recurring': False, + 'time': datetime.datetime(year=2013, month=11, day=13, hour=8, minute=16).strftime("%H:%M%z"), + 'vibe': 'DEFAULT', + 'weekDays': ['MONDAY'], + }, + 'method': 'POST' + } + ) + self.common_api_test('add_alarm', + (), + {'device_id': 'FOO', + 'alarm_time': datetime.datetime(year=2013, month=11, day=13, hour=8, minute=16), + 'week_days': ['MONDAY'], 'recurring': True, 'enabled': False, 'label': 'ugh', + 'snooze_length': 5, + 'snooze_count': 5 + }, + (url,), + {'data': + {'enabled': False, + 'recurring': True, + 'label': 'ugh', + 'snoozeLength': 5, + 'snoozeCount': 5, + 'time': datetime.datetime(year=2013, month=11, day=13, hour=8, minute=16).strftime("%H:%M%z"), + 'vibe': 'DEFAULT', + 'weekDays': ['MONDAY'], + }, + 'method': 'POST'} + ) + url = "%s/-/devices/tracker/%s/alarms/%s.json" % (URLBASE, 'FOO', 'BAR') + self.common_api_test('update_alarm', + (), + {'device_id': 'FOO', + 'alarm_id': 'BAR', + 'alarm_time': datetime.datetime(year=2013, month=11, day=13, hour=8, minute=16), + 'week_days': ['MONDAY'], 'recurring': True, 'enabled': False, 'label': 'ugh', + 'snooze_length': 5, + 'snooze_count': 5 + }, + (url,), + {'data': + {'enabled': False, + 'recurring': True, + 'label': 'ugh', + 'snoozeLength': 5, + 'snoozeCount': 5, + 'time': datetime.datetime(year=2013, month=11, day=13, hour=8, minute=16).strftime("%H:%M%z"), + 'vibe': 'DEFAULT', + 'weekDays': ['MONDAY'], + }, + 'method': 'POST'} + ) diff --git a/fitbit_tests/test_auth.py b/fitbit_tests/test_auth.py index ea8a0ea..5bc2a2b 100644 --- a/fitbit_tests/test_auth.py +++ b/fitbit_tests/test_auth.py @@ -1,7 +1,7 @@ from unittest import TestCase from fitbit import Fitbit import mock -import oauth2 as oauth +from requests_oauthlib import OAuth1Session class AuthTest(TestCase): """Add tests for auth part of API @@ -9,80 +9,52 @@ class AuthTest(TestCase): make sure we call the right oauth calls, respond correctly based on the responses """ client_kwargs = { - "consumer_key": "", - "consumer_secret": "", - "user_key": None, - "user_secret": None, - } + 'client_key': '', + 'client_secret': '', + 'user_key': None, + 'user_secret': None, + 'callback_uri': 'CALLBACK_URL' + } def test_fetch_request_token(self): # fetch_request_token needs to make a request and then build a token from the response fb = Fitbit(**self.client_kwargs) - callback_url = "CALLBACK_URL" - parameters = {'oauth_callback': callback_url} - with mock.patch.object(oauth.Request, 'from_consumer_and_token') as from_consumer_and_token: - mock_request = mock.Mock() - mock_request.to_header.return_value = "MOCKHEADERS" - mock_request.method = 'GET' - from_consumer_and_token.return_value = mock_request - with mock.patch('fitbit.api.FitbitOauthClient._request') as _request: - fake_response = mock.Mock() - fake_response.content = "FAKECONTENT" - fake_response.status_code = 200 - _request.return_value = fake_response - with mock.patch.object(oauth.Token, 'from_string') as from_string: - from_string.return_value = "FAKERETURNVALUE" - - retval = fb.client.fetch_request_token(parameters) - # Got the right return value - self.assertEqual("FAKERETURNVALUE", retval) - # The right parms were passed along the way to getting there - self.assertEqual(1, from_consumer_and_token.call_count) - self.assertEqual((fb.client._consumer,), from_consumer_and_token.call_args[0]) - self.assertEqual({'http_url': fb.client.request_token_url, 'parameters': parameters}, from_consumer_and_token.call_args[1]) - self.assertEqual(1, mock_request.sign_request.call_count) - self.assertEqual((fb.client._signature_method, fb.client._consumer, None), mock_request.sign_request.call_args[0]) - self.assertEqual(1, _request.call_count) - self.assertEqual((mock_request.method,fb.client.request_token_url), _request.call_args[0]) - self.assertEqual({'headers': "MOCKHEADERS"}, _request.call_args[1]) - self.assertEqual(1, from_string.call_count) - self.assertEqual(("FAKECONTENT",), from_string.call_args[0]) + with mock.patch.object(OAuth1Session, 'fetch_request_token') as frt: + frt.return_value = { + 'oauth_callback_confirmed': 'true', + 'oauth_token': 'FAKE_OAUTH_TOKEN', + 'oauth_token_secret': 'FAKE_OAUTH_TOKEN_SECRET'} + retval = fb.client.fetch_request_token() + self.assertEqual(1, frt.call_count) + # Got the right return value + self.assertEqual('true', retval.get('oauth_callback_confirmed')) + self.assertEqual('FAKE_OAUTH_TOKEN', retval.get('oauth_token')) + self.assertEqual('FAKE_OAUTH_TOKEN_SECRET', + retval.get('oauth_token_secret')) def test_authorize_token_url(self): # authorize_token_url calls oauth and returns a URL fb = Fitbit(**self.client_kwargs) - fake_token = "FAKETOKEN" - with mock.patch.object(oauth.Request, "from_token_and_callback") as from_token_and_callback: - mock_request = mock.Mock() - mock_request.to_url.return_value = "FAKEURL" - from_token_and_callback.return_value = mock_request - retval = fb.client.authorize_token_url(fake_token) - self.assertEqual("FAKEURL", retval) - self.assertEqual(1, from_token_and_callback.call_count) - kwargs = from_token_and_callback.call_args_list[0][1] - self.assertEqual({'token': fake_token, 'http_url': fb.client.authorization_url}, kwargs) + with mock.patch.object(OAuth1Session, 'authorization_url') as au: + au.return_value = 'FAKEURL' + retval = fb.client.authorize_token_url() + self.assertEqual(1, au.call_count) + self.assertEqual("FAKEURL", retval) def test_fetch_access_token(self): - fb = Fitbit(**self.client_kwargs) - fake_token = mock.Mock(key="FAKEKEY", secret="FAKESECRET") + kwargs = self.client_kwargs + kwargs['resource_owner_key'] = '' + kwargs['resource_owner_secret'] = '' + fb = Fitbit(**kwargs) fake_verifier = "FAKEVERIFIER" - with mock.patch('requests_oauthlib.OAuth1Session.fetch_access_token') as fetch_access_token: - fetch_access_token.return_value = { - 'encoded_user_id': 'FAKEUSERID', - 'oauth_token': 'FAKERETURNEDKEY', - 'oauth_token_secret': 'FAKERETURNEDSECRET' + with mock.patch.object(OAuth1Session, 'fetch_access_token') as fat: + fat.return_value = { + 'encoded_user_id': 'FAKE_USER_ID', + 'oauth_token': 'FAKE_RETURNED_KEY', + 'oauth_token_secret': 'FAKE_RETURNED_SECRET' } - retval = fb.client.fetch_access_token(fake_token, fake_verifier) - self.assertEqual("FAKERETURNEDKEY", retval.key) - self.assertEqual("FAKERETURNEDSECRET", retval.secret) - self.assertEqual('FAKEUSERID', fb.client.user_id) - - def test_fetch_access_token_error(self): - fb = Fitbit(**self.client_kwargs) - with mock.patch('requests.sessions.Session.post') as post: - post.return_value = mock.Mock(text="not a url encoded string") - fake_token = mock.Mock(key="FAKEKEY", secret="FAKESECRET") - self.assertRaises(ValueError, - fb.client.fetch_access_token, - fake_token, "fake_verifier") + retval = fb.client.fetch_access_token(fake_verifier) + self.assertEqual("FAKE_RETURNED_KEY", retval['oauth_token']) + self.assertEqual("FAKE_RETURNED_SECRET", retval['oauth_token_secret']) + self.assertEqual('FAKE_USER_ID', fb.client.user_id) diff --git a/fitbit_tests/test_exceptions.py b/fitbit_tests/test_exceptions.py index 98eba79..5c174bb 100644 --- a/fitbit_tests/test_exceptions.py +++ b/fitbit_tests/test_exceptions.py @@ -9,13 +9,12 @@ class ExceptionTest(unittest.TestCase): Tests that certain response codes raise certain exceptions """ client_kwargs = { - "consumer_key": "", - "consumer_secret": "", + "client_key": "", + "client_secret": "", "user_key": None, "user_secret": None, } - def test_response_ok(self): """ This mocks a pretty normal resource, that the request was authenticated, @@ -24,7 +23,7 @@ class ExceptionTest(unittest.TestCase): """ r = mock.Mock(spec=requests.Response) r.status_code = 200 - r.content = '{"normal": "resource"}' + r.content = b'{"normal": "resource"}' f = Fitbit(**self.client_kwargs) f.client._request = lambda *args, **kwargs: r @@ -44,7 +43,7 @@ class ExceptionTest(unittest.TestCase): """ r = mock.Mock(spec=requests.Response) r.status_code = 401 - r.content = "{'normal': 'resource'}" + r.content = b"{'normal': 'resource'}" f = Fitbit(**self.client_kwargs) f.client._request = lambda *args, **kwargs: r @@ -60,7 +59,7 @@ class ExceptionTest(unittest.TestCase): Tests other HTTP errors """ r = mock.Mock(spec=requests.Response) - r.content = "{'normal': 'resource'}" + r.content = b"{'normal': 'resource'}" f = Fitbit(**self.client_kwargs) f.client._request = lambda *args, **kwargs: r @@ -84,7 +83,7 @@ class ExceptionTest(unittest.TestCase): """ r = mock.Mock(spec=requests.Response) r.status_code = 200 - r.content = "iyam not jason" + r.content = b"iyam not jason" f = Fitbit(**self.client_kwargs) f.client._request = lambda *args, **kwargs: r @@ -96,7 +95,7 @@ class ExceptionTest(unittest.TestCase): """ r = mock.Mock(spec=requests.Response) r.status_code = 201 - r.content = '{"it\'s all": "ok"}' + r.content = b'{"it\'s all": "ok"}' f = Fitbit(**self.client_kwargs) f.client._request = lambda *args, **kwargs: r diff --git a/fitbit/gather_keys_cli.py b/gather_keys_cli.py similarity index 55% rename from fitbit/gather_keys_cli.py rename to gather_keys_cli.py index a11c2fb..c7b4523 100755 --- a/fitbit/gather_keys_cli.py +++ b/gather_keys_cli.py @@ -31,67 +31,53 @@ Instead, you'll want to create your own subclass of OAuthClient or find one that works with your web framework. """ -from api import FitbitOauthClient -import time -import oauth2 as oauth -import urlparse -import platform -import subprocess +import os +import pprint +import sys +import webbrowser +from fitbit.api import FitbitOauthClient def gather_keys(): # setup - print '** OAuth Python Library Example **' - client = FitbitOauthClient(CONSUMER_KEY, CONSUMER_SECRET) - - print '' + pp = pprint.PrettyPrinter(indent=4) + print('** OAuth Python Library Example **\n') + client = FitbitOauthClient(CLIENT_KEY, CLIENT_SECRET) # get request token - print '* Obtain a request token ...' - print '' + print('* Obtain a request token ...\n') token = client.fetch_request_token() - print 'FROM RESPONSE' - print 'key: %s' % str(token.key) - print 'secret: %s' % str(token.secret) - print 'callback confirmed? %s' % str(token.callback_confirmed) - print '' - - print '* Authorize the request token in your browser' - print '' - if platform.mac_ver(): - subprocess.Popen(['open', client.authorize_token_url(token)]) - else: - print 'open: %s' % client.authorize_token_url(token) - print '' - verifier = raw_input('Verifier: ') - print verifier - print '' + print('RESPONSE') + pp.pprint(token) + print('') + + print('* Authorize the request token in your browser\n') + stderr = os.dup(2) + os.close(2) + os.open(os.devnull, os.O_RDWR) + webbrowser.open(client.authorize_token_url()) + os.dup2(stderr, 2) + try: + verifier = raw_input('Verifier: ') + except NameError: + # Python 3.x + verifier = input('Verifier: ') # get access token - print '* Obtain an access token ...' - print '' - print 'REQUEST (via headers)' - print '' - token = client.fetch_access_token(token, verifier) - print 'FROM RESPONSE' - print 'key: %s' % str(token.key) - print 'secret: %s' % str(token.secret) - print '' - + print('\n* Obtain an access token ...\n') + token = client.fetch_access_token(verifier) + print('RESPONSE') + pp.pprint(token) + print('') -def pause(): - print '' - time.sleep(1) if __name__ == '__main__': - import sys - if not (len(sys.argv) == 3): - print "Arguments 'client key', 'client secret' are required" + print("Arguments 'client key', 'client secret' are required") sys.exit(1) - CONSUMER_KEY = sys.argv[1] - CONSUMER_SECRET = sys.argv[2] + CLIENT_KEY = sys.argv[1] + CLIENT_SECRET = sys.argv[2] gather_keys() - print 'Done.' + print('Done.') diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e26d6b8..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -oauth2>=1.5.211 -requests>=0.14.0 -python-dateutil>=1.5 -requests-oauthlib \ No newline at end of file diff --git a/requirements/base.txt b/requirements/base.txt new file mode 100644 index 0000000..f5d86ee --- /dev/null +++ b/requirements/base.txt @@ -0,0 +1,2 @@ +python-dateutil>=1.5 +requests-oauthlib>=0.4.0 diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..5ad7c4b --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,5 @@ +-r base.txt +-r test.txt + +Sphinx==1.2.2 +tox==1.7.1 diff --git a/requirements/test.txt b/requirements/test.txt new file mode 100644 index 0000000..969f7a2 --- /dev/null +++ b/requirements/test.txt @@ -0,0 +1,2 @@ +mock==1.0.1 +coverage==3.7.1 diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index 5442982..0000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,4 +0,0 @@ --r requirements.txt --r requirements_test.txt -Sphinx==1.1.3 -Mock diff --git a/requirements_test.txt b/requirements_test.txt deleted file mode 100644 index 9d11ae4..0000000 --- a/requirements_test.txt +++ /dev/null @@ -1 +0,0 @@ -mock==0.8.0 diff --git a/setup.py b/setup.py index a9063b2..c0fbf8e 100644 --- a/setup.py +++ b/setup.py @@ -5,8 +5,8 @@ import re from setuptools import setup -required = [line for line in open('requirements.txt').read().split("\n")] -required_dev = [line for line in open('requirements_dev.txt').read().split("\n") if not line.startswith("-r")] +required = [line for line in open('requirements/base.txt').read().split("\n")] +required_test = [line for line in open('requirements/test.txt').read().split("\n") if not line.startswith("-r")] fbinit = open('fitbit/__init__.py').read() author = re.search("__author__ = '([^']+)'", fbinit).group(1) @@ -17,7 +17,7 @@ setup( name='fitbit', version=version, description='Fitbit API Wrapper.', - long_description=open('README.rst').read(), + long_description=open('README.md').read(), author=author, author_email=author_email, url='https://github.com/orcasgit/python-fitbit', @@ -27,7 +27,7 @@ setup( install_requires=["distribute"] + required, license='Apache 2.0', test_suite='fitbit_tests.all_tests', - tests_require=required_dev, + tests_require=required_test, classifiers=( 'Intended Audience :: Developers', 'Natural Language :: English', @@ -35,5 +35,10 @@ setup( 'Programming Language :: Python', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: Implementation :: PyPy' ), ) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..e2f8462 --- /dev/null +++ b/tox.ini @@ -0,0 +1,24 @@ +[tox] +envlist = pypy,py34,py33,py32,py27,py26 + +[testenv] +commands = coverage run --source=fitbit setup.py test +deps = -r{toxinidir}/requirements/test.txt + +[testenv:pypy] +basepython = pypy + +[testenv:py34] +basepython = python3.4 + +[testenv:py33] +basepython = python3.3 + +[testenv:py32] +basepython = python3.2 + +[testenv:py27] +basepython = python2.7 + +[testenv:py26] +basepython = python2.6 -- Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-med/python-fitbit.git _______________________________________________ debian-med-commit mailing list [email protected] http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/debian-med-commit
